API Reference

Applications

Create, inspect, sync, deploy, and roll back App resources via the REST API

Applications API

Manage App resources (cd.wardn.space/v1alpha1). The server reads and writes App CRs in the caller's org namespace (org-<slug>) on the management cluster — the App CR is the source of truth, no Postgres table.

Requires WARDN_MGMT_KUBECONFIG. With no management cluster configured, every /apps route returns 503 (management cluster not configured). It is unset in plain docker compose up — use docker-compose.k8s.yml for a working k3s backend.

All endpoints require a session cookie or API key plus org context. {namespace} in routes is the app's destination namespace; {name} is the app name. A name in another namespace reads as 404, not a leak.

Deployments: apps sharing the same {cluster, namespace} destination form one deployment — the grouping the RBAC deployment scope (scope_id: "cluster/namespace") keys off. There is no GET /deployments endpoint — the grouping is derived from spec.destination, not a stored resource. See Deployments.

The app object

{
  "name": "my-app",
  "namespace": "default",
  "cluster": "staging-cluster",
  "source": {
    "type": "Git",
    "git": { "repoURL": "https://github.com/myorg/my-app", "branch": "main", "path": "manifests" }
  },
  "sync_status": "Synced",
  "health": "Healthy",
  "health_reason": "",
  "revision": "a1b2c3d4e5f6",
  "url": "",
  "resources": [ /* per-resource statuses */ ],
  "conditions": [ /* Kubernetes conditions */ ],
  "updated_at": "2024-01-15T10:30:00Z"
}

namespace is the destination namespace and cluster the destination cluster — metadata.namespace (the org's management namespace) isn't exposed. source round-trips the CRD block as-is; clients branch on source.type.

Endpoints

List Applications

GET /api/v1/apps

Permission: deployment:read. Lists every App CR in the org's management namespace — no query filters.

{ "apps": [ { "name": "my-app", "namespace": "default", "sync_status": "Synced", "health": "Healthy", "source": {"type": "Git", "git": {"…"}}, … } ] }

Create Application

POST /api/v1/apps

Permission: deployment:manage.

{
  "name": "my-app",
  "namespace": "default",
  "cluster": "staging-cluster",
  "source": {
    "type": "Git",
    "git": { "repoURL": "https://github.com/myorg/my-app", "branch": "main", "path": "manifests" }
  },
  "sync_policy": { "automated": true, "prune": true }
}
FieldRequiredDescription
name✅DNS-1123 label
namespace✅Destination namespace (DNS-1123 label)
cluster—Registered cluster name; validated against the org's cluster list and its namespace allowlist
source✅source.type + matching block. Git needs git.repoURL; Image needs image.repository + image.tag
sync_policy—Passed through to spec.syncPolicy

201 Created → the app object. 409 on duplicate name. HelmChart/Catalog sources are rejected with 400 — 📋 planned.

Get Application

GET /api/v1/apps/{namespace}/{name}

Permission: deployment:read. 404 when the name or destination namespace doesn't match.

App Status

GET /api/v1/apps/{namespace}/{name}/status

Permission: deployment:read. The CI contract's read side — a compact live-status view:

{
  "name": "my-app",
  "sync_status": "Synced",
  "health": "Healthy",
  "health_reason": "",
  "revision": "a1b2c3d4e5f6",
  "observed": "f6e5d4c3b2a1",
  "last_sync_at": "…",
  "frozen": false,
  "operationState": { "kind": "Sync", "phase": "Succeeded", "…": "…" }
}

?wait=<dur> (e.g. ?wait=60s, max 120s) blocks until the app reaches a terminal state — Synced+Healthy, Degraded, or Unknown — then returns it.

App Events

GET /api/v1/apps/{namespace}/{name}/events

Permission: deployment:read. Kubernetes events merged from the App CR (management cluster) and the rendered workloads (destination cluster), newest first, capped at 50:

{ "events": [ { "at": "…", "type": "Warning", "object": "Deployment/my-app", "reason": "…", "message": "…", "source": "workload" } ] }

This is a plain JSON GET — not an SSE stream. There is no server-sent events endpoint today.

Sync History

GET /api/v1/apps/{namespace}/{name}/history

Permission: deployment:read. The revision ledger kept on the CR's status:

{
  "name": "my-app",
  "revision": "a1b2c3d4e5f6",
  "previous_revisions": ["f6e5d4c3b2a1", "1234567890ab"],
  "last_sync_at": "2024-01-15T10:30:00Z"
}

Trigger Sync

POST /api/v1/apps/{namespace}/{name}/sync

Permission: deployment:sync. Writes a spec.operation {kind: Sync} patch; the operator consumes it and records the outcome in status.operationState. Takes no request body — there are no revision/dryRun/force options.

{ "message": "sync triggered", "name": "my-app", "operation_id": "uuid" }

409 when another operation is pending.

Deploy

POST /api/v1/apps/{namespace}/{name}/deploy

Permission: deployment:sync. The CI entry point — pins a tag and rolls. Writes a Deploy op (re-resolves the tag live even when it didn't change); for image sources, also sets spec.source.image.tag.

{ "tag": "v1.2.4" }

→ { "message": "deploy triggered", "name": "my-app", "tag": "v1.2.4", "operation_id": "uuid" }

Rollback

POST /api/v1/apps/{namespace}/{name}/rollback

Permission: deployment:sync. The operator applies spec.operation.revision instead of branch HEAD — use a sha from /history. For image sources the spec tag is written back too, so the next resync doesn't undo the rollback.

{ "revision": "f6e5d4c3b2a1" }

→ { "message": "rollback requested", "revision": "…", "operation_id": "uuid" }

Promote

POST /api/v1/apps/{namespace}/{name}/promote?to={target-app}

Permission: deployment:sync. Copies the source app's applied digest onto the target as a Deploy op with the byte-equal pin — "what staging runs, prod gets". Works across registries; the digest is verified present on the target registry first (502 if absent).

Clone

POST /api/v1/apps/{namespace}/{name}/clone

Permission: deployment:manage. Copies source/syncPolicy/destination onto a new App CR — the clone starts unsynced. { "name": "my-app-copy", "namespace": "optional-override" }.

Restart

POST /api/v1/apps/{namespace}/{name}/restart

Permission: deployment:sync. Rolls the app's pods — a template annotation bump on every managed workload.

Approve

POST /api/v1/apps/{namespace}/{name}/approve

Permission: deployment:sync. Releases a sync held by syncPolicy.requiresApproval — carries the staged revision so the approver ships exactly what they reviewed.

Scale

POST /api/v1/apps/{namespace}/{name}/scale

Permission: deployment:manage. Image-source apps only — sets spec.source.image.workload.replicas plus a Sync op. { "replicas": 3 }. 409 while autoscaling is enabled (the HPA owns replicas).

Freeze / Unfreeze

POST /api/v1/apps/{namespace}/{name}/freeze
POST /api/v1/apps/{namespace}/{name}/unfreeze

Permission: deployment:manage. Sets/clears spec.freeze — all syncs hold while health keeps reconciling. → { "frozen": true }

Preview

POST /api/v1/apps/preview

Permission: deployment:manage. Validates a create payload and resolves the image digest without writing anything — the "what would this do" endpoint. ?app=name includes the current spec for a client-side diff.

{ "valid": true, "resolved": { "image": "ghcr.io/acme/api", "tag": "v1.2.4", "digest": "sha256:…", "ref": "ghcr.io/acme/api@sha256:…" }, "spec": { /* echoed request */ } }

Adopt

GET /api/v1/apps/adoptable?cluster={cluster}&namespace={ns}
POST /api/v1/apps/adopt

List unmanaged Deployments/StatefulSets, then create an image-source App that field-owns the workload in place. POST body: { "cluster": "…", "namespace": "…", "name": "workload", "kind": "Deployment" }. Adoption requires the workload's selector to already match the rendered convention (app.kubernetes.io/{name,instance,managed-by}) — 409 otherwise, since selectors are immutable.

Not implemented

Endpoint from older docsStatus
GET /apps/{ns}/{name}/diff❌ Returns 404 — no diff engine exists
GET /apps/{ns}/{name}/events as SSE❌ The route returns a JSON array, not a stream
?namespace/cluster/syncStatus filters on GET /apps❌ No query filters — grouping by deployment is client-side
dryRun / force on POST /sync❌ Sync takes no body

Error model

StatusWhen
400Invalid spec (missing source fields, bad DNS labels, unsupported source type)
401No session/API key
403Missing permission, or namespace outside the cluster's allowlist
404App not found (including namespace mismatch)
409Operation already pending, duplicate name, autoscaling conflict, promote scope mismatch
503WARDN_MGMT_KUBECONFIG unset

On this page