2023-05-09 14:25:55 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2023-04-25 11:52:35 +00:00
|
|
|
package controller
|
|
|
|
|
|
|
|
import (
|
2023-05-09 14:25:55 +00:00
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
|
|
|
|
|
|
"github.com/hashicorp/consul/internal/resource"
|
2023-04-25 11:52:35 +00:00
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
|
|
)
|
|
|
|
|
|
|
|
// ForType begins building a Controller for the given resource type.
|
|
|
|
func ForType(managedType *pbresource.Type) Controller {
|
|
|
|
return Controller{managedType: managedType}
|
|
|
|
}
|
|
|
|
|
2023-05-09 14:25:55 +00:00
|
|
|
// WithReconciler changes the controller's reconciler.
|
|
|
|
func (c Controller) WithReconciler(reconciler Reconciler) Controller {
|
|
|
|
if reconciler == nil {
|
|
|
|
panic("reconciler must not be nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
c.reconciler = reconciler
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithWatch adds a watch on the given type/dependency to the controller. mapper
|
|
|
|
// will be called to determine which resources must be reconciled as a result of
|
|
|
|
// a watched resource changing.
|
|
|
|
func (c Controller) WithWatch(watchedType *pbresource.Type, mapper DependencyMapper) Controller {
|
|
|
|
if watchedType == nil {
|
|
|
|
panic("watchedType must not be nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
if mapper == nil {
|
|
|
|
panic("mapper must not be nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
c.watches = append(c.watches, watch{watchedType, mapper})
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithLogger changes the controller's logger.
|
|
|
|
func (c Controller) WithLogger(logger hclog.Logger) Controller {
|
|
|
|
if logger == nil {
|
|
|
|
panic("logger must not be nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
c.logger = logger
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// WithBackoff changes the base and maximum backoff values for the controller's
|
|
|
|
// retry rate limiter.
|
|
|
|
func (c Controller) WithBackoff(base, max time.Duration) Controller {
|
|
|
|
c.baseBackoff = base
|
|
|
|
c.maxBackoff = max
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 (c Controller) WithPlacement(placement Placement) Controller {
|
|
|
|
c.placement = placement
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a textual description of the controller, useful for debugging.
|
|
|
|
func (c Controller) String() string {
|
|
|
|
watchedTypes := make([]string, len(c.watches))
|
|
|
|
for idx, w := range c.watches {
|
|
|
|
watchedTypes[idx] = fmt.Sprintf("%q", resource.ToGVK(w.watchedType))
|
|
|
|
}
|
|
|
|
base, max := c.backoff()
|
|
|
|
return fmt.Sprintf(
|
|
|
|
"<Controller managed_type=%q, watched_types=[%s], backoff=<base=%q, max=%q>, placement=%q>",
|
|
|
|
resource.ToGVK(c.managedType),
|
|
|
|
strings.Join(watchedTypes, ", "),
|
|
|
|
base, max,
|
|
|
|
c.placement,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c Controller) backoff() (time.Duration, time.Duration) {
|
|
|
|
base := c.baseBackoff
|
|
|
|
if base == 0 {
|
|
|
|
base = 5 * time.Millisecond
|
|
|
|
}
|
|
|
|
max := c.maxBackoff
|
|
|
|
if max == 0 {
|
|
|
|
max = 1000 * time.Second
|
|
|
|
}
|
|
|
|
return base, max
|
|
|
|
}
|
|
|
|
|
2023-04-25 11:52:35 +00:00
|
|
|
// 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 ForType) to construct
|
|
|
|
// a controller, and then pass it to a Manager to be executed.
|
|
|
|
type Controller struct {
|
|
|
|
managedType *pbresource.Type
|
2023-05-09 14:25:55 +00:00
|
|
|
reconciler Reconciler
|
|
|
|
logger hclog.Logger
|
|
|
|
watches []watch
|
|
|
|
baseBackoff time.Duration
|
|
|
|
maxBackoff time.Duration
|
|
|
|
placement Placement
|
|
|
|
}
|
|
|
|
|
|
|
|
type watch struct {
|
|
|
|
watchedType *pbresource.Type
|
|
|
|
mapper DependencyMapper
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Runtime contains the dependencies required by reconcilers.
|
|
|
|
type Runtime struct {
|
|
|
|
Client pbresource.ResourceServiceClient
|
|
|
|
Logger hclog.Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
// MapOwner implements a DependencyMapper that returns the updated resource's owner.
|
|
|
|
func MapOwner(_ context.Context, _ Runtime, res *pbresource.Resource) ([]Request, error) {
|
|
|
|
var reqs []Request
|
|
|
|
if res.Owner != nil {
|
|
|
|
reqs = append(reqs, Request{ID: res.Owner})
|
|
|
|
}
|
|
|
|
return reqs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
2023-04-25 11:52:35 +00:00
|
|
|
}
|