mirror of
				https://github.com/linka-cloud/grpc.git
				synced 2025-10-31 09:31:49 +00:00 
			
		
		
		
	add registry base interface, mdns, noop implementations, add resolver, client
This commit is contained in:
		| @@ -1,6 +1,11 @@ | |||||||
| # gRPC  | # gRPC  | ||||||
|  |  | ||||||
| A utility module, implementing some of the [go-micro](https://github.com/micro/go-micro) patterns with pure gRPC ecosystem modules. | A utility module, largely taken from the [go-micro](https://github.com/micro/go-micro) patterns (and a good amount of code too...)  | ||||||
|  | with pure gRPC ecosystem modules. | ||||||
|  |  | ||||||
|  | Principles: | ||||||
|  | - Pluggable | ||||||
|  | - No singleton | ||||||
|  |  | ||||||
| Features: | Features: | ||||||
| - [x] simple configuration with options | - [x] simple configuration with options | ||||||
| @@ -28,7 +33,7 @@ Features: | |||||||
|     - [ ] auth |     - [ ] auth | ||||||
|     - [ ] cors |     - [ ] cors | ||||||
|     - [ ] logging |     - [ ] logging | ||||||
|     - [ ] tracing |     - [ ] tracing | ||||||
|     - [ ] metrics |     - [ ] metrics | ||||||
| - [ ] broker, based on nats-streaming | - [ ] broker, based on nats-streaming | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								client/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								client/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | package client | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"google.golang.org/grpc" | ||||||
|  | 	"google.golang.org/grpc/credentials" | ||||||
|  | 	"google.golang.org/grpc/resolver" | ||||||
|  |  | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry/noop" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Client interface { | ||||||
|  | 	Dial(name string, version string, opts ...grpc.DialOption) (*grpc.ClientConn, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func New(opts ...Option) (Client, error) { | ||||||
|  | 	c := &client{opts: &options{}} | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(c.opts) | ||||||
|  | 	} | ||||||
|  | 	if c.opts.registry == nil { | ||||||
|  | 		c.opts.registry = noop.New() | ||||||
|  | 	} | ||||||
|  | 	resolver.Register(c.opts.registry.ResolverBuilder()) | ||||||
|  | 	c.pool = newPool(DefaultPoolSize, DefaultPoolTTL, DefaultPoolMaxIdle, DefaultPoolMaxStreams) | ||||||
|  | 	return c, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type client struct { | ||||||
|  | 	pool *pool | ||||||
|  | 	opts *options | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (c client) Dial(name, version string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { | ||||||
|  | 	if c.opts.tlsConfig == nil && c.opts.Secure() { | ||||||
|  | 		c.opts.tlsConfig = &tls.Config{InsecureSkipVerify: true} | ||||||
|  | 	} | ||||||
|  | 	if c.opts.tlsConfig != nil { | ||||||
|  | 		opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(c.opts.tlsConfig))) | ||||||
|  | 	} | ||||||
|  | 	addr := fmt.Sprintf("%s:///%s", c.opts.registry.String(), name) | ||||||
|  | 	if version != "" { | ||||||
|  | 		addr = addr + ":" + strings.TrimSpace(version) | ||||||
|  | 	} | ||||||
|  | 	pc, err := c.pool.getConn(addr, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 	  return nil, err | ||||||
|  | 	} | ||||||
|  | 	return pc.ClientConn, nil | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								client/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								client/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | package client | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/tls" | ||||||
|  |  | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Options interface { | ||||||
|  | 	Version() string | ||||||
|  | 	Registry() registry.Registry | ||||||
|  | 	TLSConfig() *tls.Config | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Option func(*options) | ||||||
|  |  | ||||||
|  | func WithRegistry(registry registry.Registry) Option { | ||||||
|  | 	return func(o *options) { | ||||||
|  | 		o.registry = registry | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func WithVersion(version string) Option { | ||||||
|  | 	return func(o *options) { | ||||||
|  | 		o.version = version | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func WithTLSConfig(conf *tls.Config) Option { | ||||||
|  | 	return func(o *options) { | ||||||
|  | 		o.tlsConfig = conf | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func WithSecure(s bool) Option { | ||||||
|  | 	return func(o *options) { | ||||||
|  | 		o.secure = s | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type options struct { | ||||||
|  | 	registry  registry.Registry | ||||||
|  | 	version   string | ||||||
|  | 	tlsConfig *tls.Config | ||||||
|  | 	secure    bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (o *options) Version() string { | ||||||
|  | 	return o.version | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (o *options) Registry() registry.Registry { | ||||||
|  | 	return o.registry | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (o *options) TLSConfig() *tls.Config { | ||||||
|  | 	return o.tlsConfig | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (o *options) Secure() bool { | ||||||
|  | 	return o.secure | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										244
									
								
								client/pool.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								client/pool.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,244 @@ | |||||||
|  | /* | ||||||
|  | Taken from the https://github.com/micro/go-micro/client/grpc | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package client | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"google.golang.org/grpc" | ||||||
|  | 	"google.golang.org/grpc/connectivity" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// DefaultPoolSize sets the connection pool size | ||||||
|  | 	DefaultPoolSize = 100 | ||||||
|  | 	// DefaultPoolTTL sets the connection pool ttl | ||||||
|  | 	DefaultPoolTTL = time.Minute | ||||||
|  | 	// DefaultPoolMaxStreams maximum streams on a connectioin | ||||||
|  | 	// (20) | ||||||
|  | 	DefaultPoolMaxStreams = 20 | ||||||
|  |  | ||||||
|  | 	// DefaultPoolMaxIdle maximum idle conns of a pool | ||||||
|  | 	// (50) | ||||||
|  | 	DefaultPoolMaxIdle = 50 | ||||||
|  |  | ||||||
|  | 	// DefaultMaxRecvMsgSize maximum message that client can receive | ||||||
|  | 	// (4 MB). | ||||||
|  | 	DefaultMaxRecvMsgSize = 1024 * 1024 * 4 | ||||||
|  |  | ||||||
|  | 	// DefaultMaxSendMsgSize maximum message that client can send | ||||||
|  | 	// (4 MB). | ||||||
|  | 	DefaultMaxSendMsgSize = 1024 * 1024 * 4 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type pool struct { | ||||||
|  | 	size int | ||||||
|  | 	ttl  int64 | ||||||
|  |  | ||||||
|  | 	//  max streams on a *poolConn | ||||||
|  | 	maxStreams int | ||||||
|  | 	//  max idle conns | ||||||
|  | 	maxIdle int | ||||||
|  |  | ||||||
|  | 	sync.Mutex | ||||||
|  | 	conns map[string]*streamsPool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type streamsPool struct { | ||||||
|  | 	//  head of list | ||||||
|  | 	head *poolConn | ||||||
|  | 	//  busy conns list | ||||||
|  | 	busy *poolConn | ||||||
|  | 	//  the siza of list | ||||||
|  | 	count int | ||||||
|  | 	//  idle conn | ||||||
|  | 	idle int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type poolConn struct { | ||||||
|  | 	//  grpc conn | ||||||
|  | 	*grpc.ClientConn | ||||||
|  | 	err  error | ||||||
|  | 	addr string | ||||||
|  |  | ||||||
|  | 	//  pool and streams pool | ||||||
|  | 	pool    *pool | ||||||
|  | 	sp      *streamsPool | ||||||
|  | 	streams int | ||||||
|  | 	created int64 | ||||||
|  |  | ||||||
|  | 	//  list | ||||||
|  | 	pre  *poolConn | ||||||
|  | 	next *poolConn | ||||||
|  | 	in   bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newPool(size int, ttl time.Duration, idle int, ms int) *pool { | ||||||
|  | 	if ms <= 0 { | ||||||
|  | 		ms = 1 | ||||||
|  | 	} | ||||||
|  | 	if idle < 0 { | ||||||
|  | 		idle = 0 | ||||||
|  | 	} | ||||||
|  | 	return &pool{ | ||||||
|  | 		size:       size, | ||||||
|  | 		ttl:        int64(ttl.Seconds()), | ||||||
|  | 		maxStreams: ms, | ||||||
|  | 		maxIdle:    idle, | ||||||
|  | 		conns:      make(map[string]*streamsPool), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *pool) getConn(addr string, opts ...grpc.DialOption) (*poolConn, error) { | ||||||
|  | 	now := time.Now().Unix() | ||||||
|  | 	p.Lock() | ||||||
|  | 	sp, ok := p.conns[addr] | ||||||
|  | 	if !ok { | ||||||
|  | 		sp = &streamsPool{head: &poolConn{}, busy: &poolConn{}, count: 0, idle: 0} | ||||||
|  | 		p.conns[addr] = sp | ||||||
|  | 	} | ||||||
|  | 	//  while we have conns check streams and then return one | ||||||
|  | 	//  otherwise we'll create a new conn | ||||||
|  | 	conn := sp.head.next | ||||||
|  | 	for conn != nil { | ||||||
|  | 		//  check conn state | ||||||
|  | 		// https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md | ||||||
|  | 		switch conn.GetState() { | ||||||
|  | 		case connectivity.Connecting: | ||||||
|  | 			conn = conn.next | ||||||
|  | 			continue | ||||||
|  | 		case connectivity.Shutdown: | ||||||
|  | 			next := conn.next | ||||||
|  | 			if conn.streams == 0 { | ||||||
|  | 				removeConn(conn) | ||||||
|  | 				sp.idle-- | ||||||
|  | 			} | ||||||
|  | 			conn = next | ||||||
|  | 			continue | ||||||
|  | 		case connectivity.TransientFailure: | ||||||
|  | 			next := conn.next | ||||||
|  | 			if conn.streams == 0 { | ||||||
|  | 				removeConn(conn) | ||||||
|  | 				conn.ClientConn.Close() | ||||||
|  | 				sp.idle-- | ||||||
|  | 			} | ||||||
|  | 			conn = next | ||||||
|  | 			continue | ||||||
|  | 		case connectivity.Ready: | ||||||
|  | 		case connectivity.Idle: | ||||||
|  | 		} | ||||||
|  | 		//  a old conn | ||||||
|  | 		if now-conn.created > p.ttl { | ||||||
|  | 			next := conn.next | ||||||
|  | 			if conn.streams == 0 { | ||||||
|  | 				removeConn(conn) | ||||||
|  | 				conn.ClientConn.Close() | ||||||
|  | 				sp.idle-- | ||||||
|  | 			} | ||||||
|  | 			conn = next | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		//  a busy conn | ||||||
|  | 		if conn.streams >= p.maxStreams { | ||||||
|  | 			next := conn.next | ||||||
|  | 			removeConn(conn) | ||||||
|  | 			addConnAfter(conn, sp.busy) | ||||||
|  | 			conn = next | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		//  a idle conn | ||||||
|  | 		if conn.streams == 0 { | ||||||
|  | 			sp.idle-- | ||||||
|  | 		} | ||||||
|  | 		//  a good conn | ||||||
|  | 		conn.streams++ | ||||||
|  | 		p.Unlock() | ||||||
|  | 		return conn, nil | ||||||
|  | 	} | ||||||
|  | 	p.Unlock() | ||||||
|  |  | ||||||
|  | 	//  create new conn | ||||||
|  | 	cc, err := grpc.Dial(addr, opts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	conn = &poolConn{cc, nil, addr, p, sp, 1, time.Now().Unix(), nil, nil, false} | ||||||
|  |  | ||||||
|  | 	//  add conn to streams pool | ||||||
|  | 	p.Lock() | ||||||
|  | 	if sp.count < p.size { | ||||||
|  | 		addConnAfter(conn, sp.head) | ||||||
|  | 	} | ||||||
|  | 	p.Unlock() | ||||||
|  |  | ||||||
|  | 	return conn, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *pool) release(addr string, conn *poolConn, err error) { | ||||||
|  | 	p.Lock() | ||||||
|  | 	p, sp, created := conn.pool, conn.sp, conn.created | ||||||
|  | 	//  try to add conn | ||||||
|  | 	if !conn.in && sp.count < p.size { | ||||||
|  | 		addConnAfter(conn, sp.head) | ||||||
|  | 	} | ||||||
|  | 	if !conn.in { | ||||||
|  | 		p.Unlock() | ||||||
|  | 		conn.ClientConn.Close() | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	//  a busy conn | ||||||
|  | 	if conn.streams >= p.maxStreams { | ||||||
|  | 		removeConn(conn) | ||||||
|  | 		addConnAfter(conn, sp.head) | ||||||
|  | 	} | ||||||
|  | 	conn.streams-- | ||||||
|  | 	//  if streams == 0, we can do something | ||||||
|  | 	if conn.streams == 0 { | ||||||
|  | 		//  1. it has errored | ||||||
|  | 		//  2. too many idle conn or | ||||||
|  | 		//  3. conn is too old | ||||||
|  | 		now := time.Now().Unix() | ||||||
|  | 		if err != nil || sp.idle >= p.maxIdle || now-created > p.ttl { | ||||||
|  | 			removeConn(conn) | ||||||
|  | 			p.Unlock() | ||||||
|  | 			conn.ClientConn.Close() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		sp.idle++ | ||||||
|  | 	} | ||||||
|  | 	p.Unlock() | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (conn *poolConn) Close() { | ||||||
|  | 	conn.pool.release(conn.addr, conn, conn.err) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func removeConn(conn *poolConn) { | ||||||
|  | 	if conn.pre != nil { | ||||||
|  | 		conn.pre.next = conn.next | ||||||
|  | 	} | ||||||
|  | 	if conn.next != nil { | ||||||
|  | 		conn.next.pre = conn.pre | ||||||
|  | 	} | ||||||
|  | 	conn.pre = nil | ||||||
|  | 	conn.next = nil | ||||||
|  | 	conn.in = false | ||||||
|  | 	conn.sp.count-- | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func addConnAfter(conn *poolConn, after *poolConn) { | ||||||
|  | 	conn.next = after.next | ||||||
|  | 	conn.pre = after | ||||||
|  | 	if after.next != nil { | ||||||
|  | 		after.next.pre = conn | ||||||
|  | 	} | ||||||
|  | 	after.next = conn | ||||||
|  | 	conn.in = true | ||||||
|  | 	conn.sp.count++ | ||||||
|  | 	return | ||||||
|  | } | ||||||
| @@ -3,8 +3,11 @@ package main | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  |  | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/client" | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry/mdns" | ||||||
| 	"gitlab.bertha.cloud/partitio/lab/grpc/service" | 	"gitlab.bertha.cloud/partitio/lab/grpc/service" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -28,21 +31,29 @@ func (g *GreeterHandler) SayHelloStream(req *HelloStreamRequest, s Greeter_SayHe | |||||||
| } | } | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
| 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | 	name := "greeter" | ||||||
|  | 	ctx, cancel := context.WithCancel(context.Background()) | ||||||
|  | 	done := make(chan struct{}) | ||||||
|  | 	ready := make(chan struct{}) | ||||||
| 	defer cancel() | 	defer cancel() | ||||||
| 	var svc service.Service | 	var svc service.Service | ||||||
| 	var err error | 	var err error | ||||||
| 	svc, err = service.New( | 	svc, err = service.New( | ||||||
| 		service.WithContext(ctx), | 		service.WithContext(ctx), | ||||||
| 		service.WithName("Greeting"), | 		service.WithName(name), | ||||||
|  | 		service.WithVersion("v0.0.1"), | ||||||
|  | 		service.WithAddress("0.0.0.0:9991"), | ||||||
|  | 		service.WithRegistry(mdns.NewRegistry()), | ||||||
| 		service.WithReflection(true), | 		service.WithReflection(true), | ||||||
| 		service.WithSecure(true), | 		service.WithSecure(true), | ||||||
| 		service.WithAfterStart(func() error { | 		service.WithAfterStart(func() error { | ||||||
| 			fmt.Println("Server listening on", svc.Options().Address()) | 			fmt.Println("Server listening on", svc.Options().Address()) | ||||||
|  | 			close(ready) | ||||||
| 			return nil | 			return nil | ||||||
| 		}), | 		}), | ||||||
| 		service.WithAfterStop(func() error { | 		service.WithAfterStop(func() error { | ||||||
| 			fmt.Println("Stopping server") | 			fmt.Println("Stopping server") | ||||||
|  | 			close(done) | ||||||
| 			return nil | 			return nil | ||||||
| 		}), | 		}), | ||||||
| 	) | 	) | ||||||
| @@ -50,7 +61,30 @@ func main() { | |||||||
| 		panic(err) | 		panic(err) | ||||||
| 	} | 	} | ||||||
| 	RegisterGreeterServer(svc.Server(), &GreeterHandler{}) | 	RegisterGreeterServer(svc.Server(), &GreeterHandler{}) | ||||||
| 	if err := svc.Start(); err != nil { | 	go func() { | ||||||
| 		panic(err) | 		if err := svc.Start(); err != nil { | ||||||
|  | 			panic(err) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 	<-ready | ||||||
|  | 	s, err := client.New( | ||||||
|  | 		client.WithRegistry(mdns.NewRegistry()), | ||||||
|  | 		client.WithSecure(true), | ||||||
|  | 	) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | 	conn, err := s.Dial("greeter","v0.0.1") | ||||||
|  | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	g := NewGreeterClient(conn) | ||||||
|  | 	defer cancel() | ||||||
|  | 	res, err := g.SayHello(context.Background(), &HelloRequest{Name: "test"}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	logrus.Infof("received message: %s", res.Message) | ||||||
|  | 	cancel() | ||||||
|  | 	<-done | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,13 +3,16 @@ module gitlab.bertha.cloud/partitio/lab/grpc | |||||||
| go 1.13 | go 1.13 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/golang/protobuf v1.3.2 | 	github.com/golang/protobuf v1.4.0 | ||||||
|  | 	github.com/google/uuid v1.1.2 | ||||||
| 	github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 | 	github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 | ||||||
| 	github.com/jinzhu/gorm v1.9.12 | 	github.com/jinzhu/gorm v1.9.12 | ||||||
| 	github.com/nats-io/stan.go v0.6.0 // indirect | 	github.com/miekg/dns v1.1.35 | ||||||
|  | 	github.com/sirupsen/logrus v1.4.2 | ||||||
| 	github.com/spf13/cobra v0.0.5 | 	github.com/spf13/cobra v0.0.5 | ||||||
| 	github.com/spf13/pflag v1.0.3 |  | ||||||
| 	github.com/spf13/viper v1.6.2 | 	github.com/spf13/viper v1.6.2 | ||||||
| 	go.uber.org/multierr v1.1.0 | 	github.com/stretchr/testify v1.4.0 | ||||||
|  | 	go.uber.org/multierr v1.3.0 | ||||||
|  | 	golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 | ||||||
| 	google.golang.org/grpc v1.26.0 | 	google.golang.org/grpc v1.26.0 | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										465
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										465
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,118 +1,377 @@ | |||||||
| cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||||
|  | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||||
|  | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= | ||||||
|  | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= | ||||||
|  | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= | ||||||
|  | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= | ||||||
|  | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= | ||||||
|  | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= | ||||||
|  | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= | ||||||
|  | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= | ||||||
|  | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= | ||||||
|  | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= | ||||||
|  | contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= | ||||||
|  | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= | ||||||
|  | github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= | ||||||
|  | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= | ||||||
|  | github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= | ||||||
|  | github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw= | ||||||
|  | github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= | ||||||
|  | github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= | ||||||
|  | github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM= | ||||||
|  | github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U= | ||||||
|  | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= | ||||||
|  | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= | ||||||
|  | github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= | ||||||
|  | github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= | ||||||
|  | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= | ||||||
|  | github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= | ||||||
|  | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= | ||||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||||
|  | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= | ||||||
|  | github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= | ||||||
|  | github.com/Microsoft/hcsshim v0.8.7-0.20191101173118-65519b62243c/go.mod h1:7xhjOwRV2+0HXGmM0jxaEu+ZiXJFoVZOTfL/dmqbrD8= | ||||||
| github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= | ||||||
|  | github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= | ||||||
|  | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= | ||||||
|  | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= | ||||||
|  | github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.0/go.mod h1:zpDJeKyp9ScW4NNrbdr+Eyxvry3ilGPewKoXw3XGN1k= | ||||||
|  | github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE= | ||||||
|  | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= | ||||||
| github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | ||||||
| github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= | ||||||
|  | github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190808125512-07798873deee/go.mod h1:myCDvQSzCW+wB1WAlocEru4wMGJxy+vlxHdhegi1CDQ= | ||||||
|  | github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= | ||||||
|  | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= | ||||||
|  | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= | ||||||
| github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= | ||||||
|  | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= | ||||||
|  | github.com/aws/aws-sdk-go v1.23.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= | ||||||
|  | github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= | ||||||
| github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= | ||||||
| github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||||
|  | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= | ||||||
|  | github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= | ||||||
|  | github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= | ||||||
|  | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= | ||||||
|  | github.com/bwmarrin/discordgo v0.20.2/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= | ||||||
|  | github.com/caddyserver/certmagic v0.10.6/go.mod h1:Y8jcUBctgk/IhpAzlHKfimZNyXCkfGgRTC0orl8gROQ= | ||||||
|  | github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= | ||||||
|  | github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||||
| github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= | ||||||
|  | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= | ||||||
| github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | ||||||
|  | github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY= | ||||||
|  | github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= | ||||||
|  | github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= | ||||||
|  | github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= | ||||||
|  | github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= | ||||||
|  | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= | ||||||
|  | github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= | ||||||
|  | github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= | ||||||
|  | github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= | ||||||
|  | github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= | ||||||
| github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | ||||||
|  | github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | ||||||
| github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= | ||||||
|  | github.com/coreos/etcd v3.3.18+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= | ||||||
| github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= | ||||||
| github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= | ||||||
|  | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= | ||||||
| github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= | ||||||
|  | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= | ||||||
| github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= | ||||||
|  | github.com/cpu/goacmedns v0.0.1/go.mod h1:sesf/pNnCYwUevQEQfEwY0Y3DydlQWSGZbaMElOWxok= | ||||||
| github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= | ||||||
|  | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= | ||||||
|  | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= | ||||||
|  | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
|  | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
|  | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= | ||||||
| github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | ||||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | ||||||
| github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= | ||||||
|  | github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= | ||||||
|  | github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= | ||||||
|  | github.com/dnsimple/dnsimple-go v0.30.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg= | ||||||
|  | github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= | ||||||
|  | github.com/docker/docker v1.4.2-0.20191101170500-ac7306503d23/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= | ||||||
|  | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= | ||||||
|  | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= | ||||||
|  | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= | ||||||
|  | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= | ||||||
|  | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= | ||||||
|  | github.com/ef-ds/deque v1.0.4-0.20190904040645-54cb57c252a1/go.mod h1:HvODWzv6Y6kBf3Ah2WzN1bHjDUezGLaAhwuWVwfpEJs= | ||||||
|  | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= | ||||||
| github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | ||||||
| github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||||
|  | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= | ||||||
| github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= | ||||||
|  | github.com/evanphx/json-patch/v5 v5.0.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= | ||||||
|  | github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE= | ||||||
|  | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= | ||||||
|  | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= | ||||||
|  | github.com/forestgiant/sliceutil v0.0.0-20160425183142-94783f95db6c/go.mod h1:pFdJbAhRf7rh6YYMUdIQGyzne6zYL1tCUW8QV2B3UfY= | ||||||
| github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= | ||||||
| github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||||||
|  | github.com/fsouza/go-dockerclient v1.6.0/go.mod h1:YWwtNPuL4XTX1SKJQk86cWPmmqwx+4np9qfPbb+znGc= | ||||||
| github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= | ||||||
|  | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= | ||||||
|  | github.com/go-acme/lego/v3 v3.4.0/go.mod h1:xYbLDuxq3Hy4bMUT1t9JIuz6GWIWb3m5X+TeTHYaT7M= | ||||||
|  | github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= | ||||||
|  | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= | ||||||
|  | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= | ||||||
|  | github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= | ||||||
|  | github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= | ||||||
|  | github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM= | ||||||
|  | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= | ||||||
|  | github.com/go-ini/ini v1.44.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= | ||||||
| github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||||||
| github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= | ||||||
| github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | ||||||
|  | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= | ||||||
| github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | ||||||
| github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= | ||||||
|  | github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= | ||||||
|  | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= | ||||||
|  | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= | ||||||
|  | github.com/gobwas/ws v1.0.3/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= | ||||||
|  | github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= | ||||||
|  | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= | ||||||
| github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||||
|  | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||||
| github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= | github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= | ||||||
| github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= | ||||||
|  | github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= | ||||||
|  | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= | ||||||
| github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= | ||||||
|  | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= | ||||||
| github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | ||||||
| github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | ||||||
|  | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | ||||||
| github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= | ||||||
|  | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= | ||||||
|  | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= | ||||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
|  | github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= | ||||||
| github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
| github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= | ||||||
| github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
|  | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= | ||||||
|  | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= | ||||||
|  | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= | ||||||
|  | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= | ||||||
|  | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= | ||||||
|  | github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= | ||||||
|  | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= | ||||||
|  | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= | ||||||
|  | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= | ||||||
| github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= | ||||||
|  | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= | ||||||
| github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= | ||||||
|  | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||||
|  | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||||
|  | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= | ||||||
|  | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||||
|  | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= | ||||||
|  | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
|  | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= | ||||||
|  | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= | ||||||
|  | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= | ||||||
|  | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= | ||||||
|  | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= | ||||||
|  | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= | ||||||
|  | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= | ||||||
|  | github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= | ||||||
|  | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= | ||||||
| github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | ||||||
|  | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | ||||||
|  | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= | ||||||
|  | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= | ||||||
|  | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= | ||||||
|  | github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= | ||||||
| github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= | ||||||
|  | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
| github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= | ||||||
| github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 h1:THDBEeQ9xZ8JEaCLyLQqXMMdRqNr0QAUJTIkQAUtFjg= | github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 h1:THDBEeQ9xZ8JEaCLyLQqXMMdRqNr0QAUJTIkQAUtFjg= | ||||||
| github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= | github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE= | ||||||
| github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= | ||||||
|  | github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= | ||||||
| github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= | ||||||
|  | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= | ||||||
|  | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= | ||||||
|  | github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||||
|  | github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= | ||||||
|  | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | ||||||
|  | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | ||||||
|  | github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= | ||||||
| github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | ||||||
| github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||||||
|  | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||||
|  | github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= | ||||||
|  | github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= | ||||||
| github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= | ||||||
| github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= | ||||||
|  | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= | ||||||
|  | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= | ||||||
| github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= | github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= | ||||||
| github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= | github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= | ||||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||||
|  | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= | ||||||
| github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||||
|  | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= | ||||||
| github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= | ||||||
|  | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||||
|  | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||||
|  | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||||
|  | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||||
|  | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | ||||||
|  | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | ||||||
| github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||||||
| github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= | ||||||
|  | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= | ||||||
| github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= | ||||||
| github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | ||||||
|  | github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= | ||||||
|  | github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= | ||||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||||||
|  | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= | ||||||
| github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= | ||||||
|  | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||||
|  | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||||
|  | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||||
|  | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||||
|  | github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= | ||||||
|  | github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= | ||||||
|  | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= | ||||||
| github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||||
|  | github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= | ||||||
|  | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||||
|  | github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA= | ||||||
|  | github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ= | ||||||
|  | github.com/lucas-clemente/quic-go v0.14.1/go.mod h1:Vn3/Fb0/77b02SGhQk36KzOUmXgVpFfizUfW5WMaqyU= | ||||||
| github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= | ||||||
| github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= | ||||||
| github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= | ||||||
|  | github.com/marten-seemann/chacha20 v0.2.0/go.mod h1:HSdjFau7GzYRj+ahFNwsO3ouVJr1HFkWoEwNDb4TMtE= | ||||||
|  | github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI= | ||||||
|  | github.com/marten-seemann/qtls v0.4.1/go.mod h1:pxVXcHHw1pNIt8Qo0pwSYQEoZ8yYOOPXTCZLQQunvRc= | ||||||
|  | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= | ||||||
|  | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= | ||||||
|  | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= | ||||||
|  | github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw= | ||||||
| github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||||
|  | github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= | ||||||
| github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | ||||||
|  | github.com/micro/cli/v2 v2.1.2/go.mod h1:EguNh6DAoWKm9nmk+k/Rg0H3lQnDxqzu5x5srOtGtYg= | ||||||
|  | github.com/micro/go-micro/v2 v2.9.1 h1:+S9koIrNWARjpP6k2TZ7kt0uC9zUJtNXzIdZTZRms7Q= | ||||||
|  | github.com/micro/go-micro/v2 v2.9.1/go.mod h1:x55ZM3Puy0FyvvkR3e0ha0xsE9DFwfPSUMWAIbFY0SY= | ||||||
|  | github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= | ||||||
|  | github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= | ||||||
|  | github.com/miekg/dns v1.1.35 h1:oTfOaDH+mZkdcgdIjH6yBajRGtIwcwcaR+rt23ZSrJs= | ||||||
|  | github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= | ||||||
| github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= | ||||||
|  | github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0= | ||||||
|  | github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= | ||||||
| github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= | ||||||
| github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= | ||||||
|  | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
|  | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
|  | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||||
|  | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||||
|  | github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= | ||||||
| github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | ||||||
| github.com/nats-io/jwt v0.3.0 h1:xdnzwFETV++jNc4W1mw//qFyJGb2ABOombmZJQS4+Qo= | github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= | ||||||
| github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= | github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= | ||||||
| github.com/nats-io/nats.go v1.9.1 h1:ik3HbLhZ0YABLto7iX80pZLPw/6dx3T+++MZJwLnMrQ= | github.com/nats-io/nats-server/v2 v2.1.6/go.mod h1:BL1NOtaBQ5/y97djERRVWNouMW7GT3gxnmbE/eC8u8A= | ||||||
| github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= | github.com/nats-io/nats.go v1.9.2/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE= | ||||||
| github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4= | github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= | ||||||
| github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= | github.com/nats-io/nkeys v0.1.4/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= | ||||||
| github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= | ||||||
| github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= | ||||||
| github.com/nats-io/stan.go v0.6.0 h1:26IJPeykh88d8KVLT4jJCIxCyUBOC5/IQup8oWD/QYY= | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= | ||||||
| github.com/nats-io/stan.go v0.6.0/go.mod h1:eIcD5bi3pqbHT/xIIvXMwvzXYElgouBvaVRftaE+eac= | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= | ||||||
|  | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= | ||||||
|  | github.com/nlopes/slack v0.6.1-0.20191106133607-d06c2a2b3249/go.mod h1:JzQ9m3PMAqcpeCam7UaHSuBuupz7CmpjehYMayT6YOk= | ||||||
|  | github.com/nrdcg/auroradns v1.0.0/go.mod h1:6JPXKzIRzZzMqtTDgueIhTi6rFf1QvYE/HzqidhOhjw= | ||||||
|  | github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= | ||||||
|  | github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ= | ||||||
|  | github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= | ||||||
| github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= | ||||||
|  | github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= | ||||||
|  | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||||
|  | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||||
|  | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||||
|  | github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= | ||||||
|  | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= | ||||||
|  | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= | ||||||
|  | github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= | ||||||
|  | github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= | ||||||
|  | github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= | ||||||
|  | github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= | ||||||
| github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= | ||||||
|  | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= | ||||||
|  | github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= | ||||||
|  | github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ= | ||||||
|  | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= | ||||||
|  | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= | ||||||
| github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= | ||||||
| github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= | ||||||
|  | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= | ||||||
| github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
| github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
|  | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||||
|  | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||||
| github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | ||||||
|  | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= | ||||||
| github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= | ||||||
|  | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= | ||||||
|  | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= | ||||||
| github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | ||||||
|  | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | ||||||
| github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
| github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
| github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= | ||||||
|  | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | ||||||
| github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | ||||||
|  | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | ||||||
|  | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= | ||||||
| github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | ||||||
|  | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | ||||||
| github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | ||||||
|  | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | ||||||
|  | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= | ||||||
|  | github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= | ||||||
| github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= | ||||||
|  | github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= | ||||||
|  | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= | ||||||
| github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= | ||||||
|  | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||||
| github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= | ||||||
|  | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||||
|  | github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ= | ||||||
|  | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= | ||||||
|  | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= | ||||||
|  | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= | ||||||
| github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= | ||||||
|  | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= | ||||||
|  | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= | ||||||
| github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= | ||||||
|  | github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= | ||||||
|  | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= | ||||||
| github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | ||||||
|  | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= | ||||||
|  | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= | ||||||
| github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= | ||||||
| github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= | ||||||
| github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= | ||||||
| @@ -132,85 +391,275 @@ github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfD | |||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
| github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||||
|  | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||||
|  | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||||
| github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= | ||||||
| github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= | ||||||
|  | github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= | ||||||
|  | github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= | ||||||
|  | github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY= | ||||||
| github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= | ||||||
|  | github.com/tmc/grpc-websocket-proxy v0.0.0-20200122045848-3419fae592fc/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= | ||||||
|  | github.com/transip/gotransip v0.0.0-20190812104329-6d8d9179b66f/go.mod h1:i0f4R4o2HM0m3DZYQWsj6/MEowD57VzoH0v3d7igeFY= | ||||||
|  | github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= | ||||||
| github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= | ||||||
| github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= | ||||||
|  | github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= | ||||||
|  | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= | ||||||
|  | github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA= | ||||||
|  | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= | ||||||
|  | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= | ||||||
|  | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= | ||||||
|  | github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= | ||||||
|  | github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= | ||||||
| github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= | ||||||
| github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= | ||||||
| go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= | ||||||
|  | go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= | ||||||
|  | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= | ||||||
|  | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= | ||||||
|  | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= | ||||||
|  | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= | ||||||
|  | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= | ||||||
| go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= | go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= | ||||||
| go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= | ||||||
|  | go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= | ||||||
|  | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= | ||||||
| go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= | ||||||
| go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= | ||||||
|  | go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= | ||||||
|  | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= | ||||||
|  | go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= | ||||||
|  | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= | ||||||
|  | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= | ||||||
| go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= | ||||||
|  | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= | ||||||
|  | golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
| golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
|  | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
| golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
|  | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
|  | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
|  | golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= | ||||||
|  | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
|  | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
|  | golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
|  | golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
|  | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= | ||||||
| golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
|  | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
|  | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
|  | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= | ||||||
|  | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||||
|  | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||||
|  | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= | ||||||
|  | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= | ||||||
|  | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= | ||||||
|  | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= | ||||||
|  | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= | ||||||
| golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||||
| golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= | ||||||
|  | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||||
| golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||||
|  | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||||
|  | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||||
|  | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||||
|  | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE= | ||||||
|  | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= | ||||||
|  | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= | ||||||
|  | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= | ||||||
|  | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= | ||||||
|  | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= | ||||||
|  | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= | ||||||
|  | golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
|  | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
|  | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
|  | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
|  | golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||||
| golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
|  | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
|  | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||||
| golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= | ||||||
| golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= | ||||||
|  | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= | ||||||
|  | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE= | ||||||
|  | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= | ||||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||||
|  | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
|  | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= | ||||||
|  | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= | ||||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
|  | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= | ||||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= | ||||||
| golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= | ||||||
|  | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= | ||||||
|  | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
|  | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
|  | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= | ||||||
|  | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||||
|  | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||||
| golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||||
|  | golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||||
|  | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||||
| golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|  | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|  | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
| golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
| golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= | ||||||
| golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||||
|  | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||||
|  | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||||
| golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||||
|  | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= | ||||||
|  | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= | ||||||
| golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= | ||||||
|  | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= | ||||||
|  | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= | ||||||
|  | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= | ||||||
|  | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= | ||||||
|  | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361 h1:RIIXAeV6GvDBuADKumTODatUqANFZ+5BPMnzsy4hulY= | ||||||
|  | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= | ||||||
|  | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||||
|  | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
|  | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= | ||||||
|  | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= | ||||||
|  | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= | ||||||
|  | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= | ||||||
|  | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= | ||||||
|  | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= | ||||||
| google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | ||||||
|  | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= | ||||||
| google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||||
|  | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||||
|  | google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= | ||||||
|  | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= | ||||||
| google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||||
|  | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||||
|  | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= | ||||||
|  | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= | ||||||
|  | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= | ||||||
|  | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= | ||||||
|  | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | ||||||
| google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= | ||||||
| google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | ||||||
|  | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= | ||||||
|  | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= | ||||||
|  | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= | ||||||
|  | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= | ||||||
| google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | ||||||
|  | google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | ||||||
|  | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= | ||||||
| google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= | ||||||
|  | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= | ||||||
|  | google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | ||||||
| google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | ||||||
| google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= | google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= | ||||||
| google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= | ||||||
|  | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= | ||||||
|  | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= | ||||||
|  | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= | ||||||
|  | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= | ||||||
|  | google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= | ||||||
|  | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= | ||||||
|  | google.golang.org/protobuf v1.22.0 h1:cJv5/xdbk1NnMPR1VP9+HU6gupuG9MLBoH1r6RHZ2MY= | ||||||
|  | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||||
| gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
|  | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= | ||||||
| gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
|  | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
|  | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= | ||||||
|  | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
|  | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= | ||||||
|  | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||||
|  | gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= | ||||||
|  | gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
|  | gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
| gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= | ||||||
| gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
|  | gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= | ||||||
|  | gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc= | ||||||
| gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= | ||||||
|  | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= | ||||||
|  | gopkg.in/telegram-bot-api.v4 v4.6.4/go.mod h1:5DpGO5dbumb40px+dXcwCpcjmeHNYLpk0bp3XRNvWDM= | ||||||
|  | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||||
|  | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= | ||||||
| gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= | ||||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
| gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= | ||||||
| gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||||
|  | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= | ||||||
|  | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||||
| honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||||
|  | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||||
|  | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||||
| honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||||
|  | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= | ||||||
|  | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= | ||||||
|  | k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= | ||||||
|  | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= | ||||||
|  | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								registry/mdns/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								registry/mdns/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | # Compiled Object files, Static and Dynamic libs (Shared Objects) | ||||||
|  | *.o | ||||||
|  | *.a | ||||||
|  | *.so | ||||||
|  |  | ||||||
|  | # Folders | ||||||
|  | _obj | ||||||
|  | _test | ||||||
|  |  | ||||||
|  | # Architecture specific extensions/prefixes | ||||||
|  | *.[568vq] | ||||||
|  | [568vq].out | ||||||
|  |  | ||||||
|  | *.cgo1.go | ||||||
|  | *.cgo2.c | ||||||
|  | _cgo_defun.c | ||||||
|  | _cgo_gotypes.go | ||||||
|  | _cgo_export.* | ||||||
|  |  | ||||||
|  | _testmain.go | ||||||
|  |  | ||||||
|  | *.exe | ||||||
|  | *.test | ||||||
							
								
								
									
										508
									
								
								registry/mdns/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										508
									
								
								registry/mdns/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,508 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log" | ||||||
|  | 	"net" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | 	"golang.org/x/net/ipv4" | ||||||
|  | 	"golang.org/x/net/ipv6" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ServiceEntry is returned after we query for a service | ||||||
|  | type ServiceEntry struct { | ||||||
|  | 	Name       string | ||||||
|  | 	Host       string | ||||||
|  | 	AddrV4     net.IP | ||||||
|  | 	AddrV6     net.IP | ||||||
|  | 	Port       int | ||||||
|  | 	Info       string | ||||||
|  | 	InfoFields []string | ||||||
|  | 	TTL        int | ||||||
|  | 	Type       uint16 | ||||||
|  |  | ||||||
|  | 	Addr net.IP // @Deprecated | ||||||
|  |  | ||||||
|  | 	hasTXT bool | ||||||
|  | 	sent   bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // complete is used to check if we have all the info we need | ||||||
|  | func (s *ServiceEntry) complete() bool { | ||||||
|  |  | ||||||
|  | 	return (len(s.AddrV4) > 0 || len(s.AddrV6) > 0 || len(s.Addr) > 0) && s.Port != 0 && s.hasTXT | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // QueryParam is used to customize how a Lookup is performed | ||||||
|  | type QueryParam struct { | ||||||
|  | 	Service             string               // Service to lookup | ||||||
|  | 	Domain              string               // Lookup domain, default "local" | ||||||
|  | 	Type                uint16               // Lookup type, defaults to dns.TypePTR | ||||||
|  | 	Context             context.Context      // Context | ||||||
|  | 	Timeout             time.Duration        // Lookup timeout, default 1 second. Ignored if Context is provided | ||||||
|  | 	Interface           *net.Interface       // Multicast interface to use | ||||||
|  | 	Entries             chan<- *ServiceEntry // Entries Channel | ||||||
|  | 	WantUnicastResponse bool                 // Unicast response desired, as per 5.4 in RFC | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DefaultParams is used to return a default set of QueryParam's | ||||||
|  | func DefaultParams(service string) *QueryParam { | ||||||
|  | 	return &QueryParam{ | ||||||
|  | 		Service:             service, | ||||||
|  | 		Domain:              "local", | ||||||
|  | 		Timeout:             time.Second, | ||||||
|  | 		Entries:             make(chan *ServiceEntry), | ||||||
|  | 		WantUnicastResponse: false, // TODO(reddaly): Change this default. | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Query looks up a given service, in a domain, waiting at most | ||||||
|  | // for a timeout before finishing the query. The results are streamed | ||||||
|  | // to a channel. Sends will not block, so clients should make sure to | ||||||
|  | // either read or buffer. | ||||||
|  | func Query(params *QueryParam) error { | ||||||
|  | 	// Create a new client | ||||||
|  | 	client, err := newClient() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer client.Close() | ||||||
|  |  | ||||||
|  | 	// Set the multicast interface | ||||||
|  | 	if params.Interface != nil { | ||||||
|  | 		if err := client.setInterface(params.Interface, false); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Ensure defaults are set | ||||||
|  | 	if params.Domain == "" { | ||||||
|  | 		params.Domain = "local" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if params.Context == nil { | ||||||
|  | 		if params.Timeout == 0 { | ||||||
|  | 			params.Timeout = time.Second | ||||||
|  | 		} | ||||||
|  | 		params.Context, _ = context.WithTimeout(context.Background(), params.Timeout) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Run the query | ||||||
|  | 	return client.query(params) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Listen listens indefinitely for multicast updates | ||||||
|  | func Listen(entries chan<- *ServiceEntry, exit chan struct{}) error { | ||||||
|  | 	// Create a new client | ||||||
|  | 	client, err := newClient() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer client.Close() | ||||||
|  |  | ||||||
|  | 	client.setInterface(nil, true) | ||||||
|  |  | ||||||
|  | 	// Start listening for response packets | ||||||
|  | 	msgCh := make(chan *dns.Msg, 32) | ||||||
|  |  | ||||||
|  | 	go client.recv(client.ipv4UnicastConn, msgCh) | ||||||
|  | 	go client.recv(client.ipv6UnicastConn, msgCh) | ||||||
|  | 	go client.recv(client.ipv4MulticastConn, msgCh) | ||||||
|  | 	go client.recv(client.ipv6MulticastConn, msgCh) | ||||||
|  |  | ||||||
|  | 	ip := make(map[string]*ServiceEntry) | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case <-exit: | ||||||
|  | 			return nil | ||||||
|  | 		case <-client.closedCh: | ||||||
|  | 			return nil | ||||||
|  | 		case m := <-msgCh: | ||||||
|  | 			e := messageToEntry(m, ip) | ||||||
|  | 			if e == nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Check if this entry is complete | ||||||
|  | 			if e.complete() { | ||||||
|  | 				if e.sent { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				e.sent = true | ||||||
|  | 				entries <- e | ||||||
|  | 				ip = make(map[string]*ServiceEntry) | ||||||
|  | 			} else { | ||||||
|  | 				// Fire off a node specific query | ||||||
|  | 				m := new(dns.Msg) | ||||||
|  | 				m.SetQuestion(e.Name, dns.TypePTR) | ||||||
|  | 				m.RecursionDesired = false | ||||||
|  | 				if err := client.sendQuery(m); err != nil { | ||||||
|  | 					log.Printf("[ERR] mdns: Failed to query instance %s: %v", e.Name, err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Lookup is the same as Query, however it uses all the default parameters | ||||||
|  | func Lookup(service string, entries chan<- *ServiceEntry) error { | ||||||
|  | 	params := DefaultParams(service) | ||||||
|  | 	params.Entries = entries | ||||||
|  | 	return Query(params) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Client provides a query interface that can be used to | ||||||
|  | // search for service providers using mDNS | ||||||
|  | type client struct { | ||||||
|  | 	ipv4UnicastConn *net.UDPConn | ||||||
|  | 	ipv6UnicastConn *net.UDPConn | ||||||
|  |  | ||||||
|  | 	ipv4MulticastConn *net.UDPConn | ||||||
|  | 	ipv6MulticastConn *net.UDPConn | ||||||
|  |  | ||||||
|  | 	closed    bool | ||||||
|  | 	closedCh  chan struct{} // TODO(reddaly): This doesn't appear to be used. | ||||||
|  | 	closeLock sync.Mutex | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewClient creates a new mdns Client that can be used to query | ||||||
|  | // for records | ||||||
|  | func newClient() (*client, error) { | ||||||
|  | 	// TODO(reddaly): At least attempt to bind to the port required in the spec. | ||||||
|  | 	// Create a IPv4 listener | ||||||
|  | 	uconn4, err4 := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) | ||||||
|  | 	uconn6, err6 := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0}) | ||||||
|  | 	if err4 != nil && err6 != nil { | ||||||
|  | 		log.Printf("[ERR] mdns: Failed to bind to udp port: %v %v", err4, err6) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if uconn4 == nil && uconn6 == nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to bind to any unicast udp port") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if uconn4 == nil { | ||||||
|  | 		uconn4 = &net.UDPConn{} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if uconn6 == nil { | ||||||
|  | 		uconn6 = &net.UDPConn{} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	mconn4, err4 := net.ListenUDP("udp4", mdnsWildcardAddrIPv4) | ||||||
|  | 	mconn6, err6 := net.ListenUDP("udp6", mdnsWildcardAddrIPv6) | ||||||
|  | 	if err4 != nil && err6 != nil { | ||||||
|  | 		log.Printf("[ERR] mdns: Failed to bind to udp port: %v %v", err4, err6) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if mconn4 == nil && mconn6 == nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to bind to any multicast udp port") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if mconn4 == nil { | ||||||
|  | 		mconn4 = &net.UDPConn{} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if mconn6 == nil { | ||||||
|  | 		mconn6 = &net.UDPConn{} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	p1 := ipv4.NewPacketConn(mconn4) | ||||||
|  | 	p2 := ipv6.NewPacketConn(mconn6) | ||||||
|  | 	p1.SetMulticastLoopback(true) | ||||||
|  | 	p2.SetMulticastLoopback(true) | ||||||
|  |  | ||||||
|  | 	ifaces, err := net.Interfaces() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var errCount1, errCount2 int | ||||||
|  |  | ||||||
|  | 	for _, iface := range ifaces { | ||||||
|  | 		if err := p1.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||||
|  | 			errCount1++ | ||||||
|  | 		} | ||||||
|  | 		if err := p2.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||||
|  | 			errCount2++ | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(ifaces) == errCount1 && len(ifaces) == errCount2 { | ||||||
|  | 		return nil, fmt.Errorf("Failed to join multicast group on all interfaces!") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	c := &client{ | ||||||
|  | 		ipv4MulticastConn: mconn4, | ||||||
|  | 		ipv6MulticastConn: mconn6, | ||||||
|  | 		ipv4UnicastConn:   uconn4, | ||||||
|  | 		ipv6UnicastConn:   uconn6, | ||||||
|  | 		closedCh:          make(chan struct{}), | ||||||
|  | 	} | ||||||
|  | 	return c, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Close is used to cleanup the client | ||||||
|  | func (c *client) Close() error { | ||||||
|  | 	c.closeLock.Lock() | ||||||
|  | 	defer c.closeLock.Unlock() | ||||||
|  |  | ||||||
|  | 	if c.closed { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	c.closed = true | ||||||
|  |  | ||||||
|  | 	close(c.closedCh) | ||||||
|  |  | ||||||
|  | 	if c.ipv4UnicastConn != nil { | ||||||
|  | 		c.ipv4UnicastConn.Close() | ||||||
|  | 	} | ||||||
|  | 	if c.ipv6UnicastConn != nil { | ||||||
|  | 		c.ipv6UnicastConn.Close() | ||||||
|  | 	} | ||||||
|  | 	if c.ipv4MulticastConn != nil { | ||||||
|  | 		c.ipv4MulticastConn.Close() | ||||||
|  | 	} | ||||||
|  | 	if c.ipv6MulticastConn != nil { | ||||||
|  | 		c.ipv6MulticastConn.Close() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // setInterface is used to set the query interface, uses sytem | ||||||
|  | // default if not provided | ||||||
|  | func (c *client) setInterface(iface *net.Interface, loopback bool) error { | ||||||
|  | 	p := ipv4.NewPacketConn(c.ipv4UnicastConn) | ||||||
|  | 	if err := p.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	p2 := ipv6.NewPacketConn(c.ipv6UnicastConn) | ||||||
|  | 	if err := p2.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	p = ipv4.NewPacketConn(c.ipv4MulticastConn) | ||||||
|  | 	if err := p.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	p2 = ipv6.NewPacketConn(c.ipv6MulticastConn) | ||||||
|  | 	if err := p2.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if loopback { | ||||||
|  | 		p.SetMulticastLoopback(true) | ||||||
|  | 		p2.SetMulticastLoopback(true) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // query is used to perform a lookup and stream results | ||||||
|  | func (c *client) query(params *QueryParam) error { | ||||||
|  | 	// Create the service name | ||||||
|  | 	serviceAddr := fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain)) | ||||||
|  |  | ||||||
|  | 	// Start listening for response packets | ||||||
|  | 	msgCh := make(chan *dns.Msg, 32) | ||||||
|  | 	go c.recv(c.ipv4UnicastConn, msgCh) | ||||||
|  | 	go c.recv(c.ipv6UnicastConn, msgCh) | ||||||
|  | 	go c.recv(c.ipv4MulticastConn, msgCh) | ||||||
|  | 	go c.recv(c.ipv6MulticastConn, msgCh) | ||||||
|  |  | ||||||
|  | 	// Send the query | ||||||
|  | 	m := new(dns.Msg) | ||||||
|  | 	if params.Type == dns.TypeNone { | ||||||
|  | 		m.SetQuestion(serviceAddr, dns.TypePTR) | ||||||
|  | 	} else { | ||||||
|  | 		m.SetQuestion(serviceAddr, params.Type) | ||||||
|  | 	} | ||||||
|  | 	// RFC 6762, section 18.12.  Repurposing of Top Bit of qclass in Question | ||||||
|  | 	// Section | ||||||
|  | 	// | ||||||
|  | 	// In the Question Section of a Multicast DNS query, the top bit of the qclass | ||||||
|  | 	// field is used to indicate that unicast responses are preferred for this | ||||||
|  | 	// particular question.  (See Section 5.4.) | ||||||
|  | 	if params.WantUnicastResponse { | ||||||
|  | 		m.Question[0].Qclass |= 1 << 15 | ||||||
|  | 	} | ||||||
|  | 	m.RecursionDesired = false | ||||||
|  | 	if err := c.sendQuery(m); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Map the in-progress responses | ||||||
|  | 	inprogress := make(map[string]*ServiceEntry) | ||||||
|  |  | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case resp := <-msgCh: | ||||||
|  | 			inp := messageToEntry(resp, inprogress) | ||||||
|  |  | ||||||
|  | 			if inp == nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			if len(resp.Question) == 0 || resp.Question[0].Name != m.Question[0].Name { | ||||||
|  | 				// discard anything which we've not asked for | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Check if this entry is complete | ||||||
|  | 			if inp.complete() { | ||||||
|  | 				if inp.sent { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				inp.sent = true | ||||||
|  | 				select { | ||||||
|  | 				case params.Entries <- inp: | ||||||
|  | 				case <-params.Context.Done(): | ||||||
|  | 					return nil | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				// Fire off a node specific query | ||||||
|  | 				m := new(dns.Msg) | ||||||
|  | 				m.SetQuestion(inp.Name, inp.Type) | ||||||
|  | 				m.RecursionDesired = false | ||||||
|  | 				if err := c.sendQuery(m); err != nil { | ||||||
|  | 					log.Printf("[ERR] mdns: Failed to query instance %s: %v", inp.Name, err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		case <-params.Context.Done(): | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // sendQuery is used to multicast a query out | ||||||
|  | func (c *client) sendQuery(q *dns.Msg) error { | ||||||
|  | 	buf, err := q.Pack() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if c.ipv4UnicastConn != nil { | ||||||
|  | 		c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr) | ||||||
|  | 	} | ||||||
|  | 	if c.ipv6UnicastConn != nil { | ||||||
|  | 		c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // recv is used to receive until we get a shutdown | ||||||
|  | func (c *client) recv(l *net.UDPConn, msgCh chan *dns.Msg) { | ||||||
|  | 	if l == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	buf := make([]byte, 65536) | ||||||
|  | 	for { | ||||||
|  | 		c.closeLock.Lock() | ||||||
|  | 		if c.closed { | ||||||
|  | 			c.closeLock.Unlock() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		c.closeLock.Unlock() | ||||||
|  | 		n, err := l.Read(buf) | ||||||
|  | 		if err != nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		msg := new(dns.Msg) | ||||||
|  | 		if err := msg.Unpack(buf[:n]); err != nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		select { | ||||||
|  | 		case msgCh <- msg: | ||||||
|  | 		case <-c.closedCh: | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ensureName is used to ensure the named node is in progress | ||||||
|  | func ensureName(inprogress map[string]*ServiceEntry, name string, typ uint16) *ServiceEntry { | ||||||
|  | 	if inp, ok := inprogress[name]; ok { | ||||||
|  | 		return inp | ||||||
|  | 	} | ||||||
|  | 	inp := &ServiceEntry{ | ||||||
|  | 		Name: name, | ||||||
|  | 		Type: typ, | ||||||
|  | 	} | ||||||
|  | 	inprogress[name] = inp | ||||||
|  | 	return inp | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // alias is used to setup an alias between two entries | ||||||
|  | func alias(inprogress map[string]*ServiceEntry, src, dst string, typ uint16) { | ||||||
|  | 	srcEntry := ensureName(inprogress, src, typ) | ||||||
|  | 	inprogress[dst] = srcEntry | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func messageToEntry(m *dns.Msg, inprogress map[string]*ServiceEntry) *ServiceEntry { | ||||||
|  | 	var inp *ServiceEntry | ||||||
|  |  | ||||||
|  | 	for _, answer := range append(m.Answer, m.Extra...) { | ||||||
|  | 		// TODO(reddaly): Check that response corresponds to serviceAddr? | ||||||
|  | 		switch rr := answer.(type) { | ||||||
|  | 		case *dns.PTR: | ||||||
|  | 			// Create new entry for this | ||||||
|  | 			inp = ensureName(inprogress, rr.Ptr, rr.Hdr.Rrtype) | ||||||
|  | 			if inp.complete() { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		case *dns.SRV: | ||||||
|  | 			// Check for a target mismatch | ||||||
|  | 			if rr.Target != rr.Hdr.Name { | ||||||
|  | 				alias(inprogress, rr.Hdr.Name, rr.Target, rr.Hdr.Rrtype) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Get the port | ||||||
|  | 			inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype) | ||||||
|  | 			if inp.complete() { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			inp.Host = rr.Target | ||||||
|  | 			inp.Port = int(rr.Port) | ||||||
|  | 		case *dns.TXT: | ||||||
|  | 			// Pull out the txt | ||||||
|  | 			inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype) | ||||||
|  | 			if inp.complete() { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			inp.Info = strings.Join(rr.Txt, "|") | ||||||
|  | 			inp.InfoFields = rr.Txt | ||||||
|  | 			inp.hasTXT = true | ||||||
|  | 		case *dns.A: | ||||||
|  | 			// Pull out the IP | ||||||
|  | 			inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype) | ||||||
|  | 			if inp.complete() { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			inp.Addr = rr.A // @Deprecated | ||||||
|  | 			inp.AddrV4 = rr.A | ||||||
|  | 		case *dns.AAAA: | ||||||
|  | 			// Pull out the IP | ||||||
|  | 			inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype) | ||||||
|  | 			if inp.complete() { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			inp.Addr = rr.AAAA // @Deprecated | ||||||
|  | 			inp.AddrV6 = rr.AAAA | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if inp != nil { | ||||||
|  | 			inp.TTL = int(answer.Header().Ttl) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return inp | ||||||
|  | } | ||||||
							
								
								
									
										84
									
								
								registry/mdns/dns_sd.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								registry/mdns/dns_sd.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import "github.com/miekg/dns" | ||||||
|  |  | ||||||
|  | // DNSSDService is a service that complies with the DNS-SD (RFC 6762) and MDNS | ||||||
|  | // (RFC 6762) specs for local, multicast-DNS-based discovery. | ||||||
|  | // | ||||||
|  | // DNSSDService implements the Zone interface and wraps an MDNSService instance. | ||||||
|  | // To deploy an mDNS service that is compliant with DNS-SD, it's recommended to | ||||||
|  | // register only the wrapped instance with the server. | ||||||
|  | // | ||||||
|  | // Example usage: | ||||||
|  | //     service := &mdns.DNSSDService{ | ||||||
|  | //       MDNSService: &mdns.MDNSService{ | ||||||
|  | // 	       Instance: "My Foobar Service", | ||||||
|  | // 	       Service: "_foobar._tcp", | ||||||
|  | // 	       Port:    8000, | ||||||
|  | //        } | ||||||
|  | //      } | ||||||
|  | //      server, err := mdns.NewServer(&mdns.Config{Zone: service}) | ||||||
|  | //      if err != nil { | ||||||
|  | //        log.Fatalf("Error creating server: %v", err) | ||||||
|  | //      } | ||||||
|  | //      defer server.Shutdown() | ||||||
|  | type DNSSDService struct { | ||||||
|  | 	MDNSService *MDNSService | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Records returns DNS records in response to a DNS question. | ||||||
|  | // | ||||||
|  | // This function returns the DNS response of the underlying MDNSService | ||||||
|  | // instance.  It also returns a PTR record for a request for " | ||||||
|  | // _services._dns-sd._udp.<Domain>", as described in section 9 of RFC 6763 | ||||||
|  | // ("Service Type Enumeration"), to allow browsing of the underlying MDNSService | ||||||
|  | // instance. | ||||||
|  | func (s *DNSSDService) Records(q dns.Question) []dns.RR { | ||||||
|  | 	var recs []dns.RR | ||||||
|  | 	if q.Name == "_services._dns-sd._udp."+s.MDNSService.Domain+"." { | ||||||
|  | 		recs = s.dnssdMetaQueryRecords(q) | ||||||
|  | 	} | ||||||
|  | 	return append(recs, s.MDNSService.Records(q)...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // dnssdMetaQueryRecords returns the DNS records in response to a "meta-query" | ||||||
|  | // issued to browse for DNS-SD services, as per section 9. of RFC6763. | ||||||
|  | // | ||||||
|  | // A meta-query has a name of the form "_services._dns-sd._udp.<Domain>" where | ||||||
|  | // Domain is a fully-qualified domain, such as "local." | ||||||
|  | func (s *DNSSDService) dnssdMetaQueryRecords(q dns.Question) []dns.RR { | ||||||
|  | 	// Intended behavior, as described in the RFC: | ||||||
|  | 	//     ...it may be useful for network administrators to find the list of | ||||||
|  | 	//     advertised service types on the network, even if those Service Names | ||||||
|  | 	//     are just opaque identifiers and not particularly informative in | ||||||
|  | 	//     isolation. | ||||||
|  | 	// | ||||||
|  | 	//     For this purpose, a special meta-query is defined.  A DNS query for PTR | ||||||
|  | 	//     records with the name "_services._dns-sd._udp.<Domain>" yields a set of | ||||||
|  | 	//     PTR records, where the rdata of each PTR record is the two-abel | ||||||
|  | 	//     <Service> name, plus the same domain, e.g., "_http._tcp.<Domain>". | ||||||
|  | 	//     Including the domain in the PTR rdata allows for slightly better name | ||||||
|  | 	//     compression in Unicast DNS responses, but only the first two labels are | ||||||
|  | 	//     relevant for the purposes of service type enumeration.  These two-label | ||||||
|  | 	//     service types can then be used to construct subsequent Service Instance | ||||||
|  | 	//     Enumeration PTR queries, in this <Domain> or others, to discover | ||||||
|  | 	//     instances of that service type. | ||||||
|  | 	return []dns.RR{ | ||||||
|  | 		&dns.PTR{ | ||||||
|  | 			Hdr: dns.RR_Header{ | ||||||
|  | 				Name:   q.Name, | ||||||
|  | 				Rrtype: dns.TypePTR, | ||||||
|  | 				Class:  dns.ClassINET, | ||||||
|  | 				Ttl:    defaultTTL, | ||||||
|  | 			}, | ||||||
|  | 			Ptr: s.MDNSService.serviceAddr, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Announcement returns DNS records that should be broadcast during the initial | ||||||
|  | // availability of the service, as described in section 8.3 of RFC 6762. | ||||||
|  | // TODO(reddaly): Add this when Announcement is added to the mdns.Zone interface. | ||||||
|  | //func (s *DNSSDService) Announcement() []dns.RR { | ||||||
|  | //	return s.MDNSService.Announcement() | ||||||
|  | //} | ||||||
							
								
								
									
										69
									
								
								registry/mdns/dns_sd_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								registry/mdns/dns_sd_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type mockMDNSService struct{} | ||||||
|  |  | ||||||
|  | func (s *mockMDNSService) Records(q dns.Question) []dns.RR { | ||||||
|  | 	return []dns.RR{ | ||||||
|  | 		&dns.PTR{ | ||||||
|  | 			Hdr: dns.RR_Header{ | ||||||
|  | 				Name:   "fakerecord", | ||||||
|  | 				Rrtype: dns.TypePTR, | ||||||
|  | 				Class:  dns.ClassINET, | ||||||
|  | 				Ttl:    42, | ||||||
|  | 			}, | ||||||
|  | 			Ptr: "fake.local.", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *mockMDNSService) Announcement() []dns.RR { | ||||||
|  | 	return []dns.RR{ | ||||||
|  | 		&dns.PTR{ | ||||||
|  | 			Hdr: dns.RR_Header{ | ||||||
|  | 				Name:   "fakeannounce", | ||||||
|  | 				Rrtype: dns.TypePTR, | ||||||
|  | 				Class:  dns.ClassINET, | ||||||
|  | 				Ttl:    42, | ||||||
|  | 			}, | ||||||
|  | 			Ptr: "fake.local.", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestDNSSDServiceRecords(t *testing.T) { | ||||||
|  | 	s := &DNSSDService{ | ||||||
|  | 		MDNSService: &MDNSService{ | ||||||
|  | 			serviceAddr: "_foobar._tcp.local.", | ||||||
|  | 			Domain:      "local", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:   "_services._dns-sd._udp.local.", | ||||||
|  | 		Qtype:  dns.TypePTR, | ||||||
|  | 		Qclass: dns.ClassINET, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if got, want := len(recs), 1; got != want { | ||||||
|  | 		t.Fatalf("s.Records(%v) returned %v records, want %v", q, got, want) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	want := dns.RR(&dns.PTR{ | ||||||
|  | 		Hdr: dns.RR_Header{ | ||||||
|  | 			Name:   "_services._dns-sd._udp.local.", | ||||||
|  | 			Rrtype: dns.TypePTR, | ||||||
|  | 			Class:  dns.ClassINET, | ||||||
|  | 			Ttl:    defaultTTL, | ||||||
|  | 		}, | ||||||
|  | 		Ptr: "_foobar._tcp.local.", | ||||||
|  | 	}) | ||||||
|  | 	if got := recs[0]; !reflect.DeepEqual(got, want) { | ||||||
|  | 		t.Errorf("s.Records()[0] = %v, want %v", got, want) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										626
									
								
								registry/mdns/mdns.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										626
									
								
								registry/mdns/mdns.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,626 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"compress/zlib" | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"net" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"google.golang.org/grpc/resolver" | ||||||
|  |  | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry" | ||||||
|  | 	resolver2 "gitlab.bertha.cloud/partitio/lab/grpc/resolver" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// use a .grpc domain rather than .local | ||||||
|  | 	mdnsDomain = "grpc" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type mdnsTxt struct { | ||||||
|  | 	Service   string | ||||||
|  | 	Version   string | ||||||
|  | 	Metadata  map[string]string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type mdnsEntry struct { | ||||||
|  | 	id   string | ||||||
|  | 	node *Server | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type mdnsRegistry struct { | ||||||
|  | 	opts registry.Options | ||||||
|  | 	// the mdns domain | ||||||
|  | 	domain string | ||||||
|  |  | ||||||
|  | 	sync.Mutex | ||||||
|  | 	services map[string][]*mdnsEntry | ||||||
|  |  | ||||||
|  | 	mtx sync.RWMutex | ||||||
|  |  | ||||||
|  | 	// watchers | ||||||
|  | 	watchers map[string]*mdnsWatcher | ||||||
|  |  | ||||||
|  | 	// listener | ||||||
|  | 	listener chan *ServiceEntry | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) ResolverBuilder() resolver.Builder { | ||||||
|  | 	return resolver2.New(m) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) Scheme() string { | ||||||
|  | 	panic("implement me") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type mdnsWatcher struct { | ||||||
|  | 	id   string | ||||||
|  | 	wo   registry.WatchOptions | ||||||
|  | 	ch   chan *ServiceEntry | ||||||
|  | 	exit chan struct{} | ||||||
|  | 	// the mdns domain | ||||||
|  | 	domain string | ||||||
|  | 	// the registry | ||||||
|  | 	registry *mdnsRegistry | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func encode(txt *mdnsTxt) ([]string, error) { | ||||||
|  | 	b, err := json.Marshal(txt) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var buf bytes.Buffer | ||||||
|  | 	defer buf.Reset() | ||||||
|  |  | ||||||
|  | 	w := zlib.NewWriter(&buf) | ||||||
|  | 	if _, err := w.Write(b); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	w.Close() | ||||||
|  |  | ||||||
|  | 	encoded := hex.EncodeToString(buf.Bytes()) | ||||||
|  |  | ||||||
|  | 	// individual txt limit | ||||||
|  | 	if len(encoded) <= 255 { | ||||||
|  | 		return []string{encoded}, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// split encoded string | ||||||
|  | 	var record []string | ||||||
|  |  | ||||||
|  | 	for len(encoded) > 255 { | ||||||
|  | 		record = append(record, encoded[:255]) | ||||||
|  | 		encoded = encoded[255:] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	record = append(record, encoded) | ||||||
|  |  | ||||||
|  | 	return record, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func decode(record []string) (*mdnsTxt, error) { | ||||||
|  | 	encoded := strings.Join(record, "") | ||||||
|  |  | ||||||
|  | 	hr, err := hex.DecodeString(encoded) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	br := bytes.NewReader(hr) | ||||||
|  | 	zr, err := zlib.NewReader(br) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rbuf, err := ioutil.ReadAll(zr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var txt *mdnsTxt | ||||||
|  |  | ||||||
|  | 	if err := json.Unmarshal(rbuf, &txt); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return txt, nil | ||||||
|  | } | ||||||
|  | func newRegistry(opts ...registry.Option) registry.Registry { | ||||||
|  | 	options := registry.Options{ | ||||||
|  | 		Context: context.Background(), | ||||||
|  | 		Timeout: time.Millisecond * 100, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&options) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// set the domain | ||||||
|  | 	domain := mdnsDomain | ||||||
|  |  | ||||||
|  | 	d, ok := options.Context.Value("domain").(string) | ||||||
|  | 	if ok { | ||||||
|  | 		domain = d | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	reg := &mdnsRegistry{ | ||||||
|  | 		opts:     options, | ||||||
|  | 		domain:   domain, | ||||||
|  | 		services: make(map[string][]*mdnsEntry), | ||||||
|  | 		watchers: make(map[string]*mdnsWatcher), | ||||||
|  | 	} | ||||||
|  | 	return reg | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) Init(opts ...registry.Option) error { | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&m.opts) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) Options() registry.Options { | ||||||
|  | 	return m.opts | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) Register(service *registry.Service, opts ...registry.RegisterOption) error { | ||||||
|  | 	m.Lock() | ||||||
|  | 	defer m.Unlock() | ||||||
|  |  | ||||||
|  | 	entries, ok := m.services[service.Name] | ||||||
|  | 	// first entry, create wildcard used for list queries | ||||||
|  | 	if !ok { | ||||||
|  | 		s, err := NewMDNSService( | ||||||
|  | 			service.Name, | ||||||
|  | 			"_services", | ||||||
|  | 			m.domain+".", | ||||||
|  | 			"", | ||||||
|  | 			9999, | ||||||
|  | 			[]net.IP{net.ParseIP("0.0.0.0")}, | ||||||
|  | 			nil, | ||||||
|  | 		) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		srv, err := NewServer(&Config{Zone: &DNSSDService{MDNSService: s}}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// append the wildcard entry | ||||||
|  | 		entries = append(entries, &mdnsEntry{id: "*", node: srv}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var gerr error | ||||||
|  |  | ||||||
|  | 	for _, node := range service.Nodes { | ||||||
|  | 		var seen bool | ||||||
|  | 		var e *mdnsEntry | ||||||
|  |  | ||||||
|  | 		for _, entry := range entries { | ||||||
|  | 			if node.Id == entry.id { | ||||||
|  | 				seen = true | ||||||
|  | 				e = entry | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// already registered, continue | ||||||
|  | 		if seen { | ||||||
|  | 			continue | ||||||
|  | 			// doesn't exist | ||||||
|  | 		} else { | ||||||
|  | 			e = &mdnsEntry{} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		txt, err := encode(&mdnsTxt{ | ||||||
|  | 			Service:   service.Name, | ||||||
|  | 			Version:   service.Version, | ||||||
|  | 			Metadata:  node.Metadata, | ||||||
|  | 		}) | ||||||
|  |  | ||||||
|  | 		if err != nil { | ||||||
|  | 			gerr = err | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		host, pt, err := net.SplitHostPort(node.Address) | ||||||
|  | 		if err != nil { | ||||||
|  | 			gerr = err | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		port, _ := strconv.Atoi(pt) | ||||||
|  |  | ||||||
|  | 		// we got here, new node | ||||||
|  | 		s, err := NewMDNSService( | ||||||
|  | 			node.Id, | ||||||
|  | 			service.Name, | ||||||
|  | 			m.domain+".", | ||||||
|  | 			"", | ||||||
|  | 			port, | ||||||
|  | 			[]net.IP{net.ParseIP(host)}, | ||||||
|  | 			txt, | ||||||
|  | 		) | ||||||
|  | 		if err != nil { | ||||||
|  | 			gerr = err | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		srv, err := NewServer(&Config{Zone: s, LocalhostChecking: true}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			gerr = err | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		e.id = node.Id | ||||||
|  | 		e.node = srv | ||||||
|  | 		entries = append(entries, e) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// save | ||||||
|  | 	m.services[service.Name] = entries | ||||||
|  |  | ||||||
|  | 	return gerr | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) Deregister(service *registry.Service, opts ...registry.DeregisterOption) error { | ||||||
|  | 	m.Lock() | ||||||
|  | 	defer m.Unlock() | ||||||
|  |  | ||||||
|  | 	var newEntries []*mdnsEntry | ||||||
|  |  | ||||||
|  | 	// loop existing entries, check if any match, shutdown those that do | ||||||
|  | 	for _, entry := range m.services[service.Name] { | ||||||
|  | 		var remove bool | ||||||
|  |  | ||||||
|  | 		for _, node := range service.Nodes { | ||||||
|  | 			if node.Id == entry.id { | ||||||
|  | 				entry.node.Shutdown() | ||||||
|  | 				remove = true | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// keep it? | ||||||
|  | 		if !remove { | ||||||
|  | 			newEntries = append(newEntries, entry) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// last entry is the wildcard for list queries. Remove it. | ||||||
|  | 	if len(newEntries) == 1 && newEntries[0].id == "*" { | ||||||
|  | 		newEntries[0].node.Shutdown() | ||||||
|  | 		delete(m.services, service.Name) | ||||||
|  | 	} else { | ||||||
|  | 		m.services[service.Name] = newEntries | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) GetService(service string, opts ...registry.GetOption) ([]*registry.Service, error) { | ||||||
|  | 	serviceMap := make(map[string]*registry.Service) | ||||||
|  | 	entries := make(chan *ServiceEntry, 10) | ||||||
|  | 	done := make(chan bool) | ||||||
|  |  | ||||||
|  | 	p := DefaultParams(service) | ||||||
|  | 	// set context with timeout | ||||||
|  | 	var cancel context.CancelFunc | ||||||
|  | 	p.Context, cancel = context.WithTimeout(context.Background(), m.opts.Timeout) | ||||||
|  | 	defer cancel() | ||||||
|  | 	// set entries channel | ||||||
|  | 	p.Entries = entries | ||||||
|  | 	// set the domain | ||||||
|  | 	p.Domain = m.domain | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		for { | ||||||
|  | 			select { | ||||||
|  | 			case e := <-entries: | ||||||
|  | 				// list record so skip | ||||||
|  | 				if p.Service == "_services" { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				if p.Domain != m.domain { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				if e.TTL == 0 { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				txt, err := decode(e.InfoFields) | ||||||
|  | 				if err != nil { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				if txt.Service != service { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				s, ok := serviceMap[txt.Version] | ||||||
|  | 				if !ok { | ||||||
|  | 					s = ®istry.Service{ | ||||||
|  | 						Name:      txt.Service, | ||||||
|  | 						Version:   txt.Version, | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				addr := "" | ||||||
|  | 				// prefer ipv4 addrs | ||||||
|  | 				if len(e.AddrV4) > 0 { | ||||||
|  | 					addr = e.AddrV4.String() | ||||||
|  | 					// else use ipv6 | ||||||
|  | 				} else if len(e.AddrV6) > 0 { | ||||||
|  | 					addr = "[" + e.AddrV6.String() + "]" | ||||||
|  | 				} else { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				s.Nodes = append(s.Nodes, ®istry.Node{ | ||||||
|  | 					Id:       strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+"."), | ||||||
|  | 					Address:  fmt.Sprintf("%s:%d", addr, e.Port), | ||||||
|  | 					Metadata: txt.Metadata, | ||||||
|  | 				}) | ||||||
|  |  | ||||||
|  | 				serviceMap[txt.Version] = s | ||||||
|  | 			case <-p.Context.Done(): | ||||||
|  | 				close(done) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	// execute the query | ||||||
|  | 	if err := Query(p); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// wait for completion | ||||||
|  | 	<-done | ||||||
|  |  | ||||||
|  | 	// create list and return | ||||||
|  | 	services := make([]*registry.Service, 0, len(serviceMap)) | ||||||
|  |  | ||||||
|  | 	for _, service := range serviceMap { | ||||||
|  | 		services = append(services, service) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return services, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) { | ||||||
|  | 	serviceMap := make(map[string]bool) | ||||||
|  | 	entries := make(chan *ServiceEntry, 10) | ||||||
|  | 	done := make(chan bool) | ||||||
|  |  | ||||||
|  | 	p := DefaultParams("_services") | ||||||
|  | 	// set context with timeout | ||||||
|  | 	var cancel context.CancelFunc | ||||||
|  | 	p.Context, cancel = context.WithTimeout(context.Background(), m.opts.Timeout) | ||||||
|  | 	defer cancel() | ||||||
|  | 	// set entries channel | ||||||
|  | 	p.Entries = entries | ||||||
|  | 	// set domain | ||||||
|  | 	p.Domain = m.domain | ||||||
|  |  | ||||||
|  | 	var services []*registry.Service | ||||||
|  |  | ||||||
|  | 	go func() { | ||||||
|  | 		for { | ||||||
|  | 			select { | ||||||
|  | 			case e := <-entries: | ||||||
|  | 				if e.TTL == 0 { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				if !strings.HasSuffix(e.Name, p.Domain+".") { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				name := strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+".") | ||||||
|  | 				if !serviceMap[name] { | ||||||
|  | 					serviceMap[name] = true | ||||||
|  | 					services = append(services, ®istry.Service{Name: name}) | ||||||
|  | 				} | ||||||
|  | 			case <-p.Context.Done(): | ||||||
|  | 				close(done) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	// execute query | ||||||
|  | 	if err := Query(p); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// wait till done | ||||||
|  | 	<-done | ||||||
|  |  | ||||||
|  | 	return services, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) { | ||||||
|  | 	var wo registry.WatchOptions | ||||||
|  | 	for _, o := range opts { | ||||||
|  | 		o(&wo) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	md := &mdnsWatcher{ | ||||||
|  | 		id:       uuid.New().String(), | ||||||
|  | 		wo:       wo, | ||||||
|  | 		ch:       make(chan *ServiceEntry, 32), | ||||||
|  | 		exit:     make(chan struct{}), | ||||||
|  | 		domain:   m.domain, | ||||||
|  | 		registry: m, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	m.mtx.Lock() | ||||||
|  | 	defer m.mtx.Unlock() | ||||||
|  |  | ||||||
|  | 	// save the watcher | ||||||
|  | 	m.watchers[md.id] = md | ||||||
|  |  | ||||||
|  | 	// check of the listener exists | ||||||
|  | 	if m.listener != nil { | ||||||
|  | 		return md, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// start the listener | ||||||
|  | 	go func() { | ||||||
|  | 		// go to infinity | ||||||
|  | 		for { | ||||||
|  | 			m.mtx.Lock() | ||||||
|  |  | ||||||
|  | 			// just return if there are no watchers | ||||||
|  | 			if len(m.watchers) == 0 { | ||||||
|  | 				m.listener = nil | ||||||
|  | 				m.mtx.Unlock() | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// check existing listener | ||||||
|  | 			if m.listener != nil { | ||||||
|  | 				m.mtx.Unlock() | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// reset the listener | ||||||
|  | 			exit := make(chan struct{}) | ||||||
|  | 			ch := make(chan *ServiceEntry, 32) | ||||||
|  | 			m.listener = ch | ||||||
|  |  | ||||||
|  | 			m.mtx.Unlock() | ||||||
|  |  | ||||||
|  | 			// send messages to the watchers | ||||||
|  | 			go func() { | ||||||
|  | 				send := func(w *mdnsWatcher, e *ServiceEntry) { | ||||||
|  | 					select { | ||||||
|  | 					case w.ch <- e: | ||||||
|  | 					default: | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				for { | ||||||
|  | 					select { | ||||||
|  | 					case <-exit: | ||||||
|  | 						return | ||||||
|  | 					case e, ok := <-ch: | ||||||
|  | 						if !ok { | ||||||
|  | 							return | ||||||
|  | 						} | ||||||
|  | 						m.mtx.RLock() | ||||||
|  | 						// send service entry to all watchers | ||||||
|  | 						for _, w := range m.watchers { | ||||||
|  | 							send(w, e) | ||||||
|  | 						} | ||||||
|  | 						m.mtx.RUnlock() | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 			}() | ||||||
|  |  | ||||||
|  | 			// start listening, blocking call | ||||||
|  | 			Listen(ch, exit) | ||||||
|  |  | ||||||
|  | 			// Listen has unblocked | ||||||
|  | 			// kill the saved listener | ||||||
|  | 			m.mtx.Lock() | ||||||
|  | 			m.listener = nil | ||||||
|  | 			close(ch) | ||||||
|  | 			m.mtx.Unlock() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	return md, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsRegistry) String() string { | ||||||
|  | 	return "mdns" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsWatcher) Next() (*registry.Result, error) { | ||||||
|  | 	for { | ||||||
|  | 		select { | ||||||
|  | 		case e := <-m.ch: | ||||||
|  | 			txt, err := decode(e.InfoFields) | ||||||
|  | 			if err != nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if len(txt.Service) == 0 || len(txt.Version) == 0 { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Filter watch options | ||||||
|  | 			// wo.Service: Only keep services we care about | ||||||
|  | 			if len(m.wo.Service) > 0 && txt.Service != m.wo.Service { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			var action string | ||||||
|  | 			if e.TTL == 0 { | ||||||
|  | 				action = "delete" | ||||||
|  | 			} else { | ||||||
|  | 				action = "create" | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			service := ®istry.Service{ | ||||||
|  | 				Name:      txt.Service, | ||||||
|  | 				Version:   txt.Version, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// skip anything without the domain we care about | ||||||
|  | 			suffix := fmt.Sprintf(".%s.%s.", service.Name, m.domain) | ||||||
|  | 			if !strings.HasSuffix(e.Name, suffix) { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			var addr string | ||||||
|  | 			if len(e.AddrV4) > 0 { | ||||||
|  | 				addr = e.AddrV4.String() | ||||||
|  | 			} else if len(e.AddrV6) > 0 { | ||||||
|  | 				addr = "[" + e.AddrV6.String() + "]" | ||||||
|  | 			} else { | ||||||
|  | 				addr = e.Addr.String() | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			service.Nodes = append(service.Nodes, ®istry.Node{ | ||||||
|  | 				Id:       strings.TrimSuffix(e.Name, suffix), | ||||||
|  | 				Address:  fmt.Sprintf("%s:%d", addr, e.Port), | ||||||
|  | 				Metadata: txt.Metadata, | ||||||
|  | 			}) | ||||||
|  |  | ||||||
|  | 			return ®istry.Result{ | ||||||
|  | 				Action:  action, | ||||||
|  | 				Service: service, | ||||||
|  | 			}, nil | ||||||
|  | 		case <-m.exit: | ||||||
|  | 			return nil, registry.ErrWatcherStopped | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *mdnsWatcher) Stop() { | ||||||
|  | 	select { | ||||||
|  | 	case <-m.exit: | ||||||
|  | 		return | ||||||
|  | 	default: | ||||||
|  | 		close(m.exit) | ||||||
|  | 		// remove self from the registry | ||||||
|  | 		m.registry.mtx.Lock() | ||||||
|  | 		delete(m.registry.watchers, m.id) | ||||||
|  | 		m.registry.mtx.Unlock() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewRegistry returns a new default registry which is mdns | ||||||
|  | func NewRegistry(opts ...registry.Option) registry.Registry { | ||||||
|  | 	return newRegistry(opts...) | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								registry/mdns/mdns_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								registry/mdns/mdns_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  |  | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestRegistry(t *testing.T) { | ||||||
|  | 	assert := assert.New(t) | ||||||
|  | 	reg := NewRegistry() | ||||||
|  | 	svc := ®istry.Service{Name: "test", Nodes: []*registry.Node{{Id: "test-1", Address: "127.0.0.1:8888"}}} | ||||||
|  | 	if err := reg.Register(svc); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	svcs, err := reg.GetService("test") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	assert.Len(svcs, 1) | ||||||
|  | 	assert.Contains(svcs, svc) | ||||||
|  | 	if err := reg.Deregister(svc); err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	svcs, err = reg.ListServices() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	assert.Len(svcs, 0) | ||||||
|  | } | ||||||
							
								
								
									
										517
									
								
								registry/mdns/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										517
									
								
								registry/mdns/server.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,517 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"net" | ||||||
|  | 	"sync" | ||||||
|  | 	"sync/atomic" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"golang.org/x/net/ipv4" | ||||||
|  | 	"golang.org/x/net/ipv6" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	mdnsGroupIPv4 = net.ParseIP("224.0.0.251") | ||||||
|  | 	mdnsGroupIPv6 = net.ParseIP("ff02::fb") | ||||||
|  |  | ||||||
|  | 	// mDNS wildcard addresses | ||||||
|  | 	mdnsWildcardAddrIPv4 = &net.UDPAddr{ | ||||||
|  | 		IP:   net.ParseIP("224.0.0.0"), | ||||||
|  | 		Port: 5353, | ||||||
|  | 	} | ||||||
|  | 	mdnsWildcardAddrIPv6 = &net.UDPAddr{ | ||||||
|  | 		IP:   net.ParseIP("ff02::"), | ||||||
|  | 		Port: 5353, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// mDNS endpoint addresses | ||||||
|  | 	ipv4Addr = &net.UDPAddr{ | ||||||
|  | 		IP:   mdnsGroupIPv4, | ||||||
|  | 		Port: 5353, | ||||||
|  | 	} | ||||||
|  | 	ipv6Addr = &net.UDPAddr{ | ||||||
|  | 		IP:   mdnsGroupIPv6, | ||||||
|  | 		Port: 5353, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // GetMachineIP is a func which returns the outbound IP of this machine. | ||||||
|  | // Used by the server to determine whether to attempt send the response on a local address | ||||||
|  | type GetMachineIP func() net.IP | ||||||
|  |  | ||||||
|  | // Config is used to configure the mDNS server | ||||||
|  | type Config struct { | ||||||
|  | 	// Zone must be provided to support responding to queries | ||||||
|  | 	Zone Zone | ||||||
|  |  | ||||||
|  | 	// Iface if provided binds the multicast listener to the given | ||||||
|  | 	// interface. If not provided, the system default multicase interface | ||||||
|  | 	// is used. | ||||||
|  | 	Iface *net.Interface | ||||||
|  |  | ||||||
|  | 	// Port If it is not 0, replace the port 5353 with this port number. | ||||||
|  | 	Port int | ||||||
|  |  | ||||||
|  | 	// GetMachineIP is a function to return the IP of the local machine | ||||||
|  | 	GetMachineIP GetMachineIP | ||||||
|  | 	// LocalhostChecking if enabled asks the server to also send responses to 0.0.0.0 if the target IP | ||||||
|  | 	// is this host (as defined by GetMachineIP). Useful in case machine is on a VPN which blocks comms on non standard ports | ||||||
|  | 	LocalhostChecking bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Server is an mDNS server used to listen for mDNS queries and respond if we | ||||||
|  | // have a matching local record | ||||||
|  | type Server struct { | ||||||
|  | 	config *Config | ||||||
|  |  | ||||||
|  | 	ipv4List *net.UDPConn | ||||||
|  | 	ipv6List *net.UDPConn | ||||||
|  |  | ||||||
|  | 	shutdown     bool | ||||||
|  | 	shutdownCh   chan struct{} | ||||||
|  | 	shutdownLock sync.Mutex | ||||||
|  | 	wg           sync.WaitGroup | ||||||
|  |  | ||||||
|  | 	outboundIP net.IP | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewServer is used to create a new mDNS server from a config | ||||||
|  | func NewServer(config *Config) (*Server, error) { | ||||||
|  | 	setCustomPort(config.Port) | ||||||
|  |  | ||||||
|  | 	// Create the listeners | ||||||
|  | 	// Create wildcard connections (because :5353 can be already taken by other apps) | ||||||
|  | 	ipv4List, _ := net.ListenUDP("udp4", mdnsWildcardAddrIPv4) | ||||||
|  | 	ipv6List, _ := net.ListenUDP("udp6", mdnsWildcardAddrIPv6) | ||||||
|  | 	if ipv4List == nil && ipv6List == nil { | ||||||
|  | 		return nil, fmt.Errorf("[ERR] mdns: Failed to bind to any udp port!") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ipv4List == nil { | ||||||
|  | 		ipv4List = &net.UDPConn{} | ||||||
|  | 	} | ||||||
|  | 	if ipv6List == nil { | ||||||
|  | 		ipv6List = &net.UDPConn{} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Join multicast groups to receive announcements | ||||||
|  | 	p1 := ipv4.NewPacketConn(ipv4List) | ||||||
|  | 	p2 := ipv6.NewPacketConn(ipv6List) | ||||||
|  | 	p1.SetMulticastLoopback(true) | ||||||
|  | 	p2.SetMulticastLoopback(true) | ||||||
|  |  | ||||||
|  | 	if config.Iface != nil { | ||||||
|  | 		if err := p1.JoinGroup(config.Iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		if err := p2.JoinGroup(config.Iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		ifaces, err := net.Interfaces() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		errCount1, errCount2 := 0, 0 | ||||||
|  | 		for _, iface := range ifaces { | ||||||
|  | 			if err := p1.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||||
|  | 				errCount1++ | ||||||
|  | 			} | ||||||
|  | 			if err := p2.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||||
|  | 				errCount2++ | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if len(ifaces) == errCount1 && len(ifaces) == errCount2 { | ||||||
|  | 			return nil, fmt.Errorf("Failed to join multicast group on all interfaces!") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ipFunc := getOutboundIP | ||||||
|  | 	if config.GetMachineIP != nil { | ||||||
|  | 		ipFunc = config.GetMachineIP | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	s := &Server{ | ||||||
|  | 		config:     config, | ||||||
|  | 		ipv4List:   ipv4List, | ||||||
|  | 		ipv6List:   ipv6List, | ||||||
|  | 		shutdownCh: make(chan struct{}), | ||||||
|  | 		outboundIP: ipFunc(), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	go s.recv(s.ipv4List) | ||||||
|  | 	go s.recv(s.ipv6List) | ||||||
|  |  | ||||||
|  | 	s.wg.Add(1) | ||||||
|  | 	go s.probe() | ||||||
|  |  | ||||||
|  | 	return s, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Shutdown is used to shutdown the listener | ||||||
|  | func (s *Server) Shutdown() error { | ||||||
|  | 	s.shutdownLock.Lock() | ||||||
|  | 	defer s.shutdownLock.Unlock() | ||||||
|  |  | ||||||
|  | 	if s.shutdown { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	s.shutdown = true | ||||||
|  | 	close(s.shutdownCh) | ||||||
|  | 	s.unregister() | ||||||
|  |  | ||||||
|  | 	if s.ipv4List != nil { | ||||||
|  | 		s.ipv4List.Close() | ||||||
|  | 	} | ||||||
|  | 	if s.ipv6List != nil { | ||||||
|  | 		s.ipv6List.Close() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	s.wg.Wait() | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // recv is a long running routine to receive packets from an interface | ||||||
|  | func (s *Server) recv(c *net.UDPConn) { | ||||||
|  | 	if c == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	buf := make([]byte, 65536) | ||||||
|  | 	for { | ||||||
|  | 		s.shutdownLock.Lock() | ||||||
|  | 		if s.shutdown { | ||||||
|  | 			s.shutdownLock.Unlock() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		s.shutdownLock.Unlock() | ||||||
|  | 		n, from, err := c.ReadFrom(buf) | ||||||
|  | 		if err != nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if err := s.parsePacket(buf[:n], from); err != nil { | ||||||
|  | 			logrus.Errorf("[ERR] mdns: Failed to handle query: %v", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // parsePacket is used to parse an incoming packet | ||||||
|  | func (s *Server) parsePacket(packet []byte, from net.Addr) error { | ||||||
|  | 	var msg dns.Msg | ||||||
|  | 	if err := msg.Unpack(packet); err != nil { | ||||||
|  | 		logrus.Errorf("[ERR] mdns: Failed to unpack packet: %v", err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	// TODO: This is a bit of a hack | ||||||
|  | 	// We decided to ignore some mDNS answers for the time being | ||||||
|  | 	// See: https://tools.ietf.org/html/rfc6762#section-7.2 | ||||||
|  | 	msg.Truncated = false | ||||||
|  | 	return s.handleQuery(&msg, from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleQuery is used to handle an incoming query | ||||||
|  | func (s *Server) handleQuery(query *dns.Msg, from net.Addr) error { | ||||||
|  | 	if query.Opcode != dns.OpcodeQuery { | ||||||
|  | 		// "In both multicast query and multicast response messages, the OPCODE MUST | ||||||
|  | 		// be zero on transmission (only standard queries are currently supported | ||||||
|  | 		// over multicast).  Multicast DNS messages received with an OPCODE other | ||||||
|  | 		// than zero MUST be silently ignored."  Note: OpcodeQuery == 0 | ||||||
|  | 		return fmt.Errorf("mdns: received query with non-zero Opcode %v: %v", query.Opcode, *query) | ||||||
|  | 	} | ||||||
|  | 	if query.Rcode != 0 { | ||||||
|  | 		// "In both multicast query and multicast response messages, the Response | ||||||
|  | 		// Code MUST be zero on transmission.  Multicast DNS messages received with | ||||||
|  | 		// non-zero Response Codes MUST be silently ignored." | ||||||
|  | 		return fmt.Errorf("mdns: received query with non-zero Rcode %v: %v", query.Rcode, *query) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// TODO(reddaly): Handle "TC (Truncated) Bit": | ||||||
|  | 	//    In query messages, if the TC bit is set, it means that additional | ||||||
|  | 	//    Known-Answer records may be following shortly.  A responder SHOULD | ||||||
|  | 	//    record this fact, and wait for those additional Known-Answer records, | ||||||
|  | 	//    before deciding whether to respond.  If the TC bit is clear, it means | ||||||
|  | 	//    that the querying host has no additional Known Answers. | ||||||
|  | 	if query.Truncated { | ||||||
|  | 		return fmt.Errorf("[ERR] mdns: support for DNS requests with high truncated bit not implemented: %v", *query) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var unicastAnswer, multicastAnswer []dns.RR | ||||||
|  |  | ||||||
|  | 	// Handle each question | ||||||
|  | 	for _, q := range query.Question { | ||||||
|  | 		mrecs, urecs := s.handleQuestion(q) | ||||||
|  | 		multicastAnswer = append(multicastAnswer, mrecs...) | ||||||
|  | 		unicastAnswer = append(unicastAnswer, urecs...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// See section 18 of RFC 6762 for rules about DNS headers. | ||||||
|  | 	resp := func(unicast bool) *dns.Msg { | ||||||
|  | 		// 18.1: ID (Query Identifier) | ||||||
|  | 		// 0 for multicast response, query.Id for unicast response | ||||||
|  | 		id := uint16(0) | ||||||
|  | 		if unicast { | ||||||
|  | 			id = query.Id | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var answer []dns.RR | ||||||
|  | 		if unicast { | ||||||
|  | 			answer = unicastAnswer | ||||||
|  | 		} else { | ||||||
|  | 			answer = multicastAnswer | ||||||
|  | 		} | ||||||
|  | 		if len(answer) == 0 { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return &dns.Msg{ | ||||||
|  | 			MsgHdr: dns.MsgHdr{ | ||||||
|  | 				Id: id, | ||||||
|  |  | ||||||
|  | 				// 18.2: QR (Query/Response) Bit - must be set to 1 in response. | ||||||
|  | 				Response: true, | ||||||
|  |  | ||||||
|  | 				// 18.3: OPCODE - must be zero in response (OpcodeQuery == 0) | ||||||
|  | 				Opcode: dns.OpcodeQuery, | ||||||
|  |  | ||||||
|  | 				// 18.4: AA (Authoritative Answer) Bit - must be set to 1 | ||||||
|  | 				Authoritative: true, | ||||||
|  |  | ||||||
|  | 				// The following fields must all be set to 0: | ||||||
|  | 				// 18.5: TC (TRUNCATED) Bit | ||||||
|  | 				// 18.6: RD (Recursion Desired) Bit | ||||||
|  | 				// 18.7: RA (Recursion Available) Bit | ||||||
|  | 				// 18.8: Z (Zero) Bit | ||||||
|  | 				// 18.9: AD (Authentic Data) Bit | ||||||
|  | 				// 18.10: CD (Checking Disabled) Bit | ||||||
|  | 				// 18.11: RCODE (Response Code) | ||||||
|  | 			}, | ||||||
|  | 			// 18.12 pertains to questions (handled by handleQuestion) | ||||||
|  | 			// 18.13 pertains to resource records (handled by handleQuestion) | ||||||
|  |  | ||||||
|  | 			// 18.14: Name Compression - responses should be compressed (though see | ||||||
|  | 			// caveats in the RFC), so set the Compress bit (part of the dns library | ||||||
|  | 			// API, not part of the DNS packet) to true. | ||||||
|  | 			Compress: true, | ||||||
|  | 			Question: query.Question, | ||||||
|  | 			Answer:   answer, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if mresp := resp(false); mresp != nil { | ||||||
|  | 		if err := s.sendResponse(mresp, from); err != nil { | ||||||
|  | 			return fmt.Errorf("mdns: error sending multicast response: %v", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if uresp := resp(true); uresp != nil { | ||||||
|  | 		if err := s.sendResponse(uresp, from); err != nil { | ||||||
|  | 			return fmt.Errorf("mdns: error sending unicast response: %v", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // handleQuestion is used to handle an incoming question | ||||||
|  | // | ||||||
|  | // The response to a question may be transmitted over multicast, unicast, or | ||||||
|  | // both.  The return values are DNS records for each transmission type. | ||||||
|  | func (s *Server) handleQuestion(q dns.Question) (multicastRecs, unicastRecs []dns.RR) { | ||||||
|  | 	records := s.config.Zone.Records(q) | ||||||
|  | 	if len(records) == 0 { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Handle unicast and multicast responses. | ||||||
|  | 	// TODO(reddaly): The decision about sending over unicast vs. multicast is not | ||||||
|  | 	// yet fully compliant with RFC 6762.  For example, the unicast bit should be | ||||||
|  | 	// ignored if the records in question are close to TTL expiration.  For now, | ||||||
|  | 	// we just use the unicast bit to make the decision, as per the spec: | ||||||
|  | 	//     RFC 6762, section 18.12.  Repurposing of Top Bit of qclass in Question | ||||||
|  | 	//     Section | ||||||
|  | 	// | ||||||
|  | 	//     In the Question Section of a Multicast DNS query, the top bit of the | ||||||
|  | 	//     qclass field is used to indicate that unicast responses are preferred | ||||||
|  | 	//     for this particular question.  (See Section 5.4.) | ||||||
|  | 	if q.Qclass&(1<<15) != 0 { | ||||||
|  | 		return nil, records | ||||||
|  | 	} | ||||||
|  | 	return records, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *Server) probe() { | ||||||
|  | 	defer s.wg.Done() | ||||||
|  |  | ||||||
|  | 	sd, ok := s.config.Zone.(*MDNSService) | ||||||
|  | 	if !ok { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	name := fmt.Sprintf("%s.%s.%s.", sd.Instance, trimDot(sd.Service), trimDot(sd.Domain)) | ||||||
|  |  | ||||||
|  | 	q := new(dns.Msg) | ||||||
|  | 	q.SetQuestion(name, dns.TypePTR) | ||||||
|  | 	q.RecursionDesired = false | ||||||
|  |  | ||||||
|  | 	srv := &dns.SRV{ | ||||||
|  | 		Hdr: dns.RR_Header{ | ||||||
|  | 			Name:   name, | ||||||
|  | 			Rrtype: dns.TypeSRV, | ||||||
|  | 			Class:  dns.ClassINET, | ||||||
|  | 			Ttl:    defaultTTL, | ||||||
|  | 		}, | ||||||
|  | 		Priority: 0, | ||||||
|  | 		Weight:   0, | ||||||
|  | 		Port:     uint16(sd.Port), | ||||||
|  | 		Target:   sd.HostName, | ||||||
|  | 	} | ||||||
|  | 	txt := &dns.TXT{ | ||||||
|  | 		Hdr: dns.RR_Header{ | ||||||
|  | 			Name:   name, | ||||||
|  | 			Rrtype: dns.TypeTXT, | ||||||
|  | 			Class:  dns.ClassINET, | ||||||
|  | 			Ttl:    defaultTTL, | ||||||
|  | 		}, | ||||||
|  | 		Txt: sd.TXT, | ||||||
|  | 	} | ||||||
|  | 	q.Ns = []dns.RR{srv, txt} | ||||||
|  |  | ||||||
|  | 	randomizer := rand.New(rand.NewSource(time.Now().UnixNano())) | ||||||
|  |  | ||||||
|  | 	for i := 0; i < 3; i++ { | ||||||
|  | 		if err := s.SendMulticast(q); err != nil { | ||||||
|  | 			logrus.Errorf("mdns: failed to send probe: %v", err) | ||||||
|  | 		} | ||||||
|  | 		time.Sleep(time.Duration(randomizer.Intn(250)) * time.Millisecond) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	resp := new(dns.Msg) | ||||||
|  | 	resp.MsgHdr.Response = true | ||||||
|  |  | ||||||
|  | 	// set for query | ||||||
|  | 	q.SetQuestion(name, dns.TypeANY) | ||||||
|  |  | ||||||
|  | 	resp.Answer = append(resp.Answer, s.config.Zone.Records(q.Question[0])...) | ||||||
|  |  | ||||||
|  | 	// reset | ||||||
|  | 	q.SetQuestion(name, dns.TypePTR) | ||||||
|  |  | ||||||
|  | 	// From RFC6762 | ||||||
|  | 	//    The Multicast DNS responder MUST send at least two unsolicited | ||||||
|  | 	//    responses, one second apart. To provide increased robustness against | ||||||
|  | 	//    packet loss, a responder MAY send up to eight unsolicited responses, | ||||||
|  | 	//    provided that the interval between unsolicited responses increases by | ||||||
|  | 	//    at least a factor of two with every response sent. | ||||||
|  | 	timeout := 1 * time.Second | ||||||
|  | 	timer := time.NewTimer(timeout) | ||||||
|  | 	for i := 0; i < 3; i++ { | ||||||
|  | 		if err := s.SendMulticast(resp); err != nil { | ||||||
|  | 			logrus.Errorf("mdns: failed to send announcement: %v", err) | ||||||
|  | 		} | ||||||
|  | 		select { | ||||||
|  | 		case <-timer.C: | ||||||
|  | 			timeout *= 2 | ||||||
|  | 			timer.Reset(timeout) | ||||||
|  | 		case <-s.shutdownCh: | ||||||
|  | 			timer.Stop() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SendMulticast us used to send a multicast response packet | ||||||
|  | func (s *Server) SendMulticast(msg *dns.Msg) error { | ||||||
|  | 	buf, err := msg.Pack() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if s.ipv4List != nil { | ||||||
|  | 		s.ipv4List.WriteToUDP(buf, ipv4Addr) | ||||||
|  | 	} | ||||||
|  | 	if s.ipv6List != nil { | ||||||
|  | 		s.ipv6List.WriteToUDP(buf, ipv6Addr) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // sendResponse is used to send a response packet | ||||||
|  | func (s *Server) sendResponse(resp *dns.Msg, from net.Addr) error { | ||||||
|  | 	// TODO(reddaly): Respect the unicast argument, and allow sending responses | ||||||
|  | 	// over multicast. | ||||||
|  | 	buf, err := resp.Pack() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Determine the socket to send from | ||||||
|  | 	addr := from.(*net.UDPAddr) | ||||||
|  | 	conn := s.ipv4List | ||||||
|  | 	backupTarget := net.IPv4zero | ||||||
|  |  | ||||||
|  | 	if addr.IP.To4() == nil { | ||||||
|  | 		conn = s.ipv6List | ||||||
|  | 		backupTarget = net.IPv6zero | ||||||
|  | 	} | ||||||
|  | 	_, err = conn.WriteToUDP(buf, addr) | ||||||
|  | 	// If the address we're responding to is this machine then we can also attempt sending on 0.0.0.0 | ||||||
|  | 	// This covers the case where this machine is using a VPN and certain ports are blocked so the response never gets there | ||||||
|  | 	// Sending two responses is OK | ||||||
|  | 	if s.config.LocalhostChecking && addr.IP.Equal(s.outboundIP) { | ||||||
|  | 		// ignore any errors, this is best efforts | ||||||
|  | 		conn.WriteToUDP(buf, &net.UDPAddr{IP: backupTarget, Port: addr.Port}) | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *Server) unregister() error { | ||||||
|  | 	sd, ok := s.config.Zone.(*MDNSService) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	atomic.StoreUint32(&sd.TTL, 0) | ||||||
|  | 	name := fmt.Sprintf("%s.%s.%s.", sd.Instance, trimDot(sd.Service), trimDot(sd.Domain)) | ||||||
|  |  | ||||||
|  | 	q := new(dns.Msg) | ||||||
|  | 	q.SetQuestion(name, dns.TypeANY) | ||||||
|  |  | ||||||
|  | 	resp := new(dns.Msg) | ||||||
|  | 	resp.MsgHdr.Response = true | ||||||
|  | 	resp.Answer = append(resp.Answer, s.config.Zone.Records(q.Question[0])...) | ||||||
|  |  | ||||||
|  | 	return s.SendMulticast(resp) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func setCustomPort(port int) { | ||||||
|  | 	if port != 0 { | ||||||
|  | 		if mdnsWildcardAddrIPv4.Port != port { | ||||||
|  | 			mdnsWildcardAddrIPv4.Port = port | ||||||
|  | 		} | ||||||
|  | 		if mdnsWildcardAddrIPv6.Port != port { | ||||||
|  | 			mdnsWildcardAddrIPv6.Port = port | ||||||
|  | 		} | ||||||
|  | 		if ipv4Addr.Port != port { | ||||||
|  | 			ipv4Addr.Port = port | ||||||
|  | 		} | ||||||
|  | 		if ipv6Addr.Port != port { | ||||||
|  | 			ipv6Addr.Port = port | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getOutboundIP returns the IP address of this machine as seen when dialling out | ||||||
|  | func getOutboundIP() net.IP { | ||||||
|  | 	conn, err := net.Dial("udp", "8.8.8.8:80") | ||||||
|  | 	if err != nil { | ||||||
|  | 		// no net connectivity maybe so fallback | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	defer conn.Close() | ||||||
|  |  | ||||||
|  | 	localAddr := conn.LocalAddr().(*net.UDPAddr) | ||||||
|  |  | ||||||
|  | 	return localAddr.IP | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								registry/mdns/server_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								registry/mdns/server_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestServer_StartStop(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	serv, err := NewServer(&Config{Zone: s, LocalhostChecking: true}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("err: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer serv.Shutdown() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestServer_Lookup(t *testing.T) { | ||||||
|  | 	serv, err := NewServer(&Config{Zone: makeServiceWithServiceName(t, "_foobar._tcp"), LocalhostChecking: true}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("err: %v", err) | ||||||
|  | 	} | ||||||
|  | 	defer serv.Shutdown() | ||||||
|  |  | ||||||
|  | 	entries := make(chan *ServiceEntry, 1) | ||||||
|  | 	found := false | ||||||
|  | 	doneCh := make(chan struct{}) | ||||||
|  | 	go func() { | ||||||
|  | 		select { | ||||||
|  | 		case e := <-entries: | ||||||
|  | 			if e.Name != "hostname._foobar._tcp.local." { | ||||||
|  | 				t.Fatalf("bad: %v", e) | ||||||
|  | 			} | ||||||
|  | 			if e.Port != 80 { | ||||||
|  | 				t.Fatalf("bad: %v", e) | ||||||
|  | 			} | ||||||
|  | 			if e.Info != "Local web server" { | ||||||
|  | 				t.Fatalf("bad: %v", e) | ||||||
|  | 			} | ||||||
|  | 			found = true | ||||||
|  |  | ||||||
|  | 		case <-time.After(80 * time.Millisecond): | ||||||
|  | 			t.Fatalf("timeout") | ||||||
|  | 		} | ||||||
|  | 		close(doneCh) | ||||||
|  | 	}() | ||||||
|  |  | ||||||
|  | 	params := &QueryParam{ | ||||||
|  | 		Service: "_foobar._tcp", | ||||||
|  | 		Domain:  "local", | ||||||
|  | 		Timeout: 50 * time.Millisecond, | ||||||
|  | 		Entries: entries, | ||||||
|  | 	} | ||||||
|  | 	err = Query(params) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("err: %v", err) | ||||||
|  | 	} | ||||||
|  | 	<-doneCh | ||||||
|  | 	if !found { | ||||||
|  | 		t.Fatalf("record not found") | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										309
									
								
								registry/mdns/zone.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										309
									
								
								registry/mdns/zone.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,309 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync/atomic" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// defaultTTL is the default TTL value in returned DNS records in seconds. | ||||||
|  | 	defaultTTL = 120 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Zone is the interface used to integrate with the server and | ||||||
|  | // to serve records dynamically | ||||||
|  | type Zone interface { | ||||||
|  | 	// Records returns DNS records in response to a DNS question. | ||||||
|  | 	Records(q dns.Question) []dns.RR | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MDNSService is used to export a named service by implementing a Zone | ||||||
|  | type MDNSService struct { | ||||||
|  | 	Instance     string   // Instance name (e.g. "hostService name") | ||||||
|  | 	Service      string   // Service name (e.g. "_http._tcp.") | ||||||
|  | 	Domain       string   // If blank, assumes "local" | ||||||
|  | 	HostName     string   // Host machine DNS name (e.g. "mymachine.net.") | ||||||
|  | 	Port         int      // Service Port | ||||||
|  | 	IPs          []net.IP // IP addresses for the service's host | ||||||
|  | 	TXT          []string // Service TXT records | ||||||
|  | 	TTL          uint32 | ||||||
|  | 	serviceAddr  string // Fully qualified service address | ||||||
|  | 	instanceAddr string // Fully qualified instance address | ||||||
|  | 	enumAddr     string // _services._dns-sd._udp.<domain> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // validateFQDN returns an error if the passed string is not a fully qualified | ||||||
|  | // hdomain name (more specifically, a hostname). | ||||||
|  | func validateFQDN(s string) error { | ||||||
|  | 	if len(s) == 0 { | ||||||
|  | 		return fmt.Errorf("FQDN must not be blank") | ||||||
|  | 	} | ||||||
|  | 	if s[len(s)-1] != '.' { | ||||||
|  | 		return fmt.Errorf("FQDN must end in period: %s", s) | ||||||
|  | 	} | ||||||
|  | 	// TODO(reddaly): Perform full validation. | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewMDNSService returns a new instance of MDNSService. | ||||||
|  | // | ||||||
|  | // If domain, hostName, or ips is set to the zero value, then a default value | ||||||
|  | // will be inferred from the operating system. | ||||||
|  | // | ||||||
|  | // TODO(reddaly): This interface may need to change to account for "unique | ||||||
|  | // record" conflict rules of the mDNS protocol.  Upon startup, the server should | ||||||
|  | // check to ensure that the instance name does not conflict with other instance | ||||||
|  | // names, and, if required, select a new name.  There may also be conflicting | ||||||
|  | // hostName A/AAAA records. | ||||||
|  | func NewMDNSService(instance, service, domain, hostName string, port int, ips []net.IP, txt []string) (*MDNSService, error) { | ||||||
|  | 	// Sanity check inputs | ||||||
|  | 	if instance == "" { | ||||||
|  | 		return nil, fmt.Errorf("missing service instance name") | ||||||
|  | 	} | ||||||
|  | 	if service == "" { | ||||||
|  | 		return nil, fmt.Errorf("missing service name") | ||||||
|  | 	} | ||||||
|  | 	if port == 0 { | ||||||
|  | 		return nil, fmt.Errorf("missing service port") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Set default domain | ||||||
|  | 	if domain == "" { | ||||||
|  | 		domain = "local." | ||||||
|  | 	} | ||||||
|  | 	if err := validateFQDN(domain); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("domain %q is not a fully-qualified domain name: %v", domain, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get host information if no host is specified. | ||||||
|  | 	if hostName == "" { | ||||||
|  | 		var err error | ||||||
|  | 		hostName, err = os.Hostname() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("could not determine host: %v", err) | ||||||
|  | 		} | ||||||
|  | 		hostName = fmt.Sprintf("%s.", hostName) | ||||||
|  | 	} | ||||||
|  | 	if err := validateFQDN(hostName); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("hostName %q is not a fully-qualified domain name: %v", hostName, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(ips) == 0 { | ||||||
|  | 		var err error | ||||||
|  | 		ips, err = net.LookupIP(trimDot(hostName)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			// Try appending the host domain suffix and lookup again | ||||||
|  | 			// (required for Linux-based hosts) | ||||||
|  | 			tmpHostName := fmt.Sprintf("%s%s", hostName, domain) | ||||||
|  |  | ||||||
|  | 			ips, err = net.LookupIP(trimDot(tmpHostName)) | ||||||
|  |  | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("could not determine host IP addresses for %s", hostName) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	for _, ip := range ips { | ||||||
|  | 		if ip.To4() == nil && ip.To16() == nil { | ||||||
|  | 			return nil, fmt.Errorf("invalid IP address in IPs list: %v", ip) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &MDNSService{ | ||||||
|  | 		Instance:     instance, | ||||||
|  | 		Service:      service, | ||||||
|  | 		Domain:       domain, | ||||||
|  | 		HostName:     hostName, | ||||||
|  | 		Port:         port, | ||||||
|  | 		IPs:          ips, | ||||||
|  | 		TXT:          txt, | ||||||
|  | 		TTL:          defaultTTL, | ||||||
|  | 		serviceAddr:  fmt.Sprintf("%s.%s.", trimDot(service), trimDot(domain)), | ||||||
|  | 		instanceAddr: fmt.Sprintf("%s.%s.%s.", instance, trimDot(service), trimDot(domain)), | ||||||
|  | 		enumAddr:     fmt.Sprintf("_services._dns-sd._udp.%s.", trimDot(domain)), | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // trimDot is used to trim the dots from the start or end of a string | ||||||
|  | func trimDot(s string) string { | ||||||
|  | 	return strings.Trim(s, ".") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Records returns DNS records in response to a DNS question. | ||||||
|  | func (m *MDNSService) Records(q dns.Question) []dns.RR { | ||||||
|  | 	switch q.Name { | ||||||
|  | 	case m.enumAddr: | ||||||
|  | 		return m.serviceEnum(q) | ||||||
|  | 	case m.serviceAddr: | ||||||
|  | 		return m.serviceRecords(q) | ||||||
|  | 	case m.instanceAddr: | ||||||
|  | 		return m.instanceRecords(q) | ||||||
|  | 	case m.HostName: | ||||||
|  | 		if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA { | ||||||
|  | 			return m.instanceRecords(q) | ||||||
|  | 		} | ||||||
|  | 		fallthrough | ||||||
|  | 	default: | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *MDNSService) serviceEnum(q dns.Question) []dns.RR { | ||||||
|  | 	switch q.Qtype { | ||||||
|  | 	case dns.TypeANY: | ||||||
|  | 		fallthrough | ||||||
|  | 	case dns.TypePTR: | ||||||
|  | 		rr := &dns.PTR{ | ||||||
|  | 			Hdr: dns.RR_Header{ | ||||||
|  | 				Name:   q.Name, | ||||||
|  | 				Rrtype: dns.TypePTR, | ||||||
|  | 				Class:  dns.ClassINET, | ||||||
|  | 				Ttl:    atomic.LoadUint32(&m.TTL), | ||||||
|  | 			}, | ||||||
|  | 			Ptr: m.serviceAddr, | ||||||
|  | 		} | ||||||
|  | 		return []dns.RR{rr} | ||||||
|  | 	default: | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // serviceRecords is called when the query matches the service name | ||||||
|  | func (m *MDNSService) serviceRecords(q dns.Question) []dns.RR { | ||||||
|  | 	switch q.Qtype { | ||||||
|  | 	case dns.TypeANY: | ||||||
|  | 		fallthrough | ||||||
|  | 	case dns.TypePTR: | ||||||
|  | 		// Build a PTR response for the service | ||||||
|  | 		rr := &dns.PTR{ | ||||||
|  | 			Hdr: dns.RR_Header{ | ||||||
|  | 				Name:   q.Name, | ||||||
|  | 				Rrtype: dns.TypePTR, | ||||||
|  | 				Class:  dns.ClassINET, | ||||||
|  | 				Ttl:    atomic.LoadUint32(&m.TTL), | ||||||
|  | 			}, | ||||||
|  | 			Ptr: m.instanceAddr, | ||||||
|  | 		} | ||||||
|  | 		servRec := []dns.RR{rr} | ||||||
|  |  | ||||||
|  | 		// Get the instance records | ||||||
|  | 		instRecs := m.instanceRecords(dns.Question{ | ||||||
|  | 			Name:  m.instanceAddr, | ||||||
|  | 			Qtype: dns.TypeANY, | ||||||
|  | 		}) | ||||||
|  |  | ||||||
|  | 		// Return the service record with the instance records | ||||||
|  | 		return append(servRec, instRecs...) | ||||||
|  | 	default: | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // serviceRecords is called when the query matches the instance name | ||||||
|  | func (m *MDNSService) instanceRecords(q dns.Question) []dns.RR { | ||||||
|  | 	switch q.Qtype { | ||||||
|  | 	case dns.TypeANY: | ||||||
|  | 		// Get the SRV, which includes A and AAAA | ||||||
|  | 		recs := m.instanceRecords(dns.Question{ | ||||||
|  | 			Name:  m.instanceAddr, | ||||||
|  | 			Qtype: dns.TypeSRV, | ||||||
|  | 		}) | ||||||
|  |  | ||||||
|  | 		// Add the TXT record | ||||||
|  | 		recs = append(recs, m.instanceRecords(dns.Question{ | ||||||
|  | 			Name:  m.instanceAddr, | ||||||
|  | 			Qtype: dns.TypeTXT, | ||||||
|  | 		})...) | ||||||
|  | 		return recs | ||||||
|  |  | ||||||
|  | 	case dns.TypeA: | ||||||
|  | 		var rr []dns.RR | ||||||
|  | 		for _, ip := range m.IPs { | ||||||
|  | 			if ip4 := ip.To4(); ip4 != nil { | ||||||
|  | 				rr = append(rr, &dns.A{ | ||||||
|  | 					Hdr: dns.RR_Header{ | ||||||
|  | 						Name:   m.HostName, | ||||||
|  | 						Rrtype: dns.TypeA, | ||||||
|  | 						Class:  dns.ClassINET, | ||||||
|  | 						Ttl:    atomic.LoadUint32(&m.TTL), | ||||||
|  | 					}, | ||||||
|  | 					A: ip4, | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return rr | ||||||
|  |  | ||||||
|  | 	case dns.TypeAAAA: | ||||||
|  | 		var rr []dns.RR | ||||||
|  | 		for _, ip := range m.IPs { | ||||||
|  | 			if ip.To4() != nil { | ||||||
|  | 				// TODO(reddaly): IPv4 addresses could be encoded in IPv6 format and | ||||||
|  | 				// putinto AAAA records, but the current logic puts ipv4-encodable | ||||||
|  | 				// addresses into the A records exclusively.  Perhaps this should be | ||||||
|  | 				// configurable? | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if ip16 := ip.To16(); ip16 != nil { | ||||||
|  | 				rr = append(rr, &dns.AAAA{ | ||||||
|  | 					Hdr: dns.RR_Header{ | ||||||
|  | 						Name:   m.HostName, | ||||||
|  | 						Rrtype: dns.TypeAAAA, | ||||||
|  | 						Class:  dns.ClassINET, | ||||||
|  | 						Ttl:    atomic.LoadUint32(&m.TTL), | ||||||
|  | 					}, | ||||||
|  | 					AAAA: ip16, | ||||||
|  | 				}) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return rr | ||||||
|  |  | ||||||
|  | 	case dns.TypeSRV: | ||||||
|  | 		// Create the SRV Record | ||||||
|  | 		srv := &dns.SRV{ | ||||||
|  | 			Hdr: dns.RR_Header{ | ||||||
|  | 				Name:   q.Name, | ||||||
|  | 				Rrtype: dns.TypeSRV, | ||||||
|  | 				Class:  dns.ClassINET, | ||||||
|  | 				Ttl:    atomic.LoadUint32(&m.TTL), | ||||||
|  | 			}, | ||||||
|  | 			Priority: 10, | ||||||
|  | 			Weight:   1, | ||||||
|  | 			Port:     uint16(m.Port), | ||||||
|  | 			Target:   m.HostName, | ||||||
|  | 		} | ||||||
|  | 		recs := []dns.RR{srv} | ||||||
|  |  | ||||||
|  | 		// Add the A record | ||||||
|  | 		recs = append(recs, m.instanceRecords(dns.Question{ | ||||||
|  | 			Name:  m.instanceAddr, | ||||||
|  | 			Qtype: dns.TypeA, | ||||||
|  | 		})...) | ||||||
|  |  | ||||||
|  | 		// Add the AAAA record | ||||||
|  | 		recs = append(recs, m.instanceRecords(dns.Question{ | ||||||
|  | 			Name:  m.instanceAddr, | ||||||
|  | 			Qtype: dns.TypeAAAA, | ||||||
|  | 		})...) | ||||||
|  | 		return recs | ||||||
|  |  | ||||||
|  | 	case dns.TypeTXT: | ||||||
|  | 		txt := &dns.TXT{ | ||||||
|  | 			Hdr: dns.RR_Header{ | ||||||
|  | 				Name:   q.Name, | ||||||
|  | 				Rrtype: dns.TypeTXT, | ||||||
|  | 				Class:  dns.ClassINET, | ||||||
|  | 				Ttl:    atomic.LoadUint32(&m.TTL), | ||||||
|  | 			}, | ||||||
|  | 			Txt: m.TXT, | ||||||
|  | 		} | ||||||
|  | 		return []dns.RR{txt} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										275
									
								
								registry/mdns/zone_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								registry/mdns/zone_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | |||||||
|  | package mdns | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"net" | ||||||
|  | 	"reflect" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func makeService(t *testing.T) *MDNSService { | ||||||
|  | 	return makeServiceWithServiceName(t, "_http._tcp") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func makeServiceWithServiceName(t *testing.T, service string) *MDNSService { | ||||||
|  | 	m, err := NewMDNSService( | ||||||
|  | 		"hostname", | ||||||
|  | 		service, | ||||||
|  | 		"local.", | ||||||
|  | 		"testhost.", | ||||||
|  | 		80, // port | ||||||
|  | 		[]net.IP{net.IP([]byte{192, 168, 0, 42}), net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc")}, | ||||||
|  | 		[]string{"Local web server"}) // TXT | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("err: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return m | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestNewMDNSService_BadParams(t *testing.T) { | ||||||
|  | 	for _, test := range []struct { | ||||||
|  | 		testName string | ||||||
|  | 		hostName string | ||||||
|  | 		domain   string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			"NewMDNSService should fail when passed hostName that is not a legal fully-qualified domain name", | ||||||
|  | 			"hostname", // not legal FQDN - should be "hostname." or "hostname.local.", etc. | ||||||
|  | 			"local.",   // legal | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"NewMDNSService should fail when passed domain that is not a legal fully-qualified domain name", | ||||||
|  | 			"hostname.", // legal | ||||||
|  | 			"local",     // should be "local." | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		_, err := NewMDNSService( | ||||||
|  | 			"instance name", | ||||||
|  | 			"_http._tcp", | ||||||
|  | 			test.domain, | ||||||
|  | 			test.hostName, | ||||||
|  | 			80, // port | ||||||
|  | 			[]net.IP{net.IP([]byte{192, 168, 0, 42})}, | ||||||
|  | 			[]string{"Local web server"}) // TXT | ||||||
|  | 		if err == nil { | ||||||
|  | 			t.Fatalf("%s: error expected, but got none", test.testName) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_BadAddr(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:  "random", | ||||||
|  | 		Qtype: dns.TypeANY, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if len(recs) != 0 { | ||||||
|  | 		t.Fatalf("bad: %v", recs) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_ServiceAddr(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:  "_http._tcp.local.", | ||||||
|  | 		Qtype: dns.TypeANY, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if got, want := len(recs), 5; got != want { | ||||||
|  | 		t.Fatalf("got %d records, want %d: %v", got, want, recs) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ptr, ok := recs[0].(*dns.PTR); !ok { | ||||||
|  | 		t.Errorf("recs[0] should be PTR record, got: %v, all records: %v", recs[0], recs) | ||||||
|  | 	} else if got, want := ptr.Ptr, "hostname._http._tcp.local."; got != want { | ||||||
|  | 		t.Fatalf("bad PTR record %v: got %v, want %v", ptr, got, want) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if _, ok := recs[1].(*dns.SRV); !ok { | ||||||
|  | 		t.Errorf("recs[1] should be SRV record, got: %v, all reccords: %v", recs[1], recs) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[2].(*dns.A); !ok { | ||||||
|  | 		t.Errorf("recs[2] should be A record, got: %v, all records: %v", recs[2], recs) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[3].(*dns.AAAA); !ok { | ||||||
|  | 		t.Errorf("recs[3] should be AAAA record, got: %v, all records: %v", recs[3], recs) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[4].(*dns.TXT); !ok { | ||||||
|  | 		t.Errorf("recs[4] should be TXT record, got: %v, all records: %v", recs[4], recs) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	q.Qtype = dns.TypePTR | ||||||
|  | 	if recs2 := s.Records(q); !reflect.DeepEqual(recs, recs2) { | ||||||
|  | 		t.Fatalf("PTR question should return same result as ANY question: ANY => %v, PTR => %v", recs, recs2) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_InstanceAddr_ANY(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:  "hostname._http._tcp.local.", | ||||||
|  | 		Qtype: dns.TypeANY, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if len(recs) != 4 { | ||||||
|  | 		t.Fatalf("bad: %v", recs) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[0].(*dns.SRV); !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[0]) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[1].(*dns.A); !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[1]) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[2].(*dns.AAAA); !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[2]) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[3].(*dns.TXT); !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[3]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_InstanceAddr_SRV(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:  "hostname._http._tcp.local.", | ||||||
|  | 		Qtype: dns.TypeSRV, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if len(recs) != 3 { | ||||||
|  | 		t.Fatalf("bad: %v", recs) | ||||||
|  | 	} | ||||||
|  | 	srv, ok := recs[0].(*dns.SRV) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[0]) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[1].(*dns.A); !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[1]) | ||||||
|  | 	} | ||||||
|  | 	if _, ok := recs[2].(*dns.AAAA); !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[2]) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if srv.Port != uint16(s.Port) { | ||||||
|  | 		t.Fatalf("bad: %v", recs[0]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_InstanceAddr_A(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:  "hostname._http._tcp.local.", | ||||||
|  | 		Qtype: dns.TypeA, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if len(recs) != 1 { | ||||||
|  | 		t.Fatalf("bad: %v", recs) | ||||||
|  | 	} | ||||||
|  | 	a, ok := recs[0].(*dns.A) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[0]) | ||||||
|  | 	} | ||||||
|  | 	if !bytes.Equal(a.A, []byte{192, 168, 0, 42}) { | ||||||
|  | 		t.Fatalf("bad: %v", recs[0]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_InstanceAddr_AAAA(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:  "hostname._http._tcp.local.", | ||||||
|  | 		Qtype: dns.TypeAAAA, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if len(recs) != 1 { | ||||||
|  | 		t.Fatalf("bad: %v", recs) | ||||||
|  | 	} | ||||||
|  | 	a4, ok := recs[0].(*dns.AAAA) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[0]) | ||||||
|  | 	} | ||||||
|  | 	ip6 := net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc") | ||||||
|  | 	if got := len(ip6); got != net.IPv6len { | ||||||
|  | 		t.Fatalf("test IP failed to parse (len = %d, want %d)", got, net.IPv6len) | ||||||
|  | 	} | ||||||
|  | 	if !bytes.Equal(a4.AAAA, ip6) { | ||||||
|  | 		t.Fatalf("bad: %v", recs[0]) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_InstanceAddr_TXT(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:  "hostname._http._tcp.local.", | ||||||
|  | 		Qtype: dns.TypeTXT, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if len(recs) != 1 { | ||||||
|  | 		t.Fatalf("bad: %v", recs) | ||||||
|  | 	} | ||||||
|  | 	txt, ok := recs[0].(*dns.TXT) | ||||||
|  | 	if !ok { | ||||||
|  | 		t.Fatalf("bad: %v", recs[0]) | ||||||
|  | 	} | ||||||
|  | 	if got, want := txt.Txt, s.TXT; !reflect.DeepEqual(got, want) { | ||||||
|  | 		t.Fatalf("TXT record mismatch for %v: got %v, want %v", recs[0], got, want) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_HostNameQuery(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	for _, test := range []struct { | ||||||
|  | 		q    dns.Question | ||||||
|  | 		want []dns.RR | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			dns.Question{Name: "testhost.", Qtype: dns.TypeA}, | ||||||
|  | 			[]dns.RR{&dns.A{ | ||||||
|  | 				Hdr: dns.RR_Header{ | ||||||
|  | 					Name:   "testhost.", | ||||||
|  | 					Rrtype: dns.TypeA, | ||||||
|  | 					Class:  dns.ClassINET, | ||||||
|  | 					Ttl:    120, | ||||||
|  | 				}, | ||||||
|  | 				A: net.IP([]byte{192, 168, 0, 42}), | ||||||
|  | 			}}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			dns.Question{Name: "testhost.", Qtype: dns.TypeAAAA}, | ||||||
|  | 			[]dns.RR{&dns.AAAA{ | ||||||
|  | 				Hdr: dns.RR_Header{ | ||||||
|  | 					Name:   "testhost.", | ||||||
|  | 					Rrtype: dns.TypeAAAA, | ||||||
|  | 					Class:  dns.ClassINET, | ||||||
|  | 					Ttl:    120, | ||||||
|  | 				}, | ||||||
|  | 				AAAA: net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc"), | ||||||
|  | 			}}, | ||||||
|  | 		}, | ||||||
|  | 	} { | ||||||
|  | 		if got := s.Records(test.q); !reflect.DeepEqual(got, test.want) { | ||||||
|  | 			t.Errorf("hostname query failed: s.Records(%v) = %v, want %v", test.q, got, test.want) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestMDNSService_serviceEnum_PTR(t *testing.T) { | ||||||
|  | 	s := makeService(t) | ||||||
|  | 	q := dns.Question{ | ||||||
|  | 		Name:  "_services._dns-sd._udp.local.", | ||||||
|  | 		Qtype: dns.TypePTR, | ||||||
|  | 	} | ||||||
|  | 	recs := s.Records(q) | ||||||
|  | 	if len(recs) != 1 { | ||||||
|  | 		t.Fatalf("bad: %v", recs) | ||||||
|  | 	} | ||||||
|  | 	if ptr, ok := recs[0].(*dns.PTR); !ok { | ||||||
|  | 		t.Errorf("recs[0] should be PTR record, got: %v, all records: %v", recs[0], recs) | ||||||
|  | 	} else if got, want := ptr.Ptr, "_http._tcp.local."; got != want { | ||||||
|  | 		t.Fatalf("bad PTR record %v: got %v, want %v", ptr, got, want) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								registry/noop/registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								registry/noop/registry.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | package noop | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"google.golang.org/grpc/resolver" | ||||||
|  |  | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry" | ||||||
|  | 	resolver2 "gitlab.bertha.cloud/partitio/lab/grpc/resolver" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | func New() registry.Registry { | ||||||
|  | 	return &noop{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type noop struct {} | ||||||
|  |  | ||||||
|  | func (n noop) ResolverBuilder() resolver.Builder { | ||||||
|  | 	return resolver2.New(n) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noop) Init(option ...registry.Option) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noop) Options() registry.Options { | ||||||
|  | 	return registry.Options{} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noop) Register(service *registry.Service, option ...registry.RegisterOption) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noop) Deregister(service *registry.Service, option ...registry.DeregisterOption) error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noop) GetService(s string, option ...registry.GetOption) ([]*registry.Service, error) { | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noop) ListServices(option ...registry.ListOption) ([]*registry.Service, error) { | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noop) Watch(option ...registry.WatchOption) (registry.Watcher, error) { | ||||||
|  | 	return noopWatcher{}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noop) String() string { | ||||||
|  | 	return "passthroug" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type noopWatcher struct {} | ||||||
|  |  | ||||||
|  | func (n noopWatcher) Next() (*registry.Result, error) { | ||||||
|  | 	return ®istry.Result{}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (n noopWatcher) Stop() {} | ||||||
|  |  | ||||||
							
								
								
									
										115
									
								
								registry/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								registry/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | package registry | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Options struct { | ||||||
|  | 	Addrs     []string | ||||||
|  | 	Timeout   time.Duration | ||||||
|  | 	Secure    bool | ||||||
|  | 	TLSConfig *tls.Config | ||||||
|  | 	// Other options for implementations of the interface | ||||||
|  | 	// can be stored in a context | ||||||
|  | 	Context context.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type RegisterOptions struct { | ||||||
|  | 	TTL time.Duration | ||||||
|  | 	// Other options for implementations of the interface | ||||||
|  | 	// can be stored in a context | ||||||
|  | 	Context context.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type WatchOptions struct { | ||||||
|  | 	// Specify a service to watch | ||||||
|  | 	// If blank, the watch is for all services | ||||||
|  | 	Service string | ||||||
|  | 	// Other options for implementations of the interface | ||||||
|  | 	// can be stored in a context | ||||||
|  | 	Context context.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type DeregisterOptions struct { | ||||||
|  | 	Context context.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GetOptions struct { | ||||||
|  | 	Context context.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ListOptions struct { | ||||||
|  | 	Context context.Context | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Addrs is the registry addresses to use | ||||||
|  | func Addrs(addrs ...string) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Addrs = addrs | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func Timeout(t time.Duration) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Timeout = t | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Secure communication with the registry | ||||||
|  | func Secure(b bool) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.Secure = b | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Specify TLS Config | ||||||
|  | func TLSConfig(t *tls.Config) Option { | ||||||
|  | 	return func(o *Options) { | ||||||
|  | 		o.TLSConfig = t | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RegisterTTL(t time.Duration) RegisterOption { | ||||||
|  | 	return func(o *RegisterOptions) { | ||||||
|  | 		o.TTL = t | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func RegisterContext(ctx context.Context) RegisterOption { | ||||||
|  | 	return func(o *RegisterOptions) { | ||||||
|  | 		o.Context = ctx | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Watch a service | ||||||
|  | func WatchService(name string) WatchOption { | ||||||
|  | 	return func(o *WatchOptions) { | ||||||
|  | 		o.Service = name | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func WatchContext(ctx context.Context) WatchOption { | ||||||
|  | 	return func(o *WatchOptions) { | ||||||
|  | 		o.Context = ctx | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func DeregisterContext(ctx context.Context) DeregisterOption { | ||||||
|  | 	return func(o *DeregisterOptions) { | ||||||
|  | 		o.Context = ctx | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetContext(ctx context.Context) GetOption { | ||||||
|  | 	return func(o *GetOptions) { | ||||||
|  | 		o.Context = ctx | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ListContext(ctx context.Context) ListOption { | ||||||
|  | 	return func(o *ListOptions) { | ||||||
|  | 		o.Context = ctx | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								registry/registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								registry/registry.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | // Package registry is an interface for service discovery | ||||||
|  | package registry | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  |  | ||||||
|  | 	"google.golang.org/grpc/resolver" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// Not found error when GetService is called | ||||||
|  | 	ErrNotFound = errors.New("service not found") | ||||||
|  | 	// Watcher stopped error when watcher is stopped | ||||||
|  | 	ErrWatcherStopped = errors.New("watcher stopped") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // The registry provides an interface for service discovery | ||||||
|  | // and an abstraction over varying implementations | ||||||
|  | type Registry interface { | ||||||
|  | 	ResolverBuilder() resolver.Builder | ||||||
|  | 	Init(...Option) error | ||||||
|  | 	Options() Options | ||||||
|  | 	Register(*Service, ...RegisterOption) error | ||||||
|  | 	Deregister(*Service, ...DeregisterOption) error | ||||||
|  | 	GetService(string, ...GetOption) ([]*Service, error) | ||||||
|  | 	ListServices(...ListOption) ([]*Service, error) | ||||||
|  | 	Watch(...WatchOption) (Watcher, error) | ||||||
|  | 	String() string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Service struct { | ||||||
|  | 	Name      string            `json:"name"` | ||||||
|  | 	Version   string            `json:"version"` | ||||||
|  | 	Metadata  map[string]string `json:"metadata"` | ||||||
|  | 	Nodes     []*Node           `json:"nodes"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Node struct { | ||||||
|  | 	Id       string            `json:"id"` | ||||||
|  | 	Address  string            `json:"address"` | ||||||
|  | 	Metadata map[string]string `json:"metadata"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type Option func(*Options) | ||||||
|  |  | ||||||
|  | type RegisterOption func(*RegisterOptions) | ||||||
|  |  | ||||||
|  | type WatchOption func(*WatchOptions) | ||||||
|  |  | ||||||
|  | type DeregisterOption func(*DeregisterOptions) | ||||||
|  |  | ||||||
|  | type GetOption func(*GetOptions) | ||||||
|  |  | ||||||
|  | type ListOption func(*ListOptions) | ||||||
							
								
								
									
										56
									
								
								registry/watcher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								registry/watcher.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | package registry | ||||||
|  |  | ||||||
|  | import "time" | ||||||
|  |  | ||||||
|  | // Watcher is an interface that returns updates | ||||||
|  | // about services within the registry. | ||||||
|  | type Watcher interface { | ||||||
|  | 	// Next is a blocking call | ||||||
|  | 	Next() (*Result, error) | ||||||
|  | 	Stop() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Result is returned by a call to Next on | ||||||
|  | // the watcher. Actions can be create, update, delete | ||||||
|  | type Result struct { | ||||||
|  | 	Action  string | ||||||
|  | 	Service *Service | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // EventType defines registry event type | ||||||
|  | type EventType int | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// Create is emitted when a new service is registered | ||||||
|  | 	Create EventType = iota | ||||||
|  | 	// Delete is emitted when an existing service is deregsitered | ||||||
|  | 	Delete | ||||||
|  | 	// Update is emitted when an existing servicec is updated | ||||||
|  | 	Update | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // String returns human readable event type | ||||||
|  | func (t EventType) String() string { | ||||||
|  | 	switch t { | ||||||
|  | 	case Create: | ||||||
|  | 		return "create" | ||||||
|  | 	case Delete: | ||||||
|  | 		return "delete" | ||||||
|  | 	case Update: | ||||||
|  | 		return "update" | ||||||
|  | 	default: | ||||||
|  | 		return "unknown" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Event is registry event | ||||||
|  | type Event struct { | ||||||
|  | 	// Id is registry id | ||||||
|  | 	Id string | ||||||
|  | 	// Type defines type of event | ||||||
|  | 	Type EventType | ||||||
|  | 	// Timestamp is event timestamp | ||||||
|  | 	Timestamp time.Time | ||||||
|  | 	// Service is registry service | ||||||
|  | 	Service *Service | ||||||
|  | } | ||||||
							
								
								
									
										86
									
								
								resolver/resolver.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								resolver/resolver.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | |||||||
|  | package resolver | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"google.golang.org/grpc/resolver" | ||||||
|  |  | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func New(reg registry.Registry) resolver.Builder { | ||||||
|  | 	return &builder{reg: reg} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type builder struct { | ||||||
|  | 	reg registry.Registry | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { | ||||||
|  | 	rslvr :=  &resolvr{reg: r.reg, target: target, cc: cc} | ||||||
|  | 	go rslvr.run() | ||||||
|  | 	return rslvr, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r builder) Scheme() string { | ||||||
|  | 	return r.reg.String() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type resolvr struct { | ||||||
|  | 	reg    registry.Registry | ||||||
|  | 	target resolver.Target | ||||||
|  | 	cc     resolver.ClientConn | ||||||
|  | 	addrs  []resolver.Address | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *resolvr) run() { | ||||||
|  | 	if r.reg.String() == "noop" { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	var name, version string | ||||||
|  | 	parts := strings.Split(r.target.Endpoint, ":") | ||||||
|  | 	name = parts[0] | ||||||
|  | 	if len(parts) > 1 { | ||||||
|  | 		version = parts[1] | ||||||
|  | 	} | ||||||
|  | 	svc, err := r.reg.GetService(name) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	for _, v := range svc { | ||||||
|  | 		if v.Name != name || v.Version != version { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		for _, vv := range v.Nodes { | ||||||
|  | 			r.addrs = append(r.addrs, resolver.Address{Addr: vv.Address}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	r.cc.UpdateState(resolver.State{Addresses: r.addrs}) | ||||||
|  | 	w, err := r.reg.Watch(registry.WatchService(r.target.Endpoint)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	defer w.Stop() | ||||||
|  | 	for { | ||||||
|  | 		res, err := w.Next() | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// TODO(adphi): implement | ||||||
|  | 		switch res.Action { | ||||||
|  | 		case "create": | ||||||
|  |  | ||||||
|  | 		case "delete": | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  | 		r.cc.UpdateState(resolver.State{Addresses: r.addrs}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *resolvr) ResolveNow(options resolver.ResolveNowOptions) { | ||||||
|  | 	if r.reg.String() == "noop" { | ||||||
|  | 		r.cc.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: r.target.Endpoint}}}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r *resolvr) Close() {} | ||||||
| @@ -6,12 +6,15 @@ import ( | |||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
|  | 	"net" | ||||||
|  |  | ||||||
| 	"github.com/jinzhu/gorm" | 	"github.com/jinzhu/gorm" | ||||||
| 	"go.uber.org/multierr" | 	"go.uber.org/multierr" | ||||||
| 	"google.golang.org/grpc" | 	"google.golang.org/grpc" | ||||||
| 	 |  | ||||||
| 	"gitlab.bertha.cloud/partitio/lab/grpc/certs" | 	"gitlab.bertha.cloud/partitio/lab/grpc/certs" | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry" | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/utils/addr" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| /* | /* | ||||||
| @@ -27,32 +30,34 @@ GLOBAL OPTIONS: | |||||||
| 	--ca_cert				  CA_CERT | 	--ca_cert				  CA_CERT | ||||||
| 	--server_cert			  SERVER_CERT | 	--server_cert			  SERVER_CERT | ||||||
| 	--server_key			  SERVER_KEY | 	--server_key			  SERVER_KEY | ||||||
| 	 |  | ||||||
|    	--register_ttl            REGISTER_TTL |    	--register_ttl            REGISTER_TTL | ||||||
| 	--register_interval       REGISTER_INTERVAL | 	--register_interval       REGISTER_INTERVAL | ||||||
| 	 |  | ||||||
| 	--server_address          SERVER_ADDRESS	    | 	--server_address          SERVER_ADDRESS | ||||||
|    	--server_name             SERVER_NAME |    	--server_name             SERVER_NAME | ||||||
| 	    |  | ||||||
| 	--broker                  BROKER | 	--broker                  BROKER | ||||||
|    	--broker_address          BROKER_ADDRESS |    	--broker_address          BROKER_ADDRESS | ||||||
| 	    |  | ||||||
| 	--registry                REGISTRY | 	--registry                REGISTRY | ||||||
|    	--registry_address        REGISTRY_ADDRESS |    	--registry_address        REGISTRY_ADDRESS | ||||||
| 	    |  | ||||||
| 	--db_path                 DB_PATH | 	--db_path                 DB_PATH | ||||||
| */ | */ | ||||||
|  |  | ||||||
| type Options interface { | type Options interface { | ||||||
| 	Context() context.Context | 	Context() context.Context | ||||||
| 	Name() string | 	Name() string | ||||||
|  | 	Version() string | ||||||
| 	Address() string | 	Address() string | ||||||
| 	Reflection() bool | 	Reflection() bool | ||||||
| 	Secure() bool |  | ||||||
| 	CACert() string | 	CACert() string | ||||||
| 	Cert() string | 	Cert() string | ||||||
| 	Key() string | 	Key() string | ||||||
| 	TLSConfig() *tls.Config | 	TLSConfig() *tls.Config | ||||||
|  | 	Secure() bool | ||||||
|  | 	Registry() registry.Registry | ||||||
| 	DB() *gorm.DB | 	DB() *gorm.DB | ||||||
| 	BeforeStart() []func() error | 	BeforeStart() []func() error | ||||||
| 	AfterStart() []func() error | 	AfterStart() []func() error | ||||||
| @@ -90,6 +95,18 @@ func WithName(name string) Option { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func WithVersion(version string) Option { | ||||||
|  | 	return func(o *options) { | ||||||
|  | 		o.version = version | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func WithRegistry(registry registry.Registry) Option { | ||||||
|  | 	return func(o *options) { | ||||||
|  | 		o.registry = registry | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| // WithContext specifies a context for the service. | // WithContext specifies a context for the service. | ||||||
| // Can be used to signal shutdown of the service. | // Can be used to signal shutdown of the service. | ||||||
| // Can be used for extra option values. | // Can be used for extra option values. | ||||||
| @@ -215,6 +232,7 @@ func WithSubscriberInterceptor(w ...interface{}) Option { | |||||||
| type options struct { | type options struct { | ||||||
| 	ctx        context.Context | 	ctx        context.Context | ||||||
| 	name       string | 	name       string | ||||||
|  | 	version string | ||||||
| 	address    string | 	address    string | ||||||
| 	secure     bool | 	secure     bool | ||||||
| 	reflection bool | 	reflection bool | ||||||
| @@ -224,6 +242,8 @@ type options struct { | |||||||
| 	tlsConfig  *tls.Config | 	tlsConfig  *tls.Config | ||||||
| 	db         *gorm.DB | 	db         *gorm.DB | ||||||
|  |  | ||||||
|  | 	registry registry.Registry | ||||||
|  |  | ||||||
| 	beforeStart []func() error | 	beforeStart []func() error | ||||||
| 	afterStart  []func() error | 	afterStart  []func() error | ||||||
| 	beforeStop  []func() error | 	beforeStop  []func() error | ||||||
| @@ -237,13 +257,17 @@ type options struct { | |||||||
| 	clientInterceptors       []grpc.UnaryClientInterceptor | 	clientInterceptors       []grpc.UnaryClientInterceptor | ||||||
| 	streamClientInterceptors []grpc.StreamClientInterceptor | 	streamClientInterceptors []grpc.StreamClientInterceptor | ||||||
|  |  | ||||||
| 	error error | 	error   error | ||||||
| } | } | ||||||
|  |  | ||||||
| func (o *options) Name() string { | func (o *options) Name() string { | ||||||
| 	return o.name | 	return o.name | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (o *options) Version() string { | ||||||
|  | 	return o.version | ||||||
|  | } | ||||||
|  |  | ||||||
| func (o *options) Context() context.Context { | func (o *options) Context() context.Context { | ||||||
| 	return o.ctx | 	return o.ctx | ||||||
| } | } | ||||||
| @@ -252,12 +276,12 @@ func (o *options) Address() string { | |||||||
| 	return o.address | 	return o.address | ||||||
| } | } | ||||||
|  |  | ||||||
| func (o *options) Reflection() bool { | func (o *options) Registry() registry.Registry { | ||||||
| 	return o.reflection | 	return o.registry | ||||||
| } | } | ||||||
|  |  | ||||||
| func (o *options) Secure() bool { | func (o *options) Reflection() bool { | ||||||
| 	return o.secure | 	return o.reflection | ||||||
| } | } | ||||||
|  |  | ||||||
| func (o *options) CACert() string { | func (o *options) CACert() string { | ||||||
| @@ -276,6 +300,10 @@ func (o *options) TLSConfig() *tls.Config { | |||||||
| 	return o.tlsConfig | 	return o.tlsConfig | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (o *options) Secure() bool { | ||||||
|  | 	return o.secure | ||||||
|  | } | ||||||
|  |  | ||||||
| func (o *options) DB() *gorm.DB { | func (o *options) DB() *gorm.DB { | ||||||
| 	return o.db | 	return o.db | ||||||
| } | } | ||||||
| @@ -317,20 +345,36 @@ func (o *options) StreamClientInterceptors() []grpc.StreamClientInterceptor { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (o *options) parseTLSConfig() error { | func (o *options) parseTLSConfig() error { | ||||||
| 	if (o.tlsConfig != nil) { | 	if o.tlsConfig != nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	if !o.hasTLSConfig() { | 	if !o.hasTLSConfig() { | ||||||
| 		if !o.secure { | 		if !o.secure { | ||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
| 		cert, err := certs.New(o.address, "localhost", "127.0.0.1", o.name) | 		var hosts []string | ||||||
|  | 		if host, _, err := net.SplitHostPort(o.address); err == nil { | ||||||
|  | 			if len(host) == 0 { | ||||||
|  | 				hosts = addr.IPs() | ||||||
|  | 			} else { | ||||||
|  | 				hosts = []string{host} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		for i, h := range hosts { | ||||||
|  | 			a, err := addr.Extract(h) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			hosts[i] = a | ||||||
|  | 		} | ||||||
|  | 		// generate a certificate | ||||||
|  | 		cert, err := certs.New(hosts...) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		o.tlsConfig = &tls.Config{ | 		o.tlsConfig = &tls.Config{ | ||||||
| 			Certificates:       []tls.Certificate{cert}, | 			Certificates: []tls.Certificate{cert}, | ||||||
| 			InsecureSkipVerify: true, | 			ClientAuth:   tls.NoClientCert, | ||||||
| 		} | 		} | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -4,15 +4,25 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"net" | 	"net" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/google/uuid" | ||||||
| 	grpcmiddleware "github.com/grpc-ecosystem/go-grpc-middleware" | 	grpcmiddleware "github.com/grpc-ecosystem/go-grpc-middleware" | ||||||
| 	"github.com/jinzhu/gorm" | 	"github.com/jinzhu/gorm" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
| 	"go.uber.org/multierr" | 	"go.uber.org/multierr" | ||||||
| 	"google.golang.org/grpc" | 	"google.golang.org/grpc" | ||||||
| 	"google.golang.org/grpc/credentials" | 	"google.golang.org/grpc/credentials" | ||||||
| 	"google.golang.org/grpc/reflection" | 	"google.golang.org/grpc/reflection" | ||||||
|  |  | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry" | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/registry/noop" | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/utils/addr" | ||||||
|  | 	"gitlab.bertha.cloud/partitio/lab/grpc/utils/backoff" | ||||||
|  | 	net2 "gitlab.bertha.cloud/partitio/lab/grpc/utils/net" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type Service interface { | type Service interface { | ||||||
| @@ -37,6 +47,10 @@ type service struct { | |||||||
| 	list    net.Listener | 	list    net.Listener | ||||||
| 	mu      sync.Mutex | 	mu      sync.Mutex | ||||||
| 	running bool | 	running bool | ||||||
|  |  | ||||||
|  | 	id     string | ||||||
|  | 	regSvc *registry.Service | ||||||
|  | 	closed chan struct{} | ||||||
| } | } | ||||||
|  |  | ||||||
| func newService(opts ...Option) (*service, error) { | func newService(opts ...Option) (*service, error) { | ||||||
| @@ -44,6 +58,7 @@ func newService(opts ...Option) (*service, error) { | |||||||
| 	s := &service{ | 	s := &service{ | ||||||
| 		opts: parseFlags(NewOptions()), | 		opts: parseFlags(NewOptions()), | ||||||
| 		cmd:  cmd, | 		cmd:  cmd, | ||||||
|  | 		id:   uuid.New().String(), | ||||||
| 	} | 	} | ||||||
| 	s.mu.Lock() | 	s.mu.Lock() | ||||||
| 	defer s.mu.Unlock() | 	defer s.mu.Unlock() | ||||||
| @@ -62,6 +77,9 @@ func newService(opts ...Option) (*service, error) { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  | 	if s.opts.registry == nil { | ||||||
|  | 		s.opts.registry = noop.New() | ||||||
|  | 	} | ||||||
| 	var err error | 	var err error | ||||||
| 	s.list, err = net.Listen("tcp", s.opts.address) | 	s.list, err = net.Listen("tcp", s.opts.address) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -78,13 +96,12 @@ func newService(opts ...Option) (*service, error) { | |||||||
| 		return s.run() | 		return s.run() | ||||||
| 	} | 	} | ||||||
| 	gopts := []grpc.ServerOption{ | 	gopts := []grpc.ServerOption{ | ||||||
| 		grpc.Creds(credentials.NewTLS(s.opts.tlsConfig)), |  | ||||||
| 		grpc.UnaryInterceptor( | 		grpc.UnaryInterceptor( | ||||||
| 			grpcmiddleware.ChainUnaryServer(s.opts.serverInterceptors...), | 			grpcmiddleware.ChainUnaryServer(s.opts.serverInterceptors...), | ||||||
| 		), | 		), | ||||||
| 	} | 	} | ||||||
| 	if s.opts.tlsConfig != nil { | 	if s.opts.tlsConfig != nil { | ||||||
| 		gopts = append(gopts) | 		gopts = append(gopts, grpc.Creds(credentials.NewTLS(s.opts.tlsConfig))) | ||||||
| 	} | 	} | ||||||
| 	s.server = grpc.NewServer(append(gopts, s.opts.serverOpts...)...) | 	s.server = grpc.NewServer(append(gopts, s.opts.serverOpts...)...) | ||||||
| 	if s.opts.reflection { | 	if s.opts.reflection { | ||||||
| @@ -109,15 +126,95 @@ func (s *service) Cmd() *cobra.Command { | |||||||
| 	return s.cmd | 	return s.cmd | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (s *service) register() error { | ||||||
|  | 	const ( | ||||||
|  | 		defaultRegisterInterval = time.Second * 30 | ||||||
|  | 		defaultRegisterTTL      = time.Second * 90 | ||||||
|  | 	) | ||||||
|  | 	regFunc := func(service *registry.Service) error { | ||||||
|  | 		var regErr error | ||||||
|  |  | ||||||
|  | 		for i := 0; i < 3; i++ { | ||||||
|  | 			// set the ttl | ||||||
|  | 			rOpts := []registry.RegisterOption{registry.RegisterTTL(defaultRegisterTTL)} | ||||||
|  | 			// attempt to register | ||||||
|  | 			if err := s.opts.Registry().Register(service, rOpts...); err != nil { | ||||||
|  | 				// set the error | ||||||
|  | 				regErr = err | ||||||
|  | 				// backoff then retry | ||||||
|  | 				time.Sleep(backoff.Do(i + 1)) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			// success so nil error | ||||||
|  | 			regErr = nil | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return regErr | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	var advt, host, port string | ||||||
|  |  | ||||||
|  | 	//// check the advertise address first | ||||||
|  | 	//// if it exists then use it, otherwise | ||||||
|  | 	//// use the address | ||||||
|  | 	//if len(config.Advertise) > 0 { | ||||||
|  | 	//	advt = config.Advertise | ||||||
|  | 	//} else { | ||||||
|  | 		advt = s.opts.address | ||||||
|  | 	//} | ||||||
|  |  | ||||||
|  | 	if cnt := strings.Count(advt, ":"); cnt >= 1 { | ||||||
|  | 		// ipv6 address in format [host]:port or ipv4 host:port | ||||||
|  | 		host, port, err = net.SplitHostPort(advt) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		host = s.opts.address | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	addr, err := addr.Extract(host) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// register service | ||||||
|  | 	node := ®istry.Node{ | ||||||
|  | 		Id:      s.opts.name + "-" + s.id, | ||||||
|  | 		Address: net2.HostPort(addr, port), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	s.regSvc = ®istry.Service{ | ||||||
|  | 		Name: s.opts.name, | ||||||
|  | 		Version:   s.opts.version, | ||||||
|  | 		Nodes: []*registry.Node{node}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// register the service | ||||||
|  | 	if err := regFunc(s.regSvc); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (s *service) run() error { | func (s *service) run() error { | ||||||
| 	s.mu.Lock() | 	s.mu.Lock() | ||||||
|  | 	s.closed = make(chan struct{}) | ||||||
| 	for i := range s.opts.beforeStart { | 	for i := range s.opts.beforeStart { | ||||||
| 		if err := s.opts.beforeStart[i](); err != nil { | 		if err := s.opts.beforeStart[i](); err != nil { | ||||||
|  | 			s.mu.Unlock() | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	s.running = true |  | ||||||
| 	s.opts.address = s.list.Addr().String() | 	s.opts.address = s.list.Addr().String() | ||||||
|  |  | ||||||
|  | 	if err := s.register(); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	s.running = true | ||||||
| 	errs := make(chan error) | 	errs := make(chan error) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		errs <- s.server.Serve(s.list) | 		errs <- s.server.Serve(s.list) | ||||||
| @@ -130,7 +227,11 @@ func (s *service) run() error { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	s.mu.Unlock() | 	s.mu.Unlock() | ||||||
| 	return <-errs | 	if err :=  <-errs; err != nil{ | ||||||
|  | 		logrus.Error(err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *service) Start() error { | func (s *service) Start() error { | ||||||
| @@ -138,11 +239,15 @@ func (s *service) Start() error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (s *service) Stop() error { | func (s *service) Stop() error { | ||||||
|  | 	defer close(s.closed) | ||||||
| 	s.mu.Lock() | 	s.mu.Lock() | ||||||
| 	defer s.mu.Unlock() | 	defer s.mu.Unlock() | ||||||
| 	if !s.running { | 	if !s.running { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  | 	if err := s.opts.registry.Deregister(s.regSvc); err != nil { | ||||||
|  | 		logrus.Errorf("failed to deregister service: %v", err) | ||||||
|  | 	} | ||||||
| 	for i := range s.opts.beforeStop { | 	for i := range s.opts.beforeStop { | ||||||
| 		if err := s.opts.beforeStop[i](); err != nil { | 		if err := s.opts.beforeStop[i](); err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -153,9 +258,10 @@ func (s *service) Stop() error { | |||||||
| 	s.running = false | 	s.running = false | ||||||
| 	for i := range s.opts.afterStop { | 	for i := range s.opts.afterStop { | ||||||
| 		if err := s.opts.afterStop[i](); err != nil { | 		if err := s.opts.afterStop[i](); err != nil { | ||||||
|  | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	logrus.Info("server stopped") | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -164,5 +270,6 @@ func (s *service) Close() error { | |||||||
| 	if s.opts.db != nil { | 	if s.opts.db != nil { | ||||||
| 		err = multierr.Append(s.opts.db.Close(), err) | 		err = multierr.Append(s.opts.db.Close(), err) | ||||||
| 	} | 	} | ||||||
|  | 	<-s.closed | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										167
									
								
								utils/addr/addr.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								utils/addr/addr.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | |||||||
|  | package addr | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	privateBlocks []*net.IPNet | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	for _, b := range []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10", "fd00::/8"} { | ||||||
|  | 		if _, block, err := net.ParseCIDR(b); err == nil { | ||||||
|  | 			privateBlocks = append(privateBlocks, block) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isPrivateIP(ipAddr string) bool { | ||||||
|  | 	ip := net.ParseIP(ipAddr) | ||||||
|  | 	for _, priv := range privateBlocks { | ||||||
|  | 		if priv.Contains(ip) { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsLocal tells us whether an ip is local | ||||||
|  | func IsLocal(addr string) bool { | ||||||
|  | 	// extract the host | ||||||
|  | 	host, _, err := net.SplitHostPort(addr) | ||||||
|  | 	if err == nil { | ||||||
|  | 		addr = host | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check if its localhost | ||||||
|  | 	if addr == "localhost" { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// check against all local ips | ||||||
|  | 	for _, ip := range IPs() { | ||||||
|  | 		if addr == ip { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Extract returns a real ip | ||||||
|  | func Extract(addr string) (string, error) { | ||||||
|  | 	// if addr specified then its returned | ||||||
|  | 	if len(addr) > 0 && (addr != "0.0.0.0" && addr != "[::]" && addr != "::") { | ||||||
|  | 		return addr, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ifaces, err := net.Interfaces() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", fmt.Errorf("Failed to get interfaces! Err: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	//nolint:prealloc | ||||||
|  | 	var addrs []net.Addr | ||||||
|  | 	var loAddrs []net.Addr | ||||||
|  | 	for _, iface := range ifaces { | ||||||
|  | 		ifaceAddrs, err := iface.Addrs() | ||||||
|  | 		if err != nil { | ||||||
|  | 			// ignore error, interface can disappear from system | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if iface.Flags&net.FlagLoopback != 0 { | ||||||
|  | 			loAddrs = append(loAddrs, ifaceAddrs...) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		addrs = append(addrs, ifaceAddrs...) | ||||||
|  | 	} | ||||||
|  | 	addrs = append(addrs, loAddrs...) | ||||||
|  |  | ||||||
|  | 	var ipAddr string | ||||||
|  | 	var publicIP string | ||||||
|  |  | ||||||
|  | 	for _, rawAddr := range addrs { | ||||||
|  | 		var ip net.IP | ||||||
|  | 		switch addr := rawAddr.(type) { | ||||||
|  | 		case *net.IPAddr: | ||||||
|  | 			ip = addr.IP | ||||||
|  | 		case *net.IPNet: | ||||||
|  | 			ip = addr.IP | ||||||
|  | 		default: | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !isPrivateIP(ip.String()) { | ||||||
|  | 			publicIP = ip.String() | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		ipAddr = ip.String() | ||||||
|  | 		break | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// return private ip | ||||||
|  | 	if len(ipAddr) > 0 { | ||||||
|  | 		a := net.ParseIP(ipAddr) | ||||||
|  | 		if a == nil { | ||||||
|  | 			return "", fmt.Errorf("ip addr %s is invalid", ipAddr) | ||||||
|  | 		} | ||||||
|  | 		return a.String(), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// return public or virtual ip | ||||||
|  | 	if len(publicIP) > 0 { | ||||||
|  | 		a := net.ParseIP(publicIP) | ||||||
|  | 		if a == nil { | ||||||
|  | 			return "", fmt.Errorf("ip addr %s is invalid", publicIP) | ||||||
|  | 		} | ||||||
|  | 		return a.String(), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return "", fmt.Errorf("No IP address found, and explicit IP not provided") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IPs returns all known ips | ||||||
|  | func IPs() []string { | ||||||
|  | 	ifaces, err := net.Interfaces() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var ipAddrs []string | ||||||
|  |  | ||||||
|  | 	for _, i := range ifaces { | ||||||
|  | 		addrs, err := i.Addrs() | ||||||
|  | 		if err != nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, addr := range addrs { | ||||||
|  | 			var ip net.IP | ||||||
|  | 			switch v := addr.(type) { | ||||||
|  | 			case *net.IPNet: | ||||||
|  | 				ip = v.IP | ||||||
|  | 			case *net.IPAddr: | ||||||
|  | 				ip = v.IP | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if ip == nil { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// dont skip ipv6 addrs | ||||||
|  | 			/* | ||||||
|  | 				ip = ip.To4() | ||||||
|  | 				if ip == nil { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 			*/ | ||||||
|  |  | ||||||
|  | 			ipAddrs = append(ipAddrs, ip.String()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ipAddrs | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								utils/addr/addr_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								utils/addr/addr_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | package addr | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestIsLocal(t *testing.T) { | ||||||
|  | 	testData := []struct { | ||||||
|  | 		addr   string | ||||||
|  | 		expect bool | ||||||
|  | 	}{ | ||||||
|  | 		{"localhost", true}, | ||||||
|  | 		{"localhost:8080", true}, | ||||||
|  | 		{"127.0.0.1", true}, | ||||||
|  | 		{"127.0.0.1:1001", true}, | ||||||
|  | 		{"80.1.1.1", false}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, d := range testData { | ||||||
|  | 		res := IsLocal(d.addr) | ||||||
|  | 		if res != d.expect { | ||||||
|  | 			t.Fatalf("expected %t got %t", d.expect, res) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestExtractor(t *testing.T) { | ||||||
|  | 	testData := []struct { | ||||||
|  | 		addr   string | ||||||
|  | 		expect string | ||||||
|  | 		parse  bool | ||||||
|  | 	}{ | ||||||
|  | 		{"127.0.0.1", "127.0.0.1", false}, | ||||||
|  | 		{"10.0.0.1", "10.0.0.1", false}, | ||||||
|  | 		{"", "", true}, | ||||||
|  | 		{"0.0.0.0", "", true}, | ||||||
|  | 		{"[::]", "", true}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, d := range testData { | ||||||
|  | 		addr, err := Extract(d.addr) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Errorf("Unexpected error %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if d.parse { | ||||||
|  | 			ip := net.ParseIP(addr) | ||||||
|  | 			if ip == nil { | ||||||
|  | 				t.Error("Unexpected nil IP") | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 		} else if addr != d.expect { | ||||||
|  | 			t.Errorf("Expected %s got %s", d.expect, addr) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								utils/backoff/backoff.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								utils/backoff/backoff.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | // Package backoff provides backoff functionality | ||||||
|  | package backoff | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"math" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Do is a function x^e multiplied by a factor of 0.1 second. | ||||||
|  | // Result is limited to 2 minute. | ||||||
|  | func Do(attempts int) time.Duration { | ||||||
|  | 	if attempts > 13 { | ||||||
|  | 		return 2 * time.Minute | ||||||
|  | 	} | ||||||
|  | 	return time.Duration(math.Pow(float64(attempts), math.E)) * time.Millisecond * 100 | ||||||
|  | } | ||||||
							
								
								
									
										120
									
								
								utils/net/net.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								utils/net/net.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | package net | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // HostPort format addr and port suitable for dial | ||||||
|  | func HostPort(addr string, port interface{}) string { | ||||||
|  | 	host := addr | ||||||
|  | 	if strings.Count(addr, ":") > 0 { | ||||||
|  | 		host = fmt.Sprintf("[%s]", addr) | ||||||
|  | 	} | ||||||
|  | 	// when port is blank or 0, host is a queue name | ||||||
|  | 	if v, ok := port.(string); ok && v == "" { | ||||||
|  | 		return host | ||||||
|  | 	} else if v, ok := port.(int); ok && v == 0 && net.ParseIP(host) == nil { | ||||||
|  | 		return host | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fmt.Sprintf("%s:%v", host, port) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Listen takes addr:portmin-portmax and binds to the first available port | ||||||
|  | // Example: Listen("localhost:5000-6000", fn) | ||||||
|  | func Listen(addr string, fn func(string) (net.Listener, error)) (net.Listener, error) { | ||||||
|  |  | ||||||
|  | 	if strings.Count(addr, ":") == 1 && strings.Count(addr, "-") == 0 { | ||||||
|  | 		return fn(addr) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// host:port || host:min-max | ||||||
|  | 	host, ports, err := net.SplitHostPort(addr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// try to extract port range | ||||||
|  | 	prange := strings.Split(ports, "-") | ||||||
|  |  | ||||||
|  | 	// single port | ||||||
|  | 	if len(prange) < 2 { | ||||||
|  | 		return fn(addr) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// we have a port range | ||||||
|  |  | ||||||
|  | 	// extract min port | ||||||
|  | 	min, err := strconv.Atoi(prange[0]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.New("unable to extract port range") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// extract max port | ||||||
|  | 	max, err := strconv.Atoi(prange[1]) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.New("unable to extract port range") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// range the ports | ||||||
|  | 	for port := min; port <= max; port++ { | ||||||
|  | 		// try bind to host:port | ||||||
|  | 		ln, err := fn(HostPort(host, port)) | ||||||
|  | 		if err == nil { | ||||||
|  | 			return ln, nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// hit max port | ||||||
|  | 		if port == max { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// why are we here? | ||||||
|  | 	return nil, fmt.Errorf("unable to bind to %s", addr) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Proxy returns the proxy and the address if it exits | ||||||
|  | func Proxy(service string, address []string) (string, []string, bool) { | ||||||
|  | 	var hasProxy bool | ||||||
|  |  | ||||||
|  | 	// get proxy. we parse out address if present | ||||||
|  | 	if prx := os.Getenv("MICRO_PROXY"); len(prx) > 0 { | ||||||
|  | 		// default name | ||||||
|  | 		if prx == "service" { | ||||||
|  | 			prx = "go.micro.proxy" | ||||||
|  | 			address = nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// check if its an address | ||||||
|  | 		if v := strings.Split(prx, ":"); len(v) > 1 { | ||||||
|  | 			address = []string{prx} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		service = prx | ||||||
|  | 		hasProxy = true | ||||||
|  |  | ||||||
|  | 		return service, address, hasProxy | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if prx := os.Getenv("MICRO_NETWORK"); len(prx) > 0 { | ||||||
|  | 		// default name | ||||||
|  | 		if prx == "service" { | ||||||
|  | 			prx = "go.micro.network" | ||||||
|  | 		} | ||||||
|  | 		service = prx | ||||||
|  | 		hasProxy = true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if prx := os.Getenv("MICRO_NETWORK_ADDRESS"); len(prx) > 0 { | ||||||
|  | 		address = []string{prx} | ||||||
|  | 		hasProxy = true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return service, address, hasProxy | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								utils/net/net_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								utils/net/net_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | package net | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestListen(t *testing.T) { | ||||||
|  | 	fn := func(addr string) (net.Listener, error) { | ||||||
|  | 		return net.Listen("tcp", addr) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// try to create a number of listeners | ||||||
|  | 	for i := 0; i < 10; i++ { | ||||||
|  | 		l, err := Listen("localhost:10000-11000", fn) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatal(err) | ||||||
|  | 		} | ||||||
|  | 		defer l.Close() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// TODO nats case test | ||||||
|  | 	// natsAddr := "_INBOX.bID2CMRvlNp0vt4tgNBHWf" | ||||||
|  | 	// Expect addr DO NOT has extra ":" at the end! | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestProxyEnv checks whether we have proxy/network settings in env | ||||||
|  | func TestProxyEnv(t *testing.T) { | ||||||
|  | 	service := "foo" | ||||||
|  | 	address := []string{"bar"} | ||||||
|  |  | ||||||
|  | 	s, a, ok := Proxy(service, address) | ||||||
|  | 	if ok { | ||||||
|  | 		t.Fatal("Should not have proxy", s, a, ok) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	test := func(key, val, expectSrv, expectAddr string) { | ||||||
|  | 		// set env | ||||||
|  | 		os.Setenv(key, val) | ||||||
|  |  | ||||||
|  | 		s, a, ok := Proxy(service, address) | ||||||
|  | 		if !ok { | ||||||
|  | 			t.Fatal("Expected proxy") | ||||||
|  | 		} | ||||||
|  | 		if len(expectSrv) > 0 && s != expectSrv { | ||||||
|  | 			t.Fatal("Expected proxy service", expectSrv, "got", s) | ||||||
|  | 		} | ||||||
|  | 		if len(expectAddr) > 0 { | ||||||
|  | 			if len(a) == 0 || a[0] != expectAddr { | ||||||
|  | 				t.Fatal("Expected proxy address", expectAddr, "got", a) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		os.Unsetenv(key) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	test("MICRO_PROXY", "service", "go.micro.proxy", "") | ||||||
|  | 	test("MICRO_NETWORK", "service", "go.micro.network", "") | ||||||
|  | 	test("MICRO_NETWORK_ADDRESS", "10.0.0.1:8081", "", "10.0.0.1:8081") | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user