- 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).
95 lines
4.6 KiB
Markdown
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.
|