Configuration

App CRD spec — source types, destination, sync policy, and status fields

Configuration

The App custom resource (cd.wardn.space/v1alpha1, resource apps, short name app) is the core abstraction in Wardn. It defines what to deploy (a pluggable source), how to deploy it (syncPolicy, freeze), and where (destination).

Renamed CRD. The previous GitApplication kind was replaced by App — same API group, richer spec. All examples below use the current schema.

Full Example — Git source

apiVersion: cd.wardn.space/v1alpha1
kind: App
metadata:
  name: my-app
  namespace: org-my-org        # the org's management-cluster namespace
spec:
  source:
    type: Git                  # Git | Image | HelmChart | Catalog
    git:
      repoURL: https://github.com/myorg/my-app
      branch: main
      path: manifests
      manifestType: Auto       # Auto | Raw | Helm | Kustomize

  destination:
    namespace: default         # required — where manifests are applied
    cluster: staging-cluster   # optional — empty = management cluster

  syncPolicy:
    automated: true
    prune: true
    # selfHeal: true           # 📋 declared, not wired in the reconciler
    # requiresApproval: true

  freeze: false                # hold all syncs; health still reconciles

  notifications:
    slack:
      webhookURL: https://hooks.slack.com/services/T.../B.../xxx
    onSyncSuccess: true
    onSyncFailure: true
    # onHealthDegraded: true   # 📋 declared, not wired in the dispatcher

  webhook:
    enabled: true
    # secretRef: webhook-secret  # 📋 reserved; signature validation not wired

Source

spec.source selects where manifests come from. A CEL rule enforces that exactly one block matching type is set.

typeBlockStatus
Gitsource.git✅ Implemented
Imagesource.image✅ Implemented
HelmChartsource.helmChart📋 Schema reserved — no resolver yet
Catalogsource.catalog📋 Schema reserved — no resolver yet

source.git

FieldTypeRequiredDefaultDescription
repoURLstringYes—Git repository URL (clone runs the git binary — public repos only today)
branchstringNomainGit branch to track
pathstringNo.Directory containing manifests
manifestTypeenumNoAutoAuto detects kustomization.yaml → Kustomize, Chart.yaml → Helm, else Raw
helmobjectNo—Helm config for manifestType: Helm (see below)
kustomizeobjectNo—Kustomize overrides layered onto kustomization.yaml
substitutionmapNo—Flux-style ${VAR} map applied post-render

source.git.helm

FieldTypeDescription
valuesFilestringValues file path relative to path
releaseNamestringRelease name (defaults to app name)
namespacestringRelease namespace
inlineValuesstringRaw YAML applied on top of valuesFile

📋 Helm rendering is not implemented. manifestType: Helm (or a detected Chart.yaml) currently fails the reconcile. The helm config block exists for the future implementation.

source.git.kustomize

FieldTypeDescription
images[]stringImage overrides (newName=newTag entries)
namePrefixstringPrepended to all rendered resource names
components[]stringKustomize component paths to include

source.image

No-YAML deploys — the operator renders a Deployment + Service (+ optional Ingress/HPA) from a container image.

FieldTypeRequiredDefaultDescription
registryRefstringNo—Name of a registered wardn registry credential
repositorystringYes—Image repository (e.g. ghcr.io/acme/api); with registryRef, relative to the registry host
tagstringYes—Image tag
tagPolicyenumNoPinnedPinned resolves the tag once per sync; Follow redeploys when the digest moves (server tag poller)
pullSecretRefstringNo—Target-cluster imagePullSecrets secret name
workloadobjectNo—Rendered Deployment config (below)
ingressobjectNo—enabled, host, tlsSecretRef
autoscalingobjectNo—enabled, min, max, cpuPercent (HPA)

source.image.workload

FieldTypeDefaultDescription
portint3280Container port
replicasint321Replica count
env[]EnvVar—{name, value} literal, {name, secretRef:{name,key}} from an existing Secret, or {name, store:{id,path,key}} from a wardn secret store
resourcesobject100m/128Mi requestsrequests/limits maps (cpu/memory keys)
probeobjectTCP probe on portdisabled, path (switches to httpGet), initialDelaySeconds, periodSeconds, failureThreshold

source.helmChart — 📋 planned

Schema exists (repoURL, chart, version, values) but no resolver is registered — a HelmChart app fails with an unsupported-source error.

source.catalog — 📋 planned

Schema exists (template, params) but is not implemented.

Destination

FieldTypeRequiredDescription
namespacestringYesTarget namespace on the cluster
clusterstringNoRegistered cluster name; empty = the management cluster the operator runs on

Remote clusters are resolved at reconcile time — the operator fetches the kubeconfig from the server (/api/v1/internal/clusters/:name/kubeconfig), so no credential secrets sit on the management cluster.

