Use controller-runtime by defining a custom resource (CRD) and implementing a Reconciler that watches for changes, fetches the desired state, and updates the cluster to match it. The library handles the complex control loop, event filtering, and leader election automatically, so you only need to write the business logic inside the Reconcile method.
Start by scaffolding a project using controller-gen and make targets to generate the boilerplate code for your CRD and manager. Below is a minimal example of a Reconciler that ensures a Deployment exists whenever a custom MyResource is created:
// myresource_controller.go
func (r *MyResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 1. Fetch the custom resource
var myRes myv1.MyResource
if err := r.Get(ctx, req.NamespacedName, &myRes); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. Define the desired state (e.g., a Deployment)
desiredDeployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: myRes.Name,
Namespace: myRes.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: myRes.Spec.Replicas,
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "app",
Image: myRes.Spec.Image,
}},
},
},
},
}
// 3. Compare current state with desired state and apply changes
// This uses the client to create or update the Deployment
if err := controllerutil.SetControllerReference(&myRes, desiredDeployment, r.Scheme); err != nil {
return ctrl.Result{}, err
}
if err := r.Create(ctx, desiredDeployment); err != nil {
if !apierrors.IsAlreadyExists(err) {
return ctrl.Result{}, err
}
} else {
// If it exists, you would typically Update here if specs differ
// For brevity, we assume Create handles the initial case
}
return ctrl.Result{}, nil
}
To run this, you typically set up a main.go that initializes the manager and registers your controller:
// main.go
func main() {
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
})
if err != nil {
os.Exit(1)
}
if err = (&myv1.MyResourceReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}).
SetupWithManager(mgr); err != nil {
os.Exit(1)
}
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
os.Exit(1)
}
}
Finally, generate the CRD manifests and apply them to your cluster using make manifests and kubectl apply -f config/crd/bases. The controller-runtime framework will automatically handle the watch events, retry logic on failure, and leader election if you deploy multiple replicas of your operator.