Some checks failed
Run Check Script / check (pull_request) Failing after 1m52s
194 lines
8.3 KiB
Markdown
194 lines
8.3 KiB
Markdown
# Architecture Decision Record: Deploy Architecture — Scores, Deploy Crates, and the E2E Contract
|
|
|
|
Initial Author: Jean-Gabriel Gill-Couture
|
|
|
|
Initial Date: 2026-05-18
|
|
|
|
Last Updated Date: 2026-05-20
|
|
|
|
## Status
|
|
|
|
Accepted. Extends the Score-Topology-Interpret pattern documented
|
|
in `CLAUDE.md` (ADR-002, ADR-003) with the *deploy* side of the
|
|
contract: what a deploy crate is, how e2e harnesses relate to
|
|
production deploys, how the CLI surface is shaped, and the
|
|
smoke-test-on-deploy semantics.
|
|
|
|
## Context
|
|
|
|
Three failure modes recur in tooling that ships infrastructure as
|
|
code, and Harmony exists in part to defeat them. This ADR locks
|
|
the deploy-time discipline that keeps them out.
|
|
|
|
1. **Manifests outside the type system.** YAML/HCL configurations
|
|
are validated at runtime, not compile time — the original
|
|
"YAML mud pit" that ADR-005 names. A Rust framework that
|
|
re-introduces raw `Deployment` / `Service` / `ConfigMap`
|
|
structs in test harnesses, examples, or CLI helpers has only
|
|
dressed up the same anti-pattern in Rust syntax: the typed
|
|
Scores get a clean sample size of one (production), and
|
|
everything else can silently diverge.
|
|
|
|
2. **Deploy logic with no canonical home.** A "how to apply
|
|
component X end-to-end" routine that lives in three places
|
|
(an example crate, a CLI subcommand, ad-hoc orchestration in
|
|
a test harness) will drift. The framework needs one address
|
|
per deployable component, and every consumer of that
|
|
component composes from there.
|
|
|
|
3. **"Applied" is not "working."** `helm install` returns
|
|
success the moment the API server accepts the manifest, and
|
|
leaves the operator to debug downstream. Harmony's whole
|
|
reason for existing is to shorten that feedback loop — a
|
|
deploy primitive that doesn't itself verify the result keeps
|
|
the loop open.
|
|
|
|
## Decision
|
|
|
|
Nine principles, grouped.
|
|
|
|
### Deployment as Scores
|
|
|
|
1. **Deploy with Scores, not handrolled manifests.** Capability
|
|
traits + compile-time bounds are the contract. No
|
|
`k8s_openapi::api::*` structs outside of `Score::interpret`
|
|
bodies. Test harnesses, examples, and CLI helpers compose
|
|
`*Score` types — they never reimplement deploys.
|
|
|
|
2. **E2E uses the same Scores as production.** Only the
|
|
`Topology` instance changes (local k3d, remote OKD,
|
|
bare-metal HA, …). A test harness is a `Score`-composer
|
|
running against a test Topology. If e2e needs something prod
|
|
doesn't, add the knob to the Score — don't fork the manifest
|
|
in the harness.
|
|
|
|
3. **One Score per deployable component.** Composition is the
|
|
user-facing primitive: a `MyAppScore` pulls in
|
|
`PostgresScore`, `HttpServerScore`, etc. Don't build
|
|
monolithic "deploy everything" Scores. Each primitive Score
|
|
must be independently testable and substitutable.
|
|
|
|
4. **Deploy returns only after smoke-test success.** Every Score
|
|
owns a readiness + smoke-test contract that the framework
|
|
runs and blocks on. Convergence errors must be actionable, in
|
|
the style of `rustc`'s error messages, not "exit code 1 from
|
|
helm". The implementation shape of the smoke-test contract is
|
|
deferred (see §Out of scope); the principle is locked in.
|
|
|
|
### Where deploy logic lives
|
|
|
|
5. **Deploy logic lives in a `*-deploy` crate** that depends on
|
|
both `harmony` and the runtime crate it deploys. Runtime
|
|
binaries (the artefacts that ship to constrained devices and
|
|
to in-cluster pods) stay free of the `harmony` dep. One
|
|
deploy crate per app area, holding every component-Score for
|
|
that app plus the `main.rs` that drives them via
|
|
`harmony_cli`. The same crate is the single import for any
|
|
consumer — CLI, e2e harness, future control planes.
|
|
|
|
`harmony` core stays focused on framework primitives and
|
|
reusable provider modules (DNS, K8s resources, Helm charts,
|
|
NATS, PostgreSQL, …). It is not a parking lot for
|
|
application-specific deploy Scores.
|
|
|
|
### Topology selection
|
|
|
|
6. **Topologies are compile-time, selected at runtime.** A
|
|
deploy binary statically lists its supported topologies; the
|
|
operator picks one at deploy time. Adding a brand-new
|
|
topology backend (AWS, GCP, …) is a rebuild — acceptable
|
|
cost, because dynamic-discovery topologies like
|
|
`K8sAnywhereTopology` already cover "any physical place that
|
|
runs k8s". No `Box<dyn Topology>` plugin loaders.
|
|
|
|
### Framework evolution
|
|
|
|
7. **Extend Scores with companions, not API changes.** New
|
|
capabilities the framework wants to attach to Scores
|
|
(planning, dry-run, observability, eventually smoke-test)
|
|
default to a *companion* type or trait that wraps a Score
|
|
rather than a new method on `Score` / `Interpret`. The base
|
|
public API stays simple. The exception is principles every
|
|
Score must honor (which may force a required method) — but
|
|
only after the principle has been validated in practice via
|
|
the companion-first iteration.
|
|
|
|
### CLI
|
|
|
|
8. **CLI: hybrid, staged.** Today (B): first-party tools ship as
|
|
separate `harmony-*` binaries built on the existing
|
|
`harmony_cli` crate. Tomorrow (C): a top-level `harmony`
|
|
binary discovers `harmony-*` plugin binaries on `$PATH`
|
|
(`kubectl`-style) so a third-party `MyAppScore` author gets
|
|
`harmony deploy my-app` for free. The plugin protocol is
|
|
deferred (see §Out of scope).
|
|
|
|
### Error handling
|
|
|
|
9. **`thiserror` almost everywhere; `anyhow` only at binary
|
|
glue.** Library code, public crate boundaries, anything a
|
|
caller might want to match on — typed errors via `thiserror`.
|
|
`anyhow` is reserved for `main.rs`-level glue where the error
|
|
is just printed.
|
|
|
|
## Out of scope (deferred, not rejected)
|
|
|
|
- **Score derive macro / deployment DSL.** Strategic intent from
|
|
day one; the framework's value-add concentrates here. Separate
|
|
design effort.
|
|
- **Score registry** (Crichton-style:
|
|
<https://willcrichton.net/rust-api-type-patterns/registries.html>).
|
|
Real itch — examples and Scores are hard to discover today.
|
|
Research + ADR first.
|
|
- **Inventory as capability-defined physical assets.** Inventory
|
|
is under-engineered today; the original idea is to represent
|
|
physical infrastructure (building → cable → switch port → MAC)
|
|
but most use cases ignore it. Decomposing inventory into a
|
|
capability set is a deep redesign.
|
|
- **Plug-in CLI discovery layer (C in principle 8).** The fix
|
|
for the "too many disconnected CLIs" cohesion problem.
|
|
Roadmap item, dedicated future effort.
|
|
- **`Application features` ↔ `capabilities` relationship.** An
|
|
in-progress concept the project lead is personally unsure
|
|
about. Not resolved in this ADR.
|
|
- **Concrete smoke-test contract shape (principle 4).** Whether
|
|
smoke-test lives as a separate trait, a required method on
|
|
`Score`, a companion struct, or a typestate is open. Until
|
|
it's locked, deploy crates implement per-Score readiness
|
|
checks inside `interpret` bodies — the principle is what
|
|
travels with the Score, not yet the trait shape.
|
|
|
|
## Consequences
|
|
|
|
- New deployable components are authored as `*Score` types in a
|
|
`*-deploy` crate, not in `harmony` core. `harmony` core is
|
|
framework primitives plus reusable provider modules; it does
|
|
not accumulate application-specific deploy logic.
|
|
- Test harnesses are Score-composers. A harness that finds
|
|
itself building `Deployment` / `Service` / `ConfigMap` structs
|
|
is the signal that a Score is missing, not that the harness
|
|
needs a special path.
|
|
- Every Score owns its readiness story. Whatever shape the
|
|
smoke-test contract eventually takes, the Score is the home
|
|
for the logic — not a parallel test fixture.
|
|
- Adding a new deploy backend (a new topology) is a deploy-
|
|
binary rebuild. Dynamic loading of topologies is rejected by
|
|
this ADR, and that posture is load-bearing for the
|
|
compile-time-safety guarantees in CLAUDE.md.
|
|
- New framework-level capabilities (dry-run, observability,
|
|
smoke-test) ride in on companion types first. Only after a
|
|
companion proves out does it earn a place in the `Score` /
|
|
`Interpret` public API.
|
|
|
|
## References
|
|
|
|
- `CLAUDE.md` — Score-Topology-Interpret pattern, capability
|
|
design rules.
|
|
- `docs/adr/002-hexagonal-architecture.md` — domain/adapter split
|
|
this builds on.
|
|
- `docs/adr/005-interactive-project.md` — the original "no
|
|
YAML-mud-pit" call (Rust DSL over YAML/HCL).
|
|
- `docs/adr/009-helm-and-kustomize-handling.md` — established
|
|
pattern: external charts inflate into the same Score pipeline.
|
|
- `harmony_agent/deploy` — `*-deploy` crate exemplar.
|