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