mirror of
				https://github.com/go-micro/go-micro.git
				synced 2025-10-30 23:27:41 +02:00 
			
		
		
		
	Add a selection of plugins to the core repo (#2755)
* WIP * fix: default memory registry, add registrations for mdns, nats * fix: same for broker * fix: add more * fix: http port * rename redis * chore: linting
This commit is contained in:
		| @@ -41,7 +41,7 @@ type Subscriber interface { | ||||
|  | ||||
| var ( | ||||
| 	// DefaultBroker is the default Broker. | ||||
| 	DefaultBroker = NewBroker() | ||||
| 	DefaultBroker = NewMemoryBroker() | ||||
| ) | ||||
|  | ||||
| func Init(opts ...Option) error { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| // Package broker provides a http based message broker | ||||
| package broker | ||||
| // Package http provides a http based message broker | ||||
| package http | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| @@ -16,6 +16,7 @@ import ( | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/google/uuid" | ||||
| 	"go-micro.dev/v5/broker" | ||||
| 	"go-micro.dev/v5/codec/json" | ||||
| 	merr "go-micro.dev/v5/errors" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| @@ -29,7 +30,7 @@ import ( | ||||
| 
 | ||||
| // HTTP Broker is a point to point async broker. | ||||
| type httpBroker struct { | ||||
| 	opts Options | ||||
| 	opts broker.Options | ||||
| 
 | ||||
| 	r registry.Registry | ||||
| 
 | ||||
| @@ -51,8 +52,8 @@ type httpBroker struct { | ||||
| } | ||||
| 
 | ||||
| type httpSubscriber struct { | ||||
| 	opts  SubscribeOptions | ||||
| 	fn    Handler | ||||
| 	opts  broker.SubscribeOptions | ||||
| 	fn    broker.Handler | ||||
| 	svc   *registry.Service | ||||
| 	hb    *httpBroker | ||||
| 	id    string | ||||
| @@ -61,7 +62,7 @@ type httpSubscriber struct { | ||||
| 
 | ||||
| type httpEvent struct { | ||||
| 	err error | ||||
| 	m   *Message | ||||
| 	m   *broker.Message | ||||
| 	t   string | ||||
| } | ||||
| 
 | ||||
| @@ -108,8 +109,8 @@ func newTransport(config *tls.Config) *http.Transport { | ||||
| 	return t | ||||
| } | ||||
| 
 | ||||
| func newHttpBroker(opts ...Option) Broker { | ||||
| 	options := *NewOptions(opts...) | ||||
| func newHttpBroker(opts ...broker.Option) broker.Broker { | ||||
| 	options := *broker.NewOptions(opts...) | ||||
| 
 | ||||
| 	options.Registry = registry.DefaultRegistry | ||||
| 	options.Codec = json.Marshaler{} | ||||
| @@ -161,7 +162,7 @@ func (h *httpEvent) Error() error { | ||||
| 	return h.err | ||||
| } | ||||
| 
 | ||||
| func (h *httpEvent) Message() *Message { | ||||
| func (h *httpEvent) Message() *broker.Message { | ||||
| 	return h.m | ||||
| } | ||||
| 
 | ||||
| @@ -169,7 +170,7 @@ func (h *httpEvent) Topic() string { | ||||
| 	return h.t | ||||
| } | ||||
| 
 | ||||
| func (h *httpSubscriber) Options() SubscribeOptions { | ||||
| func (h *httpSubscriber) Options() broker.SubscribeOptions { | ||||
| 	return h.opts | ||||
| } | ||||
| 
 | ||||
| @@ -308,7 +309,7 @@ func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	var m *Message | ||||
| 	var m *broker.Message | ||||
| 	if err = h.opts.Codec.Unmarshal(b, &m); err != nil { | ||||
| 		errr := merr.InternalServerError("go.micro.broker", "Error parsing request body: %v", err) | ||||
| 		w.WriteHeader(500) | ||||
| @@ -330,7 +331,7 @@ func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||||
| 	id := req.Form.Get("id") | ||||
| 
 | ||||
| 	//nolint:prealloc | ||||
| 	var subs []Handler | ||||
| 	var subs []broker.Handler | ||||
| 
 | ||||
| 	h.RLock() | ||||
| 	for _, subscriber := range h.subscribers[topic] { | ||||
| @@ -458,7 +459,7 @@ func (h *httpBroker) Disconnect() error { | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (h *httpBroker) Init(opts ...Option) error { | ||||
| func (h *httpBroker) Init(opts ...broker.Option) error { | ||||
| 	h.RLock() | ||||
| 	if h.running { | ||||
| 		h.RUnlock() | ||||
| @@ -505,13 +506,13 @@ func (h *httpBroker) Init(opts ...Option) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (h *httpBroker) Options() Options { | ||||
| func (h *httpBroker) Options() broker.Options { | ||||
| 	return h.opts | ||||
| } | ||||
| 
 | ||||
| func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) error { | ||||
| func (h *httpBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error { | ||||
| 	// create the message first | ||||
| 	m := &Message{ | ||||
| 	m := &broker.Message{ | ||||
| 		Header: make(map[string]string), | ||||
| 		Body:   msg.Body, | ||||
| 	} | ||||
| @@ -637,10 +638,10 @@ func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) { | ||||
| func (h *httpBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { | ||||
| 	var err error | ||||
| 	var host, port string | ||||
| 	options := NewSubscribeOptions(opts...) | ||||
| 	options := broker.NewSubscribeOptions(opts...) | ||||
| 
 | ||||
| 	// parse address for host, port | ||||
| 	host, port, err = net.SplitHostPort(h.Address()) | ||||
| @@ -705,7 +706,7 @@ func (h *httpBroker) String() string { | ||||
| 	return "http" | ||||
| } | ||||
| 
 | ||||
| // NewBroker returns a new http broker. | ||||
| func NewBroker(opts ...Option) Broker { | ||||
| // NewHttpBroker returns a new http broker. | ||||
| func NewHttpBroker(opts ...broker.Option) broker.Broker { | ||||
| 	return newHttpBroker(opts...) | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package broker_test | ||||
| package http_test | ||||
| 
 | ||||
| import ( | ||||
| 	"sync" | ||||
| @@ -7,6 +7,7 @@ import ( | ||||
| 
 | ||||
| 	"github.com/google/uuid" | ||||
| 	"go-micro.dev/v5/broker" | ||||
| 	"go-micro.dev/v5/broker/http" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
| 
 | ||||
| @@ -60,7 +61,7 @@ func sub(b *testing.B, c int) { | ||||
| 	b.StopTimer() | ||||
| 	m := newTestRegistry() | ||||
| 
 | ||||
| 	brker := broker.NewBroker(broker.Registry(m)) | ||||
| 	brker := http.NewHttpBroker(broker.Registry(m)) | ||||
| 	topic := uuid.New().String() | ||||
| 
 | ||||
| 	if err := brker.Init(); err != nil { | ||||
| @@ -121,7 +122,7 @@ func sub(b *testing.B, c int) { | ||||
| func pub(b *testing.B, c int) { | ||||
| 	b.StopTimer() | ||||
| 	m := newTestRegistry() | ||||
| 	brk := broker.NewBroker(broker.Registry(m)) | ||||
| 	brk := http.NewHttpBroker(broker.Registry(m)) | ||||
| 	topic := uuid.New().String() | ||||
| 
 | ||||
| 	if err := brk.Init(); err != nil { | ||||
| @@ -190,7 +191,7 @@ func pub(b *testing.B, c int) { | ||||
| 
 | ||||
| func TestBroker(t *testing.T) { | ||||
| 	m := newTestRegistry() | ||||
| 	b := broker.NewBroker(broker.Registry(m)) | ||||
| 	b := http.NewHttpBroker(broker.Registry(m)) | ||||
| 
 | ||||
| 	if err := b.Init(); err != nil { | ||||
| 		t.Fatalf("Unexpected init error: %v", err) | ||||
| @@ -239,7 +240,7 @@ func TestBroker(t *testing.T) { | ||||
| 
 | ||||
| func TestConcurrentSubBroker(t *testing.T) { | ||||
| 	m := newTestRegistry() | ||||
| 	b := broker.NewBroker(broker.Registry(m)) | ||||
| 	b := http.NewHttpBroker(broker.Registry(m)) | ||||
| 
 | ||||
| 	if err := b.Init(); err != nil { | ||||
| 		t.Fatalf("Unexpected init error: %v", err) | ||||
| @@ -298,7 +299,7 @@ func TestConcurrentSubBroker(t *testing.T) { | ||||
| 
 | ||||
| func TestConcurrentPubBroker(t *testing.T) { | ||||
| 	m := newTestRegistry() | ||||
| 	b := broker.NewBroker(broker.Registry(m)) | ||||
| 	b := http.NewHttpBroker(broker.Registry(m)) | ||||
| 
 | ||||
| 	if err := b.Init(); err != nil { | ||||
| 		t.Fatalf("Unexpected init error: %v", err) | ||||
							
								
								
									
										17
									
								
								broker/nats/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								broker/nats/context.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| package nats | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"go-micro.dev/v5/broker" | ||||
| ) | ||||
|  | ||||
| // setBrokerOption returns a function to setup a context with given value. | ||||
| func setBrokerOption(k, v interface{}) broker.Option { | ||||
| 	return func(o *broker.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, k, v) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										315
									
								
								broker/nats/nats.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								broker/nats/nats.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,315 @@ | ||||
| // Package nats provides a NATS broker | ||||
| package nats | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	natsp "github.com/nats-io/nats.go" | ||||
| 	"go-micro.dev/v5/broker" | ||||
| 	"go-micro.dev/v5/codec/json" | ||||
| 	"go-micro.dev/v5/logger" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| type natsBroker struct { | ||||
| 	sync.Once | ||||
| 	sync.RWMutex | ||||
|  | ||||
| 	// indicate if we're connected | ||||
| 	connected bool | ||||
|  | ||||
| 	addrs []string | ||||
| 	conn  *natsp.Conn | ||||
| 	opts  broker.Options | ||||
| 	nopts natsp.Options | ||||
|  | ||||
| 	// should we drain the connection | ||||
| 	drain   bool | ||||
| 	closeCh chan (error) | ||||
| } | ||||
|  | ||||
| type subscriber struct { | ||||
| 	s    *natsp.Subscription | ||||
| 	opts broker.SubscribeOptions | ||||
| } | ||||
|  | ||||
| type publication struct { | ||||
| 	t   string | ||||
| 	err error | ||||
| 	m   *broker.Message | ||||
| } | ||||
|  | ||||
| func (p *publication) Topic() string { | ||||
| 	return p.t | ||||
| } | ||||
|  | ||||
| func (p *publication) Message() *broker.Message { | ||||
| 	return p.m | ||||
| } | ||||
|  | ||||
| func (p *publication) Ack() error { | ||||
| 	// nats does not support acking | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (p *publication) Error() error { | ||||
| 	return p.err | ||||
| } | ||||
|  | ||||
| func (s *subscriber) Options() broker.SubscribeOptions { | ||||
| 	return s.opts | ||||
| } | ||||
|  | ||||
| func (s *subscriber) Topic() string { | ||||
| 	return s.s.Subject | ||||
| } | ||||
|  | ||||
| func (s *subscriber) Unsubscribe() error { | ||||
| 	return s.s.Unsubscribe() | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) Address() string { | ||||
| 	if n.conn != nil && n.conn.IsConnected() { | ||||
| 		return n.conn.ConnectedUrl() | ||||
| 	} | ||||
|  | ||||
| 	if len(n.addrs) > 0 { | ||||
| 		return n.addrs[0] | ||||
| 	} | ||||
|  | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) setAddrs(addrs []string) []string { | ||||
| 	//nolint:prealloc | ||||
| 	var cAddrs []string | ||||
| 	for _, addr := range addrs { | ||||
| 		if len(addr) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 		if !strings.HasPrefix(addr, "nats://") { | ||||
| 			addr = "nats://" + addr | ||||
| 		} | ||||
| 		cAddrs = append(cAddrs, addr) | ||||
| 	} | ||||
| 	if len(cAddrs) == 0 { | ||||
| 		cAddrs = []string{natsp.DefaultURL} | ||||
| 	} | ||||
| 	return cAddrs | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) Connect() error { | ||||
| 	n.Lock() | ||||
| 	defer n.Unlock() | ||||
|  | ||||
| 	if n.connected { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	status := natsp.CLOSED | ||||
| 	if n.conn != nil { | ||||
| 		status = n.conn.Status() | ||||
| 	} | ||||
|  | ||||
| 	switch status { | ||||
| 	case natsp.CONNECTED, natsp.RECONNECTING, natsp.CONNECTING: | ||||
| 		n.connected = true | ||||
| 		return nil | ||||
| 	default: // DISCONNECTED or CLOSED or DRAINING | ||||
| 		opts := n.nopts | ||||
| 		opts.Servers = n.addrs | ||||
| 		opts.Secure = n.opts.Secure | ||||
| 		opts.TLSConfig = n.opts.TLSConfig | ||||
|  | ||||
| 		// secure might not be set | ||||
| 		if n.opts.TLSConfig != nil { | ||||
| 			opts.Secure = true | ||||
| 		} | ||||
|  | ||||
| 		c, err := opts.Connect() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		n.conn = c | ||||
| 		n.connected = true | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) Disconnect() error { | ||||
| 	n.Lock() | ||||
| 	defer n.Unlock() | ||||
|  | ||||
| 	// drain the connection if specified | ||||
| 	if n.drain { | ||||
| 		n.conn.Drain() | ||||
| 		n.closeCh <- nil | ||||
| 	} | ||||
|  | ||||
| 	// close the client connection | ||||
| 	n.conn.Close() | ||||
|  | ||||
| 	// set not connected | ||||
| 	n.connected = false | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) Init(opts ...broker.Option) error { | ||||
| 	n.setOption(opts...) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) Options() broker.Options { | ||||
| 	return n.opts | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error { | ||||
| 	n.RLock() | ||||
| 	defer n.RUnlock() | ||||
|  | ||||
| 	if n.conn == nil { | ||||
| 		return errors.New("not connected") | ||||
| 	} | ||||
|  | ||||
| 	b, err := n.opts.Codec.Marshal(msg) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return n.conn.Publish(topic, b) | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { | ||||
| 	n.RLock() | ||||
| 	if n.conn == nil { | ||||
| 		n.RUnlock() | ||||
| 		return nil, errors.New("not connected") | ||||
| 	} | ||||
| 	n.RUnlock() | ||||
|  | ||||
| 	opt := broker.SubscribeOptions{ | ||||
| 		AutoAck: true, | ||||
| 		Context: context.Background(), | ||||
| 	} | ||||
|  | ||||
| 	for _, o := range opts { | ||||
| 		o(&opt) | ||||
| 	} | ||||
|  | ||||
| 	fn := func(msg *natsp.Msg) { | ||||
| 		var m broker.Message | ||||
| 		pub := &publication{t: msg.Subject} | ||||
| 		eh := n.opts.ErrorHandler | ||||
| 		err := n.opts.Codec.Unmarshal(msg.Data, &m) | ||||
| 		pub.err = err | ||||
| 		pub.m = &m | ||||
| 		if err != nil { | ||||
| 			m.Body = msg.Data | ||||
| 			n.opts.Logger.Log(logger.ErrorLevel, err) | ||||
| 			if eh != nil { | ||||
| 				eh(pub) | ||||
| 			} | ||||
| 			return | ||||
| 		} | ||||
| 		if err := handler(pub); err != nil { | ||||
| 			pub.err = err | ||||
| 			n.opts.Logger.Log(logger.ErrorLevel, err) | ||||
| 			if eh != nil { | ||||
| 				eh(pub) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var sub *natsp.Subscription | ||||
| 	var err error | ||||
|  | ||||
| 	n.RLock() | ||||
| 	if len(opt.Queue) > 0 { | ||||
| 		sub, err = n.conn.QueueSubscribe(topic, opt.Queue, fn) | ||||
| 	} else { | ||||
| 		sub, err = n.conn.Subscribe(topic, fn) | ||||
| 	} | ||||
| 	n.RUnlock() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &subscriber{s: sub, opts: opt}, nil | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) String() string { | ||||
| 	return "nats" | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) setOption(opts ...broker.Option) { | ||||
| 	for _, o := range opts { | ||||
| 		o(&n.opts) | ||||
| 	} | ||||
|  | ||||
| 	n.Once.Do(func() { | ||||
| 		n.nopts = natsp.GetDefaultOptions() | ||||
| 	}) | ||||
|  | ||||
| 	if nopts, ok := n.opts.Context.Value(optionsKey{}).(natsp.Options); ok { | ||||
| 		n.nopts = nopts | ||||
| 	} | ||||
|  | ||||
| 	// broker.Options have higher priority than nats.Options | ||||
| 	// only if Addrs, Secure or TLSConfig were not set through a broker.Option | ||||
| 	// we read them from nats.Option | ||||
| 	if len(n.opts.Addrs) == 0 { | ||||
| 		n.opts.Addrs = n.nopts.Servers | ||||
| 	} | ||||
|  | ||||
| 	if !n.opts.Secure { | ||||
| 		n.opts.Secure = n.nopts.Secure | ||||
| 	} | ||||
|  | ||||
| 	if n.opts.TLSConfig == nil { | ||||
| 		n.opts.TLSConfig = n.nopts.TLSConfig | ||||
| 	} | ||||
| 	n.addrs = n.setAddrs(n.opts.Addrs) | ||||
|  | ||||
| 	if n.opts.Context.Value(drainConnectionKey{}) != nil { | ||||
| 		n.drain = true | ||||
| 		n.closeCh = make(chan error) | ||||
| 		n.nopts.ClosedCB = n.onClose | ||||
| 		n.nopts.AsyncErrorCB = n.onAsyncError | ||||
| 		n.nopts.DisconnectedErrCB = n.onDisconnectedError | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) onClose(conn *natsp.Conn) { | ||||
| 	n.closeCh <- nil | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) onAsyncError(conn *natsp.Conn, sub *natsp.Subscription, err error) { | ||||
| 	// There are kinds of different async error nats might callback, but we are interested | ||||
| 	// in ErrDrainTimeout only here. | ||||
| 	if err == natsp.ErrDrainTimeout { | ||||
| 		n.closeCh <- err | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (n *natsBroker) onDisconnectedError(conn *natsp.Conn, err error) { | ||||
| 	n.closeCh <- err | ||||
| } | ||||
|  | ||||
| func NewNatsBroker(opts ...broker.Option) broker.Broker { | ||||
| 	options := broker.Options{ | ||||
| 		// Default codec | ||||
| 		Codec:    json.Marshaler{}, | ||||
| 		Context:  context.Background(), | ||||
| 		Registry: registry.DefaultRegistry, | ||||
| 		Logger:   logger.DefaultLogger, | ||||
| 	} | ||||
|  | ||||
| 	n := &natsBroker{ | ||||
| 		opts: options, | ||||
| 	} | ||||
| 	n.setOption(opts...) | ||||
|  | ||||
| 	return n | ||||
| } | ||||
							
								
								
									
										96
									
								
								broker/nats/nats_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								broker/nats/nats_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| package nats | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	natsp "github.com/nats-io/nats.go" | ||||
| 	"go-micro.dev/v5/broker" | ||||
| ) | ||||
|  | ||||
| var addrTestCases = []struct { | ||||
| 	name        string | ||||
| 	description string | ||||
| 	addrs       map[string]string // expected address : set address | ||||
| }{ | ||||
| 	{ | ||||
| 		"brokerOpts", | ||||
| 		"set broker addresses through a broker.Option in constructor", | ||||
| 		map[string]string{ | ||||
| 			"nats://192.168.10.1:5222": "192.168.10.1:5222", | ||||
| 			"nats://10.20.10.0:4222":   "10.20.10.0:4222"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		"brokerInit", | ||||
| 		"set broker addresses through a broker.Option in broker.Init()", | ||||
| 		map[string]string{ | ||||
| 			"nats://192.168.10.1:5222": "192.168.10.1:5222", | ||||
| 			"nats://10.20.10.0:4222":   "10.20.10.0:4222"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		"natsOpts", | ||||
| 		"set broker addresses through the nats.Option in constructor", | ||||
| 		map[string]string{ | ||||
| 			"nats://192.168.10.1:5222": "192.168.10.1:5222", | ||||
| 			"nats://10.20.10.0:4222":   "10.20.10.0:4222"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		"default", | ||||
| 		"check if default Address is set correctly", | ||||
| 		map[string]string{ | ||||
| 			"nats://127.0.0.1:4222": "", | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| // TestInitAddrs tests issue #100. Ensures that if the addrs is set by an option in init it will be used. | ||||
| func TestInitAddrs(t *testing.T) { | ||||
| 	for _, tc := range addrTestCases { | ||||
| 		t.Run(fmt.Sprintf("%s: %s", tc.name, tc.description), func(t *testing.T) { | ||||
| 			var br broker.Broker | ||||
| 			var addrs []string | ||||
|  | ||||
| 			for _, addr := range tc.addrs { | ||||
| 				addrs = append(addrs, addr) | ||||
| 			} | ||||
|  | ||||
| 			switch tc.name { | ||||
| 			case "brokerOpts": | ||||
| 				// we know that there are just two addrs in the dict | ||||
| 				br = NewNatsBroker(broker.Addrs(addrs[0], addrs[1])) | ||||
| 				br.Init() | ||||
| 			case "brokerInit": | ||||
| 				br = NewNatsBroker() | ||||
| 				// we know that there are just two addrs in the dict | ||||
| 				br.Init(broker.Addrs(addrs[0], addrs[1])) | ||||
| 			case "natsOpts": | ||||
| 				nopts := natsp.GetDefaultOptions() | ||||
| 				nopts.Servers = addrs | ||||
| 				br = NewNatsBroker(Options(nopts)) | ||||
| 				br.Init() | ||||
| 			case "default": | ||||
| 				br = NewNatsBroker() | ||||
| 				br.Init() | ||||
| 			} | ||||
|  | ||||
| 			natsBroker, ok := br.(*natsBroker) | ||||
| 			if !ok { | ||||
| 				t.Fatal("Expected broker to be of types *natsBroker") | ||||
| 			} | ||||
| 			// check if the same amount of addrs we set has actually been set, default | ||||
| 			// have only 1 address nats://127.0.0.1:4222 (current nats code) or | ||||
| 			// nats://localhost:4222 (older code version) | ||||
| 			if len(natsBroker.addrs) != len(tc.addrs) && tc.name != "default" { | ||||
| 				t.Errorf("Expected Addr count = %d, Actual Addr count = %d", | ||||
| 					len(natsBroker.addrs), len(tc.addrs)) | ||||
| 			} | ||||
|  | ||||
| 			for _, addr := range natsBroker.addrs { | ||||
| 				_, ok := tc.addrs[addr] | ||||
| 				if !ok { | ||||
| 					t.Errorf("Expected '%s' has not been set", addr) | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										19
									
								
								broker/nats/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								broker/nats/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| package nats | ||||
|  | ||||
| import ( | ||||
| 	natsp "github.com/nats-io/nats.go" | ||||
| 	"go-micro.dev/v5/broker" | ||||
| ) | ||||
|  | ||||
| type optionsKey struct{} | ||||
| type drainConnectionKey struct{} | ||||
|  | ||||
| // Options accepts nats.Options. | ||||
| func Options(opts natsp.Options) broker.Option { | ||||
| 	return setBrokerOption(optionsKey{}, opts) | ||||
| } | ||||
|  | ||||
| // DrainConnection will drain subscription on close. | ||||
| func DrainConnection() broker.Option { | ||||
| 	return setBrokerOption(drainConnectionKey{}, struct{}{}) | ||||
| } | ||||
							
								
								
									
										48
									
								
								cache/redis/options.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								cache/redis/options.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	rclient "github.com/go-redis/redis/v8" | ||||
| 	"go-micro.dev/v5/cache" | ||||
| ) | ||||
|  | ||||
| type redisOptionsContextKey struct{} | ||||
|  | ||||
| // WithRedisOptions sets advanced options for redis. | ||||
| func WithRedisOptions(options rclient.UniversalOptions) cache.Option { | ||||
| 	return func(o *cache.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
|  | ||||
| 		o.Context = context.WithValue(o.Context, redisOptionsContextKey{}, options) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newUniversalClient(options cache.Options) rclient.UniversalClient { | ||||
| 	if options.Context == nil { | ||||
| 		options.Context = context.Background() | ||||
| 	} | ||||
|  | ||||
| 	opts, ok := options.Context.Value(redisOptionsContextKey{}).(rclient.UniversalOptions) | ||||
| 	if !ok { | ||||
| 		addr := "redis://127.0.0.1:6379" | ||||
| 		if len(options.Address) > 0 { | ||||
| 			addr = options.Address | ||||
| 		} | ||||
|  | ||||
| 		redisOptions, err := rclient.ParseURL(addr) | ||||
| 		if err != nil { | ||||
| 			redisOptions = &rclient.Options{Addr: addr} | ||||
| 		} | ||||
|  | ||||
| 		return rclient.NewClient(redisOptions) | ||||
| 	} | ||||
|  | ||||
| 	if len(opts.Addrs) == 0 && len(options.Address) > 0 { | ||||
| 		opts.Addrs = []string{options.Address} | ||||
| 	} | ||||
|  | ||||
| 	return rclient.NewUniversalClient(&opts) | ||||
| } | ||||
							
								
								
									
										139
									
								
								cache/redis/options_test.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								cache/redis/options_test.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	rclient "github.com/go-redis/redis/v8" | ||||
| 	"go-micro.dev/v5/cache" | ||||
| ) | ||||
|  | ||||
| func Test_newUniversalClient(t *testing.T) { | ||||
| 	type fields struct { | ||||
| 		options cache.Options | ||||
| 	} | ||||
| 	type wantValues struct { | ||||
| 		username string | ||||
| 		password string | ||||
| 		address  string | ||||
| 	} | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name   string | ||||
| 		fields fields | ||||
| 		want   wantValues | ||||
| 	}{ | ||||
| 		{name: "No Url", fields: fields{options: cache.Options{}}, | ||||
| 			want: wantValues{ | ||||
| 				username: "", | ||||
| 				password: "", | ||||
| 				address:  "127.0.0.1:6379", | ||||
| 			}}, | ||||
| 		{name: "legacy Url", fields: fields{options: cache.Options{Address: "127.0.0.1:6379"}}, | ||||
| 			want: wantValues{ | ||||
| 				username: "", | ||||
| 				password: "", | ||||
| 				address:  "127.0.0.1:6379", | ||||
| 			}}, | ||||
| 		{name: "New Url", fields: fields{options: cache.Options{Address: "redis://127.0.0.1:6379"}}, | ||||
| 			want: wantValues{ | ||||
| 				username: "", | ||||
| 				password: "", | ||||
| 				address:  "127.0.0.1:6379", | ||||
| 			}}, | ||||
| 		{name: "Url with Pwd", fields: fields{options: cache.Options{Address: "redis://:password@redis:6379"}}, | ||||
| 			want: wantValues{ | ||||
| 				username: "", | ||||
| 				password: "password", | ||||
| 				address:  "redis:6379", | ||||
| 			}}, | ||||
| 		{name: "Url with username and Pwd", fields: fields{ | ||||
| 			options: cache.Options{Address: "redis://username:password@redis:6379"}}, | ||||
| 			want: wantValues{ | ||||
| 				username: "username", | ||||
| 				password: "password", | ||||
| 				address:  "redis:6379", | ||||
| 			}}, | ||||
|  | ||||
| 		{name: "Sentinel Failover client", fields: fields{ | ||||
| 			options: cache.Options{ | ||||
| 				Context: context.WithValue( | ||||
| 					context.TODO(), redisOptionsContextKey{}, | ||||
| 					rclient.UniversalOptions{MasterName: "master-name"}), | ||||
| 			}}, | ||||
| 			want: wantValues{ | ||||
| 				username: "", | ||||
| 				password: "", | ||||
| 				address:  "FailoverClient", // <- Placeholder set by NewFailoverClient | ||||
| 			}}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			univClient := newUniversalClient(tt.fields.options) | ||||
| 			client, ok := univClient.(*rclient.Client) | ||||
| 			if !ok { | ||||
| 				t.Errorf("newUniversalClient() expect a *redis.Client") | ||||
| 				return | ||||
| 			} | ||||
| 			if client.Options().Addr != tt.want.address { | ||||
| 				t.Errorf("newUniversalClient() Address = %v, want address %v", client.Options().Addr, tt.want.address) | ||||
| 			} | ||||
| 			if client.Options().Password != tt.want.password { | ||||
| 				t.Errorf("newUniversalClient() password = %v, want password %v", client.Options().Password, tt.want.password) | ||||
| 			} | ||||
| 			if client.Options().Username != tt.want.username { | ||||
| 				t.Errorf("newUniversalClient() username = %v, want username %v", client.Options().Username, tt.want.username) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func Test_newUniversalClientCluster(t *testing.T) { | ||||
| 	type fields struct { | ||||
| 		options cache.Options | ||||
| 	} | ||||
| 	type wantValues struct { | ||||
| 		username string | ||||
| 		password string | ||||
| 		addrs    []string | ||||
| 	} | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		name   string | ||||
| 		fields fields | ||||
| 		want   wantValues | ||||
| 	}{ | ||||
| 		{name: "Addrs in redis options", fields: fields{ | ||||
| 			options: cache.Options{ | ||||
| 				Address: "127.0.0.1:6379", // <- ignored | ||||
| 				Context: context.WithValue( | ||||
| 					context.TODO(), redisOptionsContextKey{}, | ||||
| 					rclient.UniversalOptions{Addrs: []string{"127.0.0.1:6381", "127.0.0.1:6382"}}), | ||||
| 			}}, | ||||
| 			want: wantValues{ | ||||
| 				username: "", | ||||
| 				password: "", | ||||
| 				addrs:    []string{"127.0.0.1:6381", "127.0.0.1:6382"}, | ||||
| 			}}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			univClient := newUniversalClient(tt.fields.options) | ||||
| 			client, ok := univClient.(*rclient.ClusterClient) | ||||
| 			if !ok { | ||||
| 				t.Errorf("newUniversalClient() expect a *redis.ClusterClient") | ||||
| 				return | ||||
| 			} | ||||
| 			if !reflect.DeepEqual(client.Options().Addrs, tt.want.addrs) { | ||||
| 				t.Errorf("newUniversalClient() Addrs = %v, want addrs %v", client.Options().Addrs, tt.want.addrs) | ||||
| 			} | ||||
| 			if client.Options().Password != tt.want.password { | ||||
| 				t.Errorf("newUniversalClient() password = %v, want password %v", client.Options().Password, tt.want.password) | ||||
| 			} | ||||
| 			if client.Options().Username != tt.want.username { | ||||
| 				t.Errorf("newUniversalClient() username = %v, want username %v", client.Options().Username, tt.want.username) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										57
									
								
								cache/redis/redis.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								cache/redis/redis.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"time" | ||||
|  | ||||
| 	rclient "github.com/go-redis/redis/v8" | ||||
| 	"go-micro.dev/v5/cache" | ||||
| ) | ||||
|  | ||||
| // NewRedisCache returns a new redis cache. | ||||
| func NewRedisCache(opts ...cache.Option) cache.Cache { | ||||
| 	options := cache.NewOptions(opts...) | ||||
| 	return &redisCache{ | ||||
| 		opts:   options, | ||||
| 		client: newUniversalClient(options), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type redisCache struct { | ||||
| 	opts   cache.Options | ||||
| 	client rclient.UniversalClient | ||||
| } | ||||
|  | ||||
| func (c *redisCache) Get(ctx context.Context, key string) (interface{}, time.Time, error) { | ||||
| 	val, err := c.client.Get(ctx, key).Bytes() | ||||
| 	if err != nil && err == rclient.Nil { | ||||
| 		return nil, time.Time{}, cache.ErrKeyNotFound | ||||
| 	} else if err != nil { | ||||
| 		return nil, time.Time{}, err | ||||
| 	} | ||||
|  | ||||
| 	dur, err := c.client.TTL(ctx, key).Result() | ||||
| 	if err != nil { | ||||
| 		return nil, time.Time{}, err | ||||
| 	} | ||||
| 	if dur == -1 { | ||||
| 		return val, time.Unix(1<<63-1, 0), nil | ||||
| 	} | ||||
| 	if dur == -2 { | ||||
| 		return val, time.Time{}, cache.ErrItemExpired | ||||
| 	} | ||||
|  | ||||
| 	return val, time.Now().Add(dur), nil | ||||
| } | ||||
|  | ||||
| func (c *redisCache) Put(ctx context.Context, key string, val interface{}, dur time.Duration) error { | ||||
| 	return c.client.Set(ctx, key, val, dur).Err() | ||||
| } | ||||
|  | ||||
| func (c *redisCache) Delete(ctx context.Context, key string) error { | ||||
| 	return c.client.Del(ctx, key).Err() | ||||
| } | ||||
|  | ||||
| func (m *redisCache) String() string { | ||||
| 	return "redis" | ||||
| } | ||||
							
								
								
									
										88
									
								
								cache/redis/redis_test.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								cache/redis/redis_test.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| package redis | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"go-micro.dev/v5/cache" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ctx              = context.TODO() | ||||
| 	key  string      = "redistestkey" | ||||
| 	val  interface{} = "hello go-micro" | ||||
| 	addr             = cache.WithAddress("redis://127.0.0.1:6379") | ||||
| ) | ||||
|  | ||||
| // TestMemCache tests the in-memory cache implementation. | ||||
| func TestCache(t *testing.T) { | ||||
| 	if len(os.Getenv("LOCAL")) == 0 { | ||||
| 		t.Skip() | ||||
| 	} | ||||
|  | ||||
| 	t.Run("CacheGetMiss", func(t *testing.T) { | ||||
| 		if _, _, err := NewRedisCache(addr).Get(ctx, key); err == nil { | ||||
| 			t.Error("expected to get no value from cache") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("CacheGetHit", func(t *testing.T) { | ||||
| 		c := NewRedisCache(addr) | ||||
|  | ||||
| 		if err := c.Put(ctx, key, val, 0); err != nil { | ||||
| 			t.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		if a, _, err := c.Get(ctx, key); err != nil { | ||||
| 			t.Errorf("Expected a value, got err: %s", err) | ||||
| 		} else if string(a.([]byte)) != val { | ||||
| 			t.Errorf("Expected '%v', got '%v'", val, a) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("CacheGetExpired", func(t *testing.T) { | ||||
| 		c := NewRedisCache(addr) | ||||
| 		d := 20 * time.Millisecond | ||||
|  | ||||
| 		if err := c.Put(ctx, key, val, d); err != nil { | ||||
| 			t.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		<-time.After(25 * time.Millisecond) | ||||
| 		if _, _, err := c.Get(ctx, key); err == nil { | ||||
| 			t.Error("expected to get no value from cache") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("CacheGetValid", func(t *testing.T) { | ||||
| 		c := NewRedisCache(addr) | ||||
| 		e := 25 * time.Millisecond | ||||
|  | ||||
| 		if err := c.Put(ctx, key, val, e); err != nil { | ||||
| 			t.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		<-time.After(20 * time.Millisecond) | ||||
| 		if _, _, err := c.Get(ctx, key); err != nil { | ||||
| 			t.Errorf("expected a value, got err: %s", err) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("CacheDeleteHit", func(t *testing.T) { | ||||
| 		c := NewRedisCache(addr) | ||||
|  | ||||
| 		if err := c.Put(ctx, key, val, 0); err != nil { | ||||
| 			t.Error(err) | ||||
| 		} | ||||
|  | ||||
| 		if err := c.Delete(ctx, key); err != nil { | ||||
| 			t.Errorf("Expected to delete an item, got err: %s", err) | ||||
| 		} | ||||
|  | ||||
| 		if _, _, err := c.Get(ctx, key); err == nil { | ||||
| 			t.Errorf("Expected error") | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										40
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								cmd/cmd.go
									
									
									
									
									
								
							| @@ -9,8 +9,12 @@ import ( | ||||
|  | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	"go-micro.dev/v5/auth" | ||||
| 	hbroker "go-micro.dev/v5/broker/http" | ||||
| 	nbroker "go-micro.dev/v5/broker/nats" | ||||
|  | ||||
| 	"go-micro.dev/v5/broker" | ||||
| 	"go-micro.dev/v5/cache" | ||||
| 	"go-micro.dev/v5/cache/redis" | ||||
| 	"go-micro.dev/v5/client" | ||||
| 	"go-micro.dev/v5/config" | ||||
| 	"go-micro.dev/v5/debug/profile" | ||||
| @@ -19,9 +23,14 @@ import ( | ||||
| 	"go-micro.dev/v5/debug/trace" | ||||
| 	"go-micro.dev/v5/logger" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	"go-micro.dev/v5/registry/consul" | ||||
| 	"go-micro.dev/v5/registry/etcd" | ||||
| 	"go-micro.dev/v5/registry/mdns" | ||||
| 	"go-micro.dev/v5/registry/nats" | ||||
| 	"go-micro.dev/v5/selector" | ||||
| 	"go-micro.dev/v5/server" | ||||
| 	"go-micro.dev/v5/store" | ||||
| 	"go-micro.dev/v5/store/mysql" | ||||
| 	"go-micro.dev/v5/transport" | ||||
| ) | ||||
|  | ||||
| @@ -228,11 +237,21 @@ var ( | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	DefaultBrokers = map[string]func(...broker.Option) broker.Broker{} | ||||
| 	DefaultBrokers = map[string]func(...broker.Option) broker.Broker{ | ||||
| 		"memory": broker.NewMemoryBroker, | ||||
| 		"http":   hbroker.NewHttpBroker, | ||||
| 		"nats":   nbroker.NewNatsBroker, | ||||
| 	} | ||||
|  | ||||
| 	DefaultClients = map[string]func(...client.Option) client.Client{} | ||||
|  | ||||
| 	DefaultRegistries = map[string]func(...registry.Option) registry.Registry{} | ||||
| 	DefaultRegistries = map[string]func(...registry.Option) registry.Registry{ | ||||
| 		"consul": consul.NewConsulRegistry, | ||||
| 		"memory": registry.NewMemoryRegistry, | ||||
| 		"nats":   nats.NewNatsRegistry, | ||||
| 		"mdns":   mdns.NewMDNSRegistry, | ||||
| 		"etcd":   etcd.NewEtcdRegistry, | ||||
| 	} | ||||
|  | ||||
| 	DefaultSelectors = map[string]func(...selector.Option) selector.Selector{} | ||||
|  | ||||
| @@ -240,7 +259,10 @@ var ( | ||||
|  | ||||
| 	DefaultTransports = map[string]func(...transport.Option) transport.Transport{} | ||||
|  | ||||
| 	DefaultStores = map[string]func(...store.Option) store.Store{} | ||||
| 	DefaultStores = map[string]func(...store.Option) store.Store{ | ||||
| 		"memory": store.NewMemoryStore, | ||||
| 		"mysql":  mysql.NewMysqlStore, | ||||
| 	} | ||||
|  | ||||
| 	DefaultTracers = map[string]func(...trace.Option) trace.Tracer{} | ||||
|  | ||||
| @@ -253,7 +275,9 @@ var ( | ||||
|  | ||||
| 	DefaultConfigs = map[string]func(...config.Option) (config.Config, error){} | ||||
|  | ||||
| 	DefaultCaches = map[string]func(...cache.Option) cache.Cache{} | ||||
| 	DefaultCaches = map[string]func(...cache.Option) cache.Cache{ | ||||
| 		"redis": redis.NewRedisCache, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| @@ -349,7 +373,7 @@ func (c *cmd) Before(ctx *cli.Context) error { | ||||
| 	if name := ctx.String("store"); len(name) > 0 { | ||||
| 		s, ok := c.opts.Stores[name] | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("Unsupported store: %s", name) | ||||
| 			return fmt.Errorf("unsupported store: %s", name) | ||||
| 		} | ||||
|  | ||||
| 		*c.opts.Store = s(store.WithClient(*c.opts.Client)) | ||||
| @@ -359,7 +383,7 @@ func (c *cmd) Before(ctx *cli.Context) error { | ||||
| 	if name := ctx.String("tracer"); len(name) > 0 { | ||||
| 		r, ok := c.opts.Tracers[name] | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("Unsupported tracer: %s", name) | ||||
| 			return fmt.Errorf("unsupported tracer: %s", name) | ||||
| 		} | ||||
|  | ||||
| 		*c.opts.Tracer = r() | ||||
| @@ -385,7 +409,7 @@ func (c *cmd) Before(ctx *cli.Context) error { | ||||
| 	if name := ctx.String("auth"); len(name) > 0 { | ||||
| 		r, ok := c.opts.Auths[name] | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("Unsupported auth: %s", name) | ||||
| 			return fmt.Errorf("unsupported auth: %s", name) | ||||
| 		} | ||||
|  | ||||
| 		*c.opts.Auth = r(authOpts...) | ||||
| @@ -417,7 +441,7 @@ func (c *cmd) Before(ctx *cli.Context) error { | ||||
| 	if name := ctx.String("profile"); len(name) > 0 { | ||||
| 		p, ok := c.opts.Profiles[name] | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("Unsupported profile: %s", name) | ||||
| 			return fmt.Errorf("unsupported profile: %s", name) | ||||
| 		} | ||||
|  | ||||
| 		*c.opts.Profile = p() | ||||
|   | ||||
							
								
								
									
										54
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								go.mod
									
									
									
									
									
								
							| @@ -6,39 +6,75 @@ toolchain go1.24.1 | ||||
|  | ||||
| require ( | ||||
| 	github.com/bitly/go-simplejson v0.5.0 | ||||
| 	github.com/davecgh/go-spew v1.1.1 | ||||
| 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc | ||||
| 	github.com/fsnotify/fsnotify v1.6.0 | ||||
| 	github.com/go-redis/redis/v8 v8.11.5 | ||||
| 	github.com/go-sql-driver/mysql v1.9.2 | ||||
| 	github.com/golang/protobuf v1.5.4 | ||||
| 	github.com/google/uuid v1.6.0 | ||||
| 	github.com/hashicorp/consul/api v1.32.1 | ||||
| 	github.com/imdario/mergo v0.3.12 | ||||
| 	github.com/kr/pretty v0.3.0 | ||||
| 	github.com/miekg/dns v1.1.43 | ||||
| 	github.com/miekg/dns v1.1.50 | ||||
| 	github.com/mitchellh/hashstructure v1.1.0 | ||||
| 	github.com/nats-io/nats.go v1.42.0 | ||||
| 	github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c | ||||
| 	github.com/patrickmn/go-cache v2.1.0+incompatible | ||||
| 	github.com/pkg/errors v0.9.1 | ||||
| 	github.com/stretchr/testify v1.10.0 | ||||
| 	github.com/urfave/cli/v2 v2.25.7 | ||||
| 	go.etcd.io/bbolt v1.4.0 | ||||
| 	golang.org/x/crypto v0.36.0 | ||||
| 	go.etcd.io/etcd/api/v3 v3.5.21 | ||||
| 	go.etcd.io/etcd/client/v3 v3.5.21 | ||||
| 	go.uber.org/zap v1.27.0 | ||||
| 	golang.org/x/crypto v0.37.0 | ||||
| 	golang.org/x/net v0.38.0 | ||||
| 	golang.org/x/sync v0.12.0 | ||||
| 	golang.org/x/sync v0.13.0 | ||||
| 	google.golang.org/grpc v1.72.1 | ||||
| 	google.golang.org/grpc/examples v0.0.0-20250514161145-5c0d55244474 | ||||
| 	google.golang.org/protobuf v1.36.6 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	filippo.io/edwards25519 v1.1.0 // indirect | ||||
| 	github.com/armon/go-metrics v0.4.1 // indirect | ||||
| 	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect | ||||
| 	github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||||
| 	github.com/coreos/go-semver v0.3.0 // indirect | ||||
| 	github.com/coreos/go-systemd/v22 v22.3.2 // indirect | ||||
| 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect | ||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||
| 	github.com/fatih/color v1.16.0 // indirect | ||||
| 	github.com/gogo/protobuf v1.3.2 // indirect | ||||
| 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||
| 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect | ||||
| 	github.com/hashicorp/go-hclog v1.5.0 // indirect | ||||
| 	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect | ||||
| 	github.com/hashicorp/go-multierror v1.1.1 // indirect | ||||
| 	github.com/hashicorp/go-rootcerts v1.0.2 // indirect | ||||
| 	github.com/hashicorp/golang-lru v0.5.4 // indirect | ||||
| 	github.com/hashicorp/serf v0.10.1 // indirect | ||||
| 	github.com/klauspost/compress v1.18.0 // indirect | ||||
| 	github.com/kr/text v0.2.0 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.6.1 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/mitchellh/go-homedir v1.1.0 // indirect | ||||
| 	github.com/mitchellh/mapstructure v1.5.0 // indirect | ||||
| 	github.com/nats-io/nkeys v0.4.11 // indirect | ||||
| 	github.com/nats-io/nuid v1.0.1 // indirect | ||||
| 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.12.0 // indirect | ||||
| 	github.com/russross/blackfriday/v2 v2.1.0 // indirect | ||||
| 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect | ||||
| 	golang.org/x/sys v0.31.0 // indirect | ||||
| 	golang.org/x/text v0.23.0 // indirect | ||||
| 	go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect | ||||
| 	go.uber.org/multierr v1.10.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect | ||||
| 	golang.org/x/mod v0.24.0 // indirect | ||||
| 	golang.org/x/sys v0.32.0 // indirect | ||||
| 	golang.org/x/text v0.24.0 // indirect | ||||
| 	golang.org/x/tools v0.31.0 // indirect | ||||
| 	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect | ||||
| 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect | ||||
| 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect | ||||
| 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										299
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										299
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,27 +1,136 @@ | ||||
| filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | ||||
| filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | ||||
| github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= | ||||
| github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | ||||
| github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/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-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= | ||||
| github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= | ||||
| github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= | ||||
| github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= | ||||
| github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= | ||||
| github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= | ||||
| github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= | ||||
| 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.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= | ||||
| github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= | ||||
| github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= | ||||
| github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= | ||||
| github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= | ||||
| github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= | ||||
| github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||
| github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= | ||||
| github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||
| github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= | ||||
| github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= | ||||
| github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= | ||||
| github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= | ||||
| github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= | ||||
| github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | ||||
| 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.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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= | ||||
| github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= | ||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||||
| github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= | ||||
| github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= | ||||
| github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= | ||||
| github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= | ||||
| github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= | ||||
| github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= | ||||
| github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= | ||||
| github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||||
| github.com/go-kit/kit v0.9.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.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | ||||
| github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= | ||||
| github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= | ||||
| github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= | ||||
| github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= | ||||
| github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= | ||||
| github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= | ||||
| github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= | ||||
| github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= | ||||
| github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= | ||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
| github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||
| github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | ||||
| github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | ||||
| github.com/golang/protobuf v1.2.0/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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= | ||||
| github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= | ||||
| github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= | ||||
| github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= | ||||
| github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= | ||||
| github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
| github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= | ||||
| github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= | ||||
| github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= | ||||
| github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= | ||||
| github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s= | ||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= | ||||
| github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= | ||||
| github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= | ||||
| github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= | ||||
| github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= | ||||
| github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= | ||||
| github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= | ||||
| github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= | ||||
| github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= | ||||
| github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= | ||||
| github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= | ||||
| github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= | ||||
| github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= | ||||
| github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= | ||||
| github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | ||||
| github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | ||||
| github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= | ||||
| github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= | ||||
| github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= | ||||
| github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= | ||||
| github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= | ||||
| github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= | ||||
| github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= | ||||
| github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||
| github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||
| github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= | ||||
| github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||
| github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= | ||||
| github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= | ||||
| github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | ||||
| github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= | ||||
| github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= | ||||
| github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= | ||||
| github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= | ||||
| github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= | ||||
| github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= | ||||
| github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= | ||||
| github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= | ||||
| github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= | ||||
| github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= | ||||
| github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||
| github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= | ||||
| github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= | ||||
| github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | ||||
| github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= | ||||
| github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= | ||||
| github.com/konsorten/go-windows-terminal-sequences v1.0.1/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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||
| github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= | ||||
| github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= | ||||
| @@ -30,30 +139,114 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| 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/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= | ||||
| github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= | ||||
| github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= | ||||
| github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= | ||||
| github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= | ||||
| github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= | ||||
| github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= | ||||
| github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||||
| github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= | ||||
| github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= | ||||
| github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | ||||
| github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= | ||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||||
| github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||
| github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= | ||||
| github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||||
| github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= | ||||
| github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= | ||||
| github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= | ||||
| github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= | ||||
| github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= | ||||
| github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= | ||||
| github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= | ||||
| github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= | ||||
| github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= | ||||
| github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= | ||||
| github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= | ||||
| github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | ||||
| github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | ||||
| 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | ||||
| github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM= | ||||
| github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= | ||||
| github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= | ||||
| github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= | ||||
| 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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= | ||||
| github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= | ||||
| github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= | ||||
| github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= | ||||
| github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= | ||||
| github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= | ||||
| github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= | ||||
| github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= | ||||
| github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= | ||||
| github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= | ||||
| github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= | ||||
| github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= | ||||
| github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= | ||||
| 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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| 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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= | ||||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= | ||||
| github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= | ||||
| github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | ||||
| github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= | ||||
| github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= | ||||
| github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | ||||
| github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||
| github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||
| github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | ||||
| github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= | ||||
| github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | ||||
| github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | ||||
| github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= | ||||
| github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= | ||||
| github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= | ||||
| github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= | ||||
| github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= | ||||
| github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||
| github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= | ||||
| github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= | ||||
| github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= | ||||
| github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= | ||||
| github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= | ||||
| 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.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= | ||||
| github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | ||||
| 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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= | ||||
| github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= | ||||
| github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||
| github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= | ||||
| github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= | ||||
| github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= | ||||
| github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= | ||||
| github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= | ||||
| github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||
| github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||
| github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= | ||||
| go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= | ||||
| go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= | ||||
| go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= | ||||
| go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= | ||||
| go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= | ||||
| go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= | ||||
| go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= | ||||
| go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= | ||||
| go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= | ||||
| go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= | ||||
| go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= | ||||
| @@ -66,24 +259,96 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J | ||||
| go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= | ||||
| go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= | ||||
| go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= | ||||
| golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= | ||||
| golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= | ||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||
| go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= | ||||
| go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | ||||
| go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= | ||||
| go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= | ||||
| golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= | ||||
| golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= | ||||
| golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= | ||||
| golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= | ||||
| golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= | ||||
| golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= | ||||
| golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| 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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | ||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= | ||||
| golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= | ||||
| golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= | ||||
| golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= | ||||
| 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= | ||||
| golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | ||||
| golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= | ||||
| golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | ||||
| golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/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-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= | ||||
| golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= | ||||
| golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= | ||||
| golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= | ||||
| golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | ||||
| golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||
| golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= | ||||
| golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= | ||||
| 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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= | ||||
| google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= | ||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= | ||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= | ||||
| google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= | ||||
| @@ -92,11 +357,19 @@ google.golang.org/grpc/examples v0.0.0-20250514161145-5c0d55244474 h1:7B8e8jJRSI | ||||
| google.golang.org/grpc/examples v0.0.0-20250514161145-5c0d55244474/go.mod h1:WPWnet+nYurNGpV0rVYHI1YuOJwVHeM3t8f76m410XM= | ||||
| google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= | ||||
| google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= | ||||
| 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 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||
| gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| 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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= | ||||
| gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||||
|   | ||||
							
								
								
									
										460
									
								
								registry/consul/consul.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								registry/consul/consul.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,460 @@ | ||||
| package consul | ||||
|  | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"runtime" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	consul "github.com/hashicorp/consul/api" | ||||
| 	hash "github.com/mitchellh/hashstructure" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	mnet "go-micro.dev/v5/util/net" | ||||
| ) | ||||
|  | ||||
| type consulRegistry struct { | ||||
| 	Address []string | ||||
| 	opts    registry.Options | ||||
|  | ||||
| 	client *consul.Client | ||||
| 	config *consul.Config | ||||
|  | ||||
| 	// connect enabled | ||||
| 	connect bool | ||||
|  | ||||
| 	queryOptions *consul.QueryOptions | ||||
|  | ||||
| 	sync.Mutex | ||||
| 	register map[string]uint64 | ||||
| 	// lastChecked tracks when a node was last checked as existing in Consul | ||||
| 	lastChecked map[string]time.Time | ||||
| } | ||||
|  | ||||
| func getDeregisterTTL(t time.Duration) time.Duration { | ||||
| 	// splay slightly for the watcher? | ||||
| 	splay := time.Second * 5 | ||||
| 	deregTTL := t + splay | ||||
|  | ||||
| 	// consul has a minimum timeout on deregistration of 1 minute. | ||||
| 	if t < time.Minute { | ||||
| 		deregTTL = time.Minute + splay | ||||
| 	} | ||||
|  | ||||
| 	return deregTTL | ||||
| } | ||||
|  | ||||
| func newTransport(config *tls.Config) *http.Transport { | ||||
| 	if config == nil { | ||||
| 		config = &tls.Config{ | ||||
| 			InsecureSkipVerify: true, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	t := &http.Transport{ | ||||
| 		Proxy: http.ProxyFromEnvironment, | ||||
| 		Dial: (&net.Dialer{ | ||||
| 			Timeout:   30 * time.Second, | ||||
| 			KeepAlive: 30 * time.Second, | ||||
| 		}).Dial, | ||||
| 		TLSHandshakeTimeout: 10 * time.Second, | ||||
| 		TLSClientConfig:     config, | ||||
| 	} | ||||
| 	runtime.SetFinalizer(&t, func(tr **http.Transport) { | ||||
| 		(*tr).CloseIdleConnections() | ||||
| 	}) | ||||
| 	return t | ||||
| } | ||||
|  | ||||
| func configure(c *consulRegistry, opts ...registry.Option) { | ||||
| 	// set opts | ||||
| 	for _, o := range opts { | ||||
| 		o(&c.opts) | ||||
| 	} | ||||
|  | ||||
| 	// use default non pooled config | ||||
| 	config := consul.DefaultNonPooledConfig() | ||||
|  | ||||
| 	if c.opts.Context != nil { | ||||
| 		// Use the consul config passed in the options, if available | ||||
| 		if co, ok := c.opts.Context.Value("consul_config").(*consul.Config); ok { | ||||
| 			config = co | ||||
| 		} | ||||
| 		if cn, ok := c.opts.Context.Value("consul_connect").(bool); ok { | ||||
| 			c.connect = cn | ||||
| 		} | ||||
|  | ||||
| 		// Use the consul query options passed in the options, if available | ||||
| 		if qo, ok := c.opts.Context.Value("consul_query_options").(*consul.QueryOptions); ok && qo != nil { | ||||
| 			c.queryOptions = qo | ||||
| 		} | ||||
| 		if as, ok := c.opts.Context.Value("consul_allow_stale").(bool); ok { | ||||
| 			c.queryOptions.AllowStale = as | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// check if there are any addrs | ||||
| 	var addrs []string | ||||
|  | ||||
| 	// iterate the options addresses | ||||
| 	for _, address := range c.opts.Addrs { | ||||
| 		// check we have a port | ||||
| 		addr, port, err := net.SplitHostPort(address) | ||||
| 		if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { | ||||
| 			port = "8500" | ||||
| 			addr = address | ||||
| 			addrs = append(addrs, net.JoinHostPort(addr, port)) | ||||
| 		} else if err == nil { | ||||
| 			addrs = append(addrs, net.JoinHostPort(addr, port)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// set the addrs | ||||
| 	if len(addrs) > 0 { | ||||
| 		c.Address = addrs | ||||
| 		config.Address = c.Address[0] | ||||
| 	} | ||||
|  | ||||
| 	if config.HttpClient == nil { | ||||
| 		config.HttpClient = new(http.Client) | ||||
| 	} | ||||
|  | ||||
| 	// requires secure connection? | ||||
| 	if c.opts.Secure || c.opts.TLSConfig != nil { | ||||
| 		config.Scheme = "https" | ||||
| 		// We're going to support InsecureSkipVerify | ||||
| 		config.HttpClient.Transport = newTransport(c.opts.TLSConfig) | ||||
| 	} | ||||
|  | ||||
| 	// set timeout | ||||
| 	if c.opts.Timeout > 0 { | ||||
| 		config.HttpClient.Timeout = c.opts.Timeout | ||||
| 	} | ||||
|  | ||||
| 	// set the config | ||||
| 	c.config = config | ||||
|  | ||||
| 	// remove client | ||||
| 	c.client = nil | ||||
|  | ||||
| 	// setup the client | ||||
| 	c.Client() | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) Init(opts ...registry.Option) error { | ||||
| 	configure(c, opts...) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) Deregister(s *registry.Service, opts ...registry.DeregisterOption) error { | ||||
| 	if len(s.Nodes) == 0 { | ||||
| 		return errors.New("require at least one node") | ||||
| 	} | ||||
|  | ||||
| 	// delete our hash and time check of the service | ||||
| 	c.Lock() | ||||
| 	delete(c.register, s.Name) | ||||
| 	delete(c.lastChecked, s.Name) | ||||
| 	c.Unlock() | ||||
|  | ||||
| 	node := s.Nodes[0] | ||||
| 	return c.Client().Agent().ServiceDeregister(node.Id) | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error { | ||||
| 	if len(s.Nodes) == 0 { | ||||
| 		return errors.New("require at least one node") | ||||
| 	} | ||||
|  | ||||
| 	var regTCPCheck bool | ||||
| 	var regInterval time.Duration | ||||
| 	var regHTTPCheck bool | ||||
| 	var httpCheckConfig consul.AgentServiceCheck | ||||
|  | ||||
| 	var options registry.RegisterOptions | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	if c.opts.Context != nil { | ||||
| 		if tcpCheckInterval, ok := c.opts.Context.Value("consul_tcp_check").(time.Duration); ok { | ||||
| 			regTCPCheck = true | ||||
| 			regInterval = tcpCheckInterval | ||||
| 		} | ||||
| 		var ok bool | ||||
| 		if httpCheckConfig, ok = c.opts.Context.Value("consul_http_check_config").(consul.AgentServiceCheck); ok { | ||||
| 			regHTTPCheck = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// create hash of service; uint64 | ||||
| 	h, err := hash.Hash(s, nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// use first node | ||||
| 	node := s.Nodes[0] | ||||
|  | ||||
| 	// get existing hash and last checked time | ||||
| 	c.Lock() | ||||
| 	v, ok := c.register[s.Name] | ||||
| 	lastChecked := c.lastChecked[s.Name] | ||||
| 	c.Unlock() | ||||
|  | ||||
| 	// if it's already registered and matches then just pass the check | ||||
| 	if ok && v == h { | ||||
| 		if options.TTL == time.Duration(0) { | ||||
| 			// ensure that our service hasn't been deregistered by Consul | ||||
| 			if time.Since(lastChecked) <= getDeregisterTTL(regInterval) { | ||||
| 				return nil | ||||
| 			} | ||||
| 			services, _, err := c.Client().Health().Checks(s.Name, c.queryOptions) | ||||
| 			if err == nil { | ||||
| 				for _, v := range services { | ||||
| 					if v.ServiceID == node.Id { | ||||
| 						return nil | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			// if the err is nil we're all good, bail out | ||||
| 			// if not, we don't know what the state is, so full re-register | ||||
| 			if err := c.Client().Agent().PassTTL("service:"+node.Id, ""); err == nil { | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// encode the tags | ||||
| 	tags := encodeMetadata(node.Metadata) | ||||
| 	tags = append(tags, encodeEndpoints(s.Endpoints)...) | ||||
| 	tags = append(tags, encodeVersion(s.Version)...) | ||||
|  | ||||
| 	var check *consul.AgentServiceCheck | ||||
|  | ||||
| 	if regTCPCheck { | ||||
| 		deregTTL := getDeregisterTTL(regInterval) | ||||
|  | ||||
| 		check = &consul.AgentServiceCheck{ | ||||
| 			TCP:                            node.Address, | ||||
| 			Interval:                       fmt.Sprintf("%v", regInterval), | ||||
| 			DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL), | ||||
| 		} | ||||
|  | ||||
| 	} else if regHTTPCheck { | ||||
| 		interval, _ := time.ParseDuration(httpCheckConfig.Interval) | ||||
| 		deregTTL := getDeregisterTTL(interval) | ||||
|  | ||||
| 		host, _, _ := net.SplitHostPort(node.Address) | ||||
| 		healthCheckURI := strings.Replace(httpCheckConfig.HTTP, "{host}", host, 1) | ||||
|  | ||||
| 		check = &consul.AgentServiceCheck{ | ||||
| 			HTTP:                           healthCheckURI, | ||||
| 			Interval:                       httpCheckConfig.Interval, | ||||
| 			Timeout:                        httpCheckConfig.Timeout, | ||||
| 			DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL), | ||||
| 		} | ||||
|  | ||||
| 		// if the TTL is greater than 0 create an associated check | ||||
| 	} else if options.TTL > time.Duration(0) { | ||||
| 		deregTTL := getDeregisterTTL(options.TTL) | ||||
|  | ||||
| 		check = &consul.AgentServiceCheck{ | ||||
| 			TTL:                            fmt.Sprintf("%v", options.TTL), | ||||
| 			DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL), | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	host, pt, _ := net.SplitHostPort(node.Address) | ||||
| 	if host == "" { | ||||
| 		host = node.Address | ||||
| 	} | ||||
| 	port, _ := strconv.Atoi(pt) | ||||
|  | ||||
| 	// register the service | ||||
| 	asr := &consul.AgentServiceRegistration{ | ||||
| 		ID:      node.Id, | ||||
| 		Name:    s.Name, | ||||
| 		Tags:    tags, | ||||
| 		Port:    port, | ||||
| 		Address: host, | ||||
| 		Meta:    node.Metadata, | ||||
| 		Check:   check, | ||||
| 	} | ||||
|  | ||||
| 	// Specify consul connect | ||||
| 	if c.connect { | ||||
| 		asr.Connect = &consul.AgentServiceConnect{ | ||||
| 			Native: true, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := c.Client().Agent().ServiceRegister(asr); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// save our hash and time check of the service | ||||
| 	c.Lock() | ||||
| 	c.register[s.Name] = h | ||||
| 	c.lastChecked[s.Name] = time.Now() | ||||
| 	c.Unlock() | ||||
|  | ||||
| 	// if the TTL is 0 we don't mess with the checks | ||||
| 	if options.TTL == time.Duration(0) { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// pass the healthcheck | ||||
| 	return c.Client().Agent().PassTTL("service:"+node.Id, "") | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) GetService(name string, opts ...registry.GetOption) ([]*registry.Service, error) { | ||||
| 	var rsp []*consul.ServiceEntry | ||||
| 	var err error | ||||
|  | ||||
| 	// if we're connect enabled only get connect services | ||||
| 	if c.connect { | ||||
| 		rsp, _, err = c.Client().Health().Connect(name, "", false, c.queryOptions) | ||||
| 	} else { | ||||
| 		rsp, _, err = c.Client().Health().Service(name, "", false, c.queryOptions) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	serviceMap := map[string]*registry.Service{} | ||||
|  | ||||
| 	for _, s := range rsp { | ||||
| 		if s.Service.Service != name { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// version is now a tag | ||||
| 		version, _ := decodeVersion(s.Service.Tags) | ||||
| 		// service ID is now the node id | ||||
| 		id := s.Service.ID | ||||
| 		// key is always the version | ||||
| 		key := version | ||||
|  | ||||
| 		// address is service address | ||||
| 		address := s.Service.Address | ||||
|  | ||||
| 		// use node address | ||||
| 		if len(address) == 0 { | ||||
| 			address = s.Node.Address | ||||
| 		} | ||||
|  | ||||
| 		svc, ok := serviceMap[key] | ||||
| 		if !ok { | ||||
| 			svc = ®istry.Service{ | ||||
| 				Endpoints: decodeEndpoints(s.Service.Tags), | ||||
| 				Name:      s.Service.Service, | ||||
| 				Version:   version, | ||||
| 			} | ||||
| 			serviceMap[key] = svc | ||||
| 		} | ||||
|  | ||||
| 		var del bool | ||||
|  | ||||
| 		for _, check := range s.Checks { | ||||
| 			// delete the node if the status is critical | ||||
| 			if check.Status == "critical" { | ||||
| 				del = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// if delete then skip the node | ||||
| 		if del { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		svc.Nodes = append(svc.Nodes, ®istry.Node{ | ||||
| 			Id:       id, | ||||
| 			Address:  mnet.HostPort(address, s.Service.Port), | ||||
| 			Metadata: decodeMetadata(s.Service.Tags), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	var services []*registry.Service | ||||
| 	for _, service := range serviceMap { | ||||
| 		services = append(services, service) | ||||
| 	} | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) { | ||||
| 	rsp, _, err := c.Client().Catalog().Services(c.queryOptions) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var services []*registry.Service | ||||
|  | ||||
| 	for service := range rsp { | ||||
| 		services = append(services, ®istry.Service{Name: service}) | ||||
| 	} | ||||
|  | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) { | ||||
| 	return newConsulWatcher(c, opts...) | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) String() string { | ||||
| 	return "consul" | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) Options() registry.Options { | ||||
| 	return c.opts | ||||
| } | ||||
|  | ||||
| func (c *consulRegistry) Client() *consul.Client { | ||||
| 	if c.client != nil { | ||||
| 		return c.client | ||||
| 	} | ||||
|  | ||||
| 	for _, addr := range c.Address { | ||||
| 		// set the address | ||||
| 		c.config.Address = addr | ||||
|  | ||||
| 		// create a new client | ||||
| 		tmpClient, _ := consul.NewClient(c.config) | ||||
|  | ||||
| 		// test the client | ||||
| 		_, err := tmpClient.Agent().Host() | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// set the client | ||||
| 		c.client = tmpClient | ||||
| 		return c.client | ||||
| 	} | ||||
|  | ||||
| 	// set the default | ||||
| 	c.client, _ = consul.NewClient(c.config) | ||||
|  | ||||
| 	// return the client | ||||
| 	return c.client | ||||
| } | ||||
|  | ||||
| func NewConsulRegistry(opts ...registry.Option) registry.Registry { | ||||
| 	cr := &consulRegistry{ | ||||
| 		opts:        registry.Options{}, | ||||
| 		register:    make(map[string]uint64), | ||||
| 		lastChecked: make(map[string]time.Time), | ||||
| 		queryOptions: &consul.QueryOptions{ | ||||
| 			AllowStale: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	configure(cr, opts...) | ||||
| 	return cr | ||||
| } | ||||
							
								
								
									
										171
									
								
								registry/consul/encoding.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								registry/consul/encoding.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| package consul | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"compress/zlib" | ||||
| 	"encoding/hex" | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
|  | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| func encode(buf []byte) string { | ||||
| 	var b bytes.Buffer | ||||
| 	defer b.Reset() | ||||
|  | ||||
| 	w := zlib.NewWriter(&b) | ||||
| 	if _, err := w.Write(buf); err != nil { | ||||
| 		return "" | ||||
| 	} | ||||
| 	w.Close() | ||||
|  | ||||
| 	return hex.EncodeToString(b.Bytes()) | ||||
| } | ||||
|  | ||||
| func decode(d string) []byte { | ||||
| 	hr, err := hex.DecodeString(d) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	br := bytes.NewReader(hr) | ||||
| 	zr, err := zlib.NewReader(br) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	rbuf, err := io.ReadAll(zr) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	zr.Close() | ||||
|  | ||||
| 	return rbuf | ||||
| } | ||||
|  | ||||
| func encodeEndpoints(en []*registry.Endpoint) []string { | ||||
| 	var tags []string | ||||
| 	for _, e := range en { | ||||
| 		if b, err := json.Marshal(e); err == nil { | ||||
| 			tags = append(tags, "e-"+encode(b)) | ||||
| 		} | ||||
| 	} | ||||
| 	return tags | ||||
| } | ||||
|  | ||||
| func decodeEndpoints(tags []string) []*registry.Endpoint { | ||||
| 	var en []*registry.Endpoint | ||||
|  | ||||
| 	// use the first format you find | ||||
| 	var ver byte | ||||
|  | ||||
| 	for _, tag := range tags { | ||||
| 		if len(tag) == 0 || tag[0] != 'e' { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// check version | ||||
| 		if ver > 0 && tag[1] != ver { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		var e *registry.Endpoint | ||||
| 		var buf []byte | ||||
|  | ||||
| 		// Old encoding was plain | ||||
| 		if tag[1] == '=' { | ||||
| 			buf = []byte(tag[2:]) | ||||
| 		} | ||||
|  | ||||
| 		// New encoding is hex | ||||
| 		if tag[1] == '-' { | ||||
| 			buf = decode(tag[2:]) | ||||
| 		} | ||||
|  | ||||
| 		if err := json.Unmarshal(buf, &e); err == nil { | ||||
| 			en = append(en, e) | ||||
| 		} | ||||
|  | ||||
| 		// set version | ||||
| 		ver = tag[1] | ||||
| 	} | ||||
| 	return en | ||||
| } | ||||
|  | ||||
| func encodeMetadata(md map[string]string) []string { | ||||
| 	var tags []string | ||||
| 	for k, v := range md { | ||||
| 		if b, err := json.Marshal(map[string]string{ | ||||
| 			k: v, | ||||
| 		}); err == nil { | ||||
| 			// new encoding | ||||
| 			tags = append(tags, "t-"+encode(b)) | ||||
| 		} | ||||
| 	} | ||||
| 	return tags | ||||
| } | ||||
|  | ||||
| func decodeMetadata(tags []string) map[string]string { | ||||
| 	md := make(map[string]string) | ||||
|  | ||||
| 	var ver byte | ||||
|  | ||||
| 	for _, tag := range tags { | ||||
| 		if len(tag) == 0 || tag[0] != 't' { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// check version | ||||
| 		if ver > 0 && tag[1] != ver { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		var kv map[string]string | ||||
| 		var buf []byte | ||||
|  | ||||
| 		// Old encoding was plain | ||||
| 		if tag[1] == '=' { | ||||
| 			buf = []byte(tag[2:]) | ||||
| 		} | ||||
|  | ||||
| 		// New encoding is hex | ||||
| 		if tag[1] == '-' { | ||||
| 			buf = decode(tag[2:]) | ||||
| 		} | ||||
|  | ||||
| 		// Now unmarshal | ||||
| 		if err := json.Unmarshal(buf, &kv); err == nil { | ||||
| 			for k, v := range kv { | ||||
| 				md[k] = v | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// set version | ||||
| 		ver = tag[1] | ||||
| 	} | ||||
| 	return md | ||||
| } | ||||
|  | ||||
| func encodeVersion(v string) []string { | ||||
| 	return []string{"v-" + encode([]byte(v))} | ||||
| } | ||||
|  | ||||
| func decodeVersion(tags []string) (string, bool) { | ||||
| 	for _, tag := range tags { | ||||
| 		if len(tag) < 2 || tag[0] != 'v' { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// Old encoding was plain | ||||
| 		if tag[1] == '=' { | ||||
| 			return tag[2:], true | ||||
| 		} | ||||
|  | ||||
| 		// New encoding is hex | ||||
| 		if tag[1] == '-' { | ||||
| 			return string(decode(tag[2:])), true | ||||
| 		} | ||||
| 	} | ||||
| 	return "", false | ||||
| } | ||||
							
								
								
									
										147
									
								
								registry/consul/encoding_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								registry/consul/encoding_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| package consul | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"testing" | ||||
|  | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| func TestEncodingEndpoints(t *testing.T) { | ||||
| 	eps := []*registry.Endpoint{ | ||||
| 		{ | ||||
| 			Name: "endpoint1", | ||||
| 			Request: ®istry.Value{ | ||||
| 				Name: "request", | ||||
| 				Type: "request", | ||||
| 			}, | ||||
| 			Response: ®istry.Value{ | ||||
| 				Name: "response", | ||||
| 				Type: "response", | ||||
| 			}, | ||||
| 			Metadata: map[string]string{ | ||||
| 				"foo1": "bar1", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "endpoint2", | ||||
| 			Request: ®istry.Value{ | ||||
| 				Name: "request", | ||||
| 				Type: "request", | ||||
| 			}, | ||||
| 			Response: ®istry.Value{ | ||||
| 				Name: "response", | ||||
| 				Type: "response", | ||||
| 			}, | ||||
| 			Metadata: map[string]string{ | ||||
| 				"foo2": "bar2", | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name: "endpoint3", | ||||
| 			Request: ®istry.Value{ | ||||
| 				Name: "request", | ||||
| 				Type: "request", | ||||
| 			}, | ||||
| 			Response: ®istry.Value{ | ||||
| 				Name: "response", | ||||
| 				Type: "response", | ||||
| 			}, | ||||
| 			Metadata: map[string]string{ | ||||
| 				"foo3": "bar3", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	testEp := func(ep *registry.Endpoint, enc string) { | ||||
| 		// encode endpoint | ||||
| 		e := encodeEndpoints([]*registry.Endpoint{ep}) | ||||
|  | ||||
| 		// check there are two tags; old and new | ||||
| 		if len(e) != 1 { | ||||
| 			t.Fatalf("Expected 1 encoded tags, got %v", e) | ||||
| 		} | ||||
|  | ||||
| 		// check old encoding | ||||
| 		var seen bool | ||||
|  | ||||
| 		for _, en := range e { | ||||
| 			if en == enc { | ||||
| 				seen = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if !seen { | ||||
| 			t.Fatalf("Expected %s but not found", enc) | ||||
| 		} | ||||
|  | ||||
| 		// decode | ||||
| 		d := decodeEndpoints([]string{enc}) | ||||
| 		if len(d) == 0 { | ||||
| 			t.Fatalf("Expected %v got %v", ep, d) | ||||
| 		} | ||||
|  | ||||
| 		// check name | ||||
| 		if d[0].Name != ep.Name { | ||||
| 			t.Fatalf("Expected ep %s got %s", ep.Name, d[0].Name) | ||||
| 		} | ||||
|  | ||||
| 		// check all the metadata exists | ||||
| 		for k, v := range ep.Metadata { | ||||
| 			if gv := d[0].Metadata[k]; gv != v { | ||||
| 				t.Fatalf("Expected key %s val %s got val %s", k, v, gv) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, ep := range eps { | ||||
| 		// JSON encoded | ||||
| 		jencoded, err := json.Marshal(ep) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		// HEX encoded | ||||
| 		hencoded := encode(jencoded) | ||||
| 		// endpoint tag | ||||
| 		hepTag := "e-" + hencoded | ||||
| 		testEp(ep, hepTag) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestEncodingVersion(t *testing.T) { | ||||
| 	testData := []struct { | ||||
| 		decoded string | ||||
| 		encoded string | ||||
| 	}{ | ||||
| 		{"1.0.0", "v-789c32d433d03300040000ffff02ce00ee"}, | ||||
| 		{"latest", "v-789cca492c492d2e01040000ffff08cc028e"}, | ||||
| 	} | ||||
|  | ||||
| 	for _, data := range testData { | ||||
| 		e := encodeVersion(data.decoded) | ||||
|  | ||||
| 		if e[0] != data.encoded { | ||||
| 			t.Fatalf("Expected %s got %s", data.encoded, e) | ||||
| 		} | ||||
|  | ||||
| 		d, ok := decodeVersion(e) | ||||
| 		if !ok { | ||||
| 			t.Fatalf("Unexpected %t for %s", ok, data.encoded) | ||||
| 		} | ||||
|  | ||||
| 		if d != data.decoded { | ||||
| 			t.Fatalf("Expected %s got %s", data.decoded, d) | ||||
| 		} | ||||
|  | ||||
| 		d, ok = decodeVersion([]string{data.encoded}) | ||||
| 		if !ok { | ||||
| 			t.Fatalf("Unexpected %t for %s", ok, data.encoded) | ||||
| 		} | ||||
|  | ||||
| 		if d != data.decoded { | ||||
| 			t.Fatalf("Expected %s got %s", data.decoded, d) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										111
									
								
								registry/consul/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								registry/consul/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| package consul | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	consul "github.com/hashicorp/consul/api" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| // Define a custom type for context keys to avoid collisions. | ||||
| type contextKey string | ||||
|  | ||||
| const consulConnectKey contextKey = "consul_connect" | ||||
| const consulConfigKey contextKey = "consul_config" | ||||
| const consulAllowStaleKey contextKey = "consul_allow_stale" | ||||
| const consulQueryOptionsKey contextKey = "consul_query_options" | ||||
| const consulTCPCheckKey contextKey = "consul_tcp_check" | ||||
| const consulHTTPCheckConfigKey contextKey = "consul_http_check_config" | ||||
|  | ||||
| // Connect specifies services should be registered as Consul Connect services. | ||||
| func Connect() registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, consulConnectKey, true) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func Config(c *consul.Config) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, consulConfigKey, c) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // AllowStale sets whether any Consul server (non-leader) can service | ||||
| // a read. This allows for lower latency and higher throughput | ||||
| // at the cost of potentially stale data. | ||||
| // Works similar to Consul DNS Config option [1]. | ||||
| // Defaults to true. | ||||
| // | ||||
| // [1] https://www.consul.io/docs/agent/options.html#allow_stale | ||||
| func AllowStale(v bool) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, consulAllowStaleKey, v) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // QueryOptions specifies the QueryOptions to be used when calling | ||||
| // Consul. See `Consul API` for more information [1]. | ||||
| // | ||||
| // [1] https://godoc.org/github.com/hashicorp/consul/api#QueryOptions | ||||
| func QueryOptions(q *consul.QueryOptions) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if q == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, consulQueryOptionsKey, q) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // TCPCheck will tell the service provider to check the service address | ||||
| // and port every `t` interval. It will enabled only if `t` is greater than 0. | ||||
| // See `TCP + Interval` for more information [1]. | ||||
| // | ||||
| // [1] https://www.consul.io/docs/agent/checks.html | ||||
| func TCPCheck(t time.Duration) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if t <= time.Duration(0) { | ||||
| 			return | ||||
| 		} | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, consulTCPCheckKey, t) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // HTTPCheck will tell the service provider to invoke the health check endpoint | ||||
| // with an interval and timeout. It will be enabled only if interval and | ||||
| // timeout are greater than 0. | ||||
| // See `HTTP + Interval` for more information [1]. | ||||
| // | ||||
| // [1] https://www.consul.io/docs/agent/checks.html | ||||
| func HTTPCheck(protocol, port, httpEndpoint string, interval, timeout time.Duration) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if interval <= time.Duration(0) || timeout <= time.Duration(0) { | ||||
| 			return | ||||
| 		} | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		check := consul.AgentServiceCheck{ | ||||
| 			HTTP:     fmt.Sprintf("%s://{host}:%s%s", protocol, port, httpEndpoint), | ||||
| 			Interval: fmt.Sprintf("%v", interval), | ||||
| 			Timeout:  fmt.Sprintf("%v", timeout), | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, consulHTTPCheckConfigKey, check) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										208
									
								
								registry/consul/registry_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								registry/consul/registry_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | ||||
| package consul | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	consul "github.com/hashicorp/consul/api" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| type mockRegistry struct { | ||||
| 	body   []byte | ||||
| 	status int | ||||
| 	err    error | ||||
| 	url    string | ||||
| } | ||||
|  | ||||
| func encodeData(obj interface{}) ([]byte, error) { | ||||
| 	buf := bytes.NewBuffer(nil) | ||||
| 	enc := json.NewEncoder(buf) | ||||
| 	if err := enc.Encode(obj); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return buf.Bytes(), nil | ||||
| } | ||||
|  | ||||
| func newMockServer(rg *mockRegistry, l net.Listener) error { | ||||
| 	mux := http.NewServeMux() | ||||
| 	mux.HandleFunc(rg.url, func(w http.ResponseWriter, r *http.Request) { | ||||
| 		if rg.err != nil { | ||||
| 			http.Error(w, rg.err.Error(), 500) | ||||
| 			return | ||||
| 		} | ||||
| 		w.WriteHeader(rg.status) | ||||
| 		w.Write(rg.body) | ||||
| 	}) | ||||
| 	return http.Serve(l, mux) | ||||
| } | ||||
|  | ||||
| func newConsulTestRegistry(r *mockRegistry) (*consulRegistry, func()) { | ||||
| 	l, err := net.Listen("tcp", "localhost:0") | ||||
| 	if err != nil { | ||||
| 		// blurgh?!! | ||||
| 		panic(err.Error()) | ||||
| 	} | ||||
| 	cfg := consul.DefaultConfig() | ||||
| 	cfg.Address = l.Addr().String() | ||||
|  | ||||
| 	go newMockServer(r, l) | ||||
|  | ||||
| 	var cr = &consulRegistry{ | ||||
| 		config:      cfg, | ||||
| 		Address:     []string{cfg.Address}, | ||||
| 		opts:        registry.Options{}, | ||||
| 		register:    make(map[string]uint64), | ||||
| 		lastChecked: make(map[string]time.Time), | ||||
| 		queryOptions: &consul.QueryOptions{ | ||||
| 			AllowStale: true, | ||||
| 		}, | ||||
| 	} | ||||
| 	cr.Client() | ||||
|  | ||||
| 	return cr, func() { | ||||
| 		l.Close() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newServiceList(svc []*consul.ServiceEntry) []byte { | ||||
| 	bts, _ := encodeData(svc) | ||||
| 	return bts | ||||
| } | ||||
|  | ||||
| func TestConsul_GetService_WithError(t *testing.T) { | ||||
| 	cr, cl := newConsulTestRegistry(&mockRegistry{ | ||||
| 		err: errors.New("client-error"), | ||||
| 		url: "/v1/health/service/service-name", | ||||
| 	}) | ||||
| 	defer cl() | ||||
|  | ||||
| 	if _, err := cr.GetService("test-service"); err == nil { | ||||
| 		t.Fatalf("Expected error not to be `nil`") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConsul_GetService_WithHealthyServiceNodes(t *testing.T) { | ||||
| 	// warning is still seen as healthy, critical is not | ||||
| 	svcs := []*consul.ServiceEntry{ | ||||
| 		newServiceEntry( | ||||
| 			"node-name-1", "node-address-1", "service-name", "v1.0.0", | ||||
| 			[]*consul.HealthCheck{ | ||||
| 				newHealthCheck("node-name-1", "service-name", "passing"), | ||||
| 				newHealthCheck("node-name-1", "service-name", "warning"), | ||||
| 			}, | ||||
| 		), | ||||
| 		newServiceEntry( | ||||
| 			"node-name-2", "node-address-2", "service-name", "v1.0.0", | ||||
| 			[]*consul.HealthCheck{ | ||||
| 				newHealthCheck("node-name-2", "service-name", "passing"), | ||||
| 				newHealthCheck("node-name-2", "service-name", "warning"), | ||||
| 			}, | ||||
| 		), | ||||
| 	} | ||||
|  | ||||
| 	cr, cl := newConsulTestRegistry(&mockRegistry{ | ||||
| 		status: 200, | ||||
| 		body:   newServiceList(svcs), | ||||
| 		url:    "/v1/health/service/service-name", | ||||
| 	}) | ||||
| 	defer cl() | ||||
|  | ||||
| 	svc, err := cr.GetService("service-name") | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Unexpected error", err) | ||||
| 	} | ||||
|  | ||||
| 	if exp, act := 1, len(svc); exp != act { | ||||
| 		t.Fatalf("Expected len of svc to be `%d`, got `%d`.", exp, act) | ||||
| 	} | ||||
|  | ||||
| 	if exp, act := 2, len(svc[0].Nodes); exp != act { | ||||
| 		t.Fatalf("Expected len of nodes to be `%d`, got `%d`.", exp, act) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConsul_GetService_WithUnhealthyServiceNode(t *testing.T) { | ||||
| 	// warning is still seen as healthy, critical is not | ||||
| 	svcs := []*consul.ServiceEntry{ | ||||
| 		newServiceEntry( | ||||
| 			"node-name-1", "node-address-1", "service-name", "v1.0.0", | ||||
| 			[]*consul.HealthCheck{ | ||||
| 				newHealthCheck("node-name-1", "service-name", "passing"), | ||||
| 				newHealthCheck("node-name-1", "service-name", "warning"), | ||||
| 			}, | ||||
| 		), | ||||
| 		newServiceEntry( | ||||
| 			"node-name-2", "node-address-2", "service-name", "v1.0.0", | ||||
| 			[]*consul.HealthCheck{ | ||||
| 				newHealthCheck("node-name-2", "service-name", "passing"), | ||||
| 				newHealthCheck("node-name-2", "service-name", "critical"), | ||||
| 			}, | ||||
| 		), | ||||
| 	} | ||||
|  | ||||
| 	cr, cl := newConsulTestRegistry(&mockRegistry{ | ||||
| 		status: 200, | ||||
| 		body:   newServiceList(svcs), | ||||
| 		url:    "/v1/health/service/service-name", | ||||
| 	}) | ||||
| 	defer cl() | ||||
|  | ||||
| 	svc, err := cr.GetService("service-name") | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Unexpected error", err) | ||||
| 	} | ||||
|  | ||||
| 	if exp, act := 1, len(svc); exp != act { | ||||
| 		t.Fatalf("Expected len of svc to be `%d`, got `%d`.", exp, act) | ||||
| 	} | ||||
|  | ||||
| 	if exp, act := 1, len(svc[0].Nodes); exp != act { | ||||
| 		t.Fatalf("Expected len of nodes to be `%d`, got `%d`.", exp, act) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestConsul_GetService_WithUnhealthyServiceNodes(t *testing.T) { | ||||
| 	// warning is still seen as healthy, critical is not | ||||
| 	svcs := []*consul.ServiceEntry{ | ||||
| 		newServiceEntry( | ||||
| 			"node-name-1", "node-address-1", "service-name", "v1.0.0", | ||||
| 			[]*consul.HealthCheck{ | ||||
| 				newHealthCheck("node-name-1", "service-name", "passing"), | ||||
| 				newHealthCheck("node-name-1", "service-name", "critical"), | ||||
| 			}, | ||||
| 		), | ||||
| 		newServiceEntry( | ||||
| 			"node-name-2", "node-address-2", "service-name", "v1.0.0", | ||||
| 			[]*consul.HealthCheck{ | ||||
| 				newHealthCheck("node-name-2", "service-name", "passing"), | ||||
| 				newHealthCheck("node-name-2", "service-name", "critical"), | ||||
| 			}, | ||||
| 		), | ||||
| 	} | ||||
|  | ||||
| 	cr, cl := newConsulTestRegistry(&mockRegistry{ | ||||
| 		status: 200, | ||||
| 		body:   newServiceList(svcs), | ||||
| 		url:    "/v1/health/service/service-name", | ||||
| 	}) | ||||
| 	defer cl() | ||||
|  | ||||
| 	svc, err := cr.GetService("service-name") | ||||
| 	if err != nil { | ||||
| 		t.Fatal("Unexpected error", err) | ||||
| 	} | ||||
|  | ||||
| 	if exp, act := 1, len(svc); exp != act { | ||||
| 		t.Fatalf("Expected len of svc to be `%d`, got `%d`.", exp, act) | ||||
| 	} | ||||
|  | ||||
| 	if exp, act := 0, len(svc[0].Nodes); exp != act { | ||||
| 		t.Fatalf("Expected len of nodes to be `%d`, got `%d`.", exp, act) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										299
									
								
								registry/consul/watcher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								registry/consul/watcher.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,299 @@ | ||||
| package consul | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/hashicorp/consul/api" | ||||
| 	"github.com/hashicorp/consul/api/watch" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	regutil "go-micro.dev/v5/util/registry" | ||||
| ) | ||||
|  | ||||
| type consulWatcher struct { | ||||
| 	r        *consulRegistry | ||||
| 	wo       registry.WatchOptions | ||||
| 	wp       *watch.Plan | ||||
| 	watchers map[string]*watch.Plan | ||||
|  | ||||
| 	next chan *registry.Result | ||||
| 	exit chan bool | ||||
|  | ||||
| 	sync.RWMutex | ||||
| 	services map[string][]*registry.Service | ||||
| } | ||||
|  | ||||
| func newConsulWatcher(cr *consulRegistry, opts ...registry.WatchOption) (registry.Watcher, error) { | ||||
| 	var wo registry.WatchOptions | ||||
| 	for _, o := range opts { | ||||
| 		o(&wo) | ||||
| 	} | ||||
|  | ||||
| 	cw := &consulWatcher{ | ||||
| 		r:        cr, | ||||
| 		wo:       wo, | ||||
| 		exit:     make(chan bool), | ||||
| 		next:     make(chan *registry.Result, 10), | ||||
| 		watchers: make(map[string]*watch.Plan), | ||||
| 		services: make(map[string][]*registry.Service), | ||||
| 	} | ||||
|  | ||||
| 	wp, err := watch.Parse(map[string]interface{}{ | ||||
| 		"service": wo.Service, | ||||
| 		"type":    "service", | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	wp.Handler = cw.serviceHandler | ||||
| 	go wp.RunWithClientAndHclog(cr.Client(), wp.Logger) | ||||
| 	cw.wp = wp | ||||
|  | ||||
| 	return cw, nil | ||||
| } | ||||
|  | ||||
| func (cw *consulWatcher) serviceHandler(idx uint64, data interface{}) { | ||||
| 	entries, ok := data.([]*api.ServiceEntry) | ||||
| 	if !ok { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	serviceMap := map[string]*registry.Service{} | ||||
| 	serviceName := "" | ||||
|  | ||||
| 	for _, e := range entries { | ||||
| 		serviceName = e.Service.Service | ||||
| 		// version is now a tag | ||||
| 		version, _ := decodeVersion(e.Service.Tags) | ||||
| 		// service ID is now the node id | ||||
| 		id := e.Service.ID | ||||
| 		// key is always the version | ||||
| 		key := version | ||||
| 		// address is service address | ||||
| 		address := e.Service.Address | ||||
|  | ||||
| 		// use node address | ||||
| 		if len(address) == 0 { | ||||
| 			address = e.Node.Address | ||||
| 		} | ||||
|  | ||||
| 		svc, ok := serviceMap[key] | ||||
| 		if !ok { | ||||
| 			svc = ®istry.Service{ | ||||
| 				Endpoints: decodeEndpoints(e.Service.Tags), | ||||
| 				Name:      e.Service.Service, | ||||
| 				Version:   version, | ||||
| 			} | ||||
| 			serviceMap[key] = svc | ||||
| 		} | ||||
|  | ||||
| 		var del bool | ||||
|  | ||||
| 		for _, check := range e.Checks { | ||||
| 			// delete the node if the status is critical | ||||
| 			if check.Status == "critical" { | ||||
| 				del = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// if delete then skip the node | ||||
| 		if del { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		svc.Nodes = append(svc.Nodes, ®istry.Node{ | ||||
| 			Id:       id, | ||||
| 			Address:  net.JoinHostPort(address, fmt.Sprint(e.Service.Port)), | ||||
| 			Metadata: decodeMetadata(e.Service.Tags), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	cw.RLock() | ||||
| 	// make a copy | ||||
| 	rservices := make(map[string][]*registry.Service) | ||||
| 	for k, v := range cw.services { | ||||
| 		rservices[k] = v | ||||
| 	} | ||||
| 	cw.RUnlock() | ||||
|  | ||||
| 	var newServices []*registry.Service | ||||
|  | ||||
| 	// serviceMap is the new set of services keyed by name+version | ||||
| 	for _, newService := range serviceMap { | ||||
| 		// append to the new set of cached services | ||||
| 		newServices = append(newServices, newService) | ||||
|  | ||||
| 		// check if the service exists in the existing cache | ||||
| 		oldServices, ok := rservices[serviceName] | ||||
| 		if !ok { | ||||
| 			// does not exist? then we're creating brand new entries | ||||
| 			cw.next <- ®istry.Result{Action: "create", Service: newService} | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// service exists. ok let's figure out what to update and delete version wise | ||||
| 		action := "create" | ||||
|  | ||||
| 		for _, oldService := range oldServices { | ||||
| 			// does this version exist? | ||||
| 			// no? then default to create | ||||
| 			if oldService.Version != newService.Version { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// yes? then it's an update | ||||
| 			action = "update" | ||||
|  | ||||
| 			var nodes []*registry.Node | ||||
| 			// check the old nodes to see if they've been deleted | ||||
| 			for _, oldNode := range oldService.Nodes { | ||||
| 				var seen bool | ||||
| 				for _, newNode := range newService.Nodes { | ||||
| 					if newNode.Id == oldNode.Id { | ||||
| 						seen = true | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 				// does the old node exist in the new set of nodes | ||||
| 				// no? then delete that shit | ||||
| 				if !seen { | ||||
| 					nodes = append(nodes, oldNode) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			// it's an update rather than creation | ||||
| 			if len(nodes) > 0 { | ||||
| 				delService := regutil.CopyService(oldService) | ||||
| 				delService.Nodes = nodes | ||||
| 				cw.next <- ®istry.Result{Action: "delete", Service: delService} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		cw.next <- ®istry.Result{Action: action, Service: newService} | ||||
| 	} | ||||
|  | ||||
| 	// Now check old versions that may not be in new services map | ||||
| 	for _, old := range rservices[serviceName] { | ||||
| 		// old version does not exist in new version map | ||||
| 		// kill it with fire! | ||||
| 		if _, ok := serviceMap[old.Version]; !ok { | ||||
| 			cw.next <- ®istry.Result{Action: "delete", Service: old} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// there are no services in the service, empty all services | ||||
| 	if len(rservices) != 0 && serviceName == "" { | ||||
| 		for _, services := range rservices { | ||||
| 			for _, service := range services { | ||||
| 				cw.next <- ®istry.Result{Action: "delete", Service: service} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	cw.Lock() | ||||
| 	cw.services[serviceName] = newServices | ||||
| 	cw.Unlock() | ||||
| } | ||||
|  | ||||
| func (cw *consulWatcher) handle(idx uint64, data interface{}) { | ||||
| 	services, ok := data.(map[string][]string) | ||||
| 	if !ok { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// add new watchers | ||||
| 	for service := range services { | ||||
| 		// Filter on watch options | ||||
| 		// wo.Service: Only watch services we care about | ||||
| 		if len(cw.wo.Service) > 0 && service != cw.wo.Service { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if _, ok := cw.watchers[service]; ok { | ||||
| 			continue | ||||
| 		} | ||||
| 		wp, err := watch.Parse(map[string]interface{}{ | ||||
| 			"type":    "service", | ||||
| 			"service": service, | ||||
| 		}) | ||||
| 		if err == nil { | ||||
| 			wp.Handler = cw.serviceHandler | ||||
| 			go wp.RunWithClientAndHclog(cw.r.Client(), wp.Logger) | ||||
| 			cw.watchers[service] = wp | ||||
| 			cw.next <- ®istry.Result{Action: "create", Service: ®istry.Service{Name: service}} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	cw.RLock() | ||||
| 	// make a copy | ||||
| 	rservices := make(map[string][]*registry.Service) | ||||
| 	for k, v := range cw.services { | ||||
| 		rservices[k] = v | ||||
| 	} | ||||
| 	cw.RUnlock() | ||||
|  | ||||
| 	// remove unknown services from registry | ||||
| 	// save the things we want to delete | ||||
| 	deleted := make(map[string][]*registry.Service) | ||||
|  | ||||
| 	for service := range rservices { | ||||
| 		if _, ok := services[service]; !ok { | ||||
| 			cw.Lock() | ||||
| 			// save this before deleting | ||||
| 			deleted[service] = cw.services[service] | ||||
| 			delete(cw.services, service) | ||||
| 			cw.Unlock() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// remove unknown services from watchers | ||||
| 	for service, w := range cw.watchers { | ||||
| 		if _, ok := services[service]; !ok { | ||||
| 			w.Stop() | ||||
| 			delete(cw.watchers, service) | ||||
| 			for _, oldService := range deleted[service] { | ||||
| 				// send a delete for the service nodes that we're removing | ||||
| 				cw.next <- ®istry.Result{Action: "delete", Service: oldService} | ||||
| 			} | ||||
| 			// sent the empty list as the last resort to indicate to delete the entire service | ||||
| 			cw.next <- ®istry.Result{Action: "delete", Service: ®istry.Service{Name: service}} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (cw *consulWatcher) Next() (*registry.Result, error) { | ||||
| 	select { | ||||
| 	case <-cw.exit: | ||||
| 		return nil, registry.ErrWatcherStopped | ||||
| 	case r, ok := <-cw.next: | ||||
| 		if !ok { | ||||
| 			return nil, registry.ErrWatcherStopped | ||||
| 		} | ||||
| 		return r, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (cw *consulWatcher) Stop() { | ||||
| 	select { | ||||
| 	case <-cw.exit: | ||||
| 		return | ||||
| 	default: | ||||
| 		close(cw.exit) | ||||
| 		if cw.wp == nil { | ||||
| 			return | ||||
| 		} | ||||
| 		cw.wp.Stop() | ||||
|  | ||||
| 		// drain results | ||||
| 		for { | ||||
| 			select { | ||||
| 			case <-cw.next: | ||||
| 			default: | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										86
									
								
								registry/consul/watcher_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								registry/consul/watcher_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| package consul | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/hashicorp/consul/api" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| func TestHealthyServiceHandler(t *testing.T) { | ||||
| 	watcher := newWatcher() | ||||
| 	serviceEntry := newServiceEntry( | ||||
| 		"node-name", "node-address", "service-name", "v1.0.0", | ||||
| 		[]*api.HealthCheck{ | ||||
| 			newHealthCheck("node-name", "service-name", "passing"), | ||||
| 		}, | ||||
| 	) | ||||
|  | ||||
| 	watcher.serviceHandler(1234, []*api.ServiceEntry{serviceEntry}) | ||||
|  | ||||
| 	if len(watcher.services["service-name"][0].Nodes) != 1 { | ||||
| 		t.Errorf("Expected length of the service nodes to be 1") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestUnhealthyServiceHandler(t *testing.T) { | ||||
| 	watcher := newWatcher() | ||||
| 	serviceEntry := newServiceEntry( | ||||
| 		"node-name", "node-address", "service-name", "v1.0.0", | ||||
| 		[]*api.HealthCheck{ | ||||
| 			newHealthCheck("node-name", "service-name", "critical"), | ||||
| 		}, | ||||
| 	) | ||||
|  | ||||
| 	watcher.serviceHandler(1234, []*api.ServiceEntry{serviceEntry}) | ||||
|  | ||||
| 	if len(watcher.services["service-name"][0].Nodes) != 0 { | ||||
| 		t.Errorf("Expected length of the service nodes to be 0") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestUnhealthyNodeServiceHandler(t *testing.T) { | ||||
| 	watcher := newWatcher() | ||||
| 	serviceEntry := newServiceEntry( | ||||
| 		"node-name", "node-address", "service-name", "v1.0.0", | ||||
| 		[]*api.HealthCheck{ | ||||
| 			newHealthCheck("node-name", "service-name", "passing"), | ||||
| 			newHealthCheck("node-name", "serfHealth", "critical"), | ||||
| 		}, | ||||
| 	) | ||||
|  | ||||
| 	watcher.serviceHandler(1234, []*api.ServiceEntry{serviceEntry}) | ||||
|  | ||||
| 	if len(watcher.services["service-name"][0].Nodes) != 0 { | ||||
| 		t.Errorf("Expected length of the service nodes to be 0") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newWatcher() *consulWatcher { | ||||
| 	return &consulWatcher{ | ||||
| 		exit:     make(chan bool), | ||||
| 		next:     make(chan *registry.Result, 10), | ||||
| 		services: make(map[string][]*registry.Service), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newHealthCheck(node, name, status string) *api.HealthCheck { | ||||
| 	return &api.HealthCheck{ | ||||
| 		Node:        node, | ||||
| 		Name:        name, | ||||
| 		Status:      status, | ||||
| 		ServiceName: name, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newServiceEntry(node, address, name, version string, checks []*api.HealthCheck) *api.ServiceEntry { | ||||
| 	return &api.ServiceEntry{ | ||||
| 		Node: &api.Node{Node: node, Address: name}, | ||||
| 		Service: &api.AgentService{ | ||||
| 			Service: name, | ||||
| 			Address: address, | ||||
| 			Tags:    encodeVersion(version), | ||||
| 		}, | ||||
| 		Checks: checks, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										418
									
								
								registry/etcd/etcd.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										418
									
								
								registry/etcd/etcd.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,418 @@ | ||||
| // Package etcd provides an etcd service registry | ||||
| package etcd | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	hash "github.com/mitchellh/hashstructure" | ||||
| 	"go-micro.dev/v5/logger" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	"go.etcd.io/etcd/api/v3/v3rpc/rpctypes" | ||||
| 	clientv3 "go.etcd.io/etcd/client/v3" | ||||
| 	"go.uber.org/zap" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	prefix = "/micro/registry/" | ||||
| ) | ||||
|  | ||||
| type etcdRegistry struct { | ||||
| 	client  *clientv3.Client | ||||
| 	options registry.Options | ||||
|  | ||||
| 	sync.RWMutex | ||||
| 	register map[string]uint64 | ||||
| 	leases   map[string]clientv3.LeaseID | ||||
| } | ||||
|  | ||||
| func NewEtcdRegistry(opts ...registry.Option) registry.Registry { | ||||
| 	e := &etcdRegistry{ | ||||
| 		options:  registry.Options{}, | ||||
| 		register: make(map[string]uint64), | ||||
| 		leases:   make(map[string]clientv3.LeaseID), | ||||
| 	} | ||||
| 	username, password := os.Getenv("ETCD_USERNAME"), os.Getenv("ETCD_PASSWORD") | ||||
| 	if len(username) > 0 && len(password) > 0 { | ||||
| 		opts = append(opts, Auth(username, password)) | ||||
| 	} | ||||
| 	address := os.Getenv("MICRO_REGISTRY_ADDRESS") | ||||
| 	if len(address) > 0 { | ||||
| 		opts = append(opts, registry.Addrs(address)) | ||||
| 	} | ||||
| 	configure(e, opts...) | ||||
| 	return e | ||||
| } | ||||
|  | ||||
| func configure(e *etcdRegistry, opts ...registry.Option) error { | ||||
| 	config := clientv3.Config{ | ||||
| 		Endpoints: []string{"127.0.0.1:2379"}, | ||||
| 	} | ||||
|  | ||||
| 	for _, o := range opts { | ||||
| 		o(&e.options) | ||||
| 	} | ||||
|  | ||||
| 	if e.options.Timeout == 0 { | ||||
| 		e.options.Timeout = 5 * time.Second | ||||
| 	} | ||||
|  | ||||
| 	if e.options.Logger == nil { | ||||
| 		e.options.Logger = logger.DefaultLogger | ||||
| 	} | ||||
|  | ||||
| 	config.DialTimeout = e.options.Timeout | ||||
|  | ||||
| 	if e.options.Secure || e.options.TLSConfig != nil { | ||||
| 		tlsConfig := e.options.TLSConfig | ||||
| 		if tlsConfig == nil { | ||||
| 			tlsConfig = &tls.Config{ | ||||
| 				InsecureSkipVerify: true, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		config.TLS = tlsConfig | ||||
| 	} | ||||
|  | ||||
| 	if e.options.Context != nil { | ||||
| 		u, ok := e.options.Context.Value(authKey{}).(*authCreds) | ||||
| 		if ok { | ||||
| 			config.Username = u.Username | ||||
| 			config.Password = u.Password | ||||
| 		} | ||||
| 		cfg, ok := e.options.Context.Value(logConfigKey{}).(*zap.Config) | ||||
| 		if ok && cfg != nil { | ||||
| 			config.LogConfig = cfg | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var cAddrs []string | ||||
|  | ||||
| 	for _, address := range e.options.Addrs { | ||||
| 		if len(address) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 		addr, port, err := net.SplitHostPort(address) | ||||
| 		if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { | ||||
| 			port = "2379" | ||||
| 			addr = address | ||||
| 			cAddrs = append(cAddrs, net.JoinHostPort(addr, port)) | ||||
| 		} else if err == nil { | ||||
| 			cAddrs = append(cAddrs, net.JoinHostPort(addr, port)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// if we got addrs then we'll update | ||||
| 	if len(cAddrs) > 0 { | ||||
| 		config.Endpoints = cAddrs | ||||
| 	} | ||||
|  | ||||
| 	cli, err := clientv3.New(config) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	e.client = cli | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func encode(s *registry.Service) string { | ||||
| 	b, _ := json.Marshal(s) | ||||
| 	return string(b) | ||||
| } | ||||
|  | ||||
| func decode(ds []byte) *registry.Service { | ||||
| 	var s *registry.Service | ||||
| 	json.Unmarshal(ds, &s) | ||||
| 	return s | ||||
| } | ||||
|  | ||||
| func nodePath(s, id string) string { | ||||
| 	service := strings.Replace(s, "/", "-", -1) | ||||
| 	node := strings.Replace(id, "/", "-", -1) | ||||
| 	return path.Join(prefix, service, node) | ||||
| } | ||||
|  | ||||
| func servicePath(s string) string { | ||||
| 	return path.Join(prefix, strings.Replace(s, "/", "-", -1)) | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) Init(opts ...registry.Option) error { | ||||
| 	return configure(e, opts...) | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) Options() registry.Options { | ||||
| 	return e.options | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) registerNode(s *registry.Service, node *registry.Node, opts ...registry.RegisterOption) error { | ||||
| 	if len(s.Nodes) == 0 { | ||||
| 		return errors.New("Require at least one node") | ||||
| 	} | ||||
|  | ||||
| 	// check existing lease cache | ||||
| 	e.RLock() | ||||
| 	leaseID, ok := e.leases[s.Name+node.Id] | ||||
| 	e.RUnlock() | ||||
|  | ||||
| 	log := e.options.Logger | ||||
|  | ||||
| 	if !ok { | ||||
| 		// missing lease, check if the key exists | ||||
| 		ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) | ||||
| 		defer cancel() | ||||
|  | ||||
| 		// look for the existing key | ||||
| 		rsp, err := e.client.Get(ctx, nodePath(s.Name, node.Id), clientv3.WithSerializable()) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// get the existing lease | ||||
| 		for _, kv := range rsp.Kvs { | ||||
| 			if kv.Lease > 0 { | ||||
| 				leaseID = clientv3.LeaseID(kv.Lease) | ||||
|  | ||||
| 				// decode the existing node | ||||
| 				srv := decode(kv.Value) | ||||
| 				if srv == nil || len(srv.Nodes) == 0 { | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				// create hash of service; uint64 | ||||
| 				h, err := hash.Hash(srv.Nodes[0], nil) | ||||
| 				if err != nil { | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				// save the info | ||||
| 				e.Lock() | ||||
| 				e.leases[s.Name+node.Id] = leaseID | ||||
| 				e.register[s.Name+node.Id] = h | ||||
| 				e.Unlock() | ||||
|  | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var leaseNotFound bool | ||||
|  | ||||
| 	// renew the lease if it exists | ||||
| 	if leaseID > 0 { | ||||
| 		log.Logf(logger.TraceLevel, "Renewing existing lease for %s %d", s.Name, leaseID) | ||||
| 		if _, err := e.client.KeepAliveOnce(context.TODO(), leaseID); err != nil { | ||||
| 			if err != rpctypes.ErrLeaseNotFound { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			log.Logf(logger.TraceLevel, "Lease not found for %s %d", s.Name, leaseID) | ||||
| 			// lease not found do register | ||||
| 			leaseNotFound = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// create hash of service; uint64 | ||||
| 	h, err := hash.Hash(node, nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// get existing hash for the service node | ||||
| 	e.Lock() | ||||
| 	v, ok := e.register[s.Name+node.Id] | ||||
| 	e.Unlock() | ||||
|  | ||||
| 	// the service is unchanged, skip registering | ||||
| 	if ok && v == h && !leaseNotFound { | ||||
| 		log.Logf(logger.TraceLevel, "Service %s node %s unchanged skipping registration", s.Name, node.Id) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	service := ®istry.Service{ | ||||
| 		Name:      s.Name, | ||||
| 		Version:   s.Version, | ||||
| 		Metadata:  s.Metadata, | ||||
| 		Endpoints: s.Endpoints, | ||||
| 		Nodes:     []*registry.Node{node}, | ||||
| 	} | ||||
|  | ||||
| 	var options registry.RegisterOptions | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	var lgr *clientv3.LeaseGrantResponse | ||||
| 	if options.TTL.Seconds() > 0 { | ||||
| 		// get a lease used to expire keys since we have a ttl | ||||
| 		lgr, err = e.client.Grant(ctx, int64(options.TTL.Seconds())) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	log.Logf(logger.TraceLevel, "Registering %s id %s with lease %v and leaseID %v and ttl %v", service.Name, node.Id, lgr, lgr.ID, options.TTL) | ||||
| 	// create an entry for the node | ||||
| 	if lgr != nil { | ||||
| 		_, err = e.client.Put(ctx, nodePath(service.Name, node.Id), encode(service), clientv3.WithLease(lgr.ID)) | ||||
| 	} else { | ||||
| 		_, err = e.client.Put(ctx, nodePath(service.Name, node.Id), encode(service)) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	e.Lock() | ||||
| 	// save our hash of the service | ||||
| 	e.register[s.Name+node.Id] = h | ||||
| 	// save our leaseID of the service | ||||
| 	if lgr != nil { | ||||
| 		e.leases[s.Name+node.Id] = lgr.ID | ||||
| 	} | ||||
| 	e.Unlock() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) Deregister(s *registry.Service, opts ...registry.DeregisterOption) error { | ||||
| 	if len(s.Nodes) == 0 { | ||||
| 		return errors.New("Require at least one node") | ||||
| 	} | ||||
|  | ||||
| 	for _, node := range s.Nodes { | ||||
| 		e.Lock() | ||||
| 		// delete our hash of the service | ||||
| 		delete(e.register, s.Name+node.Id) | ||||
| 		// delete our lease of the service | ||||
| 		delete(e.leases, s.Name+node.Id) | ||||
| 		e.Unlock() | ||||
|  | ||||
| 		ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) | ||||
| 		defer cancel() | ||||
|  | ||||
| 		e.options.Logger.Logf(logger.TraceLevel, "Deregistering %s id %s", s.Name, node.Id) | ||||
| 		_, err := e.client.Delete(ctx, nodePath(s.Name, node.Id)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error { | ||||
| 	if len(s.Nodes) == 0 { | ||||
| 		return errors.New("Require at least one node") | ||||
| 	} | ||||
|  | ||||
| 	var gerr error | ||||
|  | ||||
| 	// register each node individually | ||||
| 	for _, node := range s.Nodes { | ||||
| 		err := e.registerNode(s, node, opts...) | ||||
| 		if err != nil { | ||||
| 			gerr = err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return gerr | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) GetService(name string, opts ...registry.GetOption) ([]*registry.Service, error) { | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rsp, err := e.client.Get(ctx, servicePath(name)+"/", clientv3.WithPrefix(), clientv3.WithSerializable()) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if len(rsp.Kvs) == 0 { | ||||
| 		return nil, registry.ErrNotFound | ||||
| 	} | ||||
|  | ||||
| 	serviceMap := map[string]*registry.Service{} | ||||
|  | ||||
| 	for _, n := range rsp.Kvs { | ||||
| 		if sn := decode(n.Value); sn != nil { | ||||
| 			s, ok := serviceMap[sn.Version] | ||||
| 			if !ok { | ||||
| 				s = ®istry.Service{ | ||||
| 					Name:      sn.Name, | ||||
| 					Version:   sn.Version, | ||||
| 					Metadata:  sn.Metadata, | ||||
| 					Endpoints: sn.Endpoints, | ||||
| 				} | ||||
| 				serviceMap[s.Version] = s | ||||
| 			} | ||||
|  | ||||
| 			s.Nodes = append(s.Nodes, sn.Nodes...) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	services := make([]*registry.Service, 0, len(serviceMap)) | ||||
| 	for _, service := range serviceMap { | ||||
| 		services = append(services, service) | ||||
| 	} | ||||
|  | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) { | ||||
| 	versions := make(map[string]*registry.Service) | ||||
|  | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), e.options.Timeout) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	rsp, err := e.client.Get(ctx, prefix, clientv3.WithPrefix(), clientv3.WithSerializable()) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if len(rsp.Kvs) == 0 { | ||||
| 		return []*registry.Service{}, nil | ||||
| 	} | ||||
|  | ||||
| 	for _, n := range rsp.Kvs { | ||||
| 		sn := decode(n.Value) | ||||
| 		if sn == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		v, ok := versions[sn.Name+sn.Version] | ||||
| 		if !ok { | ||||
| 			versions[sn.Name+sn.Version] = sn | ||||
| 			continue | ||||
| 		} | ||||
| 		// append to service:version nodes | ||||
| 		v.Nodes = append(v.Nodes, sn.Nodes...) | ||||
| 	} | ||||
|  | ||||
| 	services := make([]*registry.Service, 0, len(versions)) | ||||
| 	for _, service := range versions { | ||||
| 		services = append(services, service) | ||||
| 	} | ||||
|  | ||||
| 	// sort the services | ||||
| 	sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name }) | ||||
|  | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) { | ||||
| 	return newEtcdWatcher(e, e.options.Timeout, opts...) | ||||
| } | ||||
|  | ||||
| func (e *etcdRegistry) String() string { | ||||
| 	return "etcd" | ||||
| } | ||||
							
								
								
									
										37
									
								
								registry/etcd/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								registry/etcd/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| package etcd | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	"go.uber.org/zap" | ||||
| ) | ||||
|  | ||||
| type authKey struct{} | ||||
|  | ||||
| type logConfigKey struct{} | ||||
|  | ||||
| type authCreds struct { | ||||
| 	Username string | ||||
| 	Password string | ||||
| } | ||||
|  | ||||
| // Auth allows you to specify username/password. | ||||
| func Auth(username, password string) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, authKey{}, &authCreds{Username: username, Password: password}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // LogConfig allows you to set etcd log config. | ||||
| func LogConfig(config *zap.Config) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, logConfigKey{}, config) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										91
									
								
								registry/etcd/watcher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								registry/etcd/watcher.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| package etcd | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"time" | ||||
|  | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	clientv3 "go.etcd.io/etcd/client/v3" | ||||
| ) | ||||
|  | ||||
| type etcdWatcher struct { | ||||
| 	stop    chan bool | ||||
| 	w       clientv3.WatchChan | ||||
| 	client  *clientv3.Client | ||||
| 	timeout time.Duration | ||||
| } | ||||
|  | ||||
| func newEtcdWatcher(r *etcdRegistry, timeout time.Duration, opts ...registry.WatchOption) (registry.Watcher, error) { | ||||
| 	var wo registry.WatchOptions | ||||
| 	for _, o := range opts { | ||||
| 		o(&wo) | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	stop := make(chan bool, 1) | ||||
|  | ||||
| 	go func() { | ||||
| 		<-stop | ||||
| 		cancel() | ||||
| 	}() | ||||
|  | ||||
| 	watchPath := prefix | ||||
| 	if len(wo.Service) > 0 { | ||||
| 		watchPath = servicePath(wo.Service) + "/" | ||||
| 	} | ||||
|  | ||||
| 	return &etcdWatcher{ | ||||
| 		stop:    stop, | ||||
| 		w:       r.client.Watch(ctx, watchPath, clientv3.WithPrefix(), clientv3.WithPrevKV()), | ||||
| 		client:  r.client, | ||||
| 		timeout: timeout, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (ew *etcdWatcher) Next() (*registry.Result, error) { | ||||
| 	for wresp := range ew.w { | ||||
| 		if wresp.Err() != nil { | ||||
| 			return nil, wresp.Err() | ||||
| 		} | ||||
| 		if wresp.Canceled { | ||||
| 			return nil, errors.New("could not get next") | ||||
| 		} | ||||
| 		for _, ev := range wresp.Events { | ||||
| 			service := decode(ev.Kv.Value) | ||||
| 			var action string | ||||
|  | ||||
| 			switch ev.Type { | ||||
| 			case clientv3.EventTypePut: | ||||
| 				if ev.IsCreate() { | ||||
| 					action = "create" | ||||
| 				} else if ev.IsModify() { | ||||
| 					action = "update" | ||||
| 				} | ||||
| 			case clientv3.EventTypeDelete: | ||||
| 				action = "delete" | ||||
|  | ||||
| 				// get service from prevKv | ||||
| 				service = decode(ev.PrevKv.Value) | ||||
| 			} | ||||
|  | ||||
| 			if service == nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			return ®istry.Result{ | ||||
| 				Action:  action, | ||||
| 				Service: service, | ||||
| 			}, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return nil, errors.New("could not get next") | ||||
| } | ||||
|  | ||||
| func (ew *etcdWatcher) Stop() { | ||||
| 	select { | ||||
| 	case <-ew.stop: | ||||
| 		return | ||||
| 	default: | ||||
| 		close(ew.stop) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										5
									
								
								registry/mdns/mdns.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								registry/mdns/mdns.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| package mdns | ||||
|  | ||||
| var ( | ||||
| 	DefaultRegistry = NewMDNSRegistry() | ||||
| ) | ||||
| @@ -1,5 +1,5 @@ | ||||
| // Package mdns is a multicast dns registry | ||||
| package registry | ||||
| package mdns | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| @@ -17,6 +17,7 @@ import ( | ||||
| 
 | ||||
| 	"github.com/google/uuid" | ||||
| 	log "go-micro.dev/v5/logger" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	"go-micro.dev/v5/util/mdns" | ||||
| ) | ||||
| 
 | ||||
| @@ -29,7 +30,7 @@ type mdnsTxt struct { | ||||
| 	Metadata  map[string]string | ||||
| 	Service   string | ||||
| 	Version   string | ||||
| 	Endpoints []*Endpoint | ||||
| 	Endpoints []*registry.Endpoint | ||||
| } | ||||
| 
 | ||||
| type mdnsEntry struct { | ||||
| @@ -38,7 +39,7 @@ type mdnsEntry struct { | ||||
| } | ||||
| 
 | ||||
| type mdnsRegistry struct { | ||||
| 	opts     *Options | ||||
| 	opts     *registry.Options | ||||
| 	services map[string][]*mdnsEntry | ||||
| 
 | ||||
| 	// watchers | ||||
| @@ -55,7 +56,7 @@ type mdnsRegistry struct { | ||||
| } | ||||
| 
 | ||||
| type mdnsWatcher struct { | ||||
| 	wo   WatchOptions | ||||
| 	wo   registry.WatchOptions | ||||
| 	ch   chan *mdns.ServiceEntry | ||||
| 	exit chan struct{} | ||||
| 	// the registry | ||||
| @@ -127,9 +128,9 @@ func decode(record []string) (*mdnsTxt, error) { | ||||
| 
 | ||||
| 	return txt, nil | ||||
| } | ||||
| func newRegistry(opts ...Option) Registry { | ||||
| 	mergedOpts := append([]Option{Timeout(time.Millisecond * 100)}, opts...) | ||||
| 	options := NewOptions(mergedOpts...) | ||||
| func newRegistry(opts ...registry.Option) registry.Registry { | ||||
| 	mergedOpts := append([]registry.Option{registry.Timeout(time.Millisecond * 100)}, opts...) | ||||
| 	options := registry.NewOptions(mergedOpts...) | ||||
| 
 | ||||
| 	// set the domain | ||||
| 	domain := mdnsDomain | ||||
| @@ -147,18 +148,18 @@ func newRegistry(opts ...Option) Registry { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (m *mdnsRegistry) Init(opts ...Option) error { | ||||
| func (m *mdnsRegistry) Init(opts ...registry.Option) error { | ||||
| 	for _, o := range opts { | ||||
| 		o(m.opts) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *mdnsRegistry) Options() Options { | ||||
| func (m *mdnsRegistry) Options() registry.Options { | ||||
| 	return *m.opts | ||||
| } | ||||
| 
 | ||||
| func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error { | ||||
| func (m *mdnsRegistry) Register(service *registry.Service, opts ...registry.RegisterOption) error { | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
| 
 | ||||
| @@ -263,7 +264,7 @@ func (m *mdnsRegistry) Register(service *Service, opts ...RegisterOption) error | ||||
| 	return gerr | ||||
| } | ||||
| 
 | ||||
| func (m *mdnsRegistry) Deregister(service *Service, opts ...DeregisterOption) error { | ||||
| func (m *mdnsRegistry) Deregister(service *registry.Service, opts ...registry.DeregisterOption) error { | ||||
| 	m.Lock() | ||||
| 	defer m.Unlock() | ||||
| 
 | ||||
| @@ -298,9 +299,9 @@ func (m *mdnsRegistry) Deregister(service *Service, opts ...DeregisterOption) er | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service, error) { | ||||
| func (m *mdnsRegistry) GetService(service string, opts ...registry.GetOption) ([]*registry.Service, error) { | ||||
| 	logger := m.opts.Logger | ||||
| 	serviceMap := make(map[string]*Service) | ||||
| 	serviceMap := make(map[string]*registry.Service) | ||||
| 	entries := make(chan *mdns.ServiceEntry, 10) | ||||
| 	done := make(chan bool) | ||||
| 
 | ||||
| @@ -340,7 +341,7 @@ func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service | ||||
| 
 | ||||
| 				s, ok := serviceMap[txt.Version] | ||||
| 				if !ok { | ||||
| 					s = &Service{ | ||||
| 					s = ®istry.Service{ | ||||
| 						Name:      txt.Service, | ||||
| 						Version:   txt.Version, | ||||
| 						Endpoints: txt.Endpoints, | ||||
| @@ -357,7 +358,7 @@ func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service | ||||
| 					logger.Logf(log.InfoLevel, "[mdns]: invalid endpoint received: %v", e) | ||||
| 					continue | ||||
| 				} | ||||
| 				s.Nodes = append(s.Nodes, &Node{ | ||||
| 				s.Nodes = append(s.Nodes, ®istry.Node{ | ||||
| 					Id:       strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+"."), | ||||
| 					Address:  addr, | ||||
| 					Metadata: txt.Metadata, | ||||
| @@ -380,7 +381,7 @@ func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service | ||||
| 	<-done | ||||
| 
 | ||||
| 	// create list and return | ||||
| 	services := make([]*Service, 0, len(serviceMap)) | ||||
| 	services := make([]*registry.Service, 0, len(serviceMap)) | ||||
| 
 | ||||
| 	for _, service := range serviceMap { | ||||
| 		services = append(services, service) | ||||
| @@ -389,7 +390,7 @@ func (m *mdnsRegistry) GetService(service string, opts ...GetOption) ([]*Service | ||||
| 	return services, nil | ||||
| } | ||||
| 
 | ||||
| func (m *mdnsRegistry) ListServices(opts ...ListOption) ([]*Service, error) { | ||||
| func (m *mdnsRegistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) { | ||||
| 	serviceMap := make(map[string]bool) | ||||
| 	entries := make(chan *mdns.ServiceEntry, 10) | ||||
| 	done := make(chan bool) | ||||
| @@ -404,7 +405,7 @@ func (m *mdnsRegistry) ListServices(opts ...ListOption) ([]*Service, error) { | ||||
| 	// set domain | ||||
| 	p.Domain = m.domain | ||||
| 
 | ||||
| 	var services []*Service | ||||
| 	var services []*registry.Service | ||||
| 
 | ||||
| 	go func() { | ||||
| 		for { | ||||
| @@ -419,7 +420,7 @@ func (m *mdnsRegistry) ListServices(opts ...ListOption) ([]*Service, error) { | ||||
| 				name := strings.TrimSuffix(e.Name, "."+p.Service+"."+p.Domain+".") | ||||
| 				if !serviceMap[name] { | ||||
| 					serviceMap[name] = true | ||||
| 					services = append(services, &Service{Name: name}) | ||||
| 					services = append(services, ®istry.Service{Name: name}) | ||||
| 				} | ||||
| 			case <-p.Context.Done(): | ||||
| 				close(done) | ||||
| @@ -439,8 +440,8 @@ func (m *mdnsRegistry) ListServices(opts ...ListOption) ([]*Service, error) { | ||||
| 	return services, nil | ||||
| } | ||||
| 
 | ||||
| func (m *mdnsRegistry) Watch(opts ...WatchOption) (Watcher, error) { | ||||
| 	var wo WatchOptions | ||||
| func (m *mdnsRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) { | ||||
| 	var wo registry.WatchOptions | ||||
| 	for _, o := range opts { | ||||
| 		o(&wo) | ||||
| 	} | ||||
| @@ -537,7 +538,7 @@ func (m *mdnsRegistry) String() string { | ||||
| 	return "mdns" | ||||
| } | ||||
| 
 | ||||
| func (m *mdnsWatcher) Next() (*Result, error) { | ||||
| func (m *mdnsWatcher) Next() (*registry.Result, error) { | ||||
| 	for { | ||||
| 		select { | ||||
| 		case e := <-m.ch: | ||||
| @@ -562,7 +563,7 @@ func (m *mdnsWatcher) Next() (*Result, error) { | ||||
| 				action = "create" | ||||
| 			} | ||||
| 
 | ||||
| 			service := &Service{ | ||||
| 			service := ®istry.Service{ | ||||
| 				Name:      txt.Service, | ||||
| 				Version:   txt.Version, | ||||
| 				Endpoints: txt.Endpoints, | ||||
| @@ -583,18 +584,18 @@ func (m *mdnsWatcher) Next() (*Result, error) { | ||||
| 				addr = e.Addr.String() | ||||
| 			} | ||||
| 
 | ||||
| 			service.Nodes = append(service.Nodes, &Node{ | ||||
| 			service.Nodes = append(service.Nodes, ®istry.Node{ | ||||
| 				Id:       strings.TrimSuffix(e.Name, suffix), | ||||
| 				Address:  addr, | ||||
| 				Metadata: txt.Metadata, | ||||
| 			}) | ||||
| 
 | ||||
| 			return &Result{ | ||||
| 			return ®istry.Result{ | ||||
| 				Action:  action, | ||||
| 				Service: service, | ||||
| 			}, nil | ||||
| 		case <-m.exit: | ||||
| 			return nil, ErrWatcherStopped | ||||
| 			return nil, registry.ErrWatcherStopped | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -613,6 +614,6 @@ func (m *mdnsWatcher) Stop() { | ||||
| } | ||||
| 
 | ||||
| // NewRegistry returns a new default registry which is mdns. | ||||
| func NewRegistry(opts ...Option) Registry { | ||||
| func NewMDNSRegistry(opts ...registry.Option) registry.Registry { | ||||
| 	return newRegistry(opts...) | ||||
| } | ||||
| @@ -1,9 +1,11 @@ | ||||
| package registry | ||||
| package mdns | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
| 
 | ||||
| func TestMDNS(t *testing.T) { | ||||
| @@ -12,11 +14,11 @@ func TestMDNS(t *testing.T) { | ||||
| 		t.Skip() | ||||
| 	} | ||||
| 
 | ||||
| 	testData := []*Service{ | ||||
| 	testData := []*registry.Service{ | ||||
| 		{ | ||||
| 			Name:    "test1", | ||||
| 			Version: "1.0.1", | ||||
| 			Nodes: []*Node{ | ||||
| 			Nodes: []*registry.Node{ | ||||
| 				{ | ||||
| 					Id:      "test1-1", | ||||
| 					Address: "10.0.0.1:10001", | ||||
| @@ -29,7 +31,7 @@ func TestMDNS(t *testing.T) { | ||||
| 		{ | ||||
| 			Name:    "test2", | ||||
| 			Version: "1.0.2", | ||||
| 			Nodes: []*Node{ | ||||
| 			Nodes: []*registry.Node{ | ||||
| 				{ | ||||
| 					Id:      "test2-1", | ||||
| 					Address: "10.0.0.2:10002", | ||||
| @@ -42,7 +44,7 @@ func TestMDNS(t *testing.T) { | ||||
| 		{ | ||||
| 			Name:    "test3", | ||||
| 			Version: "1.0.3", | ||||
| 			Nodes: []*Node{ | ||||
| 			Nodes: []*registry.Node{ | ||||
| 				{ | ||||
| 					Id:      "test3-1", | ||||
| 					Address: "10.0.0.3:10003", | ||||
| @@ -55,7 +57,7 @@ func TestMDNS(t *testing.T) { | ||||
| 		{ | ||||
| 			Name:    "test4", | ||||
| 			Version: "1.0.4", | ||||
| 			Nodes: []*Node{ | ||||
| 			Nodes: []*registry.Node{ | ||||
| 				{ | ||||
| 					Id:      "test4-1", | ||||
| 					Address: "[::]:10004", | ||||
| @@ -69,14 +71,14 @@ func TestMDNS(t *testing.T) { | ||||
| 
 | ||||
| 	travis := os.Getenv("TRAVIS") | ||||
| 
 | ||||
| 	var opts []Option | ||||
| 	var opts []registry.Option | ||||
| 
 | ||||
| 	if travis == "true" { | ||||
| 		opts = append(opts, Timeout(time.Millisecond*100)) | ||||
| 		opts = append(opts, registry.Timeout(time.Millisecond*100)) | ||||
| 	} | ||||
| 
 | ||||
| 	// new registry | ||||
| 	r := NewRegistry(opts...) | ||||
| 	r := NewMDNSRegistry(opts...) | ||||
| 
 | ||||
| 	for _, service := range testData { | ||||
| 		// register service | ||||
| @@ -156,14 +158,14 @@ func TestEncoding(t *testing.T) { | ||||
| 			Metadata: map[string]string{ | ||||
| 				"foo": "bar", | ||||
| 			}, | ||||
| 			Endpoints: []*Endpoint{ | ||||
| 			Endpoints: []*registry.Endpoint{ | ||||
| 				{ | ||||
| 					Name: "endpoint1", | ||||
| 					Request: &Value{ | ||||
| 					Request: ®istry.Value{ | ||||
| 						Name: "request", | ||||
| 						Type: "request", | ||||
| 					}, | ||||
| 					Response: &Value{ | ||||
| 					Response: ®istry.Value{ | ||||
| 						Name: "response", | ||||
| 						Type: "response", | ||||
| 					}, | ||||
| @@ -213,11 +215,11 @@ func TestWatcher(t *testing.T) { | ||||
| 		t.Skip() | ||||
| 	} | ||||
| 
 | ||||
| 	testData := []*Service{ | ||||
| 	testData := []*registry.Service{ | ||||
| 		{ | ||||
| 			Name:    "test1", | ||||
| 			Version: "1.0.1", | ||||
| 			Nodes: []*Node{ | ||||
| 			Nodes: []*registry.Node{ | ||||
| 				{ | ||||
| 					Id:      "test1-1", | ||||
| 					Address: "10.0.0.1:10001", | ||||
| @@ -230,7 +232,7 @@ func TestWatcher(t *testing.T) { | ||||
| 		{ | ||||
| 			Name:    "test2", | ||||
| 			Version: "1.0.2", | ||||
| 			Nodes: []*Node{ | ||||
| 			Nodes: []*registry.Node{ | ||||
| 				{ | ||||
| 					Id:      "test2-1", | ||||
| 					Address: "10.0.0.2:10002", | ||||
| @@ -243,7 +245,7 @@ func TestWatcher(t *testing.T) { | ||||
| 		{ | ||||
| 			Name:    "test3", | ||||
| 			Version: "1.0.3", | ||||
| 			Nodes: []*Node{ | ||||
| 			Nodes: []*registry.Node{ | ||||
| 				{ | ||||
| 					Id:      "test3-1", | ||||
| 					Address: "10.0.0.3:10003", | ||||
| @@ -256,7 +258,7 @@ func TestWatcher(t *testing.T) { | ||||
| 		{ | ||||
| 			Name:    "test4", | ||||
| 			Version: "1.0.4", | ||||
| 			Nodes: []*Node{ | ||||
| 			Nodes: []*registry.Node{ | ||||
| 				{ | ||||
| 					Id:      "test4-1", | ||||
| 					Address: "[::]:10004", | ||||
| @@ -268,7 +270,7 @@ func TestWatcher(t *testing.T) { | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	testFn := func(service, s *Service) { | ||||
| 	testFn := func(service, s *registry.Service) { | ||||
| 		if s == nil { | ||||
| 			t.Fatalf("Expected one result for %s got nil", service.Name) | ||||
| 		} | ||||
| @@ -298,14 +300,14 @@ func TestWatcher(t *testing.T) { | ||||
| 
 | ||||
| 	travis := os.Getenv("TRAVIS") | ||||
| 
 | ||||
| 	var opts []Option | ||||
| 	var opts []registry.Option | ||||
| 
 | ||||
| 	if travis == "true" { | ||||
| 		opts = append(opts, Timeout(time.Millisecond*100)) | ||||
| 		opts = append(opts, registry.Timeout(time.Millisecond*100)) | ||||
| 	} | ||||
| 
 | ||||
| 	// new registry | ||||
| 	r := NewRegistry(opts...) | ||||
| 	r := NewMDNSRegistry(opts...) | ||||
| 
 | ||||
| 	w, err := r.Watch() | ||||
| 	if err != nil { | ||||
							
								
								
									
										417
									
								
								registry/nats/nats.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								registry/nats/nats.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,417 @@ | ||||
| // Package nats provides a NATS registry using broadcast queries | ||||
| package nats | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/nats-io/nats.go" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| type natsRegistry struct { | ||||
| 	addrs          []string | ||||
| 	opts           registry.Options | ||||
| 	nopts          nats.Options | ||||
| 	queryTopic     string | ||||
| 	watchTopic     string | ||||
| 	registerAction string | ||||
|  | ||||
| 	sync.RWMutex | ||||
| 	conn      *nats.Conn | ||||
| 	services  map[string][]*registry.Service | ||||
| 	listeners map[string]chan bool | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	defaultQueryTopic     = "micro.nats.query" | ||||
| 	defaultWatchTopic     = "micro.nats.watch" | ||||
| 	defaultRegisterAction = "create" | ||||
| ) | ||||
|  | ||||
| func configure(n *natsRegistry, opts ...registry.Option) error { | ||||
| 	for _, o := range opts { | ||||
| 		o(&n.opts) | ||||
| 	} | ||||
|  | ||||
| 	natsOptions := nats.GetDefaultOptions() | ||||
| 	if n, ok := n.opts.Context.Value(optionsKey{}).(nats.Options); ok { | ||||
| 		natsOptions = n | ||||
| 	} | ||||
|  | ||||
| 	queryTopic := defaultQueryTopic | ||||
| 	if qt, ok := n.opts.Context.Value(queryTopicKey{}).(string); ok { | ||||
| 		queryTopic = qt | ||||
| 	} | ||||
|  | ||||
| 	watchTopic := defaultWatchTopic | ||||
| 	if wt, ok := n.opts.Context.Value(watchTopicKey{}).(string); ok { | ||||
| 		watchTopic = wt | ||||
| 	} | ||||
|  | ||||
| 	registerAction := defaultRegisterAction | ||||
| 	if ra, ok := n.opts.Context.Value(registerActionKey{}).(string); ok { | ||||
| 		registerAction = ra | ||||
| 	} | ||||
|  | ||||
| 	// Options have higher priority than nats.Options | ||||
| 	// only if Addrs, Secure or TLSConfig were not set through a Option | ||||
| 	// we read them from nats.Option | ||||
| 	if len(n.opts.Addrs) == 0 { | ||||
| 		n.opts.Addrs = natsOptions.Servers | ||||
| 	} | ||||
|  | ||||
| 	if !n.opts.Secure { | ||||
| 		n.opts.Secure = natsOptions.Secure | ||||
| 	} | ||||
|  | ||||
| 	if n.opts.TLSConfig == nil { | ||||
| 		n.opts.TLSConfig = natsOptions.TLSConfig | ||||
| 	} | ||||
|  | ||||
| 	// check & add nats:// prefix (this makes also sure that the addresses | ||||
| 	// stored in natsaddrs and n.opts.Addrs are identical) | ||||
| 	n.opts.Addrs = setAddrs(n.opts.Addrs) | ||||
|  | ||||
| 	n.addrs = n.opts.Addrs | ||||
| 	n.nopts = natsOptions | ||||
| 	n.queryTopic = queryTopic | ||||
| 	n.watchTopic = watchTopic | ||||
| 	n.registerAction = registerAction | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func setAddrs(addrs []string) []string { | ||||
| 	var cAddrs []string | ||||
| 	for _, addr := range addrs { | ||||
| 		if len(addr) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
| 		if !strings.HasPrefix(addr, "nats://") { | ||||
| 			addr = "nats://" + addr | ||||
| 		} | ||||
| 		cAddrs = append(cAddrs, addr) | ||||
| 	} | ||||
| 	if len(cAddrs) == 0 { | ||||
| 		cAddrs = []string{nats.DefaultURL} | ||||
| 	} | ||||
| 	return cAddrs | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) newConn() (*nats.Conn, error) { | ||||
| 	opts := n.nopts | ||||
| 	opts.Servers = n.addrs | ||||
| 	opts.Secure = n.opts.Secure | ||||
| 	opts.TLSConfig = n.opts.TLSConfig | ||||
|  | ||||
| 	// secure might not be set | ||||
| 	if opts.TLSConfig != nil { | ||||
| 		opts.Secure = true | ||||
| 	} | ||||
|  | ||||
| 	return opts.Connect() | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) getConn() (*nats.Conn, error) { | ||||
| 	n.Lock() | ||||
| 	defer n.Unlock() | ||||
|  | ||||
| 	if n.conn != nil { | ||||
| 		return n.conn, nil | ||||
| 	} | ||||
|  | ||||
| 	c, err := n.newConn() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	n.conn = c | ||||
|  | ||||
| 	return n.conn, nil | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) register(s *registry.Service) error { | ||||
| 	conn, err := n.getConn() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	n.Lock() | ||||
| 	defer n.Unlock() | ||||
|  | ||||
| 	// cache service | ||||
| 	n.services[s.Name] = addServices(n.services[s.Name], cp([]*registry.Service{s})) | ||||
|  | ||||
| 	// create query listener | ||||
| 	if n.listeners[s.Name] == nil { | ||||
| 		listener := make(chan bool) | ||||
|  | ||||
| 		// create a subscriber that responds to queries | ||||
| 		sub, err := conn.Subscribe(n.queryTopic, func(m *nats.Msg) { | ||||
| 			var result *registry.Result | ||||
|  | ||||
| 			if err := json.Unmarshal(m.Data, &result); err != nil { | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			var services []*registry.Service | ||||
|  | ||||
| 			switch result.Action { | ||||
| 			// is this a get query and we own the service? | ||||
| 			case "get": | ||||
| 				if result.Service.Name != s.Name { | ||||
| 					return | ||||
| 				} | ||||
| 				n.RLock() | ||||
| 				services = cp(n.services[s.Name]) | ||||
| 				n.RUnlock() | ||||
| 			// it's a list request, but we're still only a | ||||
| 			// subscriber for this service... so just get this service | ||||
| 			// totally suboptimal | ||||
| 			case "list": | ||||
| 				n.RLock() | ||||
| 				services = cp(n.services[s.Name]) | ||||
| 				n.RUnlock() | ||||
| 			default: | ||||
| 				// does not match | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// respond to query | ||||
| 			for _, service := range services { | ||||
| 				b, err := json.Marshal(service) | ||||
| 				if err != nil { | ||||
| 					continue | ||||
| 				} | ||||
| 				conn.Publish(m.Reply, b) | ||||
| 			} | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// Unsubscribe if we're told to do so | ||||
| 		go func() { | ||||
| 			<-listener | ||||
| 			sub.Unsubscribe() | ||||
| 		}() | ||||
|  | ||||
| 		n.listeners[s.Name] = listener | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) deregister(s *registry.Service) error { | ||||
| 	n.Lock() | ||||
| 	defer n.Unlock() | ||||
|  | ||||
| 	services := delServices(n.services[s.Name], cp([]*registry.Service{s})) | ||||
| 	if len(services) > 0 { | ||||
| 		n.services[s.Name] = services | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// delete cached service | ||||
| 	delete(n.services, s.Name) | ||||
|  | ||||
| 	// delete query listener | ||||
| 	if listener, lexists := n.listeners[s.Name]; lexists { | ||||
| 		close(listener) | ||||
| 		delete(n.listeners, s.Name) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) query(s string, quorum int) ([]*registry.Service, error) { | ||||
| 	conn, err := n.getConn() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var action string | ||||
| 	var service *registry.Service | ||||
|  | ||||
| 	if len(s) > 0 { | ||||
| 		action = "get" | ||||
| 		service = ®istry.Service{Name: s} | ||||
| 	} else { | ||||
| 		action = "list" | ||||
| 	} | ||||
|  | ||||
| 	inbox := nats.NewInbox() | ||||
|  | ||||
| 	response := make(chan *registry.Service, 10) | ||||
|  | ||||
| 	sub, err := conn.Subscribe(inbox, func(m *nats.Msg) { | ||||
| 		var service *registry.Service | ||||
| 		if err := json.Unmarshal(m.Data, &service); err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		select { | ||||
| 		case response <- service: | ||||
| 		case <-time.After(n.opts.Timeout): | ||||
| 		} | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer sub.Unsubscribe() | ||||
|  | ||||
| 	b, err := json.Marshal(®istry.Result{Action: action, Service: service}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := conn.PublishMsg(&nats.Msg{ | ||||
| 		Subject: n.queryTopic, | ||||
| 		Reply:   inbox, | ||||
| 		Data:    b, | ||||
| 	}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	timeoutChan := time.After(n.opts.Timeout) | ||||
|  | ||||
| 	serviceMap := make(map[string]*registry.Service) | ||||
|  | ||||
| loop: | ||||
| 	for { | ||||
| 		select { | ||||
| 		case service := <-response: | ||||
| 			key := service.Name + "-" + service.Version | ||||
| 			srv, ok := serviceMap[key] | ||||
| 			if ok { | ||||
| 				srv.Nodes = append(srv.Nodes, service.Nodes...) | ||||
| 				serviceMap[key] = srv | ||||
| 			} else { | ||||
| 				serviceMap[key] = service | ||||
| 			} | ||||
|  | ||||
| 			if quorum > 0 && len(serviceMap[key].Nodes) >= quorum { | ||||
| 				break loop | ||||
| 			} | ||||
| 		case <-timeoutChan: | ||||
| 			break loop | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var services []*registry.Service | ||||
| 	for _, service := range serviceMap { | ||||
| 		services = append(services, service) | ||||
| 	} | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) Init(opts ...registry.Option) error { | ||||
| 	return configure(n, opts...) | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) Options() registry.Options { | ||||
| 	return n.opts | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error { | ||||
| 	if err := n.register(s); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	conn, err := n.getConn() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	b, err := json.Marshal(®istry.Result{Action: n.registerAction, Service: s}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return conn.Publish(n.watchTopic, b) | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) Deregister(s *registry.Service, opts ...registry.DeregisterOption) error { | ||||
| 	if err := n.deregister(s); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	conn, err := n.getConn() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	b, err := json.Marshal(®istry.Result{Action: "delete", Service: s}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return conn.Publish(n.watchTopic, b) | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) GetService(s string, opts ...registry.GetOption) ([]*registry.Service, error) { | ||||
| 	services, err := n.query(s, getQuorum(n.opts)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) { | ||||
| 	s, err := n.query("", 0) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var services []*registry.Service | ||||
| 	serviceMap := make(map[string]*registry.Service) | ||||
|  | ||||
| 	for _, v := range s { | ||||
| 		serviceMap[v.Name] = ®istry.Service{Name: v.Name, Version: v.Version} | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range serviceMap { | ||||
| 		services = append(services, v) | ||||
| 	} | ||||
|  | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) { | ||||
| 	conn, err := n.getConn() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	sub, err := conn.SubscribeSync(n.watchTopic) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var wo registry.WatchOptions | ||||
| 	for _, o := range opts { | ||||
| 		o(&wo) | ||||
| 	} | ||||
|  | ||||
| 	return &natsWatcher{sub, wo}, nil | ||||
| } | ||||
|  | ||||
| func (n *natsRegistry) String() string { | ||||
| 	return "nats" | ||||
| } | ||||
|  | ||||
| func NewNatsRegistry(opts ...registry.Option) registry.Registry { | ||||
| 	options := registry.Options{ | ||||
| 		Timeout: time.Millisecond * 100, | ||||
| 		Context: context.Background(), | ||||
| 	} | ||||
|  | ||||
| 	n := &natsRegistry{ | ||||
| 		opts:      options, | ||||
| 		services:  make(map[string][]*registry.Service), | ||||
| 		listeners: make(map[string]chan bool), | ||||
| 	} | ||||
| 	configure(n, opts...) | ||||
| 	return n | ||||
| } | ||||
							
								
								
									
										18
									
								
								registry/nats/nats_assert_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								registry/nats/nats_assert_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| package nats_test | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func assertNoError(tb testing.TB, actual error) { | ||||
| 	if actual != nil { | ||||
| 		tb.Errorf("expected no error, got %v", actual) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func assertEqual(tb testing.TB, expected, actual interface{}) { | ||||
| 	if !reflect.DeepEqual(expected, actual) { | ||||
| 		tb.Errorf("expected %v, got %v", expected, actual) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										69
									
								
								registry/nats/nats_environment_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								registry/nats/nats_environment_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| package nats_test | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
|  | ||||
| 	log "go-micro.dev/v5/logger" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	"go-micro.dev/v5/registry/nats" | ||||
| ) | ||||
|  | ||||
| type environment struct { | ||||
| 	registryOne   registry.Registry | ||||
| 	registryTwo   registry.Registry | ||||
| 	registryThree registry.Registry | ||||
|  | ||||
| 	serviceOne registry.Service | ||||
| 	serviceTwo registry.Service | ||||
|  | ||||
| 	nodeOne   registry.Node | ||||
| 	nodeTwo   registry.Node | ||||
| 	nodeThree registry.Node | ||||
| } | ||||
|  | ||||
| var e environment | ||||
|  | ||||
| func TestMain(m *testing.M) { | ||||
| 	natsURL := os.Getenv("NATS_URL") | ||||
| 	if natsURL == "" { | ||||
| 		log.Infof("NATS_URL is undefined - skipping tests") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	e.registryOne = nats.NewNatsRegistry(registry.Addrs(natsURL), nats.Quorum(1)) | ||||
| 	e.registryTwo = nats.NewNatsRegistry(registry.Addrs(natsURL), nats.Quorum(1)) | ||||
| 	e.registryThree = nats.NewNatsRegistry(registry.Addrs(natsURL), nats.Quorum(1)) | ||||
|  | ||||
| 	e.serviceOne.Name = "one" | ||||
| 	e.serviceOne.Version = "default" | ||||
| 	e.serviceOne.Nodes = []*registry.Node{&e.nodeOne} | ||||
|  | ||||
| 	e.serviceTwo.Name = "two" | ||||
| 	e.serviceTwo.Version = "default" | ||||
| 	e.serviceTwo.Nodes = []*registry.Node{&e.nodeOne, &e.nodeTwo} | ||||
|  | ||||
| 	e.nodeOne.Id = "one" | ||||
| 	e.nodeTwo.Id = "two" | ||||
| 	e.nodeThree.Id = "three" | ||||
|  | ||||
| 	if err := e.registryOne.Register(&e.serviceOne); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if err := e.registryOne.Register(&e.serviceTwo); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	result := m.Run() | ||||
|  | ||||
| 	if err := e.registryOne.Deregister(&e.serviceOne); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if err := e.registryOne.Deregister(&e.serviceTwo); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	os.Exit(result) | ||||
| } | ||||
							
								
								
									
										87
									
								
								registry/nats/nats_options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								registry/nats/nats_options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| package nats | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/nats-io/nats.go" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| type contextQuorumKey struct{} | ||||
| type optionsKey struct{} | ||||
| type watchTopicKey struct{} | ||||
| type queryTopicKey struct{} | ||||
| type registerActionKey struct{} | ||||
|  | ||||
| var ( | ||||
| 	DefaultQuorum = 0 | ||||
| ) | ||||
|  | ||||
| func getQuorum(o registry.Options) int { | ||||
| 	if o.Context == nil { | ||||
| 		return DefaultQuorum | ||||
| 	} | ||||
|  | ||||
| 	value := o.Context.Value(contextQuorumKey{}) | ||||
| 	if v, ok := value.(int); ok { | ||||
| 		return v | ||||
| 	} else { | ||||
| 		return DefaultQuorum | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func Quorum(n int) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		o.Context = context.WithValue(o.Context, contextQuorumKey{}, n) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Options allow to inject a nats.Options struct for configuring | ||||
| // the nats connection. | ||||
| func NatsOptions(nopts nats.Options) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, optionsKey{}, nopts) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // QueryTopic allows to set a custom nats topic on which service registries | ||||
| // query (survey) other services. All registries listen on this topic and | ||||
| // then respond to the query message. | ||||
| func QueryTopic(s string) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, queryTopicKey{}, s) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WatchTopic allows to set a custom nats topic on which registries broadcast | ||||
| // changes (e.g. when services are added, updated or removed). Since we don't | ||||
| // have a central registry service, each service typically broadcasts in a | ||||
| // determined frequency on this topic. | ||||
| func WatchTopic(s string) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, watchTopicKey{}, s) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // RegisterAction allows to set the action to use when registering to nats. | ||||
| // As of now there are three different options: | ||||
| // - "create" (default) only registers if there is noone already registered under the same key. | ||||
| // - "update" only updates the registration if it already exists. | ||||
| // - "put" creates or updates a registration | ||||
| func RegisterAction(s string) registry.Option { | ||||
| 	return func(o *registry.Options) { | ||||
| 		if o.Context == nil { | ||||
| 			o.Context = context.Background() | ||||
| 		} | ||||
| 		o.Context = context.WithValue(o.Context, registerActionKey{}, s) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										5
									
								
								registry/nats/nats_registry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								registry/nats/nats_registry.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| package nats | ||||
|  | ||||
| var ( | ||||
| 	DefaultRegistry = NewNatsRegistry() | ||||
| ) | ||||
							
								
								
									
										95
									
								
								registry/nats/nats_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								registry/nats/nats_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| package nats_test | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| func TestRegister(t *testing.T) { | ||||
| 	service := registry.Service{Name: "test"} | ||||
| 	assertNoError(t, e.registryOne.Register(&service)) | ||||
| 	defer e.registryOne.Deregister(&service) | ||||
|  | ||||
| 	services, err := e.registryOne.ListServices() | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 3, len(services)) | ||||
|  | ||||
| 	services, err = e.registryTwo.ListServices() | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 3, len(services)) | ||||
| } | ||||
|  | ||||
| func TestDeregister(t *testing.T) { | ||||
| 	service1 := registry.Service{Name: "test-deregister", Version: "v1"} | ||||
| 	service2 := registry.Service{Name: "test-deregister", Version: "v2"} | ||||
|  | ||||
| 	assertNoError(t, e.registryOne.Register(&service1)) | ||||
| 	services, err := e.registryOne.GetService(service1.Name) | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 1, len(services)) | ||||
|  | ||||
| 	assertNoError(t, e.registryOne.Register(&service2)) | ||||
| 	services, err = e.registryOne.GetService(service2.Name) | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 2, len(services)) | ||||
|  | ||||
| 	assertNoError(t, e.registryOne.Deregister(&service1)) | ||||
| 	services, err = e.registryOne.GetService(service1.Name) | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 1, len(services)) | ||||
|  | ||||
| 	assertNoError(t, e.registryOne.Deregister(&service2)) | ||||
| 	services, err = e.registryOne.GetService(service1.Name) | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 0, len(services)) | ||||
| } | ||||
|  | ||||
| func TestGetService(t *testing.T) { | ||||
| 	services, err := e.registryTwo.GetService("one") | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 1, len(services)) | ||||
| 	assertEqual(t, "one", services[0].Name) | ||||
| 	assertEqual(t, 1, len(services[0].Nodes)) | ||||
| } | ||||
|  | ||||
| func TestGetServiceWithNoNodes(t *testing.T) { | ||||
| 	services, err := e.registryOne.GetService("missing") | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 0, len(services)) | ||||
| } | ||||
|  | ||||
| func TestGetServiceFromMultipleNodes(t *testing.T) { | ||||
| 	services, err := e.registryOne.GetService("two") | ||||
| 	assertNoError(t, err) | ||||
| 	assertEqual(t, 1, len(services)) | ||||
| 	assertEqual(t, "two", services[0].Name) | ||||
| 	assertEqual(t, 2, len(services[0].Nodes)) | ||||
| } | ||||
|  | ||||
| func BenchmarkGetService(b *testing.B) { | ||||
| 	for n := 0; n < b.N; n++ { | ||||
| 		services, err := e.registryTwo.GetService("one") | ||||
| 		assertNoError(b, err) | ||||
| 		assertEqual(b, 1, len(services)) | ||||
| 		assertEqual(b, "one", services[0].Name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BenchmarkGetServiceWithNoNodes(b *testing.B) { | ||||
| 	for n := 0; n < b.N; n++ { | ||||
| 		services, err := e.registryOne.GetService("missing") | ||||
| 		assertNoError(b, err) | ||||
| 		assertEqual(b, 0, len(services)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BenchmarkGetServiceFromMultipleNodes(b *testing.B) { | ||||
| 	for n := 0; n < b.N; n++ { | ||||
| 		services, err := e.registryTwo.GetService("two") | ||||
| 		assertNoError(b, err) | ||||
| 		assertEqual(b, 1, len(services)) | ||||
| 		assertEqual(b, "two", services[0].Name) | ||||
| 		assertEqual(b, 2, len(services[0].Nodes)) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										107
									
								
								registry/nats/nats_util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								registry/nats/nats_util.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| package nats | ||||
|  | ||||
| import "go-micro.dev/v5/registry" | ||||
|  | ||||
| func cp(current []*registry.Service) []*registry.Service { | ||||
| 	var services []*registry.Service | ||||
|  | ||||
| 	for _, service := range current { | ||||
| 		// copy service | ||||
| 		s := new(registry.Service) | ||||
| 		*s = *service | ||||
|  | ||||
| 		// copy nodes | ||||
| 		var nodes []*registry.Node | ||||
| 		for _, node := range service.Nodes { | ||||
| 			n := new(registry.Node) | ||||
| 			*n = *node | ||||
| 			nodes = append(nodes, n) | ||||
| 		} | ||||
| 		s.Nodes = nodes | ||||
|  | ||||
| 		// copy endpoints | ||||
| 		var eps []*registry.Endpoint | ||||
| 		for _, ep := range service.Endpoints { | ||||
| 			e := new(registry.Endpoint) | ||||
| 			*e = *ep | ||||
| 			eps = append(eps, e) | ||||
| 		} | ||||
| 		s.Endpoints = eps | ||||
|  | ||||
| 		// append service | ||||
| 		services = append(services, s) | ||||
| 	} | ||||
|  | ||||
| 	return services | ||||
| } | ||||
|  | ||||
| func addNodes(old, neu []*registry.Node) []*registry.Node { | ||||
| 	for _, n := range neu { | ||||
| 		var seen bool | ||||
| 		for i, o := range old { | ||||
| 			if o.Id == n.Id { | ||||
| 				seen = true | ||||
| 				old[i] = n | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !seen { | ||||
| 			old = append(old, n) | ||||
| 		} | ||||
| 	} | ||||
| 	return old | ||||
| } | ||||
|  | ||||
| func addServices(old, neu []*registry.Service) []*registry.Service { | ||||
| 	for _, s := range neu { | ||||
| 		var seen bool | ||||
| 		for i, o := range old { | ||||
| 			if o.Version == s.Version { | ||||
| 				s.Nodes = addNodes(o.Nodes, s.Nodes) | ||||
| 				seen = true | ||||
| 				old[i] = s | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !seen { | ||||
| 			old = append(old, s) | ||||
| 		} | ||||
| 	} | ||||
| 	return old | ||||
| } | ||||
|  | ||||
| func delNodes(old, del []*registry.Node) []*registry.Node { | ||||
| 	var nodes []*registry.Node | ||||
| 	for _, o := range old { | ||||
| 		var rem bool | ||||
| 		for _, n := range del { | ||||
| 			if o.Id == n.Id { | ||||
| 				rem = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !rem { | ||||
| 			nodes = append(nodes, o) | ||||
| 		} | ||||
| 	} | ||||
| 	return nodes | ||||
| } | ||||
|  | ||||
| func delServices(old, del []*registry.Service) []*registry.Service { | ||||
| 	var services []*registry.Service | ||||
| 	for i, o := range old { | ||||
| 		var rem bool | ||||
| 		for _, s := range del { | ||||
| 			if o.Version == s.Version { | ||||
| 				old[i].Nodes = delNodes(o.Nodes, s.Nodes) | ||||
| 				if len(old[i].Nodes) == 0 { | ||||
| 					rem = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if !rem { | ||||
| 			services = append(services, o) | ||||
| 		} | ||||
| 	} | ||||
| 	return services | ||||
| } | ||||
							
								
								
									
										39
									
								
								registry/nats/nats_watcher.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								registry/nats/nats_watcher.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| package nats | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/nats-io/nats.go" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| ) | ||||
|  | ||||
| type natsWatcher struct { | ||||
| 	sub *nats.Subscription | ||||
| 	wo  registry.WatchOptions | ||||
| } | ||||
|  | ||||
| func (n *natsWatcher) Next() (*registry.Result, error) { | ||||
| 	var result *registry.Result | ||||
| 	for { | ||||
| 		m, err := n.sub.NextMsg(time.Minute) | ||||
| 		if err != nil && err == nats.ErrTimeout { | ||||
| 			continue | ||||
| 		} else if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if err := json.Unmarshal(m.Data, &result); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if len(n.wo.Service) > 0 && result.Service.Name != n.wo.Service { | ||||
| 			continue | ||||
| 		} | ||||
| 		break | ||||
| 	} | ||||
|  | ||||
| 	return result, nil | ||||
| } | ||||
|  | ||||
| func (n *natsWatcher) Stop() { | ||||
| 	n.sub.Unsubscribe() | ||||
| } | ||||
							
								
								
									
										166
									
								
								registry/options_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								registry/options_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| //go:build nats | ||||
| // +build nats | ||||
|  | ||||
| package registry | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"sync" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/nats-io/nats.go" | ||||
| ) | ||||
|  | ||||
| var addrTestCases = []struct { | ||||
| 	name        string | ||||
| 	description string | ||||
| 	addrs       map[string]string // expected address : set address | ||||
| }{ | ||||
| 	{ | ||||
| 		"registryOption", | ||||
| 		"set registry addresses through a registry.Option in constructor", | ||||
| 		map[string]string{ | ||||
| 			"nats://192.168.10.1:5222": "192.168.10.1:5222", | ||||
| 			"nats://10.20.10.0:4222":   "10.20.10.0:4222"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		"natsOption", | ||||
| 		"set registry addresses through the nats.Option in constructor", | ||||
| 		map[string]string{ | ||||
| 			"nats://192.168.10.1:5222": "192.168.10.1:5222", | ||||
| 			"nats://10.20.10.0:4222":   "10.20.10.0:4222"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		"default", | ||||
| 		"check if default Address is set correctly", | ||||
| 		map[string]string{ | ||||
| 			nats.DefaultURL: "", | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func TestInitAddrs(t *testing.T) { | ||||
| 	for _, tc := range addrTestCases { | ||||
| 		t.Run(fmt.Sprintf("%s: %s", tc.name, tc.description), func(t *testing.T) { | ||||
| 			var reg Registry | ||||
| 			var addrs []string | ||||
|  | ||||
| 			for _, addr := range tc.addrs { | ||||
| 				addrs = append(addrs, addr) | ||||
| 			} | ||||
|  | ||||
| 			switch tc.name { | ||||
| 			case "registryOption": | ||||
| 				// we know that there are just two addrs in the dict | ||||
| 				reg = NewRegistry(Addrs(addrs[0], addrs[1])) | ||||
| 			case "natsOption": | ||||
| 				nopts := nats.GetDefaultOptions() | ||||
| 				nopts.Servers = addrs | ||||
| 				reg = NewRegistry(Options(nopts)) | ||||
| 			case "default": | ||||
| 				reg = NewRegistry() | ||||
| 			} | ||||
|  | ||||
| 			// if err := reg.Register(dummyService); err != nil { | ||||
| 			// 	t.Fatal(err) | ||||
| 			// } | ||||
|  | ||||
| 			natsRegistry, ok := reg.(*natsRegistry) | ||||
| 			if !ok { | ||||
| 				t.Fatal("Expected registry to be of types *natsRegistry") | ||||
| 			} | ||||
| 			// check if the same amount of addrs we set has actually been set | ||||
| 			if len(natsRegistry.addrs) != len(tc.addrs) { | ||||
| 				t.Errorf("Expected Addr = %v, Actual Addr = %v", | ||||
| 					natsRegistry.addrs, tc.addrs) | ||||
| 				t.Errorf("Expected Addr count = %d, Actual Addr count = %d", | ||||
| 					len(natsRegistry.addrs), len(tc.addrs)) | ||||
| 			} | ||||
|  | ||||
| 			for _, addr := range natsRegistry.addrs { | ||||
| 				_, ok := tc.addrs[addr] | ||||
| 				if !ok { | ||||
| 					t.Errorf("Expected Addr = %v, Actual Addr = %v", | ||||
| 						natsRegistry.addrs, tc.addrs) | ||||
| 					t.Errorf("Expected '%s' has not been set", addr) | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestWatchQueryTopic(t *testing.T) { | ||||
| 	natsURL := os.Getenv("NATS_URL") | ||||
| 	if natsURL == "" { | ||||
| 		log.Println("NATS_URL is undefined - skipping tests") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	watchTopic := "custom.test.watch" | ||||
| 	queryTopic := "custom.test.query" | ||||
| 	wt := WatchTopic(watchTopic) | ||||
| 	qt := QueryTopic(queryTopic) | ||||
|  | ||||
| 	// connect to NATS and subscribe to the Watch & Query topics where the | ||||
| 	// registry will publish a msg | ||||
| 	nopts := nats.GetDefaultOptions() | ||||
| 	nopts.Servers = setAddrs([]string{natsURL}) | ||||
| 	conn, err := nopts.Connect() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	wg := sync.WaitGroup{} | ||||
| 	wg.Add(2) | ||||
|  | ||||
| 	okCh := make(chan struct{}) | ||||
|  | ||||
| 	// Wait until we have received something on both topics | ||||
| 	go func() { | ||||
| 		wg.Wait() | ||||
| 		close(okCh) | ||||
| 	}() | ||||
|  | ||||
| 	// handler just calls wg.Done() | ||||
| 	rcvdHdlr := func(m *nats.Msg) { | ||||
| 		wg.Done() | ||||
| 	} | ||||
|  | ||||
| 	_, err = conn.Subscribe(queryTopic, rcvdHdlr) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	_, err = conn.Subscribe(watchTopic, rcvdHdlr) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	dummyService := &Service{ | ||||
| 		Name:    "TestInitAddr", | ||||
| 		Version: "1.0.0", | ||||
| 	} | ||||
|  | ||||
| 	reg := NewRegistry(qt, wt, Addrs(natsURL)) | ||||
|  | ||||
| 	// trigger registry to send out message on watchTopic | ||||
| 	if err := reg.Register(dummyService); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// trigger registry to send out message on queryTopic | ||||
| 	if _, err := reg.ListServices(); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// make sure that we received something on tc.topic | ||||
| 	select { | ||||
| 	case <-okCh: | ||||
| 		// fine - we received on both topics a message from the registry | ||||
| 	case <-time.After(time.Millisecond * 200): | ||||
| 		t.Fatal("timeout - no data received on watch topic") | ||||
| 	} | ||||
| } | ||||
| @@ -6,8 +6,6 @@ import ( | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	DefaultRegistry = NewRegistry() | ||||
|  | ||||
| 	// Not found error when GetService is called. | ||||
| 	ErrNotFound = errors.New("service not found") | ||||
| 	// Watcher stopped error when watcher is stopped. | ||||
| @@ -95,3 +93,7 @@ func Watch(opts ...WatchOption) (Watcher, error) { | ||||
| func String() string { | ||||
| 	return DefaultRegistry.String() | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	DefaultRegistry = NewMemoryRegistry() | ||||
| ) | ||||
|   | ||||
							
								
								
									
										238
									
								
								store/mysql/mysql.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								store/mysql/mysql.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,238 @@ | ||||
| package mysql | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 	"unicode" | ||||
|  | ||||
| 	"github.com/pkg/errors" | ||||
| 	log "go-micro.dev/v5/logger" | ||||
| 	"go-micro.dev/v5/store" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// DefaultDatabase is the database that the sql store will use if no database is provided. | ||||
| 	DefaultDatabase = "micro" | ||||
| 	// DefaultTable is the table that the sql store will use if no table is provided. | ||||
| 	DefaultTable = "micro" | ||||
| ) | ||||
|  | ||||
| type sqlStore struct { | ||||
| 	db *sql.DB | ||||
|  | ||||
| 	database string | ||||
| 	table    string | ||||
|  | ||||
| 	options store.Options | ||||
|  | ||||
| 	readPrepare, writePrepare, deletePrepare *sql.Stmt | ||||
| } | ||||
|  | ||||
| func (s *sqlStore) Init(opts ...store.Option) error { | ||||
| 	for _, o := range opts { | ||||
| 		o(&s.options) | ||||
| 	} | ||||
| 	// reconfigure | ||||
| 	return s.configure() | ||||
| } | ||||
|  | ||||
| func (s *sqlStore) Options() store.Options { | ||||
| 	return s.options | ||||
| } | ||||
|  | ||||
| func (s *sqlStore) Close() error { | ||||
| 	return s.db.Close() | ||||
| } | ||||
|  | ||||
| // List all the known records. | ||||
| func (s *sqlStore) List(opts ...store.ListOption) ([]string, error) { | ||||
| 	rows, err := s.db.Query(fmt.Sprintf("SELECT `key`, value, expiry FROM %s.%s;", s.database, s.table)) | ||||
| 	if err != nil { | ||||
| 		if err == sql.ErrNoRows { | ||||
| 			return nil, nil | ||||
| 		} | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
|  | ||||
| 	var records []string | ||||
| 	var cachedTime time.Time | ||||
|  | ||||
| 	for rows.Next() { | ||||
| 		record := &store.Record{} | ||||
| 		if err := rows.Scan(&record.Key, &record.Value, &cachedTime); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		if cachedTime.Before(time.Now()) { | ||||
| 			// record has expired | ||||
| 			go s.Delete(record.Key) | ||||
| 		} else { | ||||
| 			records = append(records, record.Key) | ||||
| 		} | ||||
| 	} | ||||
| 	rowErr := rows.Close() | ||||
| 	if rowErr != nil { | ||||
| 		// transaction rollback or something | ||||
| 		return records, rowErr | ||||
| 	} | ||||
| 	if err := rows.Err(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return records, nil | ||||
| } | ||||
|  | ||||
| // Read all records with keys. | ||||
| func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) { | ||||
| 	var options store.ReadOptions | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	// TODO: make use of options.Prefix using WHERE key LIKE = ? | ||||
|  | ||||
| 	var records []*store.Record | ||||
| 	row := s.readPrepare.QueryRow(key) | ||||
| 	record := &store.Record{} | ||||
| 	var cachedTime time.Time | ||||
|  | ||||
| 	if err := row.Scan(&record.Key, &record.Value, &cachedTime); err != nil { | ||||
| 		if err == sql.ErrNoRows { | ||||
| 			return records, store.ErrNotFound | ||||
| 		} | ||||
| 		return records, err | ||||
| 	} | ||||
| 	if cachedTime.Before(time.Now()) { | ||||
| 		// record has expired | ||||
| 		go s.Delete(key) | ||||
| 		return records, store.ErrNotFound | ||||
| 	} | ||||
| 	record.Expiry = time.Until(cachedTime) | ||||
| 	records = append(records, record) | ||||
|  | ||||
| 	return records, nil | ||||
| } | ||||
|  | ||||
| // Write records. | ||||
| func (s *sqlStore) Write(r *store.Record, opts ...store.WriteOption) error { | ||||
| 	timeCached := time.Now().Add(r.Expiry) | ||||
| 	_, err := s.writePrepare.Exec(r.Key, r.Value, timeCached, r.Value, timeCached) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "Couldn't insert record "+r.Key) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Delete records with keys. | ||||
| func (s *sqlStore) Delete(key string, opts ...store.DeleteOption) error { | ||||
| 	result, err := s.deletePrepare.Exec(key) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	_, err = result.RowsAffected() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (s *sqlStore) initDB() error { | ||||
| 	// Create the namespace's database | ||||
| 	_, err := s.db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s ;", s.database)) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	_, err = s.db.Exec(fmt.Sprintf("USE %s ;", s.database)) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "Couldn't use database") | ||||
| 	} | ||||
|  | ||||
| 	// Create a table for the namespace's prefix | ||||
| 	createSQL := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (`key` varchar(255) primary key, value blob null, expiry timestamp not null);", s.table) | ||||
| 	_, err = s.db.Exec(createSQL) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "Couldn't create table") | ||||
| 	} | ||||
|  | ||||
| 	// prepare | ||||
| 	s.readPrepare, _ = s.db.Prepare(fmt.Sprintf("SELECT `key`, value, expiry FROM %s.%s WHERE `key` = ?;", s.database, s.table)) | ||||
| 	s.writePrepare, _ = s.db.Prepare(fmt.Sprintf("INSERT INTO %s.%s (`key`, value, expiry) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE `value`= ?, `expiry` = ?", s.database, s.table)) | ||||
| 	s.deletePrepare, _ = s.db.Prepare(fmt.Sprintf("DELETE FROM %s.%s WHERE `key` = ?;", s.database, s.table)) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (s *sqlStore) configure() error { | ||||
| 	nodes := s.options.Nodes | ||||
| 	if len(nodes) == 0 { | ||||
| 		nodes = []string{"localhost:3306"} | ||||
| 	} | ||||
|  | ||||
| 	database := s.options.Database | ||||
| 	if len(database) == 0 { | ||||
| 		database = DefaultDatabase | ||||
| 	} | ||||
|  | ||||
| 	table := s.options.Table | ||||
| 	if len(table) == 0 { | ||||
| 		table = DefaultTable | ||||
| 	} | ||||
|  | ||||
| 	for _, r := range database { | ||||
| 		if !unicode.IsLetter(r) { | ||||
| 			return errors.New("store.namespace must only contain letters") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	source := nodes[0] | ||||
| 	// create source from first node | ||||
| 	db, err := sql.Open("mysql", source) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := db.Ping(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if s.db != nil { | ||||
| 		s.db.Close() | ||||
| 	} | ||||
|  | ||||
| 	// save the values | ||||
| 	s.db = db | ||||
| 	s.database = database | ||||
| 	s.table = table | ||||
|  | ||||
| 	// initialize the database | ||||
| 	return s.initDB() | ||||
| } | ||||
|  | ||||
| func (s *sqlStore) String() string { | ||||
| 	return "mysql" | ||||
| } | ||||
|  | ||||
| // New returns a new micro Store backed by sql. | ||||
| func NewMysqlStore(opts ...store.Option) store.Store { | ||||
| 	var options store.Options | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	// new store | ||||
| 	s := new(sqlStore) | ||||
| 	// set the options | ||||
| 	s.options = options | ||||
|  | ||||
| 	// configure the store | ||||
| 	if err := s.configure(); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// return store | ||||
| 	return s | ||||
| } | ||||
							
								
								
									
										69
									
								
								store/mysql/mysql_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								store/mysql/mysql_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| //go:build integration | ||||
| // +build integration | ||||
|  | ||||
| package mysql | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	_ "github.com/go-sql-driver/mysql" | ||||
| 	"go-micro.dev/v5/store" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	sqlStoreT store.Store | ||||
| ) | ||||
|  | ||||
| func TestMain(m *testing.M) { | ||||
| 	if tr := os.Getenv("TRAVIS"); len(tr) > 0 { | ||||
| 		os.Exit(0) | ||||
| 	} | ||||
|  | ||||
| 	sqlStoreT = NewMysqlStore( | ||||
| 		store.Database("testMicro"), | ||||
| 		store.Nodes("root:123@(127.0.0.1:3306)/test?charset=utf8&parseTime=true&loc=Asia%2FShanghai"), | ||||
| 	) | ||||
| 	os.Exit(m.Run()) | ||||
| } | ||||
|  | ||||
| func TestWrite(t *testing.T) { | ||||
| 	err := sqlStoreT.Write( | ||||
| 		&store.Record{ | ||||
| 			Key:    "test", | ||||
| 			Value:  []byte("foo2"), | ||||
| 			Expiry: time.Second * 200, | ||||
| 		}, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDelete(t *testing.T) { | ||||
| 	err := sqlStoreT.Delete("test") | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestRead(t *testing.T) { | ||||
| 	records, err := sqlStoreT.Read("test") | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	t.Log(string(records[0].Value)) | ||||
| } | ||||
|  | ||||
| func TestList(t *testing.T) { | ||||
| 	records, err := sqlStoreT.List() | ||||
| 	if err != nil { | ||||
| 		t.Error(err) | ||||
| 	} else { | ||||
| 		beauty, _ := json.Marshal(records) | ||||
| 		t.Log(string(beauty)) | ||||
| 	} | ||||
| } | ||||
| @@ -24,17 +24,17 @@ func expectedPort(t *testing.T, expected string, lsn Listener) { | ||||
| func TestHTTPTransportPortRange(t *testing.T) { | ||||
| 	tp := NewHTTPTransport() | ||||
|  | ||||
| 	lsn1, err := tp.Listen(":44444-44448") | ||||
| 	lsn1, err := tp.Listen(":44445-44449") | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Did not expect an error, got %s", err) | ||||
| 	} | ||||
| 	expectedPort(t, "44444", lsn1) | ||||
| 	expectedPort(t, "44445", lsn1) | ||||
|  | ||||
| 	lsn2, err := tp.Listen(":44444-44448") | ||||
| 	lsn2, err := tp.Listen(":44445-44449") | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Did not expect an error, got %s", err) | ||||
| 	} | ||||
| 	expectedPort(t, "44445", lsn2) | ||||
| 	expectedPort(t, "44446", lsn2) | ||||
|  | ||||
| 	lsn, err := tp.Listen("127.0.0.1:0") | ||||
| 	if err != nil { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user