mirror of https://github.com/hashicorp/consul
362 lines
11 KiB
Go
362 lines
11 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
|
|
"github.com/hashicorp/consul/internal/controller/cache"
|
|
"github.com/hashicorp/consul/internal/controller/cache/index"
|
|
"github.com/hashicorp/consul/internal/resource"
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
)
|
|
|
|
// DependencyMapper is called when a dependency watched via WithWatch is changed
|
|
// to determine which of the controller's managed resources need to be reconciled.
|
|
type DependencyMapper func(
|
|
ctx context.Context,
|
|
rt Runtime,
|
|
res *pbresource.Resource,
|
|
) ([]Request, error)
|
|
|
|
// Controller runs a reconciliation loop to respond to changes in resources and
|
|
// their dependencies. It is heavily inspired by Kubernetes' controller pattern:
|
|
// https://kubernetes.io/docs/concepts/architecture/controller/
|
|
//
|
|
// Use the builder methods in this package (starting with NewController) to construct
|
|
// a controller, and then pass it to a Manager to be executed.
|
|
type Controller struct {
|
|
name string
|
|
reconciler Reconciler
|
|
initializer Initializer
|
|
managedTypeWatch *watch
|
|
watches map[string]*watch
|
|
queries map[string]cache.Query
|
|
customWatches []customWatch
|
|
placement Placement
|
|
baseBackoff time.Duration
|
|
maxBackoff time.Duration
|
|
logger hclog.Logger
|
|
startCb RuntimeCallback
|
|
stopCb RuntimeCallback
|
|
// forceReconcileEvery is the time to wait after a successful reconciliation
|
|
// before forcing a reconciliation. The net result is a reconciliation of
|
|
// the managed type on a regular interval. This ensures that the state of the
|
|
// world is continually reconciled, hence correct in the face of missed events
|
|
// or other issues.
|
|
forceReconcileEvery time.Duration
|
|
}
|
|
|
|
type RuntimeCallback func(context.Context, Runtime)
|
|
|
|
// NewController creates a controller that is setup to watched the managed type.
|
|
// Extra cache indexes may be provided as well and these indexes will be automatically managed.
|
|
// Typically, further calls to other builder methods will be needed to fully configure
|
|
// the controller such as using WithReconcile to define the code that will be called
|
|
// when the managed resource needs reconciliation.
|
|
func NewController(name string, managedType *pbresource.Type, indexes ...*index.Index) *Controller {
|
|
w := &watch{
|
|
watchedType: managedType,
|
|
indexes: make(map[string]*index.Index),
|
|
}
|
|
|
|
for _, idx := range indexes {
|
|
w.addIndex(idx)
|
|
}
|
|
|
|
return &Controller{
|
|
name: name,
|
|
managedTypeWatch: w,
|
|
watches: make(map[string]*watch),
|
|
queries: make(map[string]cache.Query),
|
|
forceReconcileEvery: 8 * time.Hour,
|
|
}
|
|
}
|
|
|
|
// WithNotifyStart registers a callback to be run when the controller is being started.
|
|
// This happens prior to watches being started and with a fresh cache.
|
|
func (ctl *Controller) WithNotifyStart(start RuntimeCallback) *Controller {
|
|
ctl.startCb = start
|
|
return ctl
|
|
}
|
|
|
|
// WithNotifyStop registers a callback to be run when the controller has been stopped.
|
|
// This happens after all the watches and mapper/reconcile queues have been stopped. The
|
|
// cache will contain everything that was present when we started stopping watches.
|
|
func (ctl *Controller) WithNotifyStop(stop RuntimeCallback) *Controller {
|
|
ctl.stopCb = stop
|
|
return ctl
|
|
}
|
|
|
|
// WithReconciler changes the controller's reconciler.
|
|
func (ctl *Controller) WithReconciler(reconciler Reconciler) *Controller {
|
|
if reconciler == nil {
|
|
panic("reconciler must not be nil")
|
|
}
|
|
|
|
ctl.reconciler = reconciler
|
|
return ctl
|
|
}
|
|
|
|
// WithWatch enables watching of the specified resource type and mapping it to the managed type
|
|
// via the provided DependencyMapper. Extra cache indexes to calculate on the watched type
|
|
// may also be provided.
|
|
func (ctl *Controller) WithWatch(watchedType *pbresource.Type, mapper DependencyMapper, indexes ...*index.Index) *Controller {
|
|
key := resource.ToGVK(watchedType)
|
|
|
|
_, alreadyWatched := ctl.watches[key]
|
|
if alreadyWatched {
|
|
panic(fmt.Sprintf("resource type %q already has a configured watch", key))
|
|
}
|
|
|
|
w := newWatch(watchedType, mapper)
|
|
|
|
for _, idx := range indexes {
|
|
w.addIndex(idx)
|
|
}
|
|
|
|
ctl.watches[key] = w
|
|
|
|
return ctl
|
|
}
|
|
|
|
// WithQuery will add a named query to the controllers cache for usage during reconcile or in dependency mappers
|
|
func (ctl *Controller) WithQuery(queryName string, fn cache.Query) *Controller {
|
|
_, duplicate := ctl.queries[queryName]
|
|
if duplicate {
|
|
panic(fmt.Sprintf("a predefined cache query with name %q already exists", queryName))
|
|
}
|
|
|
|
ctl.queries[queryName] = fn
|
|
return ctl
|
|
}
|
|
|
|
// WithCustomWatch adds a new custom watch. Custom watches do not affect the controller cache.
|
|
func (ctl *Controller) WithCustomWatch(source *Source, mapper CustomDependencyMapper) *Controller {
|
|
if source == nil {
|
|
panic("source must not be nil")
|
|
}
|
|
|
|
if mapper == nil {
|
|
panic("mapper must not be nil")
|
|
}
|
|
|
|
ctl.customWatches = append(ctl.customWatches, customWatch{source, mapper})
|
|
return ctl
|
|
}
|
|
|
|
// WithLogger changes the controller's logger.
|
|
func (ctl *Controller) WithLogger(logger hclog.Logger) *Controller {
|
|
if logger == nil {
|
|
panic("logger must not be nil")
|
|
}
|
|
|
|
ctl.logger = logger
|
|
return ctl
|
|
}
|
|
|
|
// WithBackoff changes the base and maximum backoff values for the controller's
|
|
// retry rate limiter.
|
|
func (ctl *Controller) WithBackoff(base, max time.Duration) *Controller {
|
|
ctl.baseBackoff = base
|
|
ctl.maxBackoff = max
|
|
return ctl
|
|
}
|
|
|
|
// WithPlacement changes where and how many replicas of the controller will run.
|
|
// In the majority of cases, the default placement (one leader elected instance
|
|
// per cluster) is the most appropriate and you shouldn't need to override it.
|
|
func (ctl *Controller) WithPlacement(placement Placement) *Controller {
|
|
ctl.placement = placement
|
|
return ctl
|
|
}
|
|
|
|
// WithForceReconcileEvery controls how often a resource gets periodically reconciled
|
|
// to ensure that the state of the world is correct (8 hours is the default).
|
|
// This exists for tests only and should not be customized by controller authors!
|
|
func (ctl *Controller) WithForceReconcileEvery(duration time.Duration) *Controller {
|
|
ctl.logger.Warn("WithForceReconcileEvery is for testing only and should not be set by controllers")
|
|
ctl.forceReconcileEvery = duration
|
|
return ctl
|
|
}
|
|
|
|
// buildCache will construct a controller Cache given the watches/indexes that have
|
|
// been added to the controller. This is mainly to be used by the TestController and
|
|
// Manager when setting up how things
|
|
func (ctl *Controller) buildCache() cache.Cache {
|
|
c := cache.New()
|
|
|
|
addWatchToCache(c, ctl.managedTypeWatch)
|
|
for _, w := range ctl.watches {
|
|
addWatchToCache(c, w)
|
|
}
|
|
|
|
for name, query := range ctl.queries {
|
|
if err := c.AddQuery(name, query); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// dryRunMapper will trigger the appropriate DependencyMapper for an update of
|
|
// the provided type and return the requested reconciles.
|
|
//
|
|
// This is mainly to be used by the TestController.
|
|
func (ctl *Controller) dryRunMapper(
|
|
ctx context.Context,
|
|
rt Runtime,
|
|
res *pbresource.Resource,
|
|
) ([]Request, error) {
|
|
if resource.EqualType(ctl.managedTypeWatch.watchedType, res.Id.Type) {
|
|
return nil, nil // no-op
|
|
}
|
|
|
|
for _, w := range ctl.watches {
|
|
if resource.EqualType(w.watchedType, res.Id.Type) {
|
|
return w.mapper(ctx, rt, res)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("no mapper for type: %s", resource.TypeToString(res.Id.Type))
|
|
}
|
|
|
|
// String returns a textual description of the controller, useful for debugging.
|
|
func (ctl *Controller) String() string {
|
|
watchedTypes := make([]string, 0, len(ctl.watches))
|
|
for watchedType := range ctl.watches {
|
|
watchedTypes = append(watchedTypes, watchedType)
|
|
}
|
|
base, max := ctl.backoff()
|
|
return fmt.Sprintf(
|
|
"<Controller managed_type=%s, watched_types=[%s], backoff=<base=%s, max=%s>, placement=%s>",
|
|
resource.ToGVK(ctl.managedTypeWatch.watchedType),
|
|
strings.Join(watchedTypes, ", "),
|
|
base, max,
|
|
ctl.placement,
|
|
)
|
|
}
|
|
|
|
func (ctl *Controller) backoff() (time.Duration, time.Duration) {
|
|
base := ctl.baseBackoff
|
|
if base == 0 {
|
|
base = 5 * time.Millisecond
|
|
}
|
|
max := ctl.maxBackoff
|
|
if max == 0 {
|
|
max = 1000 * time.Second
|
|
}
|
|
return base, max
|
|
}
|
|
|
|
func (ctl *Controller) buildLogger(defaultLogger hclog.Logger) hclog.Logger {
|
|
logger := defaultLogger
|
|
if ctl.logger != nil {
|
|
logger = ctl.logger
|
|
}
|
|
|
|
return logger.With("controller", ctl.name, "managed_type", resource.ToGVK(ctl.managedTypeWatch.watchedType))
|
|
}
|
|
|
|
func addWatchToCache(c cache.Cache, w *watch) {
|
|
c.AddType(w.watchedType)
|
|
for _, index := range w.indexes {
|
|
if err := c.AddIndex(w.watchedType, index); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Placement determines where and how many replicas of the controller will run.
|
|
type Placement int
|
|
|
|
const (
|
|
// PlacementSingleton ensures there is a single, leader-elected, instance of
|
|
// the controller running in the cluster at any time. It's the default and is
|
|
// suitable for most use-cases.
|
|
PlacementSingleton Placement = iota
|
|
|
|
// PlacementEachServer ensures there is a replica of the controller running on
|
|
// each server in the cluster. It is useful for cases where the controller is
|
|
// responsible for applying some configuration resource to the server whenever
|
|
// it changes (e.g. rate-limit configuration). Generally, controllers in this
|
|
// placement mode should not modify resources.
|
|
PlacementEachServer
|
|
)
|
|
|
|
// String satisfies the fmt.Stringer interface.
|
|
func (p Placement) String() string {
|
|
switch p {
|
|
case PlacementSingleton:
|
|
return "singleton"
|
|
case PlacementEachServer:
|
|
return "each-server"
|
|
}
|
|
panic(fmt.Sprintf("unknown placement %d", p))
|
|
}
|
|
|
|
// Reconciler implements the business logic of a controller.
|
|
type Reconciler interface {
|
|
// Reconcile the resource identified by req.ID.
|
|
Reconcile(ctx context.Context, rt Runtime, req Request) error
|
|
}
|
|
|
|
// RequeueAfterError is an error that allows a Reconciler to override the
|
|
// exponential backoff behavior of the Controller, rather than applying
|
|
// the backoff algorithm, returning a RequeueAfterError will cause the
|
|
// Controller to reschedule the Request at a given time in the future.
|
|
type RequeueAfterError time.Duration
|
|
|
|
// Error implements the error interface.
|
|
func (r RequeueAfterError) Error() string {
|
|
return fmt.Sprintf("requeue at %s", time.Duration(r))
|
|
}
|
|
|
|
// RequeueAfter constructs a RequeueAfterError with the given duration
|
|
// setting.
|
|
func RequeueAfter(after time.Duration) error {
|
|
return RequeueAfterError(after)
|
|
}
|
|
|
|
// RequeueNow constructs a RequeueAfterError that reschedules the Request
|
|
// immediately.
|
|
func RequeueNow() error {
|
|
return RequeueAfterError(0)
|
|
}
|
|
|
|
// Request represents a request to reconcile the resource with the given ID.
|
|
type Request struct {
|
|
// ID of the resource that needs to be reconciled.
|
|
ID *pbresource.ID
|
|
}
|
|
|
|
// Key satisfies the queue.ItemType interface. It returns a string which will be
|
|
// used to de-duplicate requests in the queue.
|
|
func (r Request) Key() string {
|
|
return fmt.Sprintf(
|
|
"part=%q,ns=%q,name=%q,uid=%q",
|
|
r.ID.Tenancy.Partition,
|
|
r.ID.Tenancy.Namespace,
|
|
r.ID.Name,
|
|
r.ID.Uid,
|
|
)
|
|
}
|
|
|
|
// Initializer implements the business logic that is executed when the
|
|
// controller is first started.
|
|
type Initializer interface {
|
|
Initialize(ctx context.Context, rt Runtime) error
|
|
}
|
|
|
|
// WithInitializer changes the controller's initializer.
|
|
func (c *Controller) WithInitializer(initializer Initializer) *Controller {
|
|
c.initializer = initializer
|
|
return c
|
|
}
|