From 599f034efe115c9463cbcd3fc79827991dd32ba5 Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Sun, 12 Jul 2026 19:29:44 +0300 Subject: [PATCH 1/2] fix(decdn_node): create the fs cache-origin directory for kind: fs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `decdn_cache_origin_kind: fs`, the role rendered `[cache.origin] path = {{ decdn_cache_origin_path }}` into node.toml but never created that directory. The daemon's `FilesystemOrigin::new` fails fast if the base path is missing or is not a directory, and the unit is `Restart=always`/`RestartSec=5`, so a `kind: fs` deploy that didn't pre-create the dir out-of-band crash-looped on every (re)start. Add a `when: kind == fs` task that creates `decdn_cache_origin_path` (owner decdn:decdn, mode 0755) right after the existing data/cache/config dir task, making a `kind: fs` deploy self-contained. The sharded blob files under it ({path}/{hex[0..2]}/{hex}) are content, not config, and stay an operator concern — only the base dir is role-managed. Test coverage: switch the molecule `default` scenario from an http origin to an fs origin so the new task actually runs, and assert in verify.yml that the dir exists as decdn:decdn/0755 plus a node.toml [cache.origin] content check. http-origin template coverage is retained by the generate-keystore and slow-readiness scenarios. Closes #28 Co-Authored-By: Claude Opus 4.8 (1M context) --- ansible/molecule/default/converge.yml | 7 +++++-- ansible/molecule/default/verify.yml | 23 +++++++++++++++++++++++ ansible/roles/decdn_node/tasks/main.yml | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/ansible/molecule/default/converge.yml b/ansible/molecule/default/converge.yml index f14b9c8..b0e18b5 100644 --- a/ansible/molecule/default/converge.yml +++ b/ansible/molecule/default/converge.yml @@ -22,8 +22,11 @@ decdn_payment_channel_address: "0x1111111111111111111111111111111111111111" decdn_capacity_bond_address: "0x2222222222222222222222222222222222222222" decdn_slash_judge_address: "0x3333333333333333333333333333333333333333" - decdn_cache_origin_kind: "http" - decdn_cache_origin_url: "https://origin.example.invalid/" + # fs origin: exercises the role's "create the fs cache-origin base dir" task + # (#28) — /var/lib/decdn/origin does not pre-exist, so the run genuinely creates + # it (verify.yml asserts owner/group/mode). Keep in sync with verify.yml. + decdn_cache_origin_kind: "fs" + decdn_cache_origin_path: "/var/lib/decdn/origin" # Exercise baseline's named-sudo-users feature (see pre_tasks below). Throwaway # keys, generated for this test only — public keys, not credentials. Two users # on purpose: `alice.smith` has a '.' (so the sudoers.d filename-sanitize maps it diff --git a/ansible/molecule/default/verify.yml b/ansible/molecule/default/verify.yml index 34737ad..23dd0ad 100644 --- a/ansible/molecule/default/verify.yml +++ b/ansible/molecule/default/verify.yml @@ -90,12 +90,35 @@ if bad: print("node.toml content mismatch (got, want):", bad, file=sys.stderr) sys.exit(1) + # [cache.origin] is a nested table (3 levels), so check it separately from + # the flat two-level `want` lookups above. Mirrors converge.yml's fs origin. + origin = d.get("cache", {}).get("origin", {}) + if origin.get("kind") != "fs" or origin.get("path") != "/var/lib/decdn/origin": + print("node.toml [cache.origin] mismatch (got):", origin, file=sys.stderr) + sys.exit(1) - name: Assert node.toml is valid TOML and renders the expected content ansible.builtin.command: cmd: python3 /root/molecule-assert-node-toml.py changed_when: false + # The role must CREATE the fs origin base dir (converge sets kind: fs) — the + # daemon's FilesystemOrigin::new crash-loops if it is missing (#28). This dir + # does not pre-exist, so its presence + ownership proves the role's task ran. + - name: Stat the fs cache-origin base directory + ansible.builtin.stat: + path: /var/lib/decdn/origin + register: decdn_origin_dir + + - name: Assert the fs cache-origin dir exists (decdn:decdn, 0755, directory) + ansible.builtin.assert: + that: + - decdn_origin_dir.stat.exists and decdn_origin_dir.stat.isdir + - decdn_origin_dir.stat.pw_name == 'decdn' + - decdn_origin_dir.stat.gr_name == 'decdn' + - decdn_origin_dir.stat.mode == '0755' + fail_msg: "fs cache-origin dir missing / not a dir / wrong owner / wrong mode" + - name: Validate the systemd unit ansible.builtin.command: cmd: systemd-analyze verify /etc/systemd/system/decdn-node.service diff --git a/ansible/roles/decdn_node/tasks/main.yml b/ansible/roles/decdn_node/tasks/main.yml index d937783..1150e4d 100644 --- a/ansible/roles/decdn_node/tasks/main.yml +++ b/ansible/roles/decdn_node/tasks/main.yml @@ -205,6 +205,24 @@ loop_control: label: "{{ item.path }}" +# For a filesystem pull-through origin (kind: fs), the daemon's FilesystemOrigin::new +# fails fast if the base path is missing or is not a directory — a crash-loop on every +# (re)start. Create it here so a kind: fs deploy is self-contained. The sharded blob +# files under it ({path}/{hex[0..2]}/{hex}) are content, not config, and stay an +# operator concern — only the base dir is role-managed. 0755 (looser than the 0700 +# data dirs above) because pull-through origin blobs are public CDN content, not +# secrets, and an operator may populate the tree out-of-band as a different user. +- name: Ensure the fs cache-origin base directory exists (kind == fs) + ansible.builtin.file: + path: "{{ decdn_cache_origin_path }}" + state: directory + owner: "{{ decdn_user }}" + group: "{{ decdn_group }}" + mode: "0755" + when: + - decdn_cache_origin_kind == "fs" + - decdn_cache_origin_path | length > 0 + # --- Install the binaries ----------------------------------------------------- - name: Install decdn-node + decdn CLI ansible.builtin.import_tasks: install.yml From e3766b8ffc6824d59df9c4aac969ca175ae0e697 Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Sun, 12 Jul 2026 19:37:14 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(decdn=5Fnode):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20short-circuit=20fs-dir=20assert,=20correct=200755?= =?UTF-8?q?=20rationale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - verify.yml: combine the fs cache-origin dir assertions into one short-circuiting expression (file convention) so a missing dir routes to fail_msg instead of raising AnsibleUndefinedVariable on the absent pw_name/gr_name/mode — the regression this assert is meant to catch. - tasks/main.yml: reword the 0755 rationale — 0755 grants no write, so drop the misleading "populate as a different user" claim; state that the blobs are public content and 0755 only widens read access if the path is relocated outside the 0700 parent. Co-Authored-By: Claude Opus 4.8 (1M context) --- ansible/molecule/default/verify.yml | 12 ++++++++---- ansible/roles/decdn_node/tasks/main.yml | 9 ++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/ansible/molecule/default/verify.yml b/ansible/molecule/default/verify.yml index 23dd0ad..9a84d34 100644 --- a/ansible/molecule/default/verify.yml +++ b/ansible/molecule/default/verify.yml @@ -113,10 +113,14 @@ - name: Assert the fs cache-origin dir exists (decdn:decdn, 0755, directory) ansible.builtin.assert: that: - - decdn_origin_dir.stat.exists and decdn_origin_dir.stat.isdir - - decdn_origin_dir.stat.pw_name == 'decdn' - - decdn_origin_dir.stat.gr_name == 'decdn' - - decdn_origin_dir.stat.mode == '0755' + # Single short-circuiting expression (file convention — cf. lines 39-40, + # 59-61): a missing dir stops at `stat.exists` and routes to fail_msg + # instead of raising AnsibleUndefinedVariable on the absent pw_name/mode. + - >- + decdn_origin_dir.stat.exists and decdn_origin_dir.stat.isdir + and decdn_origin_dir.stat.pw_name == 'decdn' + and decdn_origin_dir.stat.gr_name == 'decdn' + and decdn_origin_dir.stat.mode == '0755' fail_msg: "fs cache-origin dir missing / not a dir / wrong owner / wrong mode" - name: Validate the systemd unit diff --git a/ansible/roles/decdn_node/tasks/main.yml b/ansible/roles/decdn_node/tasks/main.yml index 1150e4d..69f7f9e 100644 --- a/ansible/roles/decdn_node/tasks/main.yml +++ b/ansible/roles/decdn_node/tasks/main.yml @@ -209,9 +209,12 @@ # fails fast if the base path is missing or is not a directory — a crash-loop on every # (re)start. Create it here so a kind: fs deploy is self-contained. The sharded blob # files under it ({path}/{hex[0..2]}/{hex}) are content, not config, and stay an -# operator concern — only the base dir is role-managed. 0755 (looser than the 0700 -# data dirs above) because pull-through origin blobs are public CDN content, not -# secrets, and an operator may populate the tree out-of-band as a different user. +# operator concern — only the base dir is role-managed. mode 0755 (looser than the +# 0700 data dirs above) per issue #28: the pull-through origin blobs are public CDN +# content, not secrets, so world-readable is harmless. It grants no write — the +# daemon reads as decdn (owner) and out-of-band population runs as decdn or root +# regardless — and in the default layout the 0700 parent (/var/lib/decdn) still +# blocks traversal; 0755 only widens read access if an operator relocates the path. - name: Ensure the fs cache-origin base directory exists (kind == fs) ansible.builtin.file: path: "{{ decdn_cache_origin_path }}"