Sync Policy

FieldTypeDefaultStatus
automatedboolfalse✅ Sync on every reconcile when upstream changed (periodic resync, no manual trigger needed)
pruneboolfalse✅ Delete resources that left the tracked set
selfHealboolfalse📋 Planned, not wired — the reconciler never reads it. Automated sync re-applies upstream diffs; out-of-band kubectl edits to unchanged objects are not reverted today
requiresApprovalboolfalse✅ Stages syncs as Held; a human releases via POST /apps/{ns}/{name}/approve

Prune deletes cluster resources that exist under the app's managed label but are no longer in the rendered set. Scope the app to a dedicated namespace before enabling.

Freeze

spec.freeze: true blocks all syncs — manual and automated — while health still reconciles. Settable via POST /apps/{ns}/{name}/freeze|unfreeze; audited as app.freeze / app.unfreeze.

Notifications

Slack only today — one POST per event, no retries or dedup.

FieldTypeStatus
slack.webhookURLstring✅ Incoming webhook URL
onSyncSuccessbool✅
onSyncFailurebool✅
onHealthDegradedbool📋 Planned, not wired — the dispatcher never reads it

Teams/Email/Discord channels and message templates are planned — see the Roadmap.

Webhook

FieldTypeStatus
enabledbool✅ Required — pushes only match apps with webhook.enabled: true
secretRefstring📋 Planned — HMAC signature validation isn't wired (any POST can trigger a match today)

The operator's receiver listens on :8082 — POST /webhook/github ✅ works; POST /webhook/gitlab 📋 accepts requests but doesn't parse payloads yet.

Operation — the manual-action contract

spec.operation is written by the wardn API (never by the operator) to request an async action. The operator consumes it and records the outcome in status.operationState.

FieldTypeDescription
kindenumSync, Deploy, Rollback, Restart, Approve
initiatedAttimeRequest timestamp — a new value means a new request
idstringCorrelation UUID the server stamps; echoed in operationState
revisionstringTarget revision for Rollback / Deploy
triggeredBystringRequesting identity (user id, webhook:github)

Sync asserts desired state — a Pinned tag stays on its recorded digest. Deploy re-resolves the tag live, so the same tag pulls a moved digest. Restart rolls pods via a template annotation bump.

Status Fields

status:
  syncStatus: Synced          # Synced | OutOfSync | Syncing | Unknown
  healthStatus: Healthy       # Healthy | Degraded | Progressing | Missing | Unknown
  healthReason: ""            # e.g. "Deployment api: 2/5 ready"
  lastSyncAt: "2024-01-15T10:30:00Z"
  revision: abc1234           # last revision APPLIED — the rollback marker
  observedRevision: def5678   # newest revision seen upstream
  previousRevisions:
    - def5678
    - ghi9012
  objectHash: sha256:…        # hash of the rendered set at last apply
  url: https://app.example.com
  operationState:
    kind: Sync
    initiatedAt: "…"
    id: "uuid"
    phase: Succeeded          # Succeeded | Failed | Held
    revision: abc1234
    finishedAt: "…"
  resources:
    - apiVersion: apps/v1
      kind: Deployment
      name: my-app
      namespace: default
      syncStatus: Synced
      healthStatus: Healthy
      message: ""
  conditions:
    - type: Ready
      status: "True"

revision vs observedRevision: revision only moves on successful apply; observedRevision tracks branch HEAD. An OutOfSync app shows a newer observed sha than applied sha — and after a rollback, observed stays ahead until the next sync.

Status FieldDescription
syncStatusWhether applied state matches the rendered source
healthStatus / healthReasonAggregate resource health + the first failing reason
lastSyncAtTimestamp of last successful apply
revision / observedRevisionApplied sha vs newest upstream sha
previousRevisionsRecent applied revisions (rollback history)
objectHashsha256 of the last rendered set — lets spec edits trigger resync on the same upstream sha
resourcesPer-resource apiVersion/kind/name/namespace/syncStatus/healthStatus/message
urlPublic address when an image-source ingress is enabled
operationStateOutcome of the last consumed spec.operation
conditionsKubernetes-style conditions (Ready, SyncError)

Known Limitations

AreaStatusDetail
selfHeal📋 PlannedDeclared in schema; the reconciler never reads it
onHealthDegraded📋 PlannedDeclared in schema; the dispatcher never reads it
manifestType: Helm📋 PlannedFails the reconcile — renderer not wired
HelmChart / Catalog sources📋 PlannedSchema reserved; no resolvers
Git credentials📋 PlannedClone uses the git binary — public repos only
Webhook secretRef📋 PlannedGitHub push payloads are not signature-validated
syncWaves / hooks / ignoreDifferences📋 PlannedNot in the schema — see the Roadmap

On this page