Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ jobs:
# promises (no secrets, release installs, host-generated node wallet, localhost in
# decdn_nodes and/or sponsord_hosts, a keyed admin account, shellcheck-clean stage 1,
# a collection lock covering ansible/requirements.yml), for both templates. The boots
# themselves are the molecule `cloud-init` and `cloud-init-sponsord` scenarios
# themselves are the molecule `cloud-init` and `cloud-init-publisher` scenarios
# (molecule.yml). Runs only when cloud-init/, ansible/requirements.yml, the root
# Makefile or ci.yml changed.
cloud-init:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/molecule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
# for 14 scenarios, CPU-bound (per-task overhead roughly doubled under contention),
# with the slowest scenarios queued last. Now the wall clock is the slowest single
# scenario plus setup: ~10m for 18 scenarios (first run, PR #96), set by
# cloud-init-sponsord (~9m45s), then cloud-init (~8m) and sponsord-onramp (~7m30s);
# cloud-init-publisher (~9m45s), then cloud-init (~8m) and sponsord-onramp (~7m30s);
# most others take 2-5m. By October 2026 iroh-relay and source-build had reached
# ~11m on their side effects, which are now split over sibling scenarios (#106),
# so no scenario should go much past ~6.5m. Re-measure against the timeout when a
Expand Down
32 changes: 20 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ ansible/ # the deployment project (DevSec-hardened, lean roles)
playbooks/ # site.yml = node.yml (node operators: cache nodes) + publisher.yml (publishers: origin.yml, sponsord.yml (+ onramp), iroh_relay.yml, iroh_dns_server.yml); backup.yml, decommission.yml
roles/ # baseline, decdn_node, grafana_alloy, sponsord, sponsord_onramp, iroh_relay, iroh_dns_server
inventory/ galaxy/ molecule/ # see ansible/README.md
cloud-init/ # user-data{,-sponsord}.yaml + on-host bootstrap.sh; pinned ansible-core/collections (see its README.md)
cloud-init/ # user-data-{node,publisher}.yaml + on-host bootstrap.sh; pinned ansible-core/collections (see its README.md)
compose/ # Docker Compose deploy path for a single host (see its README.md)
compose.yaml # profiles: node, sponsord, onramp (+ sponsord), caddy
Caddyfile # mirrors roles/sponsord_onramp/templates/Caddyfile.j2
Expand Down Expand Up @@ -269,8 +269,8 @@ the pinned `iroh-relay` (re-capture it when bumping `iroh_relay_version`).
config.
- **Molecule:** under docker, `/` must be `--make-rshared` (see
`molecule/sponsord/prepare.yml`) or every credential directory is empty.
- **cloud-init:** `cloud-init/user-data-sponsord.yaml` (with the onramp), release
mode only; see the `cloud-init/` bullet below.
- **cloud-init:** `cloud-init/user-data-publisher.yaml` (with the onramp and an
origin node), release mode only; see the `cloud-init/` bullet below.

- **`ansible/roles/sponsord_onramp`** — `sponsord-onramp`, sponsord's public side (the
Turnstile gate, the installers, the `decdn-sponsored` CLI API), as a second play of
Expand Down Expand Up @@ -397,9 +397,10 @@ the pinned `iroh-relay` (re-capture it when bumping `iroh_relay_version`).
real binary's record round-trip (PUT, then `dig TXT`) is a manual check, listed
in the role README. No `monitoring/` assets yet.

- **`cloud-init/`** — the Ansible path with no control machine. Two templates,
`user-data.yaml` (a node) and `user-data-sponsord.yaml` (sponsord + onramp), share
stage 1 and differ only in their inventory. They carry only public material (the lint
- **`cloud-init/`** — the Ansible path with no control machine. Two templates, one per
persona: `user-data-node.yaml` (node operators: a cache node) and
`user-data-publisher.yaml` (publishers: an origin node + sponsord + onramp, deletable
group by group), share stage 1 and differ only in their inventory. They carry only public material (the lint
refuses secret-looking keys, credentials in URLs and unknown `bootstrap.env` keys)
and a stage-1 `decdn-bootstrap`. That script clones this repo at a pinned ref (a full
SHA is verified after checkout) and execs `cloud-init/bootstrap.sh`, which:
Expand All @@ -408,11 +409,16 @@ the pinned `iroh-relay` (re-capture it when bumping `iroh_relay_version`).
- installs the exact collections from `collections.lock.yml`;
- runs `site.yml` (which imports `node.yml` and `publisher.yml`) against localhost. The inventory
must put localhost in `decdn_nodes` and/or `sponsord_hosts` (and in
`sponsord_hosts` whenever it is in `sponsord_onramp_hosts`), or the plays match
nothing and the firewall holes never load.
`decdn_nodes` whenever it is in `decdn_origin_nodes`, in `sponsord_hosts` whenever
it is in `sponsord_onramp_hosts`), or the plays match nothing and the firewall
holes never load. The template lists an origin in `decdn_nodes` and
`decdn_origin_nodes` as siblings (the lint refuses `children:`), and the lint
requires an origin backend in `decdn_origin_nodes.vars`.

