-
Notifications
You must be signed in to change notification settings - Fork 1
Getting Started
This guide shows the current integration pattern for github.com/u-ctf/controller-fwk.
- Go 1.25+
- controller-runtime based controller
- A custom resource with a status field containing
Conditions []metav1.Conditionif you want to use the built-in Ready-condition helpers
Install the framework:
go get github.com/u-ctf/controller-fwkYour reconciler must satisfy ctrlfwk.Reconciler[K].
type WebAppReconciler struct {
client.Client
RuntimeScheme *runtime.Scheme
}
var _ ctrlfwk.Reconciler[*appsv1alpha1.WebApp] = &WebAppReconciler{}
func (WebAppReconciler) For(*appsv1alpha1.WebApp) {}If you plan to use dependencies or managed resources, also implement:
ReconcilerWithDependencies[*WebApp, WebAppContext]ReconcilerWithResources[*WebApp, WebAppContext]
If you want dynamic watches, also embed ctrlfwk.WatchCache and implement ReconcilerWithWatcher[*WebApp].
Use NewContext when you only need access to the custom resource.
fwkCtx := ctrlfwk.NewContext[*appsv1alpha1.WebApp](ctx, reconciler)Use NewContextWithData when steps, dependencies, and resources need to share typed data.
type WebAppData struct {
ConfigSecret *corev1.Secret
Deployment *appsv1.Deployment
}
type WebAppContext = *ctrlfwk.ContextWithData[*appsv1alpha1.WebApp, *WebAppData]
fwkCtx := ctrlfwk.NewContextWithData(ctx, reconciler, &WebAppData{})The current API uses NewStepperFor(ctx, logger) plus fluent builder methods.
func (reconciler *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := logf.FromContext(ctx)
fwkCtx := ctrlfwk.NewContext[*appsv1alpha1.WebApp](ctx, reconciler)
stepper := ctrlfwk.NewStepperFor[*appsv1alpha1.WebApp](fwkCtx, logger).
WithStep(ctrlfwk.NewFindControllerCustomResourceStep(fwkCtx, reconciler)).
WithStep(validationStep()).
WithFinalStep(ctrlfwk.NewReadyConditionFinalStep(fwkCtx, reconciler, ctrlfwk.SetReadyConditionFromResult(reconciler))).
Build()
return stepper.Execute(fwkCtx, req)
}Use WithFinalStep for status updates that must run on all exit paths.
NewEndStep still exists, but it only runs if all previous steps returned success.
Custom steps are plain values of type ctrlfwk.Step[K, C].
func validationStep() ctrlfwk.Step[*appsv1alpha1.WebApp, ctrlfwk.Context[*appsv1alpha1.WebApp]] {
return ctrlfwk.NewStep(
"validate-webapp",
func(ctx ctrlfwk.Context[*appsv1alpha1.WebApp], logger logr.Logger, req ctrl.Request) ctrlfwk.StepResult {
webapp := ctx.GetCustomResource()
if webapp.Spec.Image == "" {
return ctrlfwk.ResultEarlyReturn()
}
logger.Info("validated webapp", "image", webapp.Spec.Image)
return ctrlfwk.ResultSuccess()
},
)
}Available result helpers:
ResultSuccess()ResultEarlyReturn()ResultRequeueIn(duration)ResultInError(err)
The framework keeps both a clean copy and a mutable copy of the custom resource in the context. Update the mutable object, then patch through the helper.
func markNotReady(ctx ctrlfwk.Context[*appsv1alpha1.WebApp], reconciler *WebAppReconciler, reason string) error {
webapp := ctx.GetCustomResource()
meta.SetStatusCondition(&webapp.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: reason,
Message: "validation failed",
ObservedGeneration: webapp.GetGeneration(),
LastTransitionTime: metav1.Now(),
})
return ctrlfwk.PatchCustomResourceStatus(ctx, reconciler)
}If you use SetReadyConditionFromResult, the final step can manage the generic Ready condition for you.
Once your reconciler implements the relevant interfaces, the usual sequence is:
stepper := ctrlfwk.NewStepperFor[*appsv1alpha1.WebApp](fwkCtx, logger).
WithStep(ctrlfwk.NewFindControllerCustomResourceStep(fwkCtx, reconciler)).
WithStep(ctrlfwk.NewResolveDynamicDependenciesStep(fwkCtx, reconciler)).
WithStep(ctrlfwk.NewReconcileResourcesStep(fwkCtx, reconciler)).
WithFinalStep(ctrlfwk.NewReadyConditionFinalStep(fwkCtx, reconciler, ctrlfwk.SetReadyConditionFromResult(reconciler))).
Build()Without dynamic watches, the normal controller-runtime setup works:
func (reconciler *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&appsv1alpha1.WebApp{}).
Named("webapp").
Complete(reconciler)
}If you use WatchCache, build the controller instead of calling Complete, then set the controller on the cache:
func (reconciler *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
ctrler, err := ctrl.NewControllerManagedBy(mgr).
For(&appsv1alpha1.WebApp{}).
Named("webapp").
Build(reconciler)
if err != nil {
return err
}
reconciler.WatchCache.SetController(ctrler)
return nil
}- Context for typed reconciliation state.
- Dependencies for external objects.
- Resources for managed objects.
- Watcher Interface for dynamic watches.