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
17 changes: 15 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ monitoring/ # Grafana dashboards + Prometheus alert rules (maintaine
sponsord/ # sponsord's (+ promtool unit tests), Ansible/Compose only
iroh-relay/ # the relay's (+ promtool unit tests, exported-metrics.txt), Ansible and Compose
docs/ # cross-path operator docs: node-operators.md + publishers.md (the front doors), requirements.md, lifecycle.md
examples/collection-fleet/ # a consumer project for the Galaxy collections: requirements.yml, ansible.cfg, .gitignore (the inventory comes from ansible/inventory/'s examples)
scripts/ # upstream-mirror generators, the compose Alloy config generator (render-compose-alloy.sh), the decdn bump (open-decdn-bump.sh, bump_decdn.py), the release script (release.sh, git-cliff: ../cliff.toml) and its gate (+ its Artifact Hub changes generator), the molecule driver (molecule.sh), ansible/Makefile's LIMIT preflight (limit-guard.sh)
```

Expand Down Expand Up @@ -718,9 +719,21 @@ which depends on `decdn.node`). Each overlay lives in `ansible/galaxy/<collectio
`make test-scripts` checks every role ships in exactly one), and is staged into a clean
collection tree by `galaxy/build.sh <collection>` — there is **no** `galaxy.yml` at the
`ansible/` root (that would make ansible-lint treat the deploy project as a collection).
A new role goes in one `roles.txt`. Build/validate from `ansible/` with `make build` /
A new role goes in one `roles.txt`. **The playbooks ship too**, so a consumer needs only
an inventory (`ansible-playbook decdn.publisher.site`): `playbooks.txt` lists what each
collection takes from `ansible/playbooks/` (a new playbook goes in at least one; every
`import_playbook` target in the same one, so a run never mixes two releases'
`group_vars/`), `playbooks/group_vars/` ships beside them (a collection playbook loads
it as this project does, same precedence), and `galaxy/stage-playbooks.py` rewrites
role references to FQCNs at staging only: never qualify them, or add `collections:`, in
`ansible/playbooks/`, where an installed collection would then shadow `roles/`.
`decdn.node`'s node-only `backup.yml`/`decommission.yml` are a second source in
`galaxy/node/playbooks/`, kept equal to the fleet-wide node plays by `make
test-scripts`: change both. Build/validate from `ansible/` with `make build` /
`make galaxy-check` (both; `-node`/`-publisher` suffixes for one; the root's
`galaxy-build`/`galaxy-check` run both). **Publishing** is per artifact, each on
`galaxy-build`/`galaxy-check` run both; each check also installs the tarball and runs
`galaxy/check-playbooks.sh`: every playbook's syntax by FQCN and, through a probe
collection playbook, the firewall holes its `group_vars/` give). **Publishing** is per artifact, each on
its own version: `release-collection.yml` on a `node-collection-vX.Y.Z` or
`publisher-collection-vX.Y.Z` tag (the repo's Latest follows `decdn.node`; release.sh
refuses a publisher release until a node tag satisfies its `decdn.node` constraint), `release-chart.yml` on a
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ targets, so a local pass means a CI pass. Deploy targets live in
| `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. It also checks that `compose/alloy/config.alloy` (the Compose `alloy` profile's render of the same template) is current, and runs it in Alloy: every component healthy, each daemon scraped only under the profiles that start it. `ALLOY_BIN=<path>` skips the download. |
| `make lint-compose` | reads `compose/compose.yaml` as written (`yq`) against `compose/tests/bind-sources.jq` (every bind mount's `create_host_path`), then renders it 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-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, every backend key only in `decdn_origin_nodes.vars`, 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, and `limit-guard.sh` on a fixture inventory), 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), `compose/decdn-compose`'s unit tests (`compose/tests/test_decdn_compose.py`), `scripts/bump_decdn.py`'s (`scripts/tests/test_bump_decdn.py`), that `compose/alloy/config.alloy` is a fresh render of the `grafana_alloy` template (`scripts/render-compose-alloy.sh --check`; needs ansible-core, failed in CI without it) and that Compose's `grafana/alloy` image is `grafana_alloy_version`, 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 test-scripts` | `tests/scripts-test.sh`: the `ansible/Makefile` scoping guards (dry runs, and `limit-guard.sh` on a fixture inventory), 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), `compose/decdn-compose`'s unit tests (`compose/tests/test_decdn_compose.py`), `scripts/bump_decdn.py`'s (`scripts/tests/test_bump_decdn.py`), that `compose/alloy/config.alloy` is a fresh render of the `grafana_alloy` template (`scripts/render-compose-alloy.sh --check`; needs ansible-core, failed in CI without it) and that Compose's `grafana/alloy` image is `grafana_alloy_version`, 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; and the Galaxy collections' shipped playbooks: every playbook in a `playbooks.txt`, `galaxy/stage-playbooks.py` on both collections and its refusals, that it changes only role names, `decdn.node`'s `backup.yml`/`decommission.yml` against the fleet-wide node plays in both directions (needs yq, failed in CI without it), and the two roles' shared `*_backup_local_dir` default. `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) |
| `make galaxy-check` | build the `decdn.node` and `decdn.publisher` collections, run galaxy-importer's checks on each, then install each and check its playbooks by FQCN, firewall holes included (`ansible/galaxy/check-playbooks.sh`; `make -C ansible galaxy-check-node` / `galaxy-check-publisher` for one) |

`ansible-lint` is **not** a per-commit hook (it needs the collections installed). Run it
with `make lint-ansible`, or `pre-commit run ansible-lint --hook-stage manual`.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -199,5 +199,5 @@ molecule-serial: ## same selection, one scenario at a time (readable output
galaxy-build: ## stage + build the decdn.node and decdn.publisher Galaxy collection artifacts
$(MAKE) -C ansible build

galaxy-check: ## build + validate both collections (galaxy-importer)
galaxy-check: ## build + validate both collections (galaxy-importer, shipped playbooks by FQCN)
$(MAKE) -C ansible galaxy-check
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Four ways to deploy the node, cache or origin:
| Path | For | Start here |
|------|-----|------------|
| **Docker Compose** (`compose/`), recommended | One host that already runs Docker. The quickest start, [below](#quickstart-docker-compose). Profiles for a cache node or an origin, the onboarding sponsor (`sponsord` + its onramp), an iroh relay, an iroh DNS server and Grafana Alloy, driven by the `decdn-compose` wrapper (set-up, preflight, guarded restarts). | [`compose/README.md`](compose/README.md) |
| **Ansible** (`ansible/`) | VMs and bare metal, one node or a fleet. Hardens the host too (firewall, SSH, patching). Also published on Ansible Galaxy as the [`decdn.node`](https://galaxy.ansible.com/ui/repo/published/decdn/node/) and [`decdn.publisher`](https://galaxy.ansible.com/ui/repo/published/decdn/publisher/) collections. | [`ansible/README.md`](ansible/README.md) |
| **Ansible** (`ansible/`) | VMs and bare metal, one node or a fleet. Hardens the host too (firewall, SSH, patching). Also published on Ansible Galaxy as the [`decdn.node`](https://galaxy.ansible.com/ui/repo/published/decdn/node/) and [`decdn.publisher`](https://galaxy.ansible.com/ui/repo/published/decdn/publisher/) collections, roles and playbooks, so a fleet needs only an inventory of its own ([`examples/collection-fleet/`](examples/collection-fleet/README.md)). | [`ansible/README.md`](ansible/README.md) |
| **cloud-init** (`cloud-init/`) | One VM, no control machine: paste the user-data into your provider's "create server" form. It runs the Ansible playbook on the host itself, hardening included. One template per kind of operator: a cache node, or a publisher's origin with the onboarding sponsor (`sponsord` + its onramp). | [`cloud-init/README.md`](cloud-init/README.md) |
| **Helm** (`charts/decdn-node/`) | Kubernetes, one release per node, cache or origin. Published to `oci://ghcr.io/decdn/charts/decdn-node`, signed, and listed on [Artifact Hub](https://artifacthub.io/packages/helm/decdn-node/decdn-node). | [`charts/decdn-node/README.md`](charts/decdn-node/README.md) |

