Skip to main content

Getting Started

This guide shows the current integration pattern for github.com/u-ctf/controller-fwk.

Prerequisites​

  • Go 1.25+
  • controller-runtime based controller
  • A custom resource with a status field containing Conditions []metav1.Condition if you want to use the built-in Ready-condition helpers

Install the framework:

go get github.com/u-ctf/controller-fwk

1. Define the reconciler contract​

Your 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].

2. Create a framework context​

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{})

3. Build the stepper​

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.

4. Write custom steps​

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)

5. Patch status through the framework context​

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.

6. Add framework resource and dependency steps​

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()

7. Setup with manager​

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
}

Next steps​