Files
harmony/docs/guides/application-capabilities.md
Jean-Gabriel Gill-Couture 4a9bee73ad docs+deprecate: Application Capabilities guide; deprecate ApplicationFeature model
- docs/guides/application-capabilities.md: the .with(...) menu, by-reference
  wiring, and a recipe for writing a capability; disambiguates the overloaded
  'capability' (topology vs application) flagged in DX review; wired into the book.
- Deprecate ApplicationScore, features::Monitoring, features::PackagingDeployment
  (+ doc-deprecate the ApplicationFeature trait) in favour of harmony_app +
  ComposeDeploy/.with(...) (ADR-026). Legacy examples now warn; harmony lib is
  warning-clean (internal self-refs allowed).
2026-06-22 15:38:35 -04:00

95 lines
4.6 KiB
Markdown

# Application Capabilities — `.with(...)`
> **Status: in progress (feature branch).** The `harmony_app` application
> layer and capabilities described here are landing incrementally. The
> *decisions and rationale* live in
> [ADR-026](../adr/026-application-lifecycle-cli.md); this is the "how".
> Companion to [Application CLI](./application-cli.md).
A **capability** is an add-on you attach to an app deployment — a database,
monitoring, auth — by declaring it:
```rust
ComposeDeploy::from_dir("timesheet", "./app")?
.expose("frontend", "timesheet.example")
.with(Postgres::managed()) // a managed database
.with(Monitoring::new().alert(discord)); // a downtime alert
```
The app author *declares intent*; the capability deploys what it needs and
wires itself to the app. Everything else — `ship`/`deploy`/`status`/`logs`,
contexts, profiles — comes from the [application CLI](./application-cli.md).
> **Two different "capabilities" — don't confuse them.**
> - **Topology capabilities** ([catalog](../catalogs/capabilities.md)) are what
> a *cluster* can do — `K8sClient`, `DnsServer`, `HelmCommand` — exposed as
> trait bounds a `Score` requires. Infrastructure-facing.
> - **Application capabilities** (this page) are add-ons a *developer* attaches
> to their app with `.with(...)`. They live in `harmony_app::capabilities`.
## The menu
| Capability | Declares | Deploys | Wires into the app |
|---|---|---|---|
| `Postgres::managed()` | a managed PostgreSQL | a CNPG cluster `<app>-db` | `DATABASE_URL``secretKeyRef(<app>-db-app, uri)` |
| `Monitoring::new().alert(r)` | a downtime alert | a Prometheus alert rule + routes it to `r` | nothing (selector-scoped to the app's namespace) |
(`ZitadelAuth` for OIDC login is next.)
## How wiring works — by reference, never by value
A capability never passes a *value* through Harmony. Postgres' generated
password lives only in the CNPG-created `<app>-db-app` Secret; the capability
injects an env var that **references** it (`secretKeyRef`), resolved in-cluster
at runtime. This is what keeps the rendered chart publishable (it contains a
reference, not a secret) and is why the app layer needs no typed Score outputs
today (ADR-026). The trade: wiring must be expressible as a stable k8s
reference (service DNS, Secret/ConfigMap key) — true for k8s-native add-ons.
## Writing your own capability
A capability implements one small trait
(`harmony_app::capabilities::Capability`):
```rust
pub trait Capability {
/// What this capability deploys (a database, an operator, …).
fn scores(&self, app: &AppRef) -> Vec<Box<dyn Score<K8sAnywhereTopology>>> { vec![] }
/// Env injected into the app's containers, wired by reference.
fn env(&self, app: &AppRef) -> Vec<EnvVar> { vec![] }
}
```
`AppRef { name, namespace, profile }` is how the capability learns who it's
augmenting (and so derives names like `<app>-db`). Implement either method or
both, then `app.with(MyThing)`.
### Recipe (e.g. a `Redis` capability)
1. **Find the backing Score.** Capabilities *compose* existing Scores — they
do not hand-roll manifests (ADR-023). Look in `harmony::modules::*` for one
(Postgres composes `harmony::modules::postgresql::K8sPostgreSQLScore`). If
none exists, write the Score first ([Writing a Score](./writing-a-score.md)).
2. **Return it from `scores()`**, naming resources off `app` (e.g. cluster
`format!("{}-redis", app.name)`).
3. **Wire it by reference in `env()`.** Determine what stable reference the
Score exposes — a Service DNS name, or a key in a Secret/ConfigMap it
creates — and inject an `EnvVar` pointing at it. (Postgres references CNPG's
`<cluster>-app` secret's `uri` key.) The convention between a Score's output
and a capability's `env()` is currently by agreement, not type-checked —
read the Score to confirm the names it produces.
4. **Receivers and other inputs** (e.g. an alert `DiscordWebhook`) come from
`harmony::modules::*` too — `Monitoring` takes a
`harmony::modules::monitoring::alert_channel::discord_alert_channel::DiscordWebhook`.
`Postgres` and `Monitoring` in `harmony_app/src/capabilities.rs` are the two
worked examples to copy.
## Deprecated: the old `ApplicationFeature` model
The previous approach — `ApplicationScore { features: vec![Monitoring, …] }`
over `ApplicationFeature` — is **deprecated** in favor of this one. It coupled
app delivery to a fixed feature menu and the (cancelled) ArgoCD path, and
wasn't composable with plain Scores. Migrate `ApplicationScore`
`ComposeDeploy` (or your own `HarmonyApp`) + `.with(...)` capabilities.