Expand Down Expand Up @@ -131,6 +131,7 @@ This is the canonical statement; the per-path READMEs add only what is specific
| [`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 and Compose). |
| [`docs/`](docs/requirements.md) | Cross-path operator docs: the two front doors ([node operators](docs/node-operators.md), [publishers](docs/publishers.md)), requirements, lifecycle. |
| [`examples/collection-fleet/`](examples/collection-fleet/README.md) | A starting point for a private repository that deploys from the Galaxy collections alone, with only its own inventory. |
| `scripts/` | Generators for the upstream mirrors (network profiles) and Compose's Alloy config (`render-compose-alloy.sh`), and the release gate. |
| `Makefile` | Lint, test and security targets; CI runs the same ones. `make help` lists them. |
| `.github/workflows/` | CI (`ci.yml`, `molecule.yml`), releases (`release-collection.yml`, `release-chart.yml`), the weekly upstream drift check. |
Expand Down
8 changes: 5 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ would commit, and changes no file. The script:

1. **Refuses unconventional commits.** It looks at the commits since the artifact's
last tag that touched what it ships: for a collection, the roles its
`ansible/galaxy/<collection>/roles.txt` lists (the list `galaxy/build.sh` reads), that
overlay, `ansible/galaxy/build.sh` and `LICENSE`; for the chart, `charts/decdn-node/`
and `monitoring/decdn-node/`.
`ansible/galaxy/<collection>/roles.txt` lists and the playbooks its `playbooks.txt`
lists (the lists `galaxy/build.sh` and `galaxy/stage-playbooks.py` read) with
`ansible/playbooks/group_vars/`, that overlay, `ansible/galaxy/build.sh`,
`ansible/galaxy/stage-playbooks.py` and `LICENSE`; for the chart,
`charts/decdn-node/` and `monitoring/decdn-node/`.
git-cliff drops a commit it cannot parse as a Conventional Commit, from the
changelog and from the bump, and only logs a count. That covers an unconventional
subject, but also an empty scope (`feat(): …`) and a body right under the subject
Expand Down
7 changes: 7 additions & 0 deletions ansible/.ansible-lint
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ skip_list:
exclude_paths:
- build/
- galaxy/
# Imports the decdn.node collection's playbooks by FQCN, which exist only once the
# scenario's prepare has built and installed the collection, so ansible-lint's
# syntax check cannot load it. Of the playbooks it imports, node.yml is linted
# here; the node-only backup.yml and decommission.yml live under the excluded
# galaxy/, linted by galaxy-importer in the build and kept equal to playbooks/'s
# node plays by make test-scripts.
- molecule/collection/converge.yml

# Task files a molecule scenario includes (ansible-lint would otherwise read them as
# playbooks, and a `tasks/` directory would make the scenario look like a role).
Expand Down
12 changes: 10 additions & 2 deletions ansible/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,9 @@ molecule-list:
@$(molecule_env) ../scripts/molecule.sh list

# --- Galaxy collections (decdn.node, decdn.publisher) -------------------------
# Stage each collection's roles (galaxy/<collection>/roles.txt) into a clean collection
# tree and build the artifact under build/: decdn.node is what a node operator runs
# Stage each collection's roles (galaxy/<collection>/roles.txt) and playbooks
# (galaxy/<collection>/playbooks.txt) into a clean collection tree and build the
# artifact under build/: decdn.node is what a node operator runs
# (baseline + decdn_node + grafana_alloy), decdn.publisher what a publisher runs beside
# its origins (sponsord + sponsord_onramp + iroh_relay + iroh_dns_server). See
# galaxy/*/README.md. Publishing runs from a node-collection-vX.Y.Z or
Expand All @@ -281,8 +282,14 @@ build-node build-publisher: build-%:
# pipeline exit (set -o pipefail catches a crash / "No module named …" that prints
# no error keyword) AND the importer's explicit success line (it can log content
# errors while still exiting 0). Warnings are kept in build/importer-<collection>.log.
# Then galaxy/check-playbooks.sh installs the artifact and checks its playbooks as a
# consumer runs them (syntax by FQCN, the firewall holes their group_vars/ give),
# which the importer's ansible-lint, seeing one collection outside any collections
# path, cannot. decdn.publisher installs with the decdn.node it depends on.
galaxy-check: galaxy-check-node galaxy-check-publisher

galaxy-check-publisher: build-node

galaxy-check-node galaxy-check-publisher: galaxy-check-%: build-%
@set -o pipefail; \
tarball=$$(ls -1 build/decdn-$*-*.tar.gz); \
Expand All @@ -291,3 +298,4 @@ galaxy-check-node galaxy-check-publisher: galaxy-check-%: build-%
|| ! grep -q "Importer processing completed successfully" build/importer-$*.log; then \
echo "FAIL: galaxy-importer reported errors or did not complete (see build/importer-$*.log)"; exit 1; \
fi
./galaxy/check-playbooks.sh $*
Loading
Loading