registry/mdns: add domain support (#1708)

* registry: add domain options

* registry/mdns: implement domain options

* registry/mdns: return node domain in metadata when querying using wildcard

* Fix nil pointer exception

* registry/mdns: return error from deregister

* registy/mdns: rename tld => domain
This commit is contained in:
ben-toogood 2020-06-17 13:23:41 +01:00 committed by GitHub
parent 9d3365c4be
commit 3b40fde68b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 213 additions and 60 deletions

View File

@ -21,8 +21,12 @@ import (
) )
var ( var (
// use a .micro domain rather than .local // use a .micro tld rather than .local by default
mdnsDomain = "micro" defaultDomain = "micro"
// every service is written to the global domain so * domain queries work, e.g.
// calling mdns.List(registry.ListDomain("*")) will list the services across all
// domains
globalDomain = "global"
) )
type mdnsTxt struct { type mdnsTxt struct {
@ -37,13 +41,20 @@ type mdnsEntry struct {
node *mdns.Server node *mdns.Server
} }
// services are a key/value map, with the service name as a key and the value being a
// slice of mdns entries, representing the nodes with a single _services entry to be
// used for listing
type services map[string][]*mdnsEntry
type mdnsRegistry struct { type mdnsRegistry struct {
opts Options opts Options
// the mdns domain
domain string // the top level domains, these can be overriden using options
defaultDomain string
globalDomain string
sync.Mutex sync.Mutex
services map[string][]*mdnsEntry domains map[string]services
mtx sync.RWMutex mtx sync.RWMutex
@ -138,18 +149,19 @@ func newRegistry(opts ...Option) Registry {
} }
// set the domain // set the domain
domain := mdnsDomain defaultDomain := defaultDomain
d, ok := options.Context.Value("mdns.domain").(string) d, ok := options.Context.Value("mdns.domain").(string)
if ok { if ok {
domain = d defaultDomain = d
} }
return &mdnsRegistry{ return &mdnsRegistry{
opts: options, defaultDomain: defaultDomain,
domain: domain, globalDomain: globalDomain,
services: make(map[string][]*mdnsEntry), opts: options,
watchers: make(map[string]*mdnsWatcher), domains: make(map[string]services),
watchers: make(map[string]*mdnsWatcher),
} }
} }
@ -164,55 +176,66 @@ func (m *mdnsRegistry) Options() Options {
return m.opts return m.opts
} }
// createServiceMDNSEntry will create a new wildcard mdns entry for the service in the
// given domain. This wildcard mdns entry is used when listing services.
func createServiceMDNSEntry(name, domain string) (*mdnsEntry, error) {
ip := net.ParseIP("0.0.0.0")
s, err := mdns.NewMDNSService(name, "_services", domain+".", "", 9999, []net.IP{ip}, nil)
if err != nil {
return nil, err
}
srv, err := mdns.NewServer(&mdns.Config{Zone: &mdns.DNSSDService{MDNSService: s}})
if err != nil {
return nil, err
}
return &mdnsEntry{id: "*", node: srv}, nil
}
func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error { func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error {
m.Lock() m.Lock()
defer m.Unlock()
entries, ok := m.services[service.Name] // parse the options
// first entry, create wildcard used for list queries var options RegisterOptions
for _, o := range opts {
o(&options)
}
if len(options.Domain) == 0 {
options.Domain = m.defaultDomain
}
// create the domain in the memory store if it doesn't yet exist
if _, ok := m.domains[options.Domain]; !ok {
m.domains[options.Domain] = make(services)
}
// create the wildcard entry used for list queries in this domain
entries, ok := m.domains[options.Domain][service.Name]
if !ok { if !ok {
s, err := mdns.NewMDNSService( entry, err := createServiceMDNSEntry(service.Name, options.Domain)
service.Name,
"_services",
m.domain+".",
"",
9999,
[]net.IP{net.ParseIP("0.0.0.0")},
nil,
)
if err != nil { if err != nil {
m.Unlock()
return err return err
} }
entries = append(entries, entry)
srv, err := mdns.NewServer(&mdns.Config{Zone: &mdns.DNSSDService{MDNSService: s}})
if err != nil {
return err
}
// append the wildcard entry
entries = append(entries, &mdnsEntry{id: "*", node: srv})
} }
var gerr error var gerr error
for _, node := range service.Nodes { for _, node := range service.Nodes {
var seen bool var seen bool
var e *mdnsEntry
for _, entry := range entries { for _, entry := range entries {
if node.Id == entry.id { if node.Id == entry.id {
seen = true seen = true
e = entry
break break
} }
} }
// already registered, continue // this node has already been registered, continue
if seen { if seen {
continue continue
// doesn't exist
} else {
e = &mdnsEntry{}
} }
txt, err := encode(&mdnsTxt{ txt, err := encode(&mdnsTxt{
@ -241,7 +264,7 @@ func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error
s, err := mdns.NewMDNSService( s, err := mdns.NewMDNSService(
node.Id, node.Id,
service.Name, service.Name,
m.domain+".", options.Domain+".",
"", "",
port, port,
[]net.IP{net.ParseIP(host)}, []net.IP{net.ParseIP(host)},
@ -258,25 +281,70 @@ func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error
continue continue
} }
e.id = node.Id entries = append(entries, &mdnsEntry{id: node.Id, node: srv})
e.node = srv
entries = append(entries, e)
} }
// save // save the mdns entry
m.services[service.Name] = entries m.domains[options.Domain][service.Name] = entries
m.Unlock()
// register in the global Domain so it can be queried as one
if options.Domain != m.globalDomain {
srv := *service
srv.Nodes = nil
for _, n := range service.Nodes {
node := n
// set the original domain in node metadata
if node.Metadata == nil {
node.Metadata = map[string]string{"domain": options.Domain}
} else {
node.Metadata["domain"] = options.Domain
}
srv.Nodes = append(srv.Nodes, node)
}
if err := m.Register(service, append(opts, RegisterDomain(m.globalDomain))...); err != nil {
gerr = err
}
}
return gerr return gerr
} }
func (m *mdnsRegistry) Deregister(service *Service, opts ...DeregisterOption) error { func (m *mdnsRegistry) Deregister(service *Service, opts ...DeregisterOption) error {
// parse the options
var options DeregisterOptions
for _, o := range opts {
o(&options)
}
if len(options.Domain) == 0 {
options.Domain = m.defaultDomain
}
// register in the global Domain
var err error
if options.Domain != m.globalDomain {
defer func() {
err = m.Deregister(service, append(opts, DeregisterDomain(m.globalDomain))...)
}()
}
// we want to unlock before we call deregister on the global domain, so it's important this unlock
// is applied after the defer m.Deregister is called above
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
var newEntries []*mdnsEntry // the service wasn't registered, we can safely exist
if _, ok := m.domains[options.Domain]; !ok {
return err
}
// loop existing entries, check if any match, shutdown those that do // loop existing entries, check if any match, shutdown those that do
for _, entry := range m.services[service.Name] { var newEntries []*mdnsEntry
for _, entry := range m.domains[options.Domain][service.Name] {
var remove bool var remove bool
for _, node := range service.Nodes { for _, node := range service.Nodes {
@ -293,18 +361,43 @@ func (m *mdnsRegistry) Deregister(service *Service, opts ...DeregisterOption) er
} }
} }
// last entry is the wildcard for list queries. Remove it. // we have more than one entry remaining, we can exit
if len(newEntries) == 1 && newEntries[0].id == "*" { if len(newEntries) > 1 {
newEntries[0].node.Shutdown() m.domains[options.Domain][service.Name] = newEntries
delete(m.services, service.Name) return err
} else {
m.services[service.Name] = newEntries
} }
return nil // our remaining entry is not a wildcard, we can exit
if len(newEntries) == 1 && newEntries[0].id != "*" {
m.domains[options.Domain][service.Name] = newEntries
return err
}
// last entry is the wildcard for list queries. Remove it.
newEntries[0].node.Shutdown()
delete(m.domains[options.Domain], service.Name)
// check to see if we can delete the domain entry
if len(m.domains[options.Domain]) == 0 {
delete(m.domains, options.Domain)
}
return err
} }
func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service, error) { func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service, error) {
// parse the options
var options GetOptions
for _, o := range opts {
o(&options)
}
if len(options.Domain) == 0 {
options.Domain = m.defaultDomain
}
if options.Domain == WildcardDomain {
options.Domain = m.globalDomain
}
serviceMap := make(map[string]*Service) serviceMap := make(map[string]*Service)
entries := make(chan *mdns.ServiceEntry, 10) entries := make(chan *mdns.ServiceEntry, 10)
done := make(chan bool) done := make(chan bool)
@ -317,17 +410,14 @@ func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service
// set entries channel // set entries channel
p.Entries = entries p.Entries = entries
// set the domain // set the domain
p.Domain = m.domain p.Domain = options.Domain
go func() { go func() {
for { for {
select { select {
case e := <-entries: case e := <-entries:
// list record so skip // list record so skip
if p.Service == "_services" { if e.Name == "_services" {
continue
}
if p.Domain != m.domain {
continue continue
} }
if e.TTL == 0 { if e.TTL == 0 {
@ -397,6 +487,18 @@ func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service
} }
func (m *mdnsRegistry) ListServices(opts ...ListOption) ([]*Service, error) { func (m *mdnsRegistry) ListServices(opts ...ListOption) ([]*Service, error) {
// parse the options
var options ListOptions
for _, o := range opts {
o(&options)
}
if len(options.Domain) == 0 {
options.Domain = m.defaultDomain
}
if options.Domain == WildcardDomain {
options.Domain = m.globalDomain
}
serviceMap := make(map[string]bool) serviceMap := make(map[string]bool)
entries := make(chan *mdns.ServiceEntry, 10) entries := make(chan *mdns.ServiceEntry, 10)
done := make(chan bool) done := make(chan bool)
@ -409,7 +511,7 @@ func (m *mdnsRegistry) ListServices(opts ...ListOption) ([]*Service, error) {
// set entries channel // set entries channel
p.Entries = entries p.Entries = entries
// set domain // set domain
p.Domain = m.domain p.Domain = options.Domain
var services []*Service var services []*Service
@ -451,13 +553,19 @@ func (m *mdnsRegistry) Watch(opts ...WatchOption) (Watcher, error) {
for _, o := range opts { for _, o := range opts {
o(&wo) o(&wo)
} }
if len(wo.Domain) == 0 {
wo.Domain = m.defaultDomain
}
if wo.Domain == WildcardDomain {
wo.Domain = m.globalDomain
}
md := &mdnsWatcher{ md := &mdnsWatcher{
id: uuid.New().String(), id: uuid.New().String(),
wo: wo, wo: wo,
ch: make(chan *mdns.ServiceEntry, 32), ch: make(chan *mdns.ServiceEntry, 32),
exit: make(chan struct{}), exit: make(chan struct{}),
domain: m.domain, domain: wo.Domain,
registry: m, registry: m,
} }

View File

@ -21,6 +21,8 @@ type RegisterOptions struct {
// Other options for implementations of the interface // Other options for implementations of the interface
// can be stored in a context // can be stored in a context
Context context.Context Context context.Context
// Domain to register the service in
Domain string
} }
type WatchOptions struct { type WatchOptions struct {
@ -30,18 +32,26 @@ type WatchOptions struct {
// Other options for implementations of the interface // Other options for implementations of the interface
// can be stored in a context // can be stored in a context
Context context.Context Context context.Context
// Domain to watch
Domain string
} }
type DeregisterOptions struct { type DeregisterOptions struct {
Context context.Context Context context.Context
// Domain the service was registered in
Domain string
} }
type GetOptions struct { type GetOptions struct {
Context context.Context Context context.Context
// Domain to scope the request to
Domain string
} }
type ListOptions struct { type ListOptions struct {
Context context.Context Context context.Context
// Domain to scope the request to
Domain string
} }
// Addrs is the registry addresses to use // Addrs is the registry addresses to use
@ -83,6 +93,12 @@ func RegisterContext(ctx context.Context) RegisterOption {
} }
} }
func RegisterDomain(d string) RegisterOption {
return func(o *RegisterOptions) {
o.Domain = d
}
}
// Watch a service // Watch a service
func WatchService(name string) WatchOption { func WatchService(name string) WatchOption {
return func(o *WatchOptions) { return func(o *WatchOptions) {
@ -96,20 +112,44 @@ func WatchContext(ctx context.Context) WatchOption {
} }
} }
func WatchDomain(d string) WatchOption {
return func(o *WatchOptions) {
o.Domain = d
}
}
func DeregisterContext(ctx context.Context) DeregisterOption { func DeregisterContext(ctx context.Context) DeregisterOption {
return func(o *DeregisterOptions) { return func(o *DeregisterOptions) {
o.Context = ctx o.Context = ctx
} }
} }
func DeregisterDomain(d string) DeregisterOption {
return func(o *DeregisterOptions) {
o.Domain = d
}
}
func GetContext(ctx context.Context) GetOption { func GetContext(ctx context.Context) GetOption {
return func(o *GetOptions) { return func(o *GetOptions) {
o.Context = ctx o.Context = ctx
} }
} }
func GetDomain(d string) GetOption {
return func(o *GetOptions) {
o.Domain = d
}
}
func ListContext(ctx context.Context) ListOption { func ListContext(ctx context.Context) ListOption {
return func(o *ListOptions) { return func(o *ListOptions) {
o.Context = ctx o.Context = ctx
} }
} }
func ListDomain(d string) ListOption {
return func(o *ListOptions) {
o.Domain = d
}
}

View File

@ -5,6 +5,11 @@ import (
"errors" "errors"
) )
const (
// WildcardDomain indicates any domain
WildcardDomain = "*"
)
var ( var (
DefaultRegistry = NewRegistry() DefaultRegistry = NewRegistry()