From 155b35ec547851a4159ecb7916bf82e2a04c7c4e Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Thu, 8 Oct 2026 02:09:37 +0200 Subject: [PATCH] fix(sys-svc-compose): replicate rendered templates to every swarm node The swarm branch placed each rendered file on the peers through a body gated `when: not (IS_STACK_HOST | bool)`, which assumes the play carries the worker hosts. A role included as a partner of another stack runs on the stack host alone, so that body only ever evaluated on the manager, where it skipped. The odoo round of run 37553816857 shows both halves: create /opt/compose/nextcloud/volumes/infinito -> changed: [mgr-01] [swarm] Ensure parent directory exists on peer -> skipping: [mgr-01] x16 sixteen skips, one per template, and no worker touched. web-app-nextcloud mounts that directory as a `type: bind` volume of php config fragments, so the scheduler put nextcloud on a worker and swarm answered invalid mount config for type "bind": bind source path does not exist: /opt/compose/nextcloud/volumes/infinito Address the nodes through the node_hosts lookup instead. 7d9a63c98b introduced it for this failure class in svc-ai-s1 and svc-ai-libretranslate-engine, where the symptom was the same and the tell was the same single result line for the manager; the template path was the one place still deciding the host set from the play. The block stays behind IS_SWARM_MODE, so compose is untouched, and the lookup degrades to the current host where no swarm group exists. The node set is a strict superset of the previous one: the manager is added, which the render task already wrote to, and svc-swarm-node carries manager and workers only, so no file reaches a host class that did not already hold it. Co-Authored-By: Claude Opus 5 (1M context) --- .../utils/_render_one_template_apply.yml | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/roles/sys-svc-compose/tasks/utils/_render_one_template_apply.yml b/roles/sys-svc-compose/tasks/utils/_render_one_template_apply.yml index 5a98d244af..2d05c9ecd7 100644 --- a/roles/sys-svc-compose/tasks/utils/_render_one_template_apply.yml +++ b/roles/sys-svc-compose/tasks/utils/_render_one_template_apply.yml @@ -31,20 +31,24 @@ register: _role_templates_slurp no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" - - name: "[swarm] Place '{{ _tpl.dest }}' on the non-manager peers" - when: not (IS_STACK_HOST | bool) - block: - - name: "[swarm] Ensure parent directory exists on peer for '{{ _tpl.dest }}'" - ansible.builtin.file: - path: "{{ _tpl.dest | dirname }}" - state: directory - mode: "0755" + # Exception: a role included as a partner runs on the stack host alone, so a `not IS_STACK_HOST` body never evaluates anywhere and every worker stays without the file. + - name: "[swarm] Ensure parent directory of '{{ _tpl.dest }}' exists on every node" + ansible.builtin.file: + path: "{{ _tpl.dest | dirname }}" + state: directory + mode: "0755" + delegate_to: "{{ item }}" + loop: "{{ query('node_hosts') }}" + run_once: true - - name: "[swarm] Distribute '{{ _tpl.dest }}' to swarm peers" - ansible.builtin.copy: - content: "{{ _role_templates_slurp.content | b64decode }}" - dest: "{{ _tpl.dest }}" - owner: "{{ _tpl.owner }}" - group: "{{ _tpl.group }}" - mode: "{{ _tpl.mode }}" - no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}" + - name: "[swarm] Distribute '{{ _tpl.dest }}' to every node" + ansible.builtin.copy: + content: "{{ _role_templates_slurp.content | b64decode }}" + dest: "{{ _tpl.dest }}" + owner: "{{ _tpl.owner }}" + group: "{{ _tpl.group }}" + mode: "{{ _tpl.mode }}" + delegate_to: "{{ item }}" + loop: "{{ query('node_hosts') }}" + run_once: true + no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"