docs(app-manifest-spec): fix the id rule, the Quadlet claim, and the declarative overstatement
Narrative pass over the manifest spec, plus one correction to the guide I committed inba052736. - **`app.id` allowed `_`.** `is_valid_app_id` accepts lowercase ASCII letters, digits and single hyphens only — no underscores, no leading/trailing hyphen, no `--`. The spec's "alphanumeric + `-`/`_`" would have a developer write an id that fails to parse. - **"Must match the directory name"** is a convention, not a rule. The loader (`prod_orchestrator.rs:1455-1474`) walks `*/manifest.yml` and keys off `app.id`, never comparing it to the folder, so a mismatch silently registers the app under a different id. Said so rather than implying enforcement. - **The Quadlet claim was the same one architecture.md was corrected for** (f55ed6bf): install does NOT compile to a `user.slice` Quadlet unit today. `config.use_quadlet_backends` defaults false, so apps take the legacy `podman create + start` path; Quadlet is opt-in per node and companion UIs are the exception that already use it. - **"no per-app installer code"** — true of installers, but `run_pre_start_hooks` is a hardcoded `match app_id` covering seven first-party apps (bitcoin-ui, filebrowser, lnd, archy-nbxplorer, btcpay-server, fedimint-clientd, grafana). Documented as the caveat it is; anyone reading the source will find it in a minute and the doc should not look like it's hiding it. - `derived_env` now names the full closed allow-list including `{{BITCOIN_HOST}}` and what it resolves to. Correction toba052736: I wrote there that an unknown `derived_env` placeholder passes through verbatim. It doesn't — `validate_derived_template` rejects both unknown names and unbalanced `{{`. Fixed that row in the guide. Verified accurate and left alone: the capability allow-list, network_policy values, `/dev/*` device rule, volume option allow-list, bind-source confinement, the four generated_secret kinds, `hooks.pre_start` being schema-only, and the 30s reconciler interval (`BootReconciler::DEFAULT_INTERVAL`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ba052736be
commit
7adc3260a6
@@ -109,7 +109,7 @@ app:
|
||||
| `app.container.pull_policy` | Pull behavior, usually `if-not-present` |
|
||||
| `app.container.network` | Podman network setting such as `archy-net` or `pasta`; dangerous namespace-sharing modes are rejected |
|
||||
| `app.container.entrypoint` / `custom_args` | Entrypoint and command override |
|
||||
| `app.container.derived_env` | Environment values rendered from host facts. The complete placeholder set is `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}`, `{{BITCOIN_HOST}}` — anything else is left in the value verbatim rather than erroring, so a typo ships a literal `{{FOO}}` to your container |
|
||||
| `app.container.derived_env` | Environment values rendered from host facts. The complete placeholder set is `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}`, `{{BITCOIN_HOST}}`; an unknown name or an unbalanced `{{` is a parse error, so typos fail loudly |
|
||||
| `app.container.secret_env` | Environment values read from `/var/lib/archipelago/secrets/<secret_file>`, injected as podman secrets (never visible in `podman inspect` or unit files) |
|
||||
| `app.container.generated_secrets` | Secrets the orchestrator creates on first use (`hex16`/`hex32`/`base64`/`bcrypt`) — self-healing, 0600, no host provisioning |
|
||||
| `app.container.generated_certs` | Self-signed TLS certs materialised before create; CN/SANs rendered from host facts |
|
||||
|
||||
@@ -6,14 +6,22 @@ document and the code disagree, the code wins. See
|
||||
[`app-developer-guide.md`](app-developer-guide.md) for the authoring workflow._
|
||||
|
||||
Every app is a directory `apps/<id>/` containing a `manifest.yml` with a single
|
||||
top-level `app:` block. Apps are purely declarative — the orchestrator owns the
|
||||
entire lifecycle; there is no per-app installer code.
|
||||
top-level `app:` block. Apps are declarative — the orchestrator owns the entire
|
||||
lifecycle; there is no per-app installer code.
|
||||
|
||||
One honest caveat: seven first-party apps still get Rust-side pre-start work
|
||||
through a hardcoded `match app_id` in `ProdOrchestrator::run_pre_start_hooks` —
|
||||
`bitcoin-ui`, `filebrowser`, `lnd`, `archy-nbxplorer`, `btcpay-server`,
|
||||
`fedimint-clientd` and `grafana` render or repair a config before start. That is
|
||||
orchestrator code rather than a per-app installer, but it is not
|
||||
manifest-declared, and the direction of travel is to replace each case with a
|
||||
reusable manifest primitive.
|
||||
|
||||
## Top-level fields (`app:`)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `id` | string | ✅ | Lowercase alphanumeric + `-`/`_`. Must match the directory name. |
|
||||
| `id` | string | ✅ | Lowercase ASCII letters, digits and single hyphens only — **no underscores**, no leading/trailing `-`, no `--` (`is_valid_app_id`). Should match the directory name, though nothing enforces that: the loader keys off `app.id`, so a mismatch silently registers the app under the id in the file rather than the folder. |
|
||||
| `name` | string | ✅ | Display name. |
|
||||
| `version` | string | ✅ | App version shown in the UI. |
|
||||
| `description` | string | — | One-line description. |
|
||||
@@ -45,7 +53,7 @@ Exactly **one** of `image` or `build` must be present (image XOR build).
|
||||
| `network_aliases` | list of string | Extra DNS names on `network` (podman `--network-alias`) — lets stack members answer to short baked-in hostnames (`api`, `minio`, `relay`). |
|
||||
| `entrypoint` | list of string | Entrypoint override. |
|
||||
| `custom_args` | list of string | Extra positional args appended after the image. |
|
||||
| `derived_env` | list | `- { key, template }` — template rendered against host facts at apply time. Allowed placeholders: `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}` (plus dependency-resolved facts such as the active bitcoin host). Never hard-code host specifics. |
|
||||
| `derived_env` | list | `- { key, template }` — template rendered against host facts at apply time. The allow-list is exactly `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}`, `{{BITCOIN_HOST}}` (`DERIVED_PLACEHOLDERS`); an unknown name or unbalanced `{{` fails validation. `{{BITCOIN_HOST}}` resolves to whichever Bitcoin app is running (`bitcoin-knots` or `bitcoin-core`, defaulting to knots). Never hard-code host specifics. |
|
||||
| `secret_env` | list | `- { key, secret_file }` — value read from `/var/lib/archipelago/secrets/<secret_file>` and injected as a **podman secret**, so it never appears in `podman inspect` or unit files. `secret_file` must be a bare filename (no `/`, no `..`). |
|
||||
| `generated_secrets` | list | `- { name, kind }` — orchestrator materialises the secret on first use (0600, rootless service user, idempotent + self-healing). `kind ∈ hex16 | hex32 | base64 | bcrypt` (bcrypt writes `<name>` = hash and `<name>.pw` = plaintext). |
|
||||
| `generated_certs` | list | `- { crt, key, common_name?, sans? }` — self-signed TLS materialised before create; CN/SANs rendered against host facts. |
|
||||
@@ -104,11 +112,18 @@ hooks:
|
||||
|
||||
## Installation semantics
|
||||
|
||||
The orchestrator compiles the manifest into a rootless Podman **Quadlet unit
|
||||
under `user.slice`** — the container survives backend restarts and reboots, and
|
||||
a level-triggered reconciler converges drift every 30 seconds. Multi-container
|
||||
apps are sets of per-member manifests installed together via the stack
|
||||
orchestrator (`api/rpc/package/stacks.rs`) on an app-local network.
|
||||
The orchestrator compiles the manifest into a rootless Podman container that
|
||||
survives backend restarts and reboots, and a level-triggered reconciler
|
||||
converges drift every 30 seconds (`BootReconciler::DEFAULT_INTERVAL`).
|
||||
Multi-container apps are sets of per-member manifests installed together via
|
||||
the stack orchestrator (`api/rpc/package/stacks.rs`) on an app-local network.
|
||||
|
||||
**Quadlet is not the default path.** `config.use_quadlet_backends` defaults to
|
||||
`false`, so ordinary apps still take the legacy `podman create + start` path;
|
||||
the Quadlet-unit-under-`user.slice` backend is opt-in per node (config key or
|
||||
`ARCHIPELAGO_USE_QUADLET_BACKENDS`) and stays behind the flag until the
|
||||
lifecycle harness has gone green against it. Companion UI containers are the
|
||||
exception that do use Quadlet today.
|
||||
|
||||
## Distribution
|
||||
|
||||
|
||||
Reference in New Issue
Block a user