Skip to content

Getting Started

Yewolf edited this page Nov 13, 2025 · 2 revisions

Getting Started with Controller Framework

This guide will walk you through creating your first Kubernetes operator using Kubebuilder and then enhancing it with the Controller Framework. We'll focus on the core concepts: the Stepper pattern and Context usage. By the end of this tutorial, you'll have a basic operator using the step-based reconciliation approach.

Prerequisites

Before we begin, make sure you have the following tools installed:

You can verify your installations:

go version
kubebuilder version
kubectl version --client
docker version
kind version  # or minikube version

Step 1: Create a New Kubebuilder Project

Let's create a new operator that will manage a custom resource called WebApp:

# Create a new directory for your project
mkdir webapp-operator
cd webapp-operator

# Initialize a new Kubebuilder project
kubebuilder init --domain example.com --repo github.com/your-username/webapp-operator

# Create a new API and controller
kubebuilder create api --group apps --version v1 --kind WebApp --resource --controller

# When prompted, answer 'y' to create both the resource and controller

This creates a basic project structure with:

  • A WebApp custom resource definition
  • A controller scaffolded to manage WebApp resources
  • Necessary boilerplate code and configuration files

Step 2: Define Your Custom Resource

Edit the api/v1/webapp_types.go file to define a simple WebApp resource:

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// WebAppSpec defines the desired state of WebApp
type WebAppSpec struct {
	// Image is the container image to deploy
	Image string `json:"image"`

	// Replicas is the number of desired replicas
	// +kubebuilder:default=1
	// +kubebuilder:validation:Minimum=1
	// +kubebuilder:validation:Maximum=10
	Replicas int32 `json:"replicas,omitempty"`

	// Port is the port the application listens on
	// +kubebuilder:default=8080
	Port int32 `json:"port,omitempty"`
}

// WebAppStatus defines the observed state of WebApp
type WebAppStatus struct {
	// Conditions represent the latest available observations
	Conditions []metav1.Condition `json:"conditions,omitempty"`

	// Ready indicates if the WebApp is ready
	Ready bool `json:"ready,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// WebApp is the Schema for the webapps API
type WebApp struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   WebAppSpec   `json:"spec,omitempty"`
	Status WebAppStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// WebAppList contains a list of WebApp
type WebAppList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []WebApp `json:"items"`
}

func init() {
	SchemeBuilder.Register(&WebApp{}, &WebAppList{})
}

Step 3: Generate CRD and Install Dependencies

Generate the CRD manifests and install the Controller Framework:

# Generate CRD manifests
make manifests

# Add the Controller Framework dependency
go get github.com/u-ctf/controller-fwk

Step 4: Transform the Controller with Controller Framework

Now we'll transform the basic Kubebuilder controller to use the Controller Framework. We'll focus on the core concepts:

  1. Context-based approach: Use a Context object for thread-safe access to your custom resource
  2. Step-based reconciliation: Use a Stepper to orchestrate the reconciliation process
  3. Simple status management: Update status using framework helpers

Important: We need to rename the Scheme field to RuntimeScheme to avoid conflicts with the client.Client interface, which already has a Scheme() method.

Edit internal/controller/webapp_controller.go:

package controller

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	ctrlfwk "github.com/u-ctf/controller-fwk"
	appsv1alpha1 "github.com/your-username/webapp-operator/api/v1"
)

// WebAppReconciler reconciles a WebApp object
type WebAppReconciler struct {
	client.Client
	RuntimeScheme *runtime.Scheme
}

// Ensure WebAppReconciler implements the required interfaces
var _ ctrlfwk.Reconciler[*appsv1alpha1.WebApp] = &WebAppReconciler{}

// For method required by ctrlfwk.Reconciler interface
func (WebAppReconciler) For(*appsv1alpha1.WebApp) {}

//+kubebuilder:rbac:groups=apps.example.com,resources=webapps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=apps.example.com,resources=webapps/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=apps.example.com,resources=webapps/finalizers,verbs=update

// Reconcile is the main reconciliation loop
func (reconciler *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := logf.FromContext(ctx)

	// Create stepper with basic steps
	stepper := ctrlfwk.NewStepperFor[*appsv1alpha1.WebApp](logger).
		WithStep(ctrlfwk.NewFindControllerCustomResourceStep(reconciler)).
		WithStep(reconciler.newValidationStep()).
		WithStep(ctrlfwk.NewEndStep(reconciler, ctrlfwk.SetReadyCondition(reconciler))).
		Build()

	// Create framework context and execute
	fwkCtx := ctrlfwk.NewContext[*appsv1alpha1.WebApp](ctx)
	return stepper.Execute(fwkCtx, req)
}

// Custom validation step to show how to create your own steps
func (reconciler *WebAppReconciler) newValidationStep() ctrlfwk.Step[*appsv1alpha1.WebApp] {
	return ctrlfwk.Step[*appsv1alpha1.WebApp]{
		Name: "validate-webapp",
		Step: func(ctx ctrlfwk.Context[*appsv1alpha1.WebApp], logger logr.Logger, req ctrl.Request) ctrlfwk.StepResult {
			webapp := ctx.GetCustomResource()

			// Simple validation: ensure image is specified
			if webapp.Spec.Image == "" {
				logger.Info("WebApp image not specified, marking as not ready")
				webapp.Status.Ready = false
				if err := ctrlfwk.PatchCustomResourceStatus(ctx, reconciler); err != nil {
					logger.Error(err, "failed to update WebApp status")
                    return ctrlfwk.ResultInError(err)
				}
				// Return early - don't continue reconciliation if validation fails
				return ctrlfwk.ResultEarlyReturn()
			}

			// Log current state
			logger.Info("Validating WebApp", 
				"image", webapp.Spec.Image, 
				"replicas", webapp.Spec.Replicas,
				"port", webapp.Spec.Port)

            webapp.Status.Ready = true
            if err := ctrlfwk.PatchCustomResourceStatus(ctx, reconciler); err != nil {
                logger.Error(err, "failed to update WebApp status")
                return ctrlfwk.ResultInError(err)
            }

			// Validation passed, continue to next steps (the Ready condition will be set by EndStep)
			return ctrlfwk.ResultSuccess()
		},
	}
}

Framework Mental Model: The framework follows the principle that if reconciliation reaches the end (all steps complete successfully), then the resource is Ready. If validation fails or other issues occur, we:

  1. Explicitly mark the resource as NOT ready using ctrlfwk.PatchStatus()
  2. Return ctrlfwk.ResultEarlyReturn() to stop reconciliation early

This creates a clear contract: reaching the EndStep means success and readiness, while early returns indicate problems that prevent readiness.

// SetupWithManager sets up the controller with the Manager
func (reconciler *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&appsv1alpha1.WebApp{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
		Named("webapp").
		Complete(reconciler)
}

Don't forget to add the missing import for the logger:

import (
	"context"

	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	ctrlfwk "github.com/u-ctf/controller-fwk"
	appsv1alpha1 "github.com/your-username/webapp-operator/api/v1"
)

Step 5: Update Main Function

}


Don't forget to update the test files to use `RuntimeScheme` instead of `Scheme` as well. You can find these in `internal/controller/webapp_controller_test.go` if they were generated.

Update `cmd/main.go` to properly initialize the controller with the framework:

```go
// Find the existing controller setup code and replace it with:

if err := (&controller.WebAppReconciler{
	Client:        mgr.GetClient(),
	RuntimeScheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
	setupLog.Error(err, "unable to create controller", "controller", "WebApp")
	os.Exit(1)
}

Step 6: Build and Test Your Operator

Now let's build and test the operator:

# Generate manifests and build
make manifests generate fmt vet build

# Create a local Kubernetes cluster (using kind)
kind create cluster --name webapp-operator

# Install CRDs
make install

# Run the operator locally (in another terminal)
make run

Step 7: Create a Test WebApp Resource

Create a sample WebApp resource to test your operator. Create config/samples/apps_v1_webapp.yaml:

apiVersion: apps.example.com/v1
kind: WebApp
metadata:
  labels:
    app.kubernetes.io/name: webapp
    app.kubernetes.io/instance: webapp-sample
    app.kubernetes.io/part-of: webapp-operator
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: webapp-operator
  name: webapp-sample
  namespace: default
spec:
  image: nginx:1.21
  replicas: 2
  port: 80

Apply it to your cluster:

kubectl apply -f config/samples/apps_v1_webapp.yaml

Step 8: Verify Your Operator Works

Check that your operator is working:

# Check the WebApp resource
kubectl get webapp

# Check the WebApp status and conditions
kubectl get webapp webapp-sample -o yaml

# Look at the operator logs
# In your terminal running 'make run', you should see structured logs showing:
# - Step execution (find-controller-resource, validate-webapp, end)
# - Validation messages
# - Status updates

You should see output showing:

  • The WebApp resource with ready conditions
  • Structured logging from your custom validation step
  • Status updates managed by the framework

What You've Accomplished

Congratulations! You've successfully:

  1. Created a Kubebuilder project from scratch
  2. Defined a simple custom resource with proper validation
  3. Integrated Controller Framework with minimal code changes
  4. Implemented step-based reconciliation using the Stepper pattern
  5. Used Context for thread-safe resource access
  6. Created a custom validation step to understand the framework's flexibility
  7. Added automatic status management with built-in condition handling

Key Concepts You've Learned

Your operator now uses:

  • Context Pattern: Thread-safe access to your custom resource via ctx.GetCustomResource()
  • Stepper Pattern: Structured reconciliation with discrete, testable steps
  • Built-in Steps: Framework-provided steps like FindControllerCustomResourceStep and EndStep
  • Custom Steps: Your own business logic encapsulated in reusable steps
  • Automatic Status Management: Framework handles condition tracking with SetReadyCondition

Next Steps

Now that you understand the core Framework concepts:

  1. Add Resource Management: Learn how to manage Kubernetes resources in Resource Management
  2. Handle Dependencies: Learn about external dependencies in Dependencies
  3. Create Advanced Steps: Explore Custom Steps for complex business logic
  4. Optimize Performance: Learn about Watch Cache for advanced watching patterns
  5. Implement Testing: Set up tests using the Testing Guide

Understanding the Framework Flow

Here's what happens when you apply a WebApp resource:

  1. FindControllerCustomResourceStep: Loads your WebApp from the cluster and stores it in the Context
  2. Custom Validation Step: Your business logic validates the WebApp spec
  3. EndStep: Updates the WebApp status with ready conditions

The Context provides thread-safe access to your custom resource throughout all steps, and the Stepper ensures each step is executed in order with proper error handling and logging.

Troubleshooting

If you encounter issues:

  1. Check the operator logs - The framework provides detailed step-by-step logging
  2. Verify your imports - Make sure you've added the github.com/go-logr/logr import
  3. Ensure CRDs are installed with kubectl get crd
  4. Check the WebApp status with kubectl get webapp webapp-sample -o yaml

For more help, visit our Troubleshooting guide or ask questions in GitHub Discussions.