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
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ jobs:
filters: |
# ci.yml is listed because the ansible jobs' cache wiring lives here, so a
# change to it must re-run them — same reason the helm filter claims it.
# galaxy-build runs examples/collection-fleet against the built
# collections (galaxy/check-playbooks.sh), so the example counts too.
ansible:
- 'ansible/**'
- 'examples/collection-fleet/**'
- '.github/workflows/ci.yml'
# The chart shares the schema-key inventory and checker with molecule,
# and renders monitoring/decdn-node/ through a symlink; lint-helm also
Expand Down Expand Up @@ -425,7 +428,9 @@ jobs:
- name: Script and guard tests
run: make test-scripts

# Lint the workflow files themselves.
# Lint the workflow files themselves, and the one examples/collection-fleet ships
# (actionlint finds only .github/workflows/ on its own). Named paths replace its own
# discovery: every workflow here is *.yml, and a *.yaml one would go unlinted.
actionlint:
runs-on: ubuntu-latest
timeout-minutes: 10 # ~15s in practice
Expand All @@ -438,3 +443,4 @@ jobs:
with:
reporter: github-check
fail_on_error: true
actionlint_flags: .github/workflows/*.yml examples/collection-fleet/.github/workflows/*.yml
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ repos:
files: ^(ansible|cloud-init)/.*\.(ya?ml)$
exclude: ^ansible/(collections|\.ansible)/

# examples/collection-fleet's inventory and requirements.yml, under its own
# .yamllint, as the yamllint hook it ships runs them in a copy.
- id: yamllint-collection-fleet
name: yamllint (examples/collection-fleet/)
entry: yamllint -c examples/collection-fleet/.yamllint
language: python
additional_dependencies: ["yamllint==1.35.1"]
files: ^examples/collection-fleet/(inventory/.*|requirements)\.ya?ml$

# Opt-in: full ansible-lint locally (needs `make -C ansible deps` first).
# pre-commit run ansible-lint --hook-stage manual
- id: ansible-lint
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ compose/ # one-host Docker Compose path (README.md)
charts/decdn-node/ # Helm chart (README.md); ci/ values mirror molecule/schema's three plays; files/monitoring -> ../../../monitoring/decdn-node (symlink); tests/render-test.sh (make lint-helm); artifacthub-repo.yml
monitoring/ # Grafana dashboards + Prometheus rules, maintained here by hand: decdn-node/ (rendered by the chart), sponsord/, iroh-relay/ (+ exported-metrics.txt); README.md
docs/ # cross-path operator docs: node-operators.md, publishers.md, requirements.md, lifecycle.md
examples/collection-fleet/ # consumer project for the collections; its inventory/ mirrors ansible/inventory/'s templates byte for byte except the hosts-*.yml.example comment headers (make test-scripts checks both parse to the same inventory): change both
examples/collection-fleet/ # consumer project for the collections, with its own CI, pre-commit and AGENTS.md; its inventory/ mirrors ansible/inventory/'s templates byte for byte except the hosts-*.yml.example comment headers (make test-scripts checks both parse to the same inventory): change both. release.sh moves its requirements.yml floors; its action SHAs and hook revs track this repo's (make test-scripts); galaxy/check-playbooks.sh runs it against the built collections
scripts/ # upstream mirrors (sync-network-profiles.py, gen-schema-keys.py), render-compose-alloy.sh, the decdn bump (open-decdn-bump.sh, bump_decdn.py), fetch-decdn-cli.sh, release.sh (+ cliff.toml, chart-artifacthub-changes.py), molecule.sh, limit-guard.sh
```

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; 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; and `examples/collection-fleet/inventory/` against `ansible/inventory/` (the same tracked files, byte for byte but for the hosts examples' comment headers; both hosts examples parse to the same inventory, needs ansible-core, failed in CI without it), and the ignore rules that keep a `secret.*` or a filled-in `hosts.yml` there out of git, with negative cases on a fixture. `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; and `examples/collection-fleet/inventory/` against `ansible/inventory/` (the same tracked files, byte for byte but for the hosts examples' comment headers; both hosts examples parse to the same inventory, needs ansible-core, failed in CI without it), and the ignore rules that keep a `secret.*` or a filled-in `hosts.yml` there out of git, with negative cases on a fixture; and that the example's `requirements.yml` floors are the collections' `galaxy.yml` versions and its CI and pre-commit pin this repo's action SHAs and hook revs, with negative cases. `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, 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) |
| `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, and run `examples/collection-fleet` against it as a copy of it runs (its hosts example parses under its `ansible.cfg`, and every playbook it names ships and passes a syntax check) (`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
9 changes: 7 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ would commit, and changes no file. The script:
(they are all `ci`, `test`, `style`, `build` or release commits, or unconventional
ones `--allow-unconventional` leaves out), whatever the level.
3. **Sets `version:`** in `ansible/galaxy/<collection>/galaxy.yml` or
`charts/decdn-node/Chart.yaml`.
`charts/decdn-node/Chart.yaml`. A collection release also raises that collection's
floor in `examples/collection-fleet/requirements.yml` to the new version, so a fleet
copied from the example starts from it (`make test-scripts` refuses a PR where they
differ). Until the tag's publish job uploads it, that floor names a version Galaxy
does not have, and a copy of the example cannot install: so a publish that fails
or waits is one to finish promptly.
The chart's `appVersion` and its `artifacthub.io/images` tag already follow the
role's `decdn_node_version` (`make test-scripts` refuses a PR where they differ), so
a new decdn line is a chart change too: release the chart. A decdn patch release
Expand Down Expand Up @@ -100,7 +105,7 @@ would commit, and changes no file. The script:
before anything is committed.
6. **Commits** (`--execute` only) `chore(release): <tag>` and tags `<tag>`, both
signed, and pushes them with `git push --atomic origin main <tag>`: origin gets both
or neither. If a check or the commit (a pre-commit hook, say) fails, both files are
or neither. If a check or the commit (a pre-commit hook, say) fails, the files are
restored and nothing is committed. If tagging or the push fails, or the script is
stopped after the commit, the commit (and the tag) stay local, and it says whether
origin got anything and how to undo. A refusal before step 5 never touches the
Expand Down
5 changes: 4 additions & 1 deletion ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ make deploy INVENTORY=../../decdn-fleet/hosts.yml LIMIT=<host>
Ansible loads `group_vars/` and `host_vars/` from beside whichever `hosts.yml` you pass, so
the overlay carries its own: create them next to its `hosts.yml`, starting from the public
`inventory/group_vars/` and `inventory/host_vars/` templates. Git-ignore
`host_vars/*/secret.*` in the private repo, so it never tracks a per-node secret. The public
`host_vars/*/secret.*` in the private repo, so it never tracks a per-node secret (and
`group_vars/*/secret.*` for a secret a whole group shares; a `group_vars/<group>/`
directory silently replaces a `group_vars/<group>.yml` beside it, so move that file to
`<group>/main.yml` first). The public
`inventory/group_vars/` and `inventory/host_vars/` are **not** loaded for it. (Deploying from
the Galaxy collections instead? [`examples/collection-fleet/`](../examples/collection-fleet/README.md)
is a ready-made private repository for that path, inventory templates and `.gitignore` included.) Add child groups
Expand Down
50 changes: 48 additions & 2 deletions ansible/galaxy/check-playbooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
# molecule/collection runs decdn.node's);
# - runs tests/firewall-holes/check.sh on its playbooks/, which runs a probe AS a
# collection playbook: the firewall holes must come out as they do from
# ansible/playbooks/, so the shipped group_vars/ load and agree.
# ansible/playbooks/, so the shipped group_vars/ load and agree;
# - runs examples/collection-fleet as a private copy of it runs (its ansible.cfg,
# the collection's hosts example as inventory/hosts.yml): the inventory and its
# vars files load, and every decdn.<collection>.<playbook> its README, AGENTS.md
# and CI name ships and passes a syntax check. Its tracked files only, so stage a
# new one first.
# galaxy-importer cannot do the first: its ansible-lint sees one collection, outside
# any collections path, so it only warns that it cannot find the roles.
# The roles' own dependencies (devsec.hardening, ansible.posix) come from `make deps`
Expand Down Expand Up @@ -51,7 +56,9 @@ export ANSIBLE_DEPRECATION_WARNINGS=0
# file from the caller.
mkdir "$work/no-roles"
export ANSIBLE_ROLES_PATH="$work/no-roles"
unset ANSIBLE_CONFIG
# No inventory from the caller either: the example's ansible.cfg names its own, and
# fails on one that does not parse.
unset ANSIBLE_CONFIG ANSIBLE_INVENTORY ANSIBLE_INVENTORY_UNPARSED_FAILED ANSIBLE_INVENTORY_ANY_UNPARSED_IS_FAILED

inventory="$ansible_dir/tests/firewall-holes/inventory.yml"
playbooks="$work/ansible_collections/decdn/$collection/playbooks"
Expand All @@ -65,4 +72,43 @@ for pb in "$playbooks"/*.yml; do
done
((n > 0)) || { echo "check-playbooks.sh: decdn.$collection ships no playbook" >&2; exit 1; }
echo "ok decdn.$collection: $n playbooks pass a syntax check by FQCN"

# examples/collection-fleet, as a private copy of it runs: its tracked files, its
# ansible.cfg (read from the copy's directory; ANSIBLE_COLLECTIONS_PATH stands in for
# its collections/), the collection's hosts example as inventory/hosts.yml. The
# inventory loads as its CI loads it (vars files included) and holds the hosts
# example's groups, so a cfg that stopped being read (no inventory: an implicit
# localhost, exit 0) fails here. Every decdn.<collection>.<playbook> its README,
# AGENTS.md and CI name must ship, and passes a syntax check against that inventory.
example="$ansible_dir/../examples/collection-fleet"
fleet="$work/fleet"
mkdir "$fleet"
git -C "$example" ls-files -z | (cd "$example" && xargs -0 cp --parents -t "$fleet") \
|| { echo "check-playbooks.sh: could not copy examples/collection-fleet" >&2; exit 1; }
hosts_example="hosts-$collection.yml.example"
cp "$fleet/inventory/$hosts_example" "$fleet/inventory/hosts.yml"
docs=(README.md AGENTS.md .github/workflows/ci.yml)
for f in "${docs[@]}"; do
[[ -f "$fleet/$f" ]] || { echo "check-playbooks.sh: examples/collection-fleet/$f is not tracked (git add it)" >&2; exit 1; }
done
# grep exits 1 on no match: that is the check below, not a failure here.
named="$(cd "$fleet" && { grep -ohE "decdn\.$collection\.[a-z_]+" "${docs[@]}" || (($? == 1)); } | sort -u)"
[[ -n $named ]] || { echo "check-playbooks.sh: examples/collection-fleet names no decdn.$collection playbook" >&2; exit 1; }
(cd "$fleet" && ansible-inventory --list </dev/null >"$work/inventory.json" 2>"$work/inventory.log") \
|| { cat "$work/inventory.log" >&2; echo "check-playbooks.sh: examples/collection-fleet's $hosts_example does not load" >&2; exit 1; }
groups=(decdn_nodes)
[[ $collection == publisher ]] && groups+=(decdn_origin_nodes)
for g in "${groups[@]}"; do
grep -qF "\"$g\": {" "$work/inventory.json" \
|| { echo "check-playbooks.sh: examples/collection-fleet's inventory has no $g group: was its ansible.cfg read?" >&2; exit 1; }
done
n=0
for fqcn in $named; do
[[ -f "$playbooks/${fqcn##*.}.yml" ]] \
|| { echo "check-playbooks.sh: examples/collection-fleet names $fqcn, which decdn.$collection does not ship" >&2; exit 1; }
(cd "$fleet" && ansible-playbook "$fqcn" --syntax-check </dev/null >"$work/syntax.log" 2>&1) \
|| { cat "$work/syntax.log" >&2; echo "check-playbooks.sh: $fqcn fails its syntax check in examples/collection-fleet" >&2; exit 1; }
n=$((n + 1))
done
echo "ok examples/collection-fleet with $hosts_example: the inventory loads and the $n decdn.$collection playbooks it names pass a syntax check"
"$ansible_dir/tests/firewall-holes/check.sh" "$playbooks"
36 changes: 36 additions & 0 deletions examples/collection-fleet/.github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
# Dependabot — keeps the SHA-pinned GitHub Actions and the pre-commit hook revs
# current.
#
# github-actions rewrites `uses: owner/repo@<sha> # vX.Y.Z` to the new sha + comment
# pre-commit bumps the `rev:` of each repo in .pre-commit-config.yaml
#
# Ansible Galaxy (requirements.yml) is not a Dependabot ecosystem.
#
# NOT covered, bump manually:
# - the deCDN collections in requirements.yml (read each CHANGELOG first)
# - the local yamllint hook's `additional_dependencies` pin
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: ci # conventional commits -> "ci(deps): ..."
include: scope
groups:
actions:
patterns: ["*"]
- package-ecosystem: pre-commit
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: ci
include: scope
groups:
pre-commit-hooks:
patterns: ["*"]
Loading
Loading