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 APISource Resolution
source.type | Resolver | Status |
|---|---|---|
Git | internal/git — shells out to the git binary (depth-1 clone + fetch to sha) | ✅ |
Image | internal/registry — go-containerregistry digest/HEAD calls; renders Deployment+Service(+Ingress/HPA) | ✅ |
HelmChart | None registered — fails the reconcile | 📋 |
Catalog | None 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
| Setting | Default |
|---|---|
| Resync interval | 60s (--sync-period; 30s on error paths) |
| Metrics | :8080 (--metrics-bind-address) |
| Probes | :8081 (--health-probe-bind-address) |
| Webhooks | :8082 (--webhook-bind-address) |
| Leader election | enabled (--leader-elect) |
| Field manager | wardn-operator (forced SSA) |
| Managed-by label | cd.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
- Idempotent — reconciliation can run any number of times safely
- Level-triggered — sync decisions derive from observed state, not events
- Operation contract —
spec.operationis the single async-action channel;status.operationStatereports outcomes - Observable —
syncStatus/healthStatus/operationState/conditionsrecord every decision
Related
- Reconciliation Loop — step-by-step flow
- CRD Reference —
Apptypes - Webhooks — Git provider integration
- Notifications — Slack dispatcher