go-micro.dev/v5@v5.12.0/registry/consul/consul.go (about)

     1  package consul
     2  
     3  import (
     4  	"crypto/tls"
     5  	"errors"
     6  	"fmt"
     7  	"net"
     8  	"net/http"
     9  	"runtime"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	consul "github.com/hashicorp/consul/api"
    16  	hash "github.com/mitchellh/hashstructure"
    17  	"go-micro.dev/v5/registry"
    18  	mnet "go-micro.dev/v5/util/net"
    19  )
    20  
    21  type consulRegistry struct {
    22  	Address []string
    23  	opts    registry.Options
    24  
    25  	client *consul.Client
    26  	config *consul.Config
    27  
    28  	// connect enabled
    29  	connect bool
    30  
    31  	queryOptions *consul.QueryOptions
    32  
    33  	sync.Mutex
    34  	register map[string]uint64
    35  	// lastChecked tracks when a node was last checked as existing in Consul
    36  	lastChecked map[string]time.Time
    37  }
    38  
    39  func getDeregisterTTL(t time.Duration) time.Duration {
    40  	// splay slightly for the watcher?
    41  	splay := time.Second * 5
    42  	deregTTL := t + splay
    43  
    44  	// consul has a minimum timeout on deregistration of 1 minute.
    45  	if t < time.Minute {
    46  		deregTTL = time.Minute + splay
    47  	}
    48  
    49  	return deregTTL
    50  }
    51  
    52  func newTransport(config *tls.Config) *http.Transport {
    53  	if config == nil {
    54  		config = &tls.Config{
    55  			InsecureSkipVerify: true,
    56  		}
    57  	}
    58  
    59  	t := &http.Transport{
    60  		Proxy: http.ProxyFromEnvironment,
    61  		Dial: (&net.Dialer{
    62  			Timeout:   30 * time.Second,
    63  			KeepAlive: 30 * time.Second,
    64  		}).Dial,
    65  		TLSHandshakeTimeout: 10 * time.Second,
    66  		TLSClientConfig:     config,
    67  	}
    68  	runtime.SetFinalizer(&t, func(tr **http.Transport) {
    69  		(*tr).CloseIdleConnections()
    70  	})
    71  	return t
    72  }
    73  
    74  func configure(c *consulRegistry, opts ...registry.Option) {
    75  	// set opts
    76  	for _, o := range opts {
    77  		o(&c.opts)
    78  	}
    79  
    80  	// use default non pooled config
    81  	config := consul.DefaultNonPooledConfig()
    82  
    83  	if c.opts.Context != nil {
    84  		// Use the consul config passed in the options, if available
    85  		if co, ok := c.opts.Context.Value(consulConfigKey).(*consul.Config); ok {
    86  			config = co
    87  		}
    88  		if cn, ok := c.opts.Context.Value(consulConnectKey).(bool); ok {
    89  			c.connect = cn
    90  		}
    91  
    92  		// Use the consul query options passed in the options, if available
    93  		if qo, ok := c.opts.Context.Value(consulQueryOptionsKey).(*consul.QueryOptions); ok && qo != nil {
    94  			c.queryOptions = qo
    95  		}
    96  		if as, ok := c.opts.Context.Value(consulAllowStaleKey).(bool); ok {
    97  			c.queryOptions.AllowStale = as
    98  		}
    99  	}
   100  
   101  	// check if there are any addrs
   102  	var addrs []string
   103  
   104  	// iterate the options addresses
   105  	for _, address := range c.opts.Addrs {
   106  		// check we have a port
   107  		addr, port, err := net.SplitHostPort(address)
   108  		if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" {
   109  			port = "8500"
   110  			addr = address
   111  			addrs = append(addrs, net.JoinHostPort(addr, port))
   112  		} else if err == nil {
   113  			addrs = append(addrs, net.JoinHostPort(addr, port))
   114  		}
   115  	}
   116  
   117  	// set the addrs
   118  	if len(addrs) > 0 {
   119  		c.Address = addrs
   120  		config.Address = c.Address[0]
   121  	}
   122  
   123  	if config.HttpClient == nil {
   124  		config.HttpClient = new(http.Client)
   125  	}
   126  
   127  	// requires secure connection?
   128  	if c.opts.Secure || c.opts.TLSConfig != nil {
   129  		config.Scheme = "https"
   130  		// We're going to support InsecureSkipVerify
   131  		config.HttpClient.Transport = newTransport(c.opts.TLSConfig)
   132  	}
   133  
   134  	// set timeout
   135  	if c.opts.Timeout > 0 {
   136  		config.HttpClient.Timeout = c.opts.Timeout
   137  	}
   138  
   139  	// set the config
   140  	c.config = config
   141  
   142  	// remove client
   143  	c.client = nil
   144  
   145  	// setup the client
   146  	c.Client()
   147  }
   148  
   149  func (c *consulRegistry) Init(opts ...registry.Option) error {
   150  	configure(c, opts...)
   151  	return nil
   152  }
   153  
   154  func (c *consulRegistry) Deregister(s *registry.Service, opts ...registry.DeregisterOption) error {
   155  	if len(s.Nodes) == 0 {
   156  		return errors.New("require at least one node")
   157  	}
   158  
   159  	// delete our hash and time check of the service
   160  	c.Lock()
   161  	delete(c.register, s.Name)
   162  	delete(c.lastChecked, s.Name)
   163  	c.Unlock()
   164  
   165  	node := s.Nodes[0]
   166  	return c.Client().Agent().ServiceDeregister(node.Id)
   167  }
   168  
   169  func (c *consulRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
   170  	if len(s.Nodes) == 0 {
   171  		return errors.New("require at least one node")
   172  	}
   173  
   174  	var regTCPCheck bool
   175  	var regInterval time.Duration
   176  	var regHTTPCheck bool
   177  	var httpCheckConfig consul.AgentServiceCheck
   178  
   179  	var options registry.RegisterOptions
   180  	for _, o := range opts {
   181  		o(&options)
   182  	}
   183  
   184  	if c.opts.Context != nil {
   185  		if tcpCheckInterval, ok := c.opts.Context.Value(consulTCPCheckKey).(time.Duration); ok {
   186  			regTCPCheck = true
   187  			regInterval = tcpCheckInterval
   188  		}
   189  		var ok bool
   190  		if httpCheckConfig, ok = c.opts.Context.Value(consulHTTPCheckConfigKey).(consul.AgentServiceCheck); ok {
   191  			regHTTPCheck = true
   192  		}
   193  	}
   194  
   195  	// create hash of service; uint64
   196  	h, err := hash.Hash(s, nil)
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	// use first node
   202  	node := s.Nodes[0]
   203  
   204  	// get existing hash and last checked time
   205  	c.Lock()
   206  	v, ok := c.register[s.Name]
   207  	lastChecked := c.lastChecked[s.Name]
   208  	c.Unlock()
   209  
   210  	// if it's already registered and matches then just pass the check
   211  	if ok && v == h {
   212  		if options.TTL == time.Duration(0) {
   213  			// ensure that our service hasn't been deregistered by Consul
   214  			if time.Since(lastChecked) <= getDeregisterTTL(regInterval) {
   215  				return nil
   216  			}
   217  			services, _, err := c.Client().Health().Checks(s.Name, c.queryOptions)
   218  			if err == nil {
   219  				for _, v := range services {
   220  					if v.ServiceID == node.Id {
   221  						return nil
   222  					}
   223  				}
   224  			}
   225  		} else {
   226  			// if the err is nil we're all good, bail out
   227  			// if not, we don't know what the state is, so full re-register
   228  			if err := c.Client().Agent().PassTTL("service:"+node.Id, ""); err == nil {
   229  				return nil
   230  			}
   231  		}
   232  	}
   233  
   234  	// encode the tags
   235  	tags := encodeMetadata(node.Metadata)
   236  	tags = append(tags, encodeEndpoints(s.Endpoints)...)
   237  	tags = append(tags, encodeVersion(s.Version)...)
   238  
   239  	var check *consul.AgentServiceCheck
   240  
   241  	if regTCPCheck {
   242  		deregTTL := getDeregisterTTL(regInterval)
   243  
   244  		check = &consul.AgentServiceCheck{
   245  			TCP:                            node.Address,
   246  			Interval:                       fmt.Sprintf("%v", regInterval),
   247  			DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL),
   248  		}
   249  
   250  	} else if regHTTPCheck {
   251  		interval, _ := time.ParseDuration(httpCheckConfig.Interval)
   252  		deregTTL := getDeregisterTTL(interval)
   253  
   254  		host, _, _ := net.SplitHostPort(node.Address)
   255  		healthCheckURI := strings.Replace(httpCheckConfig.HTTP, "{host}", host, 1)
   256  
   257  		check = &consul.AgentServiceCheck{
   258  			HTTP:                           healthCheckURI,
   259  			Interval:                       httpCheckConfig.Interval,
   260  			Timeout:                        httpCheckConfig.Timeout,
   261  			DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL),
   262  		}
   263  
   264  		// if the TTL is greater than 0 create an associated check
   265  	} else if options.TTL > time.Duration(0) {
   266  		deregTTL := getDeregisterTTL(options.TTL)
   267  
   268  		check = &consul.AgentServiceCheck{
   269  			TTL:                            fmt.Sprintf("%v", options.TTL),
   270  			DeregisterCriticalServiceAfter: fmt.Sprintf("%v", deregTTL),
   271  		}
   272  	}
   273  
   274  	host, pt, _ := net.SplitHostPort(node.Address)
   275  	if host == "" {
   276  		host = node.Address
   277  	}
   278  	port, _ := strconv.Atoi(pt)
   279  
   280  	// register the service
   281  	asr := &consul.AgentServiceRegistration{
   282  		ID:      node.Id,
   283  		Name:    s.Name,
   284  		Tags:    tags,
   285  		Port:    port,
   286  		Address: host,
   287  		Meta:    node.Metadata,
   288  		Check:   check,
   289  	}
   290  
   291  	// Specify consul connect
   292  	if c.connect {
   293  		asr.Connect = &consul.AgentServiceConnect{
   294  			Native: true,
   295  		}
   296  	}
   297  
   298  	if err := c.Client().Agent().ServiceRegister(asr); err != nil {
   299  		return err
   300  	}
   301  
   302  	// save our hash and time check of the service
   303  	c.Lock()
   304  	c.register[s.Name] = h
   305  	c.lastChecked[s.Name] = time.Now()
   306  	c.Unlock()
   307  
   308  	// if the TTL is 0 we don't mess with the checks
   309  	if options.TTL == time.Duration(0) {
   310  		return nil
   311  	}
   312  
   313  	// pass the healthcheck
   314  	return c.Client().Agent().PassTTL("service:"+node.Id, "")
   315  }
   316  
   317  func (c *consulRegistry) GetService(name string, opts ...registry.GetOption) ([]*registry.Service, error) {
   318  	var rsp []*consul.ServiceEntry
   319  	var err error
   320  
   321  	// if we're connect enabled only get connect services
   322  	if c.connect {
   323  		rsp, _, err = c.Client().Health().Connect(name, "", false, c.queryOptions)
   324  	} else {
   325  		rsp, _, err = c.Client().Health().Service(name, "", false, c.queryOptions)
   326  	}
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	serviceMap := map[string]*registry.Service{}
   332  
   333  	for _, s := range rsp {
   334  		if s.Service.Service != name {
   335  			continue
   336  		}
   337  
   338  		// version is now a tag
   339  		version, _ := decodeVersion(s.Service.Tags)
   340  		// service ID is now the node id
   341  		id := s.Service.ID
   342  		// key is always the version
   343  		key := version
   344  
   345  		// address is service address
   346  		address := s.Service.Address
   347  
   348  		// use node address
   349  		if len(address) == 0 {
   350  			address = s.Node.Address
   351  		}
   352  
   353  		svc, ok := serviceMap[key]
   354  		if !ok {
   355  			svc = &registry.Service{
   356  				Endpoints: decodeEndpoints(s.Service.Tags),
   357  				Name:      s.Service.Service,
   358  				Version:   version,
   359  			}
   360  			serviceMap[key] = svc
   361  		}
   362  
   363  		var del bool
   364  
   365  		for _, check := range s.Checks {
   366  			// delete the node if the status is critical
   367  			if check.Status == "critical" {
   368  				del = true
   369  				break
   370  			}
   371  		}
   372  
   373  		// if delete then skip the node
   374  		if del {
   375  			continue
   376  		}
   377  
   378  		svc.Nodes = append(svc.Nodes, &registry.Node{
   379  			Id:       id,
   380  			Address:  mnet.HostPort(address, s.Service.Port),
   381  			Metadata: decodeMetadata(s.Service.Tags),
   382  		})
   383  	}
   384  
   385  	var services []*registry.Service
   386  	for _, service := range serviceMap {
   387  		services = append(services, service)
   388  	}
   389  	return services, nil
   390  }
   391  
   392  func (c *consulRegistry) ListServices(opts ...registry.ListOption) ([]*registry.Service, error) {
   393  	rsp, _, err := c.Client().Catalog().Services(c.queryOptions)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  
   398  	var services []*registry.Service
   399  
   400  	for service := range rsp {
   401  		services = append(services, &registry.Service{Name: service})
   402  	}
   403  
   404  	return services, nil
   405  }
   406  
   407  func (c *consulRegistry) Watch(opts ...registry.WatchOption) (registry.Watcher, error) {
   408  	return newConsulWatcher(c, opts...)
   409  }
   410  
   411  func (c *consulRegistry) String() string {
   412  	return "consul"
   413  }
   414  
   415  func (c *consulRegistry) Options() registry.Options {
   416  	return c.opts
   417  }
   418  
   419  func (c *consulRegistry) Client() *consul.Client {
   420  	if c.client != nil {
   421  		return c.client
   422  	}
   423  
   424  	for _, addr := range c.Address {
   425  		// set the address
   426  		c.config.Address = addr
   427  
   428  		// create a new client
   429  		tmpClient, _ := consul.NewClient(c.config)
   430  
   431  		// test the client
   432  		_, err := tmpClient.Agent().Host()
   433  		if err != nil {
   434  			continue
   435  		}
   436  
   437  		// set the client
   438  		c.client = tmpClient
   439  		return c.client
   440  	}
   441  
   442  	// set the default
   443  	c.client, _ = consul.NewClient(c.config)
   444  
   445  	// return the client
   446  	return c.client
   447  }
   448  
   449  func NewConsulRegistry(opts ...registry.Option) registry.Registry {
   450  	cr := &consulRegistry{
   451  		opts:        registry.Options{},
   452  		register:    make(map[string]uint64),
   453  		lastChecked: make(map[string]time.Time),
   454  		queryOptions: &consul.QueryOptions{
   455  			AllowStale: true,
   456  		},
   457  	}
   458  	configure(cr, opts...)
   459  	return cr
   460  }