Each group needs its secrets on the host first, at the role-default paths (the lint
forbids moving them): `/etc/decdn/decdn.env` for a node; `secret.env`, the treasury
forbids moving them): `/etc/decdn/decdn.env` for a node (an origin's too, with an S3
backend's keys; `decdn_origin_nodes` has no `SECRETS` of its own, so no `case` arm);
`secret.env`, the treasury
keystore and password, and (onramp) `turnstile-secret` under `/etc/sponsord/`. While
any is missing it runs `--tags baseline` only, lists them in
`/var/lib/decdn-bootstrap/awaiting` and records `awaiting-secret`. The operator
Expand All @@ -425,12 +431,14 @@ the pinned `iroh-relay` (re-capture it when bumping `iroh_relay_version`).
steps at the end of `bootstrap.sh`, and the README's table. A new group also needs
its own `case` arm and a place in both `HOST_GROUPS` and lint.py's `GROUPS`
(`make test-scripts` checks all three). The baseline-only run trusts every
`decdn_nodes`/`sponsord_hosts` play to run `baseline` tagged `baseline` (checked per
play at boot by `cloud-init/baseline-plays.sh`, and by `make test-scripts`). The lint
node.yml/origin.yml/sponsord.yml play to run `baseline` tagged `baseline` (checked
per play at boot by `cloud-init/baseline-plays.sh`, for `decdn_origin_nodes` in place
of `decdn_nodes` on an origin, whose play is origin.yml's; and by `make
test-scripts`). The lint
allows only the templates' top-level modules and no YAML anchors, so what it reads
is what cloud-init and Ansible read. When `ansible/requirements.yml` changes, re-sync the lock:
`make lint-cloud-init` checks it covers the requirements. The molecule `cloud-init`
and `cloud-init-sponsord` scenarios boot the real templates through cloud-init
and `cloud-init-publisher` scenarios boot the real templates through cloud-init
(skipping `baseline`) against a locally signed release mirror, built from the shared
`molecule/cloud-init/pack.yml` and `includes/`. They are the only coverage of the node's and the
onramp's release download and verify path.
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ targets, so a local pass means a CI pass. Deploy targets live in
| `make lint-helm` | chart: `helm lint --strict`, positive/negative render tests, kubeconform and `promtool check rules` on the alert rules (digest-pinned images), the shared schema-key check (needs `helm`, `yq`, `python3` ≥ 3.11, Docker). Set `DECDN_CLI=<path to decdn>` to also run the real `decdn config validate` (CI can't). |
| `make lint-alloy` | renders `roles/grafana_alloy`'s templates and validates them with the **real** digest-pinned Alloy binary. The molecule stub exits 0 for everything, so this is the only gate that proves the config loads. `ALLOY_BIN=<path>` skips the download. |
| `make lint-compose` | renders `compose/compose.yaml` with every profile on (with its example env, without env files, with an empty `.env`) and asserts `compose/tests/invariants.jq`, `inline-env.jq` and `fail-closed.jq` |
| `make lint-cloud-init` | `cloud-init schema` on `cloud-init/user-data.yaml` and `cloud-init/user-data-sponsord.yaml`, then `cloud-init/tests/lint.py` on each: only the templates' top-level modules and no YAML anchors, no secrets (only the bootstrap's own files, once each, plain `content` only, no secret-looking keys or assignments), no hardening skip, `release` installs verified against the vendored keys with a host-generated node wallet (trust knobs only in their own group's `vars`, no moved keyring or secret path), only localhost, in `decdn_nodes` and/or `sponsord_hosts` (the onramp only beside sponsord), a keyed admin account, `runcmd` exactly stage 1, shellcheck-clean scripts, and a collection lock that covers `ansible/requirements.yml` (needs `cloud-init`, `shellcheck`, `yq`). `CLOUD_INIT_FILE=<path>` checks your own filled-in copy. |
| `make test-scripts` | `tests/scripts-test.sh`: the `ansible/Makefile` scoping guards (dry runs), the molecule driver's selection guards and locks (no containers), the split scenarios' shared inventories, the release gate, `scripts/release.sh` on a fixture repo and `pr-title.yml`'s check (the former skipped without `git-cliff` or `ssh-keygen` on PATH, failed in CI), the negative cases of `lint-compose` and `lint-cloud-init` (the latter skipped without `cloud-init` on PATH), and the cloud-init bootstrap's contracts: `baseline-plays.sh` on fixtures and on the real playbooks (every `decdn_nodes`/`sponsord_hosts` play runs `baseline` tagged `baseline`), the two templates' shared stage 1, login hint and `final_message`, `bootstrap.sh`'s groups against lint.py's, and lint.py's `FORBIDDEN_VARS` against the roles' keyrings and secret paths. `UPSTREAM=<decdn checkout>` adds the sync generators' exit codes. |
| `make lint-cloud-init` | `cloud-init schema` on `cloud-init/user-data-node.yaml` and `cloud-init/user-data-publisher.yaml`, then `cloud-init/tests/lint.py` on each: only the templates' top-level modules and no YAML anchors, no secrets (only the bootstrap's own files, once each, plain `content` only, no secret-looking keys or assignments), no hardening skip, `release` installs verified against the vendored keys with a host-generated node wallet (trust knobs only in their own group's `vars`, no moved keyring or secret path), only localhost, in `decdn_nodes` and/or `sponsord_hosts` (an origin only beside `decdn_nodes` and with a backend, the onramp only beside sponsord), a keyed admin account, `runcmd` exactly stage 1, shellcheck-clean scripts, and a collection lock that covers `ansible/requirements.yml` (needs `cloud-init`, `shellcheck`, `yq`). `CLOUD_INIT_FILE=<path>` checks your own filled-in copy. |
| `make test-scripts` | `tests/scripts-test.sh`: the `ansible/Makefile` scoping guards (dry runs), the molecule driver's selection guards and locks (no containers), the split scenarios' shared inventories, the release gate, `scripts/release.sh` on a fixture repo and `pr-title.yml`'s check (the former skipped without `git-cliff` or `ssh-keygen` on PATH, failed in CI), the negative cases of `lint-compose` and `lint-cloud-init` (the latter skipped without `cloud-init` on PATH), and the cloud-init bootstrap's contracts: `baseline-plays.sh` on fixtures and on the real playbooks (every node.yml, origin.yml and sponsord.yml play runs `baseline` tagged `baseline`), the two templates' shared stage 1, login hint and `final_message`, `bootstrap.sh`'s groups against lint.py's, and lint.py's `FORBIDDEN_VARS` against the roles' keyrings and secret paths. `UPSTREAM=<decdn checkout>` adds the sync generators' exit codes. |
| `make security` | KICS IaC scan of `ansible/`, the rendered chart and `compose/` (digest-pinned engine, fail on HIGH) |
| `make galaxy-check` | build the `decdn.node` and `decdn.publisher` collections and run galaxy-importer's checks on each (`make -C ansible galaxy-check-node` / `galaxy-check-publisher` for one) |

Expand Down Expand Up @@ -88,7 +88,7 @@ they are maintained here. See [its README](monitoring/README.md).
scenario (`make molecule SCENARIOS=<one>`), the matrix read from `make molecule-list`.
The `molecule` job aggregates them: it is the one check name to require, since the
per-scenario names follow the scenario list. The `cloud-init` and
`cloud-init-sponsord` scenarios boot the real user-data templates, so they need
`cloud-init-publisher` scenarios boot the real user-data templates, so they need
network access to apt, PyPI and Galaxy. Keep each scenario well under its leg's
timeout; when one grows, split it along its side effects (as `sponsord-install`,
`sponsord-onramp-caddy`, `sponsord-onramp-source`, `iroh-relay-gate`,
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ lint-compose: ## render compose/ with its examples and check its security
# ansible/requirements.yml.
# CLOUD_INIT_FILE (one path or several) is overridable so operators can check their
# filled-in copy and tests/scripts-test.sh can feed it broken variants.
CLOUD_INIT_FILE ?= cloud-init/user-data.yaml cloud-init/user-data-sponsord.yaml
CLOUD_INIT_FILE ?= cloud-init/user-data-node.yaml cloud-init/user-data-publisher.yaml
lint-cloud-init: ## schema-check the cloud-init/ user-data templates and their invariants (needs cloud-init, shellcheck, yq)
@command -v cloud-init >/dev/null || { echo "lint-cloud-init: needs cloud-init on PATH" >&2; exit 2; }
@# An empty list would loop zero times and report success with nothing checked.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ This is the canonical statement; the per-path READMEs add only what is specific
| Path | What it is |
|------|------------|
| [`ansible/`](ansible/README.md) | The Ansible project: `inventory/`, `playbooks/` (`site.yml`, `node.yml` for node operators, `publisher.yml` for publishers with `origin.yml`, `sponsord.yml`, `iroh_relay.yml` and `iroh_dns_server.yml`, `backup.yml`, `decommission.yml`), `roles/` (`baseline`, `decdn_node`, `grafana_alloy`, `sponsord`, `sponsord_onramp`, `iroh_relay`, `iroh_dns_server`), `galaxy/` (the `decdn.node` and `decdn.publisher` collections), `molecule/`. |
| [`cloud-init/`](cloud-init/README.md) | The cloud-init deploy path: `user-data.yaml` (a node), `user-data-sponsord.yaml` (a sponsor host), the on-host `bootstrap.sh`, and the pinned ansible-core and collections it installs. |
| [`cloud-init/`](cloud-init/README.md) | The cloud-init deploy path: `user-data-node.yaml` (node operators: a cache node), `user-data-publisher.yaml` (publishers: an origin node, sponsord and its onramp), the on-host `bootstrap.sh`, and the pinned ansible-core and collections it installs. |
| [`compose/`](compose/README.md) | The Docker Compose deploy path: the node, `sponsord`, `sponsord-onramp` and Caddy, one profile each. |
| [`charts/decdn-node/`](charts/decdn-node/README.md) | The Helm chart; it renders the node's dashboards and alert rules from `monitoring/`. |
| [`monitoring/`](monitoring/README.md) | The deCDN Grafana dashboards and Prometheus alert rules, for the node (`decdn-node/`, every deploy path), sponsord (`sponsord/`, Ansible and Compose) and the iroh relay (`iroh-relay/`, Ansible). |
Expand Down
4 changes: 2 additions & 2 deletions ansible/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ decommission:
# source-build-recovery (the node built from source on the host: sandbox, stamps;
# then a rollback from a broken commit; then the install-method switch, an untrusted
# build home and a lost restart), and cloud-init and
# cloud-init-sponsord (the cloud-init/ templates, booted for real). The glob in
# cloud-init-publisher (the cloud-init/ templates, booted for real). The glob in
# scripts/molecule.sh is the authority; this list is a guide.
#
# make molecule every scenario, in parallel
Expand Down Expand Up @@ -221,7 +221,7 @@ decommission:
SCENARIOS ?= all
JOBS ?=
SLOW_FIRST ?= sponsord-onramp-source sponsord-install cloud-init source-build-recovery \
iroh-relay-gate cloud-init-sponsord sponsord source-build sponsord-onramp \
iroh-relay-gate cloud-init-publisher sponsord source-build sponsord-onramp \
sponsord-onramp-caddy iroh-relay source-build-rollback iroh-relay-certificate \
validation iroh-relay-install os-matrix grafana-cloud
MOLECULE_LOCK_PREFIX ?= /tmp/decdn-devops-molecule
Expand Down
2 changes: 1 addition & 1 deletion ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ node built from source on the host: the build sandbox, stamps, a moved branch),
`source-build-rollback` (the same converge, then a broken commit rolled back by SHA),
`source-build-recovery` (the same converge, then the install-method switch, an
untrusted earlier build home and a lost restart), and
`cloud-init` and `cloud-init-sponsord` (the `cloud-init/` templates booted for real).
`cloud-init` and `cloud-init-publisher` (the `cloud-init/` templates booted for real).
A slow scenario is split along its side effects rather than allowed to set the wall
clock on its own.
They are independent, so they run concurrently, and each line of output is prefixed with its scenario name
Expand Down
Loading
Loading