github.com/cilium/cilium@v1.16.2/pkg/kvstore/consul.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package kvstore 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/base64" 10 "errors" 11 "fmt" 12 "os" 13 "strings" 14 15 consulAPI "github.com/hashicorp/consul/api" 16 "github.com/sirupsen/logrus" 17 "gopkg.in/yaml.v3" 18 19 "github.com/cilium/cilium/pkg/backoff" 20 "github.com/cilium/cilium/pkg/controller" 21 "github.com/cilium/cilium/pkg/inctimer" 22 "github.com/cilium/cilium/pkg/lock" 23 "github.com/cilium/cilium/pkg/logging/logfields" 24 "github.com/cilium/cilium/pkg/option" 25 "github.com/cilium/cilium/pkg/spanstat" 26 "github.com/cilium/cilium/pkg/time" 27 ) 28 29 const ( 30 consulName = "consul" 31 32 // ConsulAddrOption is the string representing the key mapping to the value of the 33 // address for Consul. 34 ConsulAddrOption = "consul.address" 35 ConsulOptionConfig = "consul.tlsconfig" 36 37 // maxLockRetries is the number of retries attempted when acquiring a lock 38 maxLockRetries = 10 39 ) 40 41 type consulModule struct { 42 opts backendOptions 43 config *consulAPI.Config 44 } 45 46 var ( 47 // consulDummyAddress can be overwritten from test invokers using ldflags 48 consulDummyAddress = "https://127.0.0.1:8501" 49 // consulDummyConfigFile can be overwritten from test invokers using ldflags 50 consulDummyConfigFile = "/tmp/cilium-consul-certs/cilium-consul.yaml" 51 52 module = newConsulModule() 53 54 // ErrNotImplemented is the error which is returned when a functionality is not implemented. 55 ErrNotImplemented = errors.New("not implemented") 56 57 consulLeaseKeepaliveControllerGroup = controller.NewGroup("consul-lease-keepalive") 58 ) 59 60 func init() { 61 // register consul module for use 62 registerBackend(consulName, module) 63 } 64 65 func newConsulModule() backendModule { 66 return &consulModule{ 67 opts: backendOptions{ 68 ConsulAddrOption: &backendOption{ 69 description: "Addresses of consul cluster", 70 }, 71 ConsulOptionConfig: &backendOption{ 72 description: "Path to consul tls configuration file", 73 }, 74 }, 75 } 76 } 77 78 func ConsulDummyAddress() string { 79 return consulDummyAddress 80 } 81 82 func ConsulDummyConfigFile() string { 83 return consulDummyConfigFile 84 } 85 86 func (c *consulModule) createInstance() backendModule { 87 return newConsulModule() 88 } 89 90 func (c *consulModule) getName() string { 91 return consulName 92 } 93 94 func (c *consulModule) setConfigDummy() { 95 c.config = consulAPI.DefaultConfig() 96 c.config.Address = consulDummyAddress 97 yc := consulAPI.TLSConfig{} 98 b, err := os.ReadFile(consulDummyConfigFile) 99 if err != nil { 100 log.WithError(err).Warnf("unable to read consul tls configuration file %s", consulDummyConfigFile) 101 } 102 103 err = yaml.Unmarshal(b, &yc) 104 if err != nil { 105 log.WithError(err).Warnf("invalid consul tls configuration in %s", consulDummyConfigFile) 106 } 107 108 c.config.TLSConfig = yc 109 } 110 111 func (c *consulModule) setConfig(opts map[string]string) error { 112 return setOpts(opts, c.opts) 113 } 114 115 func (c *consulModule) setExtraConfig(opts *ExtraOptions) error { 116 return nil 117 } 118 119 func (c *consulModule) getConfig() map[string]string { 120 return getOpts(c.opts) 121 } 122 123 func (c *consulModule) newClient(ctx context.Context, opts *ExtraOptions) (BackendOperations, chan error) { 124 log.WithFields(logrus.Fields{ 125 logfields.URL: "https://slack.cilium.io", 126 }).Warning("Support for Consul as a kvstore backend has been deprecated due to lack of maintainers. If you are interested in helping to maintain Consul support in Cilium, please reach out on GitHub or the official Cilium slack") 127 128 errChan := make(chan error, 1) 129 backend, err := c.connectConsulClient(ctx, opts) 130 if err != nil { 131 errChan <- err 132 } 133 close(errChan) 134 return backend, errChan 135 } 136 137 func (c *consulModule) connectConsulClient(ctx context.Context, opts *ExtraOptions) (BackendOperations, error) { 138 if c.config == nil { 139 consulAddr, consulAddrSet := c.opts[ConsulAddrOption] 140 configPathOpt, configPathOptSet := c.opts[ConsulOptionConfig] 141 if !consulAddrSet { 142 return nil, fmt.Errorf("invalid consul configuration, please specify %s option", ConsulAddrOption) 143 } 144 145 if consulAddr.value == "" { 146 return nil, fmt.Errorf("invalid consul configuration, please specify %s option", ConsulAddrOption) 147 } 148 149 addr := consulAddr.value 150 c.config = consulAPI.DefaultConfig() 151 if configPathOptSet && configPathOpt.value != "" { 152 b, err := os.ReadFile(configPathOpt.value) 153 if err != nil { 154 return nil, fmt.Errorf("unable to read consul tls configuration file %s: %w", configPathOpt.value, err) 155 } 156 yc := consulAPI.TLSConfig{} 157 err = yaml.Unmarshal(b, &yc) 158 if err != nil { 159 return nil, fmt.Errorf("invalid consul tls configuration in %s: %w", configPathOpt.value, err) 160 } 161 c.config.TLSConfig = yc 162 } 163 164 c.config.Address = addr 165 166 } 167 client, err := newConsulClient(ctx, c.config, opts) 168 if err != nil { 169 return nil, err 170 } 171 172 return client, nil 173 } 174 175 var ( 176 maxRetries = 30 177 ) 178 179 type consulClient struct { 180 *consulAPI.Client 181 lease string 182 controllers *controller.Manager 183 extraOptions *ExtraOptions 184 disconnectedMu lock.RWMutex 185 disconnected chan struct{} 186 statusCheckErrors chan error 187 } 188 189 func newConsulClient(ctx context.Context, config *consulAPI.Config, opts *ExtraOptions) (BackendOperations, error) { 190 var ( 191 c *consulAPI.Client 192 err error 193 ) 194 if config != nil { 195 c, err = consulAPI.NewClient(config) 196 } else { 197 c, err = consulAPI.NewClient(consulAPI.DefaultConfig()) 198 } 199 if err != nil { 200 return nil, err 201 } 202 203 boff := backoff.Exponential{Min: time.Duration(100) * time.Millisecond} 204 205 for i := 0; i < maxRetries; i++ { 206 var leader string 207 leader, err = c.Status().Leader() 208 209 if err == nil { 210 if leader != "" { 211 // happy path 212 break 213 } else { 214 err = errors.New("timeout while waiting for leader to be elected") 215 } 216 } 217 log.Info("Waiting for consul to elect a leader") 218 boff.Wait(ctx) 219 } 220 221 if err != nil { 222 log.WithError(err).Fatal("Unable to contact consul server") 223 } 224 225 entry := &consulAPI.SessionEntry{ 226 TTL: fmt.Sprintf("%ds", int(option.Config.KVstoreLeaseTTL.Seconds())), 227 Behavior: consulAPI.SessionBehaviorDelete, 228 } 229 230 wo := &consulAPI.WriteOptions{} 231 lease, _, err := c.Session().Create(entry, wo.WithContext(ctx)) 232 if err != nil { 233 return nil, fmt.Errorf("unable to create default lease: %w", err) 234 } 235 236 client := &consulClient{ 237 Client: c, 238 lease: lease, 239 controllers: controller.NewManager(), 240 extraOptions: opts, 241 disconnected: make(chan struct{}), 242 statusCheckErrors: make(chan error, 128), 243 } 244 245 client.controllers.UpdateController( 246 fmt.Sprintf("consul-lease-keepalive-%p", c), 247 controller.ControllerParams{ 248 Group: consulLeaseKeepaliveControllerGroup, 249 DoFunc: func(ctx context.Context) error { 250 wo := &consulAPI.WriteOptions{} 251 _, _, err := c.Session().Renew(lease, wo.WithContext(ctx)) 252 if err != nil { 253 // consider disconnected! 254 client.disconnectedMu.Lock() 255 close(client.disconnected) 256 client.disconnected = make(chan struct{}) 257 client.disconnectedMu.Unlock() 258 } 259 return err 260 }, 261 RunInterval: option.Config.KVstoreKeepAliveInterval, 262 }, 263 ) 264 265 return client, nil 266 } 267 268 type ConsulLocker struct { 269 *consulAPI.Lock 270 } 271 272 func (cl *ConsulLocker) Unlock(ctx context.Context) error { 273 return cl.Lock.Unlock() 274 } 275 276 func (cl *ConsulLocker) Comparator() interface{} { 277 return nil 278 } 279 280 func (c *consulClient) LockPath(ctx context.Context, path string) (KVLocker, error) { 281 lockKey, err := c.LockOpts(&consulAPI.LockOptions{Key: getLockPath(path)}) 282 if err != nil { 283 return nil, err 284 } 285 286 for retries := 0; retries < maxLockRetries; retries++ { 287 ch, err := lockKey.Lock(nil) 288 switch { 289 case err != nil: 290 return nil, err 291 case ch == nil: 292 Trace("Acquiring lock timed out, retrying", nil, logrus.Fields{fieldKey: path, logfields.Attempt: retries}) 293 default: 294 return &ConsulLocker{Lock: lockKey}, err 295 } 296 297 select { 298 case <-ctx.Done(): 299 return nil, fmt.Errorf("lock cancelled via context: %w", ctx.Err()) 300 default: 301 } 302 } 303 304 return nil, fmt.Errorf("maximum retries (%d) reached", maxLockRetries) 305 } 306 307 // watch starts watching for changes in a prefix 308 func (c *consulClient) watch(ctx context.Context, w *Watcher) { 309 scope := GetScopeFromKey(strings.TrimRight(w.Prefix, "/")) 310 // Last known state of all KVPairs matching the prefix 311 localState := map[string]consulAPI.KVPair{} 312 nextIndex := uint64(0) 313 314 q := &consulAPI.QueryOptions{ 315 WaitTime: time.Second, 316 } 317 318 qo := q.WithContext(ctx) 319 320 sleepTimer, sleepTimerDone := inctimer.New() 321 defer sleepTimerDone() 322 323 for { 324 // Initialize sleep time to a millisecond as we don't 325 // want to sleep in between successful watch cycles 326 sleepTime := 1 * time.Millisecond 327 328 qo.WaitIndex = nextIndex 329 pairs, q, err := c.KV().List(w.Prefix, qo) 330 if err != nil { 331 sleepTime = 5 * time.Second 332 Trace("List of Watch failed", err, logrus.Fields{fieldPrefix: w.Prefix}) 333 } 334 335 if q != nil { 336 nextIndex = q.LastIndex 337 } 338 339 // timeout while watching for changes, re-schedule 340 if qo.WaitIndex != 0 && (q == nil || q.LastIndex == qo.WaitIndex) { 341 goto wait 342 } 343 344 for _, newPair := range pairs { 345 oldPair, ok := localState[newPair.Key] 346 347 // Keys reported for the first time must be new 348 if !ok { 349 if newPair.CreateIndex != newPair.ModifyIndex { 350 log.Debugf("consul: Previously unknown key %s received with CreateIndex(%d) != ModifyIndex(%d)", 351 newPair.Key, newPair.CreateIndex, newPair.ModifyIndex) 352 } 353 354 queueStart := spanstat.Start() 355 w.Events <- KeyValueEvent{ 356 Typ: EventTypeCreate, 357 Key: newPair.Key, 358 Value: newPair.Value, 359 } 360 trackEventQueued(scope, EventTypeCreate, queueStart.End(true).Total()) 361 } else if oldPair.ModifyIndex != newPair.ModifyIndex { 362 queueStart := spanstat.Start() 363 w.Events <- KeyValueEvent{ 364 Typ: EventTypeModify, 365 Key: newPair.Key, 366 Value: newPair.Value, 367 } 368 trackEventQueued(scope, EventTypeModify, queueStart.End(true).Total()) 369 } 370 371 // Everything left on localState will be assumed to 372 // have been deleted, therefore remove all keys in 373 // localState that still exist in the kvstore 374 delete(localState, newPair.Key) 375 } 376 377 for k, deletedPair := range localState { 378 queueStart := spanstat.Start() 379 w.Events <- KeyValueEvent{ 380 Typ: EventTypeDelete, 381 Key: deletedPair.Key, 382 Value: deletedPair.Value, 383 } 384 trackEventQueued(scope, EventTypeDelete, queueStart.End(true).Total()) 385 delete(localState, k) 386 } 387 388 for _, newPair := range pairs { 389 localState[newPair.Key] = *newPair 390 391 } 392 393 // Initial list operation has been completed, signal this 394 if qo.WaitIndex == 0 { 395 w.Events <- KeyValueEvent{Typ: EventTypeListDone} 396 } 397 398 wait: 399 select { 400 case <-sleepTimer.After(sleepTime): 401 case <-w.stopWatch: 402 close(w.Events) 403 w.stopWait.Done() 404 return 405 } 406 } 407 } 408 409 func (c *consulClient) waitForInitLock(ctx context.Context) <-chan struct{} { 410 initLockSucceeded := make(chan struct{}) 411 412 go func() { 413 for { 414 locker, err := c.LockPath(ctx, InitLockPath) 415 if err == nil { 416 locker.Unlock(context.Background()) 417 close(initLockSucceeded) 418 log.Info("Distributed lock successful, consul has quorum") 419 return 420 } 421 422 time.Sleep(100 * time.Millisecond) 423 } 424 }() 425 426 return initLockSucceeded 427 } 428 429 // Connected closes the returned channel when the consul client is connected. 430 func (c *consulClient) Connected(ctx context.Context) <-chan error { 431 ch := make(chan error) 432 go func() { 433 for { 434 qo := &consulAPI.QueryOptions{} 435 // TODO find out if there's a better way to do this for consul 436 _, _, err := c.Session().Info(c.lease, qo.WithContext(ctx)) 437 if err == nil { 438 break 439 } 440 time.Sleep(100 * time.Millisecond) 441 } 442 <-c.waitForInitLock(ctx) 443 close(ch) 444 }() 445 return ch 446 } 447 448 // Disconnected closes the returned channel when consul detects the client 449 // is disconnected from the server. 450 func (c *consulClient) Disconnected() <-chan struct{} { 451 c.disconnectedMu.RLock() 452 ch := c.disconnected 453 c.disconnectedMu.RUnlock() 454 return ch 455 } 456 457 func (c *consulClient) Status() (string, error) { 458 leader, err := c.Client.Status().Leader() 459 return "Consul: " + leader, err 460 } 461 462 func (c *consulClient) DeletePrefix(ctx context.Context, path string) (err error) { 463 defer func() { Trace("DeletePrefix", err, logrus.Fields{fieldPrefix: path}) }() 464 465 duration := spanstat.Start() 466 wo := &consulAPI.WriteOptions{} 467 _, err = c.Client.KV().DeleteTree(path, wo.WithContext(ctx)) 468 increaseMetric(path, metricDelete, "DeletePrefix", duration.EndError(err).Total(), err) 469 return err 470 } 471 472 // DeleteIfLocked deletes a key if the client is still holding the given lock. 473 func (c *consulClient) DeleteIfLocked(ctx context.Context, key string, lock KVLocker) (err error) { 474 defer func() { Trace("DeleteIfLocked", err, logrus.Fields{fieldKey: key}) }() 475 return c.delete(ctx, key) 476 } 477 478 // Delete deletes a key 479 func (c *consulClient) Delete(ctx context.Context, key string) (err error) { 480 defer func() { Trace("Delete", err, logrus.Fields{fieldKey: key}) }() 481 return c.delete(ctx, key) 482 } 483 484 func (c *consulClient) delete(ctx context.Context, key string) error { 485 duration := spanstat.Start() 486 wo := &consulAPI.WriteOptions{} 487 _, err := c.KV().Delete(key, wo.WithContext(ctx)) 488 increaseMetric(key, metricDelete, "Delete", duration.EndError(err).Total(), err) 489 return err 490 } 491 492 // GetIfLocked returns value of key if the client is still holding the given lock. 493 func (c *consulClient) GetIfLocked(ctx context.Context, key string, lock KVLocker) (bv []byte, err error) { 494 defer func() { Trace("GetIfLocked", err, logrus.Fields{fieldKey: key, fieldValue: string(bv)}) }() 495 return c.Get(ctx, key) 496 } 497 498 // Get returns value of key 499 func (c *consulClient) Get(ctx context.Context, key string) (bv []byte, err error) { 500 defer func() { Trace("Get", err, logrus.Fields{fieldKey: key, fieldValue: string(bv)}) }() 501 502 duration := spanstat.Start() 503 qo := &consulAPI.QueryOptions{} 504 pair, _, err := c.KV().Get(key, qo.WithContext(ctx)) 505 increaseMetric(key, metricRead, "Get", duration.EndError(err).Total(), err) 506 if err != nil { 507 return nil, err 508 } 509 if pair == nil { 510 return nil, nil 511 } 512 return pair.Value, nil 513 } 514 515 // UpdateIfLocked updates a key if the client is still holding the given lock. 516 func (c *consulClient) UpdateIfLocked(ctx context.Context, key string, value []byte, lease bool, lock KVLocker) error { 517 return c.Update(ctx, key, value, lease) 518 } 519 520 // Update creates or updates a key with the value 521 func (c *consulClient) Update(ctx context.Context, key string, value []byte, lease bool) (err error) { 522 defer func() { 523 Trace("Update", err, logrus.Fields{fieldKey: key, fieldValue: string(value), fieldAttachLease: lease}) 524 }() 525 526 k := &consulAPI.KVPair{Key: key, Value: value} 527 528 if lease { 529 k.Session = c.lease 530 } 531 532 opts := &consulAPI.WriteOptions{} 533 534 duration := spanstat.Start() 535 _, err = c.KV().Put(k, opts.WithContext(ctx)) 536 increaseMetric(key, metricSet, "Update", duration.EndError(err).Total(), err) 537 return err 538 } 539 540 // UpdateIfDifferentIfLocked updates a key if the value is different and if the client is still holding the given lock. 541 func (c *consulClient) UpdateIfDifferentIfLocked(ctx context.Context, key string, value []byte, lease bool, lock KVLocker) (recreated bool, err error) { 542 defer func() { 543 Trace("UpdateIfDifferentIfLocked", err, logrus.Fields{fieldKey: key, fieldValue: value, fieldAttachLease: lease, "recreated": recreated}) 544 }() 545 546 return c.updateIfDifferent(ctx, key, value, lease) 547 } 548 549 // UpdateIfDifferent updates a key if the value is different 550 func (c *consulClient) UpdateIfDifferent(ctx context.Context, key string, value []byte, lease bool) (recreated bool, err error) { 551 defer func() { 552 Trace("UpdateIfDifferent", err, logrus.Fields{fieldKey: key, fieldValue: value, fieldAttachLease: lease, "recreated": recreated}) 553 }() 554 555 return c.updateIfDifferent(ctx, key, value, lease) 556 } 557 558 func (c *consulClient) updateIfDifferent(ctx context.Context, key string, value []byte, lease bool) (bool, error) { 559 duration := spanstat.Start() 560 qo := &consulAPI.QueryOptions{} 561 getR, _, err := c.KV().Get(key, qo.WithContext(ctx)) 562 increaseMetric(key, metricRead, "Get", duration.EndError(err).Total(), err) 563 // On error, attempt update blindly 564 if err != nil || getR == nil { 565 return true, c.Update(ctx, key, value, lease) 566 } 567 568 if lease && getR.Session != c.lease { 569 return true, c.Update(ctx, key, value, lease) 570 } 571 572 // if lease is different and value is not equal then update. 573 if !bytes.Equal(getR.Value, value) { 574 return true, c.Update(ctx, key, value, lease) 575 } 576 577 return false, nil 578 } 579 580 // CreateOnlyIfLocked atomically creates a key if the client is still holding the given lock or fails if it already exists 581 func (c *consulClient) CreateOnlyIfLocked(ctx context.Context, key string, value []byte, lease bool, lock KVLocker) (success bool, err error) { 582 defer func() { 583 Trace("CreateOnlyIfLocked", err, logrus.Fields{fieldKey: key, fieldValue: value, fieldAttachLease: lease, "success": success}) 584 }() 585 return c.createOnly(ctx, key, value, lease) 586 } 587 588 // CreateOnly creates a key with the value and will fail if the key already exists 589 func (c *consulClient) CreateOnly(ctx context.Context, key string, value []byte, lease bool) (success bool, err error) { 590 defer func() { 591 Trace("CreateOnly", err, logrus.Fields{fieldKey: key, fieldValue: value, fieldAttachLease: lease, "success": success}) 592 }() 593 594 return c.createOnly(ctx, key, value, lease) 595 } 596 597 func (c *consulClient) createOnly(ctx context.Context, key string, value []byte, lease bool) (bool, error) { 598 k := &consulAPI.KVPair{ 599 Key: key, 600 Value: value, 601 CreateIndex: 0, 602 } 603 604 if lease { 605 k.Session = c.lease 606 } 607 opts := &consulAPI.WriteOptions{} 608 609 duration := spanstat.Start() 610 success, _, err := c.KV().CAS(k, opts.WithContext(ctx)) 611 increaseMetric(key, metricSet, "CreateOnly", duration.EndError(err).Total(), err) 612 if err != nil { 613 return false, fmt.Errorf("unable to compare-and-swap: %w", err) 614 } 615 return success, nil 616 } 617 618 // ListPrefixIfLocked returns a list of keys matching the prefix only if the client is still holding the given lock. 619 func (c *consulClient) ListPrefixIfLocked(ctx context.Context, prefix string, lock KVLocker) (v KeyValuePairs, err error) { 620 defer func() { Trace("ListPrefixIfLocked", err, logrus.Fields{fieldPrefix: prefix, fieldNumEntries: len(v)}) }() 621 return c.listPrefix(ctx, prefix) 622 } 623 624 // ListPrefix returns a map of matching keys 625 func (c *consulClient) ListPrefix(ctx context.Context, prefix string) (v KeyValuePairs, err error) { 626 defer func() { Trace("ListPrefix", err, logrus.Fields{fieldPrefix: prefix, fieldNumEntries: len(v)}) }() 627 return c.listPrefix(ctx, prefix) 628 } 629 630 func (c *consulClient) listPrefix(ctx context.Context, prefix string) (KeyValuePairs, error) { 631 duration := spanstat.Start() 632 qo := &consulAPI.QueryOptions{} 633 pairs, _, err := c.KV().List(prefix, qo.WithContext(ctx)) 634 increaseMetric(prefix, metricRead, "ListPrefix", duration.EndError(err).Total(), err) 635 if err != nil { 636 return nil, err 637 } 638 639 p := KeyValuePairs(make(map[string]Value, len(pairs))) 640 for i := 0; i < len(pairs); i++ { 641 p[pairs[i].Key] = Value{ 642 Data: pairs[i].Value, 643 ModRevision: pairs[i].ModifyIndex, 644 SessionID: pairs[i].Session, 645 } 646 } 647 648 return p, nil 649 } 650 651 // Close closes the consul session 652 func (c *consulClient) Close() { 653 close(c.statusCheckErrors) 654 if c.controllers != nil { 655 c.controllers.RemoveAll() 656 } 657 if c.lease != "" { 658 c.Session().Destroy(c.lease, nil) 659 } 660 } 661 662 // Encode encodes a binary slice into a character set that the backend supports 663 func (c *consulClient) Encode(in []byte) (out string) { 664 defer func() { Trace("Encode", nil, logrus.Fields{"in": in, "out": out}) }() 665 return base64.URLEncoding.EncodeToString([]byte(in)) 666 } 667 668 // Decode decodes a key previously encoded back into the original binary slice 669 func (c *consulClient) Decode(in string) (out []byte, err error) { 670 defer func() { Trace("Decode", err, logrus.Fields{"in": in, "out": out}) }() 671 return base64.URLEncoding.DecodeString(in) 672 } 673 674 // ListAndWatch implements the BackendOperations.ListAndWatch using consul 675 func (c *consulClient) ListAndWatch(ctx context.Context, prefix string, chanSize int) *Watcher { 676 w := newWatcher(prefix, chanSize) 677 678 log.WithField(fieldPrefix, prefix).Debug("Starting watcher...") 679 680 go c.watch(ctx, w) 681 682 return w 683 } 684 685 // StatusCheckErrors returns a channel which receives status check errors 686 func (c *consulClient) StatusCheckErrors() <-chan error { 687 return c.statusCheckErrors 688 } 689 690 // RegisterLeaseExpiredObserver is not implemented for the consul backend 691 func (c *consulClient) RegisterLeaseExpiredObserver(prefix string, fn func(key string)) {} 692 693 // UserEnforcePresence is not implemented for the consul backend 694 func (c *consulClient) UserEnforcePresence(ctx context.Context, name string, roles []string) error { 695 return ErrNotImplemented 696 } 697 698 // UserEnforceAbsence is not implemented for the consul backend 699 func (c *consulClient) UserEnforceAbsence(ctx context.Context, name string) error { 700 return ErrNotImplemented 701 }