diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 14798669..a975c975 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -20,6 +20,7 @@ ## Developer Guides - [Developer Guide](./guides/developer-guide.md) +- [Application CLI — Use Cases & Commands](./guides/application-cli.md) - [Writing a Score](./guides/writing-a-score.md) - [Writing a Topology](./guides/writing-a-topology.md) - [Adding Capabilities](./guides/adding-capabilities.md) diff --git a/docs/adr/012-project-delivery-automation.md b/docs/adr/012-project-delivery-automation.md index 0d8d9e60..279490d9 100644 --- a/docs/adr/012-project-delivery-automation.md +++ b/docs/adr/012-project-delivery-automation.md @@ -8,7 +8,15 @@ Last Updated Date: 2025-06-04 ## Status -Proposed +Superseded by [ADR-025](./025-application-lifecycle-cli.md). + +This ADR set the original direction for delivery automation. ADR-025 +revises it on three points: no mandatory staging (roll-forward only), +GitOps (Argo/Flux) is no longer the deploy interface (the CLI deploys +directly, context-targeted), and environments are selected by an explicit +context rather than relying on an ambient kubeconfig. The +auto-detect-and-bootstrap and CI-pipeline intents here remain valid and +are carried forward. ## Context diff --git a/docs/adr/025-application-lifecycle-cli.md b/docs/adr/025-application-lifecycle-cli.md new file mode 100644 index 00000000..0e8a4e2f --- /dev/null +++ b/docs/adr/025-application-lifecycle-cli.md @@ -0,0 +1,278 @@ +# Architecture Decision Record: Application Lifecycle CLI — Dev-Facing Verbs, Contexts, and the Declarative Boundary + +Initial Author: Sylvain Tremblay + +Initial Date: 2026-06-10 + +Last Updated Date: 2026-06-10 + +## Status + +Proposed (draft) + +Extends ADR-023 (deploy architecture) — whose CLI principle (8) only +covers *how binaries are discovered* — with the CLI's **experience +contract**: verb grammar, the context/identity model, the +declarative/operational boundary, and where "config" lives. Revises +ADR-012 (project delivery automation) on three points: no mandatory +staging, GitOps is not the deploy interface, and environment targeting +is an explicit context rather than an ambient kubeconfig. + +## Context + +We need one CLI that lets a person *or* a machine drive the full +lifecycle of a client application running on a Harmony-managed tenant — +build, deploy, inspect, operate. The first concrete case — a small team +shipping a timesheet-style app — sets the shape of the problem: they +develop locally and ship **straight to production, roll-forward only** — +no staging, no rollback. + +The audience is three-headed and **shares a single surface**: +developers (the priority for this phase), CI/automation, and autonomous +agents. The design is informed by a survey of kubectl, argocd, flux, +fly.io, heroku, pulumi, dagger, terraform, ansible, and gcloud, plus +current practice on CLIs built for AI agents. Two through-lines drove +every decision below: + +1. **Harmony already owns the vocabulary the best tools bolt on.** A + *computed* idempotent result (`Outcome`: NOOP/RUNNING/…), declarative + desired state (Scores), a reconciling engine (Maestro), and named + targets (Topologies). The CLI's job is to *surface* these, not invent + machinery. +2. **The agent/CI contract is a strict-mode projection of good human + DX.** Non-interactive, JSON, idempotent, predictable exit codes — the + same features a careful human wants. Build the human CLI right and the + machine CLI falls out. + +Constraints that bound the design: + +- **Runs locally (k3d) and on any K8s** (`K8sAnywhereTopology`) — nothing + may be OKD- or vendor-specific. +- **Harmony's anti-"YAML mud pit" / compile-time-safety ethos** (ADR-005) + forbids reintroducing untyped, runtime-validated configuration in any + form, including a TOML or YAML file of deployment knobs. + +## Decision + +Eleven principles, grouped. + +### Targeting & safety + +1. **No default context, ever.** Every command that touches a cluster + requires an explicit target: `--context ` flag, else the + `HARMONY_CONTEXT` env var, else a **hard error** that lists known + contexts. There is no "current context" and no implicit fallback — + *even local k3d* must be selected (`--context local`). This makes + "deployed to the wrong cluster" structurally impossible. + +2. **Deploy is CLI-direct and context-targeted; GitOps is not the + interface.** A developer runs `harmony app deploy --context ` + explicitly. The *same verb and the same Scores* deploy to local k3d + (to test) or to a production tenant — only the Topology selected by + the context changes (ADR-023 §2). CI runs the identical command; + git-push-triggers-deploy is optional sugar a team may add, never a + requirement. A continuous in-cluster reconciler (GitOps-style) is a + possible future Topology, not the primary path. This supersedes + ADR-012's Argo/Flux-first stance. + +### Project & identity + +3. **Implicit app, explicit context.** The app identity comes from the + project (a tiny, identity-only `Harmony.toml` at the repo root, or + `[package.metadata.harmony]` in the deploy crate) — the app *name*, + and little else. Everything else derives by convention from + *name × context*: namespace = `{tenant-from-context}-{app}`, image = + `{registry}/{project}/{app}`, workload selector = by app label. **A + behavioral knob (replicas, env, resources) in this file is a defect** + — it belongs in a Score (see §6). Apps that break the naming + convention fall back to the deploy crate; that escape hatch is not + built until a real non-conventional app exists (Rule of Three). + +### The declarative boundary + +4. **Two verb classes.** This is the load-bearing distinction. It + honors ADR-023's "no handrolled manifests" while still giving + developers day-2 ergonomics. + + | Class | Verbs | Path | Mutates desired state? | Needs compile? | + |---|---|---|---|---| + | **Declarative** | `build` · `publish` · `deploy` · `ship` (= build+publish+deploy) · `check` · `run` (one-off Job) | through the project's **Scores**, re-converge | yes | yes (runs deploy crate) | + | **Operational** | `logs` · `status` · `exec` · `forward` · `restart` · `describe` · `history` | **direct kube client**, read-only / ephemeral | no | no (metadata only) | + + `build`, `publish`, and `deploy` are independently invocable and + compose into `ship`. **`deploy` takes an explicit image + (`--image `) and only converges — it does not build.** `build` + produces a digest-pinned image, `publish` pushes it, and `ship` = + build + publish + deploy. This is what makes roll-forward recovery — + the only recovery a no-rollback team has — a plain + `harmony app deploy --image ` with no rebuild, and + it lets CI run the stages separately (digest handoff, debuggable + failures). **`publish` is Topology-specific:** against a remote context + it pushes the digest-pinned image to the registry; against a local + context it is coded to import the image directly into k3d + (`k3d image import`) instead of pushing. The verb is the same; the + Topology supplies the behavior (ADR-023 §2). + + The local inner loop is simply `harmony app ship --context local` + (build + deploy to local k3d) — there is no separate `serve` verb, + because an explicit local context plus the standard verbs already cover + it (principle 11; a second verb would be ceremony). `check` is the only pre-deploy validation we have today: it + compiles and type-checks the Scores (there is **no** computed diff + against live state — see Out of scope). + +5. **No imperative state mutation.** There is no `scale 3` or + `set-env`. Operational verbs are precisely the ones that *don't* touch + desired state — that is the basis of the split. Changing replicas, + env, resources, or routes means **editing the Score and + redeploying**. An imperative shortcut would only create drift that the + next deploy clobbers. + +6. **Three homes for "config" — none of them a config file.** + - *Desired state / behavior* (replicas, env, resources, routes, + dependencies) → **typed Scores**, git-versioned, compile-checked. + - *Target + credentials* (which cluster/tenant, how to auth) → + **context / Topology**, selected explicitly. + - *Secrets* → **OpenBao** (`SecretVault`), referenced by Scores, + fetched at deploy. Never in code, never in a file. + +7. **Per-environment values are a Score computed as a function of the + context.** Differences (prod = 3 replicas + managed Postgres; local = + 1 replica + sqlite) are expressed in typed code that branches on a + **profile tag carried by the context** — the Pulumi-stack idea in pure + Rust, validated by the compiler. The profile is a **structured field on + the context, not encoded in its name**: a Score branches on + `ctx.profile()`, never on the context name (a free human handle — + `myapp-prod` and `otherapp-prod` may share `profile = Prod`). (A dedicated + typed `Profile` input is deferred until divergence justifies it — Rule + of Three.) + +8. **Reconciliation is invocation-driven (today).** Convergence happens + when someone runs `harmony app deploy`, not continuously. Out-of-band + changes survive until the next deploy, then are overwritten. A + continuous controller that reverts drift in real time is a deliberate + future, not v1. + +### One surface for humans, CI, and agents + +9. **Machine contract, always on.** Human-readable output by default + (TTY-detected); `--json` opts into a **frozen, versioned** schema — + JSON to stdout, all logs/progress to stderr. No hidden prompts when + stdin is not a TTY. Verbs are idempotent. `Outcome` maps onto exit + codes (e.g. success/noop = 0, failure/blocked = non-zero) so CI and + agents branch programmatically without scraping text. An agent + skill/MCP surface is *derived from* this CLI later — never hand-built + in parallel. + +### Credentials + +10. **The credential source is part of the context, and degrades to + local.** A context is `{ cluster (endpoint + CA), tenant, credential + source / identity, profile }` — and carries **no `role`**: + authorization is a server-side property of the identity (Zitadel token + claims → OpenBao policy → RBAC), never a client-trusted field. + Locally, the context yields the ambient k3d/kubeconfig — no + identity infrastructure required. Remotely, the context carries a + brokered source: authenticate a Zitadel **service user** (machine + identity, role-scoped), exchange for an **OpenBao**-brokered, + short-lived, namespace-scoped Kubernetes token, and present that. The + CLI holds **nothing standing**; authorization is enforced + server-side (OpenBao policy + cluster RBAC). The broker is K8s- + agnostic by construction (standard `TokenRequest` + RBAC). Detailed + broker mechanics and tenant/identity provisioning may warrant a + follow-up ADR; provisioning is manual for now. + +### Grammar + +11. **Grammar: `harmony `.** A small, fixed set of scope + nouns organizes the surface by persona and object: **`app`** (the + developer lifecycle — `harmony app deploy`, `app logs`, `app ship`, + …), **`tenant`** (tenant-admin), **`cluster`** (cluster-admin), and + **`context`** (everyone). The *app instance stays implicit* — it is + the project's app (principle 3); `app` is the scope group, not an + instance argument, so it is `harmony app deploy`, never + `harmony app deploy myapp`. This uniform tree is predictable for humans + and breadth-first-discoverable for agents (`harmony --help` → scopes, + `harmony app --help` → verbs). We grow by adding verbs / resource + types under an existing scope, never by proliferating top-level + commands (argocd's sprawl is the anti-pattern), and add **no magic + top-level verb aliases** — there is no flat `harmony deploy` shortcut; + one way to do it. + +## Rationale + +- **No default context** trades a keystroke for the elimination of an + entire class of catastrophic mistake. For a tool whose first job is + shipping to production, that trade is obviously correct. +- **Declarative-only** is the single highest-leverage choice: every + surveyed tool that offers both imperative and declarative mutation + (kubectl `edit`/`scale` vs `apply`) suffers drift and clobbering. By + making operational verbs structurally incapable of touching desired + state, the conflict cannot occur. +- **Convention over config** keeps the project file at one line, which is + the only way to keep it from rotting into the mud pit ADR-005 rejects — + and it makes operational verbs instant and toolchain-free, since they + need only metadata + a kube client. +- **One surface** is cheaper and more correct than maintaining separate + human and machine tools, and it means an agent and a developer learn + the same verbs. + +## Consequences + +- `harmony_cli` grows from a flag-only score-runner into a verb-noun CLI; + deploy crates expose the standard verbs rather than ad-hoc clap. +- Developers never hand-edit live state; every change is code + + redeploy → reproducible, reviewable, diffable in git, with no drift + surprises (until the next deploy, per §8). +- The same command works local → prod; CI is just another caller, and + agents drive the JSON contract. +- A behavioral knob added to `Harmony.toml` is a review failure, not a + feature. +- Cost: no live "what will change" preview until a diff capability is + built (Out of scope); the v1 substitute is "deploy to local and + observe." + +## Alternatives considered + +- **GitOps-first (Argo/Flux), per ADR-012.** Rejected as the primary + interface: it forces a git round-trip for every change, hides the build + behind a controller, and answers poorly to "deploy locally to test." + Retained only as a possible future Topology. +- **A config manifest with deployment knobs** (`Harmony.toml` holding + replicas/env/resources). Rejected: it is the YAML mud pit in TOML + clothing — untyped, runtime-validated, the exact anti-pattern of + ADR-005. +- **Ambient / current context** (kubectl/fly style). Rejected: a default + target is a standing invitation to deploy to the wrong cluster. +- **A standing service-account token as a CI secret.** Rejected: a + long-lived credential where Zitadel + OpenBao can mint short-lived, + identity-bound ones. +- **No project file — pure deploy-crate discovery.** Rejected: it forces + a `cargo` compile just to learn an app's own namespace for `logs`. + +## Out of scope (deferred, not rejected) + +- **`plan` / `diff` / delta.** No capability computes desired-vs-live + state today; `Outcome` is a per-Score *result*, not a preview. This is + the eventual differentiator and deserves its own design. +- **Continuous in-cluster reconciler** (real-time drift correction). +- **Step-0 provisioning via CLI** (tenant, app, identity triple) — manual + for now. +- **Tenant dashboard** (web UI for the tenant-admin persona). +- **MCP server / agent skill manifest** (derived from the CLI later). +- **CI-platform OIDC federation** replacing the handed-off deploy key. +- **Typed `Profile` input** for per-environment values (§7). +- **Non-convention app fallback** (multi-namespace / custom layout, §3). + +## References + +- `docs/adr/012-project-delivery-automation.md` — predecessor; this ADR + revises its staging/GitOps/kubeconfig assumptions. +- `docs/adr/023-deploy-architecture.md` — deploy crates, Scores, the E2E + contract; this ADR fills in its CLI principle (8). +- `docs/adr/005-interactive-project.md` — Rust DSL over YAML/HCL + ("no mud pit"). +- `docs/adr/020-1-zitadel-openbao-secure-config-store.md` — identity + + secret backends the credential model (§10) composes. +- `docs/adr/016-Harmony-Agent-And-Global-Mesh-…md` — the mesh that a + future remote-deploy control plane would ride on. +- `CLAUDE.md` — Score-Topology-Interpret, capability rules. diff --git a/docs/guides/application-cli.md b/docs/guides/application-cli.md new file mode 100644 index 00000000..951d5af0 --- /dev/null +++ b/docs/guides/application-cli.md @@ -0,0 +1,182 @@ +# Harmony Application CLI — Use Cases & Commands + +> **Status: design target (proposed).** Today `harmony_cli` runs Scores +> via flags; none of the verbs below are implemented yet. This is the +> living reference we are building toward. The *decisions and rationale* +> live in [ADR-025](../adr/025-application-lifecycle-cli.md) — read that +> for the "why"; this doc is the "what" and "how". + +## Mental model + +Four ideas carry the whole CLI: + +- **`harmony `.** A small fixed set of scope nouns — + **`app`** (developer lifecycle), **`tenant`** (tenant-admin), + **`cluster`** (cluster-admin), **`context`** (everyone). What you *can* + run is decided server-side by your context's identity, not by the + command existing. +- **Implicit app, explicit context.** The app is *this project's app* + (inferred — you never name it). The **target is always explicit**: + `--context ` or `HARMONY_CONTEXT`. **There is no default context; + omitting it is a hard error.** Even local k3d is `--context local`. +- **Declarative vs operational.** Verbs that change desired state go + through the project's typed Scores and re-converge. Verbs that only + read or are ephemeral talk to the cluster directly and never mutate + desired state. You never imperatively edit live state — you edit a + Score and redeploy. +- **Config has three homes, none of them a config file.** Behavior → + typed Scores (in git). Target + credentials → the context. Secrets → + OpenBao. + +### Contexts & profiles + +A **context** is `{ cluster (endpoint + CA), tenant, credential source / +identity, profile }`. It carries **no role** — authorization is enforced +server-side from the identity's token (Zitadel → OpenBao → RBAC), never a +client-side setting. Contexts are defined out-of-band (user/CI config), +not in the project. + +``` +harmony context list # what can I target? +harmony context show myapp-prod # cluster, tenant, profile, identity (+ role, derived from token) +``` + +The **profile** (`local | staging | prod`) is a **structured field** the +context carries — *not* its name. A Score branches on `ctx.profile()` to +compute per-environment values (`prod` → 3 replicas + managed Postgres; +`local` → 1 replica + sqlite), and **never parses the context name**. The +name (`myapp-prod`) is a free human handle for picking `--context`; the +profile field is the authoritative value, so two differently-named contexts +can share `profile = Prod` (and `myapp-prod` vs `otherapp-prod` are two +different contexts at the same profile). See ADR-025 §7. + +Credentials ride on the context and degrade: **local** uses the ambient +k3d/kubeconfig; **remote** mints a short-lived, namespace-scoped token via +Zitadel→OpenBao. The CLI holds nothing standing (ADR-025 §10). + +--- + +## `app` — the developer lifecycle + +The verbs, by class: + +| Verb | Class | Context? | What it does | +|---|---|---|---| +| `check` | declarative | no | Compile + type-check the Scores. The only pre-deploy validation today (no live diff yet). | +| `build` | declarative | no | Build a digest-pinned OCI image; prints the digest. Environment-agnostic. | +| `publish` | declarative | **yes** | Push the image to the registry (remote) **or** `k3d image import` (local). Topology-specific. | +| `deploy` | declarative | **yes** | Converge the Scores against the context, pinned to `--image `. **Does not build.** Returns only after smoke-test. | +| `ship` | declarative | **yes** | `build` + `publish` + `deploy`, threading the digest. The everyday verb. | +| `run` | declarative | **yes** | Run a one-off Score-driven Job (migration, task). | +| `logs` | operational | **yes** | Stream/tail logs. Deep search lives in the observability tool. | +| `status` | operational | **yes** | Health, replicas, last deploy, recent events. | +| `exec` | operational | **yes** | Shell/exec into a running container. | +| `forward` | operational | **yes** | Port-forward a service to localhost. | +| `restart` | operational | **yes** | Rollout-restart a workload (pods return identical; no state change). | +| `describe` | operational | **yes** | Resource detail + events for diagnosis. | +| `history` | operational | **yes** | Deploy history — what shipped, when, which digest. | + +### Develop (inner loop) + +| I want to… | Command | +|---|---| +| Check my Scores compile/type-check | `harmony app check` | +| Build the image to test it builds | `harmony app build` | +| Deploy to local k3d and test | `harmony app ship --context local` | +| See it running locally | `harmony app status --context local` · `harmony app logs --context local -f` | + +### Ship to production + +| I want to… | Command | +|---|---| +| Build + publish + deploy to prod | `harmony app ship --context myapp-prod` | +| Deploy an already-built image | `harmony app deploy --context myapp-prod --image ` | +| Roll **forward** to a prior good build (recovery) | `harmony app deploy --context myapp-prod --image ` | +| Build / publish as separate CI stages | `harmony app build` → `harmony app publish --context myapp-prod` → `harmony app deploy …` | + +There is no rollback and no staging (the roll-forward-only model): you roll forward by +deploying a known-good digest. + +### Operate (day-2) + +| I want to… | Command | +|---|---| +| Tail logs | `harmony app logs --context myapp-prod -f` | +| Check health / what's deployed | `harmony app status --context myapp-prod` | +| Shell into the app | `harmony app exec --context myapp-prod -- /bin/sh` | +| Reach a service locally | `harmony app forward --context myapp-prod 8080:80` | +| Restart the app | `harmony app restart --context myapp-prod` | +| Run a DB migration | `harmony app run --context myapp-prod -- migrate` | +| Change replicas / env / resources | *edit the Score, then* `harmony app ship --context myapp-prod` | + +> Changing replicas/env/config is **not** an imperative command — it's a +> Score edit + redeploy. An out-of-band `kubectl scale` survives only +> until the next deploy, then is reconciled away (ADR-025 §5, §8). + +### Debug / diagnose + +| I want to… | Command | +|---|---| +| Understand why a deploy failed | read the `deploy` output (convergence errors, rustc-style) | +| See why a workload is unhealthy | `harmony app describe --context myapp-prod` · `harmony app logs …` | +| See what shipped recently | `harmony app history --context myapp-prod` | +| Deep log search / traces / metrics | open the observability tool (HyperDX/SigNoz/Grafana) — the CLI deep-links | + +--- + +## `tenant` — tenant administration (tenant-admin) + +> Mostly **manual today** (ADR-025 Out of scope: step-0 provisioning). +> Listed as the target surface; authorization is by the context's role. + +| I want to… | Command (planned) | +|---|---| +| List apps / environments in my tenant | `harmony tenant list --context my-tenant` | +| Create an environment (namespace — billed) | `harmony tenant env create --context my-tenant` | +| Add / remove a developer | `harmony tenant member add --context my-tenant` | +| See usage vs quota | `harmony tenant usage --context my-tenant` (or the dashboard) | +| Rotate an app's deploy key | `harmony tenant key rotate --context my-tenant` | + +--- + +## `cluster` — cluster administration (platform operator) + +> **Future / manual today.** Same noun-by-scope, authorization by role +> (only a cluster-admin context may run these). + +| I want to… | Command (planned) | +|---|---| +| Provision a tenant | `harmony tenant create --context ` | +| Cluster-wide status | `harmony cluster status --context ` | +| Manage platform operators | `harmony cluster operator …` | + +--- + +## Machine & agent usage + +Every verb is built for CI and autonomous agents — the same surface, +strict mode (ADR-025 §9): + +- `--json` → a frozen, versioned schema on **stdout**; logs/progress on + **stderr**. +- Non-interactive when stdin isn't a TTY — no hidden prompts. +- Idempotent verbs; `Outcome` (SUCCESS/NOOP/FAILURE/RUNNING/BLOCKED) + mapped to **exit codes** so CI/agents branch without scraping text. +- A future agent skill / MCP surface is *derived from* this reference, + not authored separately. + +Example CI release (digest pinned end-to-end): + +```sh +DIGEST=$(harmony app publish --context myapp-prod --json | jq -r .image) +harmony app deploy --context myapp-prod --image "$DIGEST" --json +``` + +--- + +## Status legend + +Nothing here is implemented yet. As verbs land, tag them: +**`[impl]`** implemented · **`[wip]`** in progress · **`[planned]`** · +**`[future]`** out of v1 scope (per ADR-025). Until the first verbs +ship, treat the whole document as `[planned]`.