Core Concepts

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:

StatusMeaning
SyncedCluster state matches the rendered source
OutOfSyncSource changed — sync needed or pending approval
SyncingReserved; the operator does not currently set this (a pending op stays OutOfSync until it lands)
UnknownInitial 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: true

When automated: true, on each reconcile Wardn:

  1. Resolves the source (git pull / image digest resolve)
  2. Compares the resolved revision and rendered object hash with the last applied values (status.revision / status.objectHash)
  3. If different — renders manifests and applies them
  4. 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: true

When prune: true, Wardn deletes Kubernetes resources that:

  1. Carry the app's managed-by label (cd.wardn.space/app) in the destination namespace, and
  2. 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: true

selfHeal 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-band kubectl edits to objects whose rendered form didn't change are not reverted — nothing in the loop diffs live state.
  • The spec.operation and objectHash plumbing in status exists so this can be wired later without a schema change.

Freeze

spec:
  freeze: true

spec.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: true

When 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.txt

The 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

TriggerHow
Periodic resyncThe reconciler requeues every ~60s (30s on error paths) and re-resolves the source — this is how automated picks up upstream changes
Manual triggerPOST /sync, /deploy, /rollback, /restart, /approve — writes spec.operation
Git push webhookGitHub push → operator patches a Sync op on matching apps — near-instant. See Webhook Triggers
Image tag pollerThe 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
    - ghi9012

See Rollback.

Sync Failures

When a sync fails:

  1. syncStatus stays OutOfSync (or Unknown on first sync)
  2. conditions gains a SyncError entry; status.operationState.phase becomes Failed with the detail in message
  3. Notifications fire if onSyncFailure is enabled
  4. The reconciler requeues after ~30s

Common failures:

  • Invalid manifests — YAML parse errors in the source path
  • Unsupported source — HelmChart/Catalog types or manifestType: Helm fail (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

Development

syncPolicy:
  automated: true
  prune: true

Full automation — the namespace tracks the branch.

Staging

syncPolicy:
  automated: true
  prune: true

Auto-sync on push; debug manually — nothing reverts your kubectl experiments on unchanged objects (there's no working self-heal yet).

Production

syncPolicy:
  requiresApproval: true

Every change stages as Held until a human approves — or omit syncPolicy entirely for purely manual syncs.

Next Steps

On this page