Operator

Architecture

How the Wardn operator works — components, dependencies, and design

Operator Architecture

The operator is a single controller built with controller-runtime — it watches App resources (cd.wardn.space/v1alpha1) and reconciles cluster state toward the rendered source.

Component Topology

┌─────────────────────────────────────────────────────────────┐
│                     Operator process                         │
│                                                              │
│  ┌──────────┐  ┌───────────┐  ┌──────────┐  ┌────────────┐ │
│  │  git     │─▶│  render   │─▶│  apply   │─▶│   health   │ │
│  │  client  │  │  raw /    │  │  SSA +   │  │  checker   │ │
│  │ (binary) │  │ kustomize │  │  prune   │  │            │ │
│  └──────────┘  └───────────┘  └──────────┘  └────────────┘ │
│  ┌──────────┐  ┌───────────┐  ┌──────────┐  ┌────────────┐ │
│  │  image   │  │  notify   │  │ webhook  │  │  cluster   │ │
│  │ resolver │  │ dispatcher│  │ receiver │  │  resolver  │ │
│  │(crane)   │  │ (slack)   │  │  (:8082) │  │(/internal) │ │
│  └──────────┘  └───────────┘  └──────────┘  └────────────┘ │
└─────────────────────────────────────────────────────────────┘
      │              │              ▲                 ▲
      ▼              ▼              │                 │
 destination    Slack webhook   GitHub push    Wardn server
 clusters                                          API

Source Resolution

source.typeResolverStatus
Gitinternal/git — shells out to the git binary (depth-1 clone + fetch to sha)✅
Imageinternal/registry — go-containerregistry digest/HEAD calls; renders Deployment+Service(+Ingress/HPA)✅
HelmChartNone registered — fails the reconcile📋
CatalogNone registered📋

Git-rendered manifests go through internal/render: Raw walks .yaml/.yml/.json, Kustomize runs a kustomization.yaml build, Auto detects, Helm 📋 fails by design.

Reconciler

Single AppReconciler registered in cmd/operator/main.go — the constructor wires its dependencies:

reconciler := &controller.AppReconciler{
    Client:      mgr.GetClient(),
    Scheme:      mgr.GetScheme(),
    Git:         git.NewClient(),
    Registry:    registry.NewClient(),
    Renderer:    render.NewRenderer(),
    Applier:     apply.NewApplier(),
    Health:      health.NewChecker(),
    Notify:      notify.NewDispatcher(),
    Clusters:    cluster.NewResolver(clusterClient),
    Recorder:    mgr.GetEventRecorderFor("wardn-operator"),
}

No separate reconcilers — App is the only CRD.

Destinations

destination.cluster resolution is centralized in internal/cluster: an empty cluster means the management cluster (in-cluster config); a name resolves via GET /internal/clusters/{name}/kubeconfig on the server, returning a client for the remote. Connection params are cached for the process lifetime.

Webhook Receiver

A small http.Server on --webhook-bind-address (:8082) handles Git pushes — POST /webhook/github patches spec.operation {kind:Sync, triggeredBy:"webhook:github"} on matching Apps; POST /webhook/gitlab 📋 accepts but doesn't act. See Webhooks.

Notifications

internal/notify.Dispatcher sends Slack-only webhooks on sync success/failure — onHealthDegraded is declared but unwired. See Notifications.

Multi-Cluster

The operator talks to one management cluster for the App CRs but fans out to many destination clusters — each reconcile resolves destination.cluster to a remote client fetched from the server API. No remote credentials live in the management cluster.

Operational Defaults

SettingDefault
Resync interval60s (--sync-period; 30s on error paths)
Metrics:8080 (--metrics-bind-address)
Probes:8081 (--health-probe-bind-address)
Webhooks:8082 (--webhook-bind-address)
Leader electionenabled (--leader-elect)
Field managerwardn-operator (forced SSA)
Managed-by labelcd.wardn.space/app

RBAC Requirements

The operator needs broad write access on destination clusters (it applies arbitrary manifests):

rules:
  - apiGroups: ["cd.wardn.space"]
    resources: ["apps"]
    verbs: ["get", "list", "watch", "update", "patch"]
  - apiGroups: ["cd.wardn.space"]
    resources: ["apps/status"]
    verbs: ["get", "update", "patch"]
  - apiGroups: ["*"]
    resources: ["*"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

Plus network access to the Wardn server (/internal/clusters/{name}/kubeconfig) — that's the only outbound dependency.

Design Principles

  1. Idempotent — reconciliation can run any number of times safely
  2. Level-triggered — sync decisions derive from observed state, not events
  3. Operation contract — spec.operation is the single async-action channel; status.operationState reports outcomes
  4. Observable — syncStatus/healthStatus/operationState/conditions record every decision

On this page