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 = ®istry.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, ®istry.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, ®istry.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 }