WebApp Operator

A Kubernetes operator that watches WebApp custom resources and manages Deployments automatically. When a user creates, updates, or deletes a WebApp CR, the reconciler ensures the underlying Deployment matches the desired state.

package main

import (
	"context"
	"fmt"
	"os"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

// --- CRD Types ---

type WebApp struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`
	Spec              WebAppSpec   `json:"spec,omitempty"`
	Status            WebAppStatus `json:"status,omitempty"`
}

type WebAppSpec struct {
	Image    string `json:"image"`
	Replicas int32  `json:"replicas"`
	Port     int32  `json:"port"`
}

type WebAppStatus struct {
	ReadyReplicas int32              `json:"readyReplicas"`
	Conditions    []metav1.Condition `json:"conditions,omitempty"`
}

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

func (w *WebApp) DeepCopyObject() runtime.Object  { cp := *w; return &cp }
func (w *WebAppList) DeepCopyObject() runtime.Object { cp := *w; return &cp }

var SchemeBuilder = runtime.NewSchemeBuilder(func(s *runtime.Scheme) error {
	s.AddKnownTypes(
		schema.GroupVersion{Group: "apps.example.com", Version: "v1"},
		&WebApp{},
		&WebAppList{},
	)
	return nil
})

// --- Reconciler ---

type WebAppReconciler struct {
	client.Client
	Recorder record.EventRecorder
	Scheme   *runtime.Scheme
}

func (r *WebAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)

	// 1. Fetch the WebApp CR
	var webapp WebApp
	if err := r.Get(ctx, req.NamespacedName, &webapp); err != nil {
		if errors.IsNotFound(err) {
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, err
	}

	// 2. Build the desired Deployment
	desired := r.desiredDeployment(&webapp)

	// 3. Check if Deployment exists
	var existing appsv1.Deployment
	err := r.Get(ctx, types.NamespacedName{
		Name:      desired.Name,
		Namespace: desired.Namespace,
	}, &existing)

	if errors.IsNotFound(err) {
		logger.Info("Creating Deployment", "name", desired.Name)
		if err := r.Create(ctx, desired); err != nil {
			return ctrl.Result{}, err
		}
		r.Recorder.Event(&webapp, corev1.EventTypeNormal, "Created",
			fmt.Sprintf("Created Deployment %s", desired.Name))
	} else if err != nil {
		return ctrl.Result{}, err
	} else {
		if *existing.Spec.Replicas != webapp.Spec.Replicas ||
			existing.Spec.Template.Spec.Containers[0].Image != webapp.Spec.Image {
			existing.Spec.Replicas = &webapp.Spec.Replicas
			existing.Spec.Template.Spec.Containers[0].Image = webapp.Spec.Image
			logger.Info("Updating Deployment", "name", existing.Name)
			if err := r.Update(ctx, &existing); err != nil {
				return ctrl.Result{}, err
			}
			r.Recorder.Event(&webapp, corev1.EventTypeNormal, "Updated",
				fmt.Sprintf("Updated Deployment %s to %d replicas", existing.Name, webapp.Spec.Replicas))
		}
	}

	// 4. Update status
	webapp.Status.ReadyReplicas = existing.Status.ReadyReplicas
	if err := r.Status().Update(ctx, &webapp); err != nil {
		logger.Error(err, "updating status")
	}

	return ctrl.Result{}, nil
}

func (r *WebAppReconciler) desiredDeployment(webapp *WebApp) *appsv1.Deployment {
	labels := map[string]string{
		"app":        webapp.Name,
		"managed-by": "webapp-operator",
	}

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("%s-deployment", webapp.Name),
			Namespace: webapp.Namespace,
			Labels:    labels,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &webapp.Spec.Replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: labels,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{Labels: labels},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "app",
							Image: webapp.Spec.Image,
							Ports: []corev1.ContainerPort{
								{ContainerPort: webapp.Spec.Port},
							},
						},
					},
				},
			},
		},
	}

	ctrl.SetControllerReference(webapp, dep, r.Scheme)
	return dep
}

func (r *WebAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&WebApp{}).
		Owns(&appsv1.Deployment{}).
		Complete(r)
}

// --- Main Entrypoint ---

func main() {
	ctrl.SetLogger(zap.New())
	logger := ctrl.Log.WithName("setup")

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		LeaderElection:   true,
		LeaderElectionID: "webapp-operator-lock",
	})
	if err != nil {
		logger.Error(err, "unable to create manager")
		os.Exit(1)
	}

	SchemeBuilder.AddToScheme(mgr.GetScheme())

	if err := (&WebAppReconciler{
		Client:   mgr.GetClient(),
		Recorder: mgr.GetEventRecorderFor("webapp-operator"),
		Scheme:   mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		logger.Error(err, "unable to create controller")
		os.Exit(1)
	}

	logger.Info("starting manager")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		logger.Error(err, "manager exited with error")
		os.Exit(1)
	}
}

// Example WebApp resource (apply with kubectl):
//
// apiVersion: apps.example.com/v1
// kind: WebApp
// metadata:
//   name: my-service
// spec:
//   image: myregistry/myapp:v2.1.0
//   replicas: 5
//   port: 8080

💻 Run locally

Copy the code above and run it on your machine

© 2026 ByteLearn.dev. Free courses for developers. · Privacy