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

4.6 KiB

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; this is the "how". Companion to Application CLI.

A capability is an add-on you attach to an app deployment — a database, monitoring, auth — by declaring it:

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.

Two different "capabilities" — don't confuse them.

  • Topology capabilities (catalog) 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_URLsecretKeyRef(<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):

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).
  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 ApplicationScoreComposeDeploy (or your own HarmonyApp) + .with(...) capabilities.