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/appsPermission: 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/appsPermission: 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 }
}| Field | Required | Description |
|---|---|---|
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}/statusPermission: 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}/eventsPermission: 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}/historyPermission: 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}/syncPermission: 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}/deployPermission: 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}/rollbackPermission: 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}/clonePermission: 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}/restartPermission: deployment:sync. Rolls the app's pods — a template annotation bump on every managed workload.
Approve
POST /api/v1/apps/{namespace}/{name}/approvePermission: 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}/scalePermission: 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}/unfreezePermission: deployment:manage. Sets/clears spec.freeze — all syncs hold while health keeps reconciling. → { "frozen": true }
Preview
POST /api/v1/apps/previewPermission: 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/adoptList 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 docs | Status |
|---|---|
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
| Status | When |
|---|---|
400 | Invalid spec (missing source fields, bad DNS labels, unsupported source type) |
401 | No session/API key |
403 | Missing permission, or namespace outside the cluster's allowlist |
404 | App not found (including namespace mismatch) |
409 | Operation already pending, duplicate name, autoscaling conflict, promote scope mismatch |
503 | WARDN_MGMT_KUBECONFIG unset |
Related
- Configuration —
AppCRD spec - Authentication — sessions and API keys
- Sync Policies — sync behavior
- Rollback guide — revision ledger and rollback semantics