Sync Policies
How Wardn decides when and how to sync applications
Sync Policies
Sync policies control when Wardn applies source changes to your cluster and what happens to resources that no longer exist in the source.
Sync States
Every App reports a syncStatus in its status:
| Status | Meaning |
|---|---|
Synced | Cluster state matches the rendered source |
OutOfSync | Source changed — sync needed or pending approval |
Syncing | Reserved; the operator does not currently set this (a pending op stays OutOfSync until it lands) |
Unknown | Initial state before the first sync completes |
Check status:
kubectl get app my-app -n org-my-org -o jsonpath='{.status.syncStatus}'Automated Sync
spec:
syncPolicy:
automated: trueWhen automated: true, on each reconcile Wardn:
- Resolves the source (git pull / image digest resolve)
- Compares the resolved revision and rendered object hash with the last applied values (
status.revision/status.objectHash) - If different — renders manifests and applies them
- Updates status to
Synced
Without automated, syncs only happen when a Sync/Deploy operation is written via the API — see Manual Sync below.
Prune
spec:
syncPolicy:
automated: true
prune: trueWhen prune: true, Wardn deletes Kubernetes resources that:
- Carry the app's managed-by label (
cd.wardn.space/app) in the destination namespace, and - Are no longer in the current rendered set
Prune is destructive — it deletes any resource in the destination namespace stamped with your app's label that left the rendered set. Scope apps to dedicated namespaces, and test in staging before production.
Self-Heal — declared, not wired
spec:
syncPolicy:
selfHeal: trueselfHeal exists in the CRD schema but the reconciler never reads it. Setting it is a no-op today.
What actually happens today:
- With
automated: true, Wardn re-applies the rendered set whenever the source changes (new revision or different object hash). Out-of-bandkubectledits to objects whose rendered form didn't change are not reverted — nothing in the loop diffs live state. - The
spec.operationandobjectHashplumbing instatusexists so this can be wired later without a schema change.
Freeze
spec:
freeze: truespec.freeze blocks all syncs — automated resyncs and manual operations alike — while health keeps reconciling. This is how you hold a deployment during an incident without deleting the app. Toggle it via POST /api/v1/apps/{ns}/{name}/freeze|unfreeze or by patching the CR.
Approval Gate
spec:
syncPolicy:
requiresApproval: trueWhen set, automated moves (periodic resyncs, image tag-poller redeploys) are staged but held: status.operationState.phase becomes Held with the staged revision. A human releases it with:
curl -X POST http://localhost:8080/api/v1/apps/default/my-app/approve -b cookies.txtThe op carries the staged revision, so the approver ships exactly what they reviewed — not whatever moved upstream while it sat held. Manual Sync/Deploy ops still execute immediately.
Manual Sync
Trigger a sync on demand (works regardless of automated):
# API — writes spec.operation {kind: Sync}
curl -X POST http://localhost:8080/api/v1/apps/default/my-app/sync -b cookies.txt
# Image-source deploy — re-resolves the tag live
curl -X POST http://localhost:8080/api/v1/apps/default/my-app/deploy \
-b cookies.txt -H "Content-Type: application/json" -d '{"tag":"v1.2.4"}'The server merge-patches spec.operation and the generation bump fires a reconcile immediately. A pending op gets a 409 instead of a silent clobber.
Sync Triggers
| Trigger | How |
|---|---|
| Periodic resync | The reconciler requeues every ~60s (30s on error paths) and re-resolves the source — this is how automated picks up upstream changes |
| Manual trigger | POST /sync, /deploy, /rollback, /restart, /approve — writes spec.operation |
| Git push webhook | GitHub push → operator patches a Sync op on matching apps — near-instant. See Webhook Triggers |
| Image tag poller | The server re-resolves tagPolicy: Follow tags every WARDN_TAG_POLL_INTERVAL (default 60s) and writes a Deploy op when the digest moves |
Sync History
Applied revisions accumulate in status.previousRevisions — the rollback ledger:
status:
revision: abc1234 # last APPLIED
observedRevision: def5678 # newest seen upstream
previousRevisions:
- def5678
- ghi9012See Rollback.
Sync Failures
When a sync fails:
syncStatusstaysOutOfSync(orUnknownon first sync)conditionsgains aSyncErrorentry;status.operationState.phasebecomesFailedwith the detail inmessage- Notifications fire if
onSyncFailureis enabled - The reconciler requeues after ~30s
Common failures:
- Invalid manifests — YAML parse errors in the source path
- Unsupported source —
HelmChart/Catalogtypes ormanifestType: Helmfail (not implemented) - Clone failures — unreachable repo (no credential support yet; public repos only)
- Namespace scope — destination namespace outside the cluster's registered allowlist
- RBAC denied — the operator's service account lacks permissions on the target cluster
Recommended Configurations
Development
syncPolicy:
automated: true
prune: trueFull automation — the namespace tracks the branch.
Staging
syncPolicy:
automated: true
prune: trueAuto-sync on push; debug manually — nothing reverts your kubectl experiments on unchanged objects (there's no working self-heal yet).
Production
syncPolicy:
requiresApproval: trueEvery change stages as Held until a human approves — or omit syncPolicy entirely for purely manual syncs.
Next Steps
- Health Checks — how Wardn determines if a sync was successful
- Rollback — revert to a previous revision
- Webhook Triggers — instant syncs on Git push