General Controller Guidelines
This document contains some general guidelines for contributing code to openMCP controllers. The goal is to align the coding and make all controllers look and behave similarly.
Reconcile Logic
Operation Annotations
The option to manually trigger or disable reconciliation for specific objects has been shown to be useful in the past. There are two operation annotations which should be supported by each controller:
openmcp.cloud/operation: reconcile
- This annotation is expected to trigger a reconciliation and then be removed by the reconciling controller.
- If the reconcile logic contains 'shortcuts' that check if something needs to be done and skip it otherwise, the annotation should cause these checks to always result in the code being executed instead of skipped.
openmcp.cloud/operation: ignore
- Resources with this annotation should not be reconciled. Simply abort the reconciliation, if this annotation is found.
The following code snippet can be used as a template for the desired behavior:
import (
apiconst "github.com/openmcp-project/openmcp-operator/api/constants"
ctrlutils "github.com/openmcp-project/controller-utils/pkg/controller"
)
// within the Reconcile method:
// handle operation annotation
hadReconcileAnnotation := false // only required if the information whether the reconciliation was triggered manually is relevant for the reconcile logic
if obj.GetAnnotations() != nil {
op, ok := obj.GetAnnotations()[apiconst.OperationAnnotation]
if ok {
switch op {
case apiconst.OperationAnnotationValueIgnore:
log.Info("Ignoring resource due to ignore operation annotation")
return reconcile.Result{}, nil
case apiconst.OperationAnnotationValueReconcile:
log.Debug("Removing reconcile operation annotation from resource")
if err := ctrlutils.EnsureAnnotation(ctx, myClient, obj, apiconst.OperationAnnotation, "", true, ctrlutils.DELETE); err != nil {
return reconcile.Result{}, fmt.Errorf("error removing operation annotation: %w", err)
}
hadReconcileAnnotation = true
}
}
}
Status Reporting
Each resource that is reconciled by a controller should include the common status in its own status:
import (
commonapi "github.com/openmcp-project/openmcp-operator/api/common"
)
type MyStatus struct {
commonapi.Status `json:",inline"`
// add more status fields if required
}
The common status contains the following fields that should be updated during reconciliation:
observedGeneration
- The value of this field should be set to the value of
metadata.generation
during each reconciliation, independent of whether the reconciliation was successful or resulted in an error.- Updating the field should be skipped if the resource has the ignore operation annotation.
- The value of this field should be set to the value of
conditions
- This is a list of conditions. It uses the same condition type that k8s also uses for its core resources, e.g. pods.
- The condition's
type
field works like a key and should be unique among the list. - Old conditions should not be deleted when updating the condition list. Each condition has an
observedGeneration
field that maps the condition to the object generation it was created for. - The condition's
status
field should be eitherTrue
,False
, orUnknown
.
phase
- The phase aggregates the resource's state into a single string. It is useful for being displayed as an additional printer column for
kubectl get
. - Unless there is a good reason for it, it should always contain one of the following values:
Terminating
, if the resource is being deleted (= has a non-zero deletion timestamp)Ready
if the resource is not being deleted and all of its conditions areTrue
Progressing
otherwise- This means that there should be at least one non-
True
condition explaining what is currently happening if the phase isProgressing
.
- This means that there should be at least one non-
- The phase aggregates the resource's state into a single string. It is useful for being displayed as an additional printer column for
The controller-utils library contains helper functions for updating conditions or even the whole status. See the documentation for further information.
Event Filtering
Not a hard requirement and also strongly depends on the purpose of the controller, but often it is useful to use the controller-runtime's ability to filter the events which cause a reconciliation. For example, often a resource's status is only modified by the controller reconciling it and a resource is mostly reconciled by only a single controller. In this case, changes to the status do not need to trigger a reconciliation, because they are already a result of one. In many cases, restricting reconciliation triggers to generation changes (which usually correspond to changes to a resource's spec
) works well.
The following snippet can be used as a template:
import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/predicate"
ctrlutils "github.com/openmcp-project/controller-utils/pkg/controller"
)
// SetupWithManager sets up the controller with the Manager.
func (r *MyReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&mypackage.MyObjType{}).
WithEventFilter(predicate.And(
predicate.Or(
predicate.GenerationChangedPredicate{},
ctrlutils.DeletionTimestampChangedPredicate{},
ctrlutils.GotAnnotationPredicate(openmcpconst.OperationAnnotation, openmcpconst.OperationAnnotationValueReconcile),
ctrlutils.LostAnnotationPredicate(openmcpconst.OperationAnnotation, openmcpconst.OperationAnnotationValueIgnore),
),
predicate.Not(
ctrlutils.HasAnnotationPredicate(openmcpconst.OperationAnnotation, openmcpconst.OperationAnnotationValueIgnore),
),
)).
Complete(r)
}
This example restricts reconciliation triggers to the following events:
- The resource generation changed, indicating a change to the
spec
of the resource.- Note that kubernetes seems to increase the generation exactly if some part of a field named
spec
changed. For resources without aspec
(e.g. secrets), the generation is not increased if the payload changes. If a resource contains additional top-level fields next tospec
, modifications to them might not cause a generation increase either.
- Note that kubernetes seems to increase the generation exactly if some part of a field named
- The resource got a deletion timestamp, meaning its deletion was triggered.
- The resource got the
openmcp.cloud/operation
annotation with valuereconcile
(or it had the annotation before and its value was changed toreconcile
).
If the resource has the openmcp.cloud/operation
annotation with value ignore
, a reconciliation is never triggered, even if the generation was increased or the deletion was triggered.