Component Reconciler
Dependent objects are - by definition - the resources returned by the Generate()
method of the used resource generator.
Whenever a component resource (that is, an instance of the component’s custom resource type) is created, udpated, or deleted,
the set of dependent object potentially changes, and the cluster state has to be synchronized with that new declared state.
This synchronization is the job of the reconciler provided by this framework.
Creating the reconciler instance
Typically, a component operator runs one reconciler which is instantiated by calling the following constructor:
package component
func NewReconciler[T Component](
name string,
resourceGenerator manifests.Generator
options ReconcilerOptions
) *Reconciler[T]
The passed type parameter T Component
is the concrete runtime type of the component’s custom resource type (respectively, a pointer to that). Furthermore,
name
is supposed to be a unique name (typically a DNS name) identifying this component operator in the cluster; ìt will be used in annotations, labels, for leader election, …resourceGenerator
is an implementation of theGenerator
interface, describing how the dependent objects are rendered from the component’s spec.options
can be used to tune the behavior of the reconciler:package component // ReconcilerOptions are creation options for a Reconciler. type ReconcilerOptions struct { // Which field manager to use in API calls. // If unspecified, the reconciler name is used. FieldOwner *string // Which finalizer to use. // If unspecified, the reconciler name is used. Finalizer *string // Default service account used for impersonation of clients. // Of course, components can still customize impersonation by implementing the ImpersonationConfiguration interface. DefaultServiceAccount *string // How to react if a dependent object exists but has no or a different owner. // If unspecified, AdoptionPolicyIfUnowned is assumed. // Can be overridden by annotation on object level. AdoptionPolicy *reconciler.AdoptionPolicy // How to perform updates to dependent objects. // If unspecified, UpdatePolicyReplace is assumed. // Can be overridden by annotation on object level. UpdatePolicy *reconciler.UpdatePolicy // How to perform deletion of dependent objects. // If unspecified, DeletePolicyDelete is assumed. // Can be overridden by annotation on object level. DeletePolicy *reconciler.DeletePolicy // Whether namespaces are auto-created if missing. // If unspecified, MissingNamespacesPolicyCreate is assumed. MissingNamespacesPolicy *reconciler.MissingNamespacesPolicy // SchemeBuilder allows to define additional schemes to be made available in the // target client. SchemeBuilder types.SchemeBuilder }
The object returned by NewReconciler
implements controller-runtime’s Reconciler
interface, and can therefore be used as a drop-in
in kubebuilder managed projects. After creation, the reconciler can be registered with the responsible controller-runtime manager instance by calling
package component
func (r *Reconciler[T]) SetupWithManager(mgr ctrl.Manager) error
The used manager mgr
has to fulfill a few requirements:
- its client must bypass informer caches for the following types:
- the type
T
itself - the type
CustomResourceDefinition
from theapiextensions.k8s.io/v1
group - the type
APIService
from theapiregistration.k8s.io/v1
group
- the type
- its scheme must recognize at least the following types:
- the types in the API group defined in this repository
- the core group (
v1
) - group
apiextensions.k8s.io/v1
- group
apiregistration.k8s.io/v1
.
Reconciler hooks
Component operators may register hooks to enhance the reconciler logic at certain points, by passing functions of type
package component
// HookFunc is the function signature that can be used to
// establish callbacks at certain points in the reconciliation logic.
// Hooks will be passed the current (potentially unsaved) state of the component.
// Post-hooks will only be called if the according operation (read, reconcile, delete)
// has been successful.
type HookFunc[T Component] func(ctx context.Context, client client.Client, component T) error
to the desired registration functions:
package component
// Register post-read hook with reconciler.
// This hook will be called after the reconciled component object has been retrieved from the Kubernetes API.
func (r *Reconciler[T]) WithPostReadHook(hook HookFunc[T]) *Reconciler[T]
// Register pre-reconcile hook with reconciler.
// This hook will be called if the reconciled component is not in deletion (has no deletionTimestamp set),
// right before the reconcilation of the dependent objects starts.
func (r *Reconciler[T]) WithPreReconcileHook(hook HookFunc[T]) *Reconciler[T]
// Register post-reconcile hook with reconciler.
// This hook will be called if the reconciled component is not in deletion (has no deletionTimestamp set),
// right after the reconcilation of the dependent objects happened, and was successful.
func (r *Reconciler[T]) WithPostReconcileHook(hook HookFunc[T]) *Reconciler[T]
// Register pre-delete hook with reconciler.
// This hook will be called if the reconciled component is in deletion (has a deletionTimestamp set),
// right before the deletion of the dependent objects starts.
func (r *Reconciler[T]) WithPreDeleteHook(hook HookFunc[T]) *Reconciler[T]
// Register post-delete hook with reconciler.
// This hook will be called if the reconciled component is in deletion (has a deletionTimestamp set),
// right after the deletion of the dependent objects happened, and was successful.
func (r *Reconciler[T]) WithPostDeleteHook(hook HookFunc[T]) *Reconciler[T]
Note that the client passed to the hook functions is the client of the manager that was used when calling SetupWithManager()
(that is, the return value of that manager’s GetClient()
method). In addition, reconcile and delete hooks (that is, all except the post-read hook) can retrieve a client for the deployment target by calling ClientFromContext()
.
Tuning the retry behavior
By default, errors returned by the component’s generator or by a registered hook will make the reconciler go
into a backoff managed by controller-runtime (which usually is an exponential backoff, capped at 10 minutes).
However, if the error is or unwraps to a types.RetriableError
, then the retry delay specified at the error
will be used instead of the backoff. Implementations should use
pacakge types
func NewRetriableError(err error, retryAfter *time.Duration) RetriableError {
return RetriableError{err: err, retryAfter: retryAfter}
}
to wrap an error into a RetriableError
. It is allowed to pass retryAfter
as nil; in that case the retry delay
will be determined by calling the component’s GetRetryInterval()
method (if the component or its spec implements
the
package component
// The RetryConfiguration interface is meant to be implemented by components (or their spec) which offer
// tweaking the retry interval (by default, it would be the value of the requeue interval).
type RetryConfiguration interface {
// Get retry interval. Should be greater than 1 minute.
GetRetryInterval() time.Duration
}
interface), or otherwise will be set to the effective requeue interval (see below).
Tuning the requeue behavior
If a component was successfully reconciled, another reconciliation will be scheduled after 10 minutes, by default. This default requeue interval may be overridden by the component by implementing the
package component
// The RequeueConfiguration interface is meant to be implemented by components (or their spec) which offer
// tweaking the requeue interval (by default, it would be 10 minutes).
type RequeueConfiguration interface {
// Get requeue interval. Should be greater than 1 minute.
GetRequeueInterval() time.Duration
}
interface.
Tuning the timeout behavior
If the dependent objects of a component do not reach a ready state after a certain time, the component state will switch from Processing
to Error
.
This timeout restarts counting whenever something changed in the component, or in the manifests of the dependent objects, and by default has the value
of the effective requeue interval, which in turn defaults to 10 minutes.
The timeout may be overridden by the component by implementing the
package component
// The TimeoutConfiguration interface is meant to be implemented by components (or their spec) which offer
// tweaking the processing timeout (by default, it would be the value of the requeue interval).
type TimeoutConfiguration interface {
// Get timeout. Should be greater than 1 minute.
GetTimeout() time.Duration
}
interface.
Tuning the handling of dependent objects
The reconciler allows to tweak how dependent objects are applied to or deleted from the cluster. To change the shipped framework defaults, a component type can implement the
package component
// The PolicyConfiguration interface is meant to be implemented by compoments (or their spec) which offer
// tweaking policies affecting the dependents handling.
type PolicyConfiguration interface {
// Get adoption policy.
// Must return a valid AdoptionPolicy, or the empty string (then the reconciler/framework default applies).
GetAdoptionPolicy() reconciler.AdoptionPolicy
// Get update policy.
// Must return a valid UpdatePolicy, or the empty string (then the reconciler/framework default applies).
GetUpdatePolicy() reconciler.UpdatePolicy
// Get delete policy.
// Must return a valid DeletePolicy, or the empty string (then the reconciler/framework default applies).
GetDeletePolicy() reconciler.DeletePolicy
// Get namspace auto-creation policy.
// Must return a valid MissingNamespacesPolicy, or the empty string (then the reconciler/framework default applies).
GetMissingNamespacesPolicy() reconciler.MissingNamespacesPolicy
}
interface. Note that most of the above policies can be overridden on a per-object level by setting certain annotations, as described here.
Declaring additional (implicit) managed types
One of component-operator-runtime’s core features is the special handling of instances of managed types. Managed types are API extension types (such as custom resource definitions or types added by federation of an aggregated API server). Instances of these types which are part of the component (so-called managed instances) are treated differently. For example, the framework tries to process these instances as late as possible when applying the component, and as early as possible when the component is deleted. Other instances of these types which are not part of the component (so-called foreign instances) block the deletion of the whole component.
Sometimes, components are implicitly adding extension types. That means that the type definition is not part of the component manifest, but the types are just created at runtime by controllers or operators contained in the component. A typical example are crossplane providers. These types are of course not recognized by the framework as managed types. However it is probably desired that (both managed and foreign) instances of these types experience the same special handling like instances of real managed types.
To make this possible, components can implement the
// The TypeConfiguration interface is meant to be implemented by compoments (or their spec) which allow
// to specify additional managed types.
type TypeConfiguration interface {
// Get additional managed types; instances of these types are handled differently during
// apply and delete; foreign instances of these types will block deletion of the component.
// The fields of the returned TypeInfo structs can be concrete api groups, kinds,
// or wildcards ("*"); in addition, groups can be specified as a pattern of the form "*.<suffix>"",
// where the wildcard matches one or multiple dns labels.
GetAdditionalManagedTypes() []reconciler.TypeInfo
}
interface. The types returned by GetAdditionalManagedTypes()
contain a group and a kind, such as
// TypeInfo represents a Kubernetes type.
type TypeInfo struct {
// API group.
Group string `json:"group"`
// API kind.
Kind string `json:"kind"`
}
To match multiple types, the following pattern syntax is supported:
- the
Group
can be just*
or have the form*.domain.suffix
; note that the second case, the asterisk matches one or multiple DNS labels - the
Kind
can be just*
, which matches any kind.