Commit Graph
88 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 fa48411ae5 fix(sys-svc-compose): wipe stale NFS volume state on dev/act swarm redeploy
Swarm reuses NFS-backed state across cluster rebuilds. A role then reads config persisted from a prior cluster while its database (a node-local volume) is fresh, causing install drift and stale-credential loops. Wipe each NFS-backed volume subdir before pre-create, gated to dev/act via RUNTIME so production state is never touched.

Hoist the shared swarm and NFS guards to the include's when, and fold the per-item mapping check into the loop via selectattr, so the two tasks no longer repeat identical conditions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 14:09:12 +02:00
kevinveenbirkenbachandClaude Opus 4.8 76935d1cbe fix(web-app-discourse): give the swarm launcher a node-local bridge network
The discourse launcher is a standalone container (docker run via ./launcher), not a swarm service, so it cannot attach to swarm-scoped overlay networks. In swarm it was left without a reachable network for its shared pg/redis/openresty peers.

The shared network util now creates a node-local bridge instead of an overlay when the role declares networks.local.force_bridge in meta/server.yml, read via the config lookup with a false default so every other caller stays byte-identical. Discourse sets force_bridge: true and connects the rebuilt launcher to the shared overlays (web/pg/redis) so swarm services stay reachable; the launcher config drops the explicit --network flags in swarm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 00:50:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c453cd2029 feat(sys-svc-compose): dump service ps/logs on swarm converge-timeout
wait_converged failed after 600 silent retries (rc=1, empty) with no clue why. Wrap the wait in a rescue that, on the manager, dumps 'container service ps --no-trunc' + 'container service logs --tail 50' and aborts with a root-cause message, so converge failures (e.g. collabora's coolmount permission error) surface immediately instead of after 20 minutes of silence. Swarm-gated; the success path is unchanged and compose skips the block entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:55:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0e973338d6 fix(sys-svc-compose): pre-create NFS dirs for sidecar volumes too
nfs_prep looped lookup('volume', application_id), which returns only the role's own meta/volumes.yml and omits sidecar volumes the stack also mounts (e.g. <entity>_redis from svc-db-redis / an sso-proxy). Their backing dir was never created, so swarm rejected the mount and the sidecar stayed 0/1 (fusiondirectory/postmarks/snipe-it 500/502). Loop the rendered compose_volumes set instead, which includes the sidecars; manager-pinned roles still skip via the placement gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:02:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d5d17f241b refactor(swarm): shared resolve_host_cid + wait_converged utils
Replace the per-role swarm host-node/container-id resolve duplicated across fider, matomo, decidim, keycloak (login + realm-import), collabora and listmonk with two shared task utils under roles/sys-svc-compose/tasks/utils/swarm/. resolve_host_cid sets <prefix>_HOST_NODE / <prefix>_LOCAL_CID (swarm: converge + node + cid delegated to the node; compose: inventory host + given cid); wait_converged is the bare convergence gate.

Teach test_variable_definitions to read templated set_fact keys like {{ prefix }}_HOST_NODE as a *_HOST_NODE name pattern (guarded so a pure {{ x }} key cannot become a catch-all), and add the playbook_dir-relative include load-failure exemptions to .ansible-lint-ignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5687643b46 style(deployment-mode): positive == 'compose' gates + enforce via lint
DEPLOYMENT_MODE is binary (swarm / compose), so a negated `!= 'swarm'` gate
always means `== 'compose'`. Replace every negated gate across roles/ and
group_vars with the positive form for readability, and add a lint
(tests/lint/ansible/variables/test_no_deployment_mode_negation.py) that forbids
`DEPLOYMENT_MODE !=` in .yml/.yaml/.j2 with a `# nocheck:
deployment-mode-negation` escape.

The compose-verb / chdir / jinja-gate lint guidance messages now suggest the
positive form too; the gate matcher (utils/annotations/task_gate.py) and its
test inputs keep recognising the legacy `!= 'swarm'` so a nocheck-marked
exception is still detected as compose-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:31:36 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8000dbe947 fix(swarm): NFS and overlay deploy correctness
Widen the openresty fan-in overlay /28 -> /24 (the /28 exhausted VIPs under mailu's services, deadlocking the stack). Pre-create NFS backing dirs for every volume of unpinned roles (placement decides, not the legacy per-volume nfs flag). Make email 'external' cluster-wide so swarm workers relay through the central mailu instead of a localhost root sender. Constructor: auto-place svc-prx-openresty, gate out unconsumed central engines, and load the svc-dns role group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:29:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b09598dc80 feat(engine): add central engine lookup + provisioning machinery
Foundation for the central svc-db-*/svc-dns-* engine roles: the engine lookup and engines descriptor, per-consumer engine credentials in the inventory manager, shared-net overlay rendering, container_depends_on, the sys-svc-engine provisioning dispatcher, and the compose_volumes NFS-placement split. base.yml.j2 gates the embedded redis sidecar on lookup('engine','redis',application_id,'local').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:29:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 64eef34401 fix(lint): revert leaf-only keyword hoists apply: cannot carry
Follow-up to 37583e807. apply: on include_tasks wraps the included tasks in a Block, so it accepts only Block-valid keywords. The earlier broad hoist wrongly moved leaf-only keywords (changed_when, delay, async, poll) into apply:, which Ansible rejects at parse time ("'<kw>' is not a valid attribute for a Block") and would have broken every compose deploy at the post_deploy include.

Revert changed_when/delay (post_deploy.yml + its 3 include sites), changed_when (bigbluebutton, moodle x2), and async/poll (sys-svc-dns) back onto the per-task lines; the include sites keep only the Block-valid apply: keys (delegate_to, run_once) or revert to the shorthand include. test_include_uniform_apply_keyword now blacklists the leaf-only keywords (async/changed_when/delay/failed_when/poll/retries/until) so only Block-valid keywords are flagged for hoisting.

Verified: no apply: block contains a leaf-only keyword; lint test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:44:37 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d074d7c278 test(lint): hoist uniform per-task keywords to include_tasks apply:
Add tests/lint/ansible/tasks/test_include_uniform_apply_keyword.py: when a file pulled in via a dynamic include_tasks carries the same hoistable task keyword on every top-level task, that keyword should be set once via apply: on the include site instead of being repeated per task. The rule is blacklist-based: it flags any uniform Ansible task keyword except the per-task-binding ones (name, register, args, vars, when, loop), and never flags the module itself.

Hoist every existing occurrence to apply: across the affected roles (delegate_to, run_once, become, notify, no_log, changed_when, delay, async, poll), dropping the per-task duplication. Behaviour is identical because apply: propagates each keyword to every included task; no_log masking via apply: was empirically verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:44:37 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6e49fe74fb feat(volumes): canonical dict-of-dicts meta schema + lookup('volume')
Migrate every role's meta/volumes.yml from ad-hoc dict-of-named-volumes
to a canonical dict-of-dicts schema where the YAML key is the semantic
short name and the entry body carries type / name / source / mounts /
nfs / mode / read_only. The optional 'name:' field is the docker volume
name (defaults to the key); the previous 'docker_name:' field is gone.

What's in this commit (210 paths):

Infrastructure (23)
- New filter plugins/filter/{compose,container}_volumes.py — render the
  top-level volumes:/configs:/secrets: section and per-service mount
  blocks from the canonical meta. container_volumes uses the SEMANTIC
  name as per-service mount source so it resolves against compose_volumes'
  top-level dict key (docker_name as source produced 'undefined volume'
  on swarm).
- New lookup plugins/lookup/{compose,container,volume}_volumes.py.
  volume(application_id, name) returns a normalised single entry;
  volume(application_id) returns the whole canonical dict.
- utils/cache/applications.py keeps a sibling registry
  _CANONICAL_VOLUMES_BY_ROLE outside the merged applications payload so
  raw Jinja source: strings never enter the templar walk (root-cause of
  the 200x cache-miss + recursive get_merged_applications storm).
- utils/roles/applications/mounts.py — schema validator + normaliser;
  list-shape is rejected.
- plugins/lookup/nginx.py: _volume_path_by_name reads canonical directly.
- scripts/tests/deploy/swarm/_context.sh: NFS_VOLUMES extraction handles
  both legacy dict and canonical dict shapes.
- roles/svc-prx-openresty/handlers/restart_and_validate.yml: gate
  compose-only validate+reload behind a single when-block; swarm uses
  service-update instead (bind-mounts of a FILE pin source inode at
  task-create time, so atomic write-tmp+rename leaves the running task
  on a deleted inode).
- roles/sys-svc-compose/tasks/utils/nfs_prep.yml: switch to
  lookup('volume', application_id) | dict2items so NFS subdir
  pre-creation works under the new cache split.

Tests (12)
- New strict lints flagging inline mount sections in compose templates,
  single-file bind mounts, and legacy lookup('config', ..., 'volumes.X.Y')
  call sites — all volume-mount surfaces MUST flow through container_volumes
  / compose_volumes / volume lookups.
- Integration: test_volumes_compose_chain + test_volume_constants_are_lookups
  accept both the legacy and the new lookup('volume', app, 'X').name forms
  during the migration window.
- Unit tests for the new lookup, filter, mounts helper, plus updated
  nginx lookup test.
- Prometheus role-wiring test reads canonical meta for the
  alert_rules.yml bind mount.

Role migrations (175)
- 62 meta/volumes.yml converted to dict-of-dicts canonical shape
- 63 templates/compose*.yml.j2 replace inline volumes:/configs:/secrets:
  with lookup('container_volumes', application_id, '<service>') |
  indent(N) per service + a single trailing lookup('compose_volumes',
  application_id) for the top-level block
- 50 vars/main.yml drop dead volume constants and refactor 23 remaining
  legacy lookup('config', ..., 'volumes.X.Y') to lookup('volume',
  application_id, 'X').Y across 9 roles

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 19:08:46 +02:00
kevinveenbirkenbach e3645e2ea3 fix(swarm-nfs): pre-create openresty bind sources + SPOT NFS state path + reorder bootstrap
- roles/svc-prx-openresty/tasks/01_core.yml: extend the existing
  nginx-config pre-create into a loop that covers all seven host
  bind-mount sources openresty's compose template references (nginx
  config + base, www data, well-known, letsencrypt webroot + base,
  TLS self-signed base). docker swarm refuses to start containers
  whose bind-mount sources do not exist on the target node; compose
  silently created them, so the gap surfaced only under swarm.

- roles/sys-svc-compose/tasks/utils/nfs_prep.yml: switch the per-volume
  pre-create from `storage.nfs.export_base` to the SPOT
  `STORAGE_NFS_STATE_PATH`, matching the rest of the repo's NFS-path
  references.

- tasks/stages/01_constructor.yml: swap the bootstrap order so
  `svc-storage` runs before `svc-registry`. The registry's volume needs
  the NFS layer ready first; the prior order caused intermittent
  init races where the registry created its volume on local disk
  before NFS was mounted, hiding the actual cluster volume. Also move
  the sys-token-store include after the bootstrap loop so it can use
  the now-mounted shared state.
2026-06-14 19:08:43 +02:00
kevinveenbirkenbach 39f4bc6685 feat(swarm): align mariadb/postgres tasks + post-deploy gate with container_address SPOT
Tier-1 changes verified live in act-12 (mariadb stack converged, postgres
stack started, swarm-readiness gate fired correctly for docker/mariadb/
postgres/openresty).

* `plugins/filter/compose_volumes.py` + `plugins/lookup/compose_volumes.py`:
  thread `dir_var_lib` through filter (required keyword, no default)
  and lookup (reads from `vars_["DIR_VAR_LIB"]`, raises if missing). Replace
  native NFS driver with a bind-mount from the host's already-NFS-mounted
  `DIR_VAR_LIB` because native `type: nfs` re-handshakes portmap, which is
  unreachable in nested DinD. Drop the FilterModule wrapper so the deprecated
  pipe form keeps failing fast per the lookup-only SPOT contract.

* `tests/unit/plugins/{filter,lookup}/test_compose_volumes.py`:
  thread `DIR_VAR_LIB` through every fixture (no default-fallbacks).

* `roles/sys-svc-compose/tasks/utils/post_deploy.yml`:
  compose-mode db-health probe gated to `DEPLOYMENT_MODE \!= 'swarm'`; new
  swarm-mode convergence gate runs the extracted `swarm_stack_ready.sh`
  via `ansible.builtin.script` with `STACK` env, addressed by absolute
  `playbook_dir`-rooted path so the include resolves from caller roles.

* `roles/sys-svc-compose/files/swarm_stack_ready.sh` (new): probe
  `docker stack services` for a single `STACK` and exit non-zero until
  every service's running/desired replica count matches.

* `roles/svc-db-mariadb/vars/main.yml`: new `MARIADB_CONTAINER_ADDRESS`
  constant wraps the `container_address` lookup so callers reference one
  SPOT instead of duplicating the lookup.

* `roles/svc-db-mariadb/tasks/01_core.yml` + `02_init.yml`:
  switch root-password probes and bootstrap SQL calls to
  `container exec -i {{ MARIADB_CONTAINER_ADDRESS }} sh -c 'mariadb -p"$MARIADB_ROOT_PASSWORD"'`.
  Using the container's own `MARIADB_ROOT_PASSWORD` env var mirrors the
  healthcheck and matches whatever quoting Docker swarm preserves from
  the env_file, sidestepping the compose-vs-swarm env_file quote-strip
  divergence.

* `roles/svc-db-postgres/files/set_postgres_superuser_password.sh`:
  `eval "container=$container_raw"` so the swarm-mode lookup payload
  `"$(/usr/bin/resolve-container-id <stack> <svc>)"` is re-evaluated by
  bash and resolves to the live task id at exec time.

* `roles/svc-db-postgres/tasks/01_core.yml`: rename arg to use the new
  swarm-aware `POSTGRES_CONTAINER_ADDRESS`.

* `utils/tests/swarm/extend_inventory.py`: when the seed app's primary
  service declares `default_placement: manager`, do not add the app to
  the worker group, so the swarm test inventory matches the
  single-replica placement-pinned semantics.
2026-06-14 19:08:43 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b18e939016 fix(sys-svc-compose): pre-create parent dir before delegated template render
Resolves Cluster D (4 swarm jobs from CI run 27063963773):
flowise, kix, mediawiki, nextcloud all failed with
"msg: Destination directory <path> does not exist" because their
templates render to non-standard subdirs (config/, volumes/mw-local/,
volumes/infinito/, volumes/) that aren't part of the default
sys-svc-compose dir-creation flow.

Pre-create task uses identical delegate_to: STACK_HOST + run_once + when
as the subsequent template task, so the parent always exists on the
correct host with no possible race.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:02:58 +02:00
kevinveenbirkenbachandClaude Opus 4.7 05f6a97bd8 refactor(stack_host_template): generalise IS_STACK_HOST template wrapper across the codebase
Renames the local action plugin from `conf_template` to
`stack_host_template` so the name carries the actual semantics
(IS_STACK_HOST gate, not nginx config), broadens the lint guard to
every bare `template:` task, and migrates every consumer found in the
audit.

Plugin / tests / lint:
  * plugins/action/conf_template.py -> plugins/action/stack_host_template.py
    (drop nginx-specific docstring; trust_as_template gate unchanged)
  * test_conf_template.py            -> test_stack_host_template.py
  * test_conf_template_migration.py  -> test_stack_host_template_migration.py
    Scope expanded from `.conf.j2` to every `template:` task; marker
    renamed to `nocheck: stack-host-template`.

Migrations (~50 tasks now using stack_host_template:):
  * sys-svc-compose/04_files.yml: compose.yml, .env, Dockerfile,
    compose.override (every per-app compose-tree file)
  * web-app-keycloak: sso_proxy upstream-config (fixes the friendica
    CI failure that prompted this refactor) + realm-import
  * web-app-taiga, web-app-matrix, web-app-opentalk, web-app-pgadmin,
    web-app-moodle, web-app-listmonk, web-app-littlejs, web-app-fediwall,
    web-app-wordpress, web-app-discourse, web-app-bigbluebutton,
    web-app-jenkins, web-app-jitsi, web-app-mobilizon, web-app-suitecrm,
    web-app-decidim, web-app-dashboard, web-app-nextcloud, web-app-xwiki,
    web-app-mailu, web-app-magento, web-app-odoo, web-app-prometheus,
    web-app-matrix, web-app-peertube, web-app-mailu (+ existing `conf_template:`
    callers updated to the new name)
  * sys-front-tls, svc-db-openldap, sys-svc-webserver-core,
    sys-svc-rdbms (dedicated DB env + initdb), sys-front-inj-css,
    sys-front-inj-dashboard, sys-front-inj-all, sys-stk-front-proxy,
    web-svc-cdn-css, web-svc-xmpp, web-svc-legal, web-svc-simpleicons,
    web-opt-rdr-domains

Nochecks (per-host or already-structurally-stack-host):
  * sys-svc-mail-smtp/postfix, sys-svc-sshd, sys-svc-container/daemon.json
  * sys-timer, sys-service, sys-svc-mail-msmtp, sys-svc-journalctl,
    sys-daemon, sys-svc-compose/utils/_render_one_template (delegate_to
    stack host with peer slurp dance)
  * dev-locales, dev-nix, desk-*, user, user-backup, user-workstation
  * svc-storage-nfs-server/exports, test-e2e-playwright stage dirs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 15:45:38 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fdf0fed9c4 fix(test-e2e-playwright): delegate to STACK_HOST instead of IS_STACK_HOST gate
The IS_STACK_HOST gate in main.yml never blocked workers because
test-e2e-playwright does not set its own application_id, so DEPLOYMENT_MODE
fell through to 'compose' and IS_STACK_HOST evaluated true on every host.
Workers then ran 02_run_one.yml and crashed on the CA cert check because
the cert only exists on the manager.

Replace with block: + delegate_to: STACK_HOST + run_once: true so the
orchestration runs once on the primary manager regardless of which hosts
are in the play.

Also rename SWARM_PRIMARY_MANAGER -> STACK_HOST repo-wide to match the
existing IS_STACK_HOST symmetry and drop the swarm-specific terminology
(the variable falls back to inventory_hostname in compose mode).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 17:07:20 +02:00
kevinveenbirkenbach c41102a4f2 fix(sys-svc-compose): delegate replicated-template render to manager
_render_one_template.yml gated render on IS_STACK_HOST and relied on
the manager being in the same play to actually execute it. nextcloud's
flow hits this in a worker-only play (uid/group fixups + config
render together), so manager was not in the play, render skipped on
both workers, and the very next task ([swarm] Slurp rendered ... from
manager) failed with File not found because the file was never
written. Affected job in CI run 27006276488: web-app-nextcloud.

Switch render to delegate_to SWARM_PRIMARY_MANAGER + run_once so it
fires once on the manager regardless of which hosts are in the play.
The pattern mirrors the slurp + copy-to-peers tasks below; in compose
mode SWARM_PRIMARY_MANAGER resolves to inventory_hostname so the
single-host case is unchanged.
2026-06-05 13:52:30 +02:00
kevinveenbirkenbach 77e4f288c0 refactor: rename SWARM_MANAGER_ACTIVE to IS_STACK_HOST
The old name suggested "is the swarm manager up?" but the variable
actually answers "is this host the one that holds the compose stack?".
It is true on the single host in compose mode and on every swarm
manager in swarm mode. The SWARM_ prefix is also misleading in compose
mode where no swarm exists.

Replace every reference across group_vars and the gating call sites
(sys-svc-compose, web-app-espocrm, web-app-bigbluebutton). Definition
itself moves to group_vars/all/18_swarm.yml line 6; the body is
unchanged so the truth value at runtime stays identical for both modes.
2026-06-05 11:12:13 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3b7bbfa71d refactor(swarm): SPOT-define SWARM_MANAGER_ACTIVE + SWARM_PRIMARY_MANAGER
Collapse two repeated swarm-vs-compose expressions into single-source-of-truth
group vars and apply across sys-svc-compose, web-app-mediawiki:

- SWARM_MANAGER_ACTIVE: true when DEPLOYMENT_MODE \!= 'swarm' or this host is
  in svc-docker-swarm-manager. Used as the gate for "execute manager-only
  deploy work here" (dir creation, file render, compose up, handler tasks).

- SWARM_PRIMARY_MANAGER: first svc-docker-swarm-manager or self under
  compose-mode fallback. Used as delegate_to: for cluster-wide ops
  (compose pull/build/up, swarm stack deploy, post-deploy DB health probe,
  network create, MediaWiki one-shot install/admin).

Pure refactor — no semantic change. Subsequent edits to either expression
now live in one file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9cacfbf959 refactor(swarm): inventory-driven topology replaces per-role swarm flags
DEPLOYMENT_MODE is now derived per role from group membership:
swarm iff len(groups[application_id]) > 1, else compose. The cluster
infrastructure flag SWARM_CLUSTER_AVAILABLE is true whenever any host
sits in svc-docker-swarm, so DB and proxy services render attachable
overlay networks even when their own group lists only one host.

All service_is_stateful flags are removed across svc-db-mariadb,
svc-db-postgres, svc-db-openldap, svc-prx-openresty, svc-docker-registry
and svc-db-mariadb's compose template; the inventory shape now expresses
the same intent. Cluster-wide handlers (swarm build/push/validate/deploy,
external network creation, DB healthcheck) gain run_once + delegate_to
the manager so they execute exactly once regardless of how many hosts
the role's play targets. CA-injection and compose validate are gated to
compose-mode only; their compose-config strictness breaks when the
deploy block coexists with container_name under swarm.

Dead per-host swarm-resolve infrastructure is removed: swarm_resolve_
container.sh, resolve_container.yml, resolve_self.yml, the MariaDB-
specific swarm_mariadb_cid.sh, the cross-host template replication
step, and the _swarm_retries / _swarm_delay defaults. With run_once +
delegate_to: manager replacing the old "render every template on every
swarm peer" pattern, every consumer becomes a noop on workers and
collapses to the manager's one-shot path.

The web-app-nextcloud memcache override mapping flips from the legacy
"single" sentinel to "compose" (semantically equivalent under the new
model). Requirement 023 is updated to reflect the new model and drops
the now-irrelevant per-role opt-out / placement-constraint clauses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1324f26407 refactor(service_is_stateful): drop | default(false) now that the flag is SPOT-defined
`service_is_stateful` is set unconditionally to `false` in
group_vars/all/18_swarm.yml; per-role overrides in vars/main.yml win by
Ansible precedence. The `| default(false)` fallback is no longer reachable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 10:06:20 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fa4eea5a86 refactor(deployment-mode): SPOT-define DEPLOYMENT_MODE in group_vars
Replace the host-scoped set_fact resolvers in sys-svc-compose/tasks/01_core.yml
and tasks/stages/01_constructor.yml with a single group_vars definition driven
by group_names. Uppercase the variable to match the SPOT convention and drop
every `| default('compose')` fallback now that the resolution is unconditional.

Role-var opt-out on a swarm host still works via Ansible's role-vars > group_vars
precedence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 10:06:20 +02:00
kevinveenbirkenbachandClaude Opus 4.7 deefd6dff1 fix(sys-svc-compose): swarm task resolve works across nodes and skips stateful services
Three coupled fixes for the Controller≠Target topology the swarm pilot
exposed in iter12, iter15, and iter17/iter18:

1. `swarm_resolve_container.sh` now locates task containers via the
   swarm state API (`docker service ps`, then `docker inspect TASK
   --format '{{.Status.ContainerStatus.ContainerID}}'`) instead of a
   local `docker ps --filter label=...` on the manager. A replica that
   landed on a worker stayed invisible to the manager-side filter and
   the role-level wait spun out after 60 retries (iter17 / iter18).
   The state check accepts both `running` and `starting` so the
   resolve unblocks before the app's healthcheck has had to pass —
   first-deploy apps like mediawiki need their install_or_update.sh
   step BEFORE they can become healthy.

2. `resolve_container.yml` invokes the script via an absolute
   `{{ playbook_dir }}/roles/sys-svc-compose/files/...` path. When the
   include_tasks was pulled in from another role (e.g. svc-db-mariadb's
   02_init.yml), ansible's `ansible.builtin.script` file-lookup
   searched the calling role's `files/` directory and aborted with
   "Could not find or access 'swarm_resolve_container.sh'" (iter15).

3. `post_deploy.yml` gates the swarm task resolve on
   `not service_is_stateful`. Stateful services (svc-docker-registry,
   svc-db-mariadb) deploy as plain compose containers on the manager,
   not as replicated swarm services — there is no `STACK_SERVICE` task
   to resolve, so the role spun out for 60 retries waiting for one
   that would never exist (iter12).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 09:53:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5adf3011fe refactor(swarm-nfs): generalize swarm+NFS chain across all roles
Migrate the mediawiki-specific swarm patterns into shared sys-svc-compose
helpers so every role inherits swarm + NFS portability:

- meta/volumes.yml: dict schema with mandatory name|path + optional nfs
  block (uid/gid/mode); 47 roles migrated, 48 lookup consumers updated to
  volumes.<key>.{name,path}
- sys-svc-compose helpers: finalize.yml (flush + post_deploy),
  post_deploy.yml (swarm-resolve + DB-healthy-wait), nfs_prep.yml
  (pre-create NFS subdirs), swarm/resolve_self.yml (canonical
  <ENTITY>_CONTAINER fact), render_replicated_templates.yml +
  _render_one_template.yml (template render + swarm-peer replication,
  per-entry notify, fileglob support)
- 15 roles migrated to render_replicated_templates.yml helper
  (mediawiki, prometheus, kix, friendica, dashboard, nextcloud,
  suitecrm, matrix, flowise, openldap)
- 5 new lint tests pin the architecture: volumes schema, compose chain,
  volume-constants-are-lookups, template-render-centralization,
  meta-no-role-var-refs
- ROLE_FILE_TEMPL_COMPOSE SPOT constant + 7 tests migrated to use it
- Handler test scanner extended to detect nested notify in
  vars: role_templates: lists

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:57:35 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3c07721303 feat(swarm-nfs): bootstrap Docker Swarm + NFS-backed shared volumes (#023)
Adds the svc-docker-swarm, svc-docker-swarm-manager, svc-storage-nfs-server
and svc-storage-nfs-client roles, the compose_volumes filter NFS support,
the test-deploy-swarm-nfs CI workflow with its swarm-nfs-pilot script set,
and the deploy_mode-aware compose/swarm wiring across mediawiki, mariadb
and sys-svc-* helpers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:57:34 +02:00
kevinveenbirkenbachandClaude Opus 4.7 19254a23b4 refactor(sys-svc-compose): rename docker_compose_flush_handlers → compose_handlers_flush
Pure 1:1 rename (69 insertions / 69 deletions across 66 files) to
finish the ``docker_*`` → ``compose_*`` alignment started in
8373468e9. The flag configures whether the compose include flushes
handlers right after run; the ``docker_`` prefix was misleading
(handlers are Ansible-side, not docker-side) and the variable is part
of the same ``sys-svc-compose`` SPOT cluster as ``compose_repository_*``.

Touches the role default, the implementation
(``sys-svc-compose/tasks/utils/up.yml``), all 60+ consumer roles,
the network-utility wrapper + its README, and the
``test_network_create_via_util.py`` lint (both the docstring guidance
and the asserted diff).

Breaking change for any external override that still sets the old
name — no alias is provided.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:43:43 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16bdbf03d7 docs: fix repo-wide drift against documentation.md style rules
Three classes of drift cleaned up across 85 .md files:

- Em-dashes: replaced ` — ` with the contextually correct separator
  (colon for `term: explanation`, period to split clauses across
  sentences/source lines, parentheses for asides, semicolon for
  tight-linked continuations). Two intentional exceptions remain
  in `docs/contributing/documentation.md` (the rule literal) and
  inside a fenced ```sh block in `docs/contributing/actions/debugging/ci.md`.
- SPOT self-labels: rewrote "This is the SPOT for X" to describe
  the subject directly ("This directory holds X" / "This file
  describes X"). Cross-references to other artefacts' canonical
  role remain unchanged.
- Heading-emoji position: moved leading-emoji headings (`# 🧩 Title`)
  to trailing position (`# Title 🧩`) per the headline rule that
  emojis MUST come after the title text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 375a5e8f05 fix(sys-svc-compose/network/create): pre-label docker_network to match compose's lifecycle check
The per-role default network is created externally by `community.docker.docker_network`
in this task before `compose up` runs. Compose then refuses to take
over the existing network with:

    network <name> was found but has incorrect label
    com.docker.compose.network set to "" (expected: "default")

Reproduced in CI run 25890032686 for web-app-matrix, web-app-gitea,
and — as a downstream of the same compose-up failure — web-app-mediawiki
(whose mariadb dependency never started).

Apply the two labels compose writes itself when it owns a network's
lifecycle: `com.docker.compose.network=default` (the network short name
under the top-level `networks:` block in our templates is `default`,
overridden to the per-entity docker name via the `name:` field) and
`com.docker.compose.project=<entity>` (matches both the explicit
`--project` flag on `compose pull` and the `basename(chdir)` default
used by `compose up` in this repo).

Different mechanism than commit 762586a03 (which set `external: true`
on the compose template and was reverted by 602c05ed3 because it broke
the first-deploy path for roles that did not go through this task) —
this fix keeps the network compose-managed at the template level and
only adds the labels the daemon would have emitted on its own.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbach a3402eac5b test(lint): group ansible lints into per-concern subdirs + add dotted-path test
Restructures tests/lint/ansible/ into five concern-keyed subdirectories
(jinja/, modules/, services/, shell/, sql/) plus the pre-existing roles/
and variables/. Each new subdir ships its own __init__.py exporting
PROJECT_ROOT and a scope-focused README.md following the project's
documentation conventions (RFC 2119 keywords, emoji-after-headline,
one-sentence-per-line, no historical context).

12 existing tests move into their concern bucket via git mv so history
follows the file. Adds tests/lint/ansible/variables/test_dotted_path_keys_exist.py
which catches the bug class where a Jinja dotted path's head resolves
via group_vars/all/ or the consuming role's own vars/defaults but a
deeper segment does not exist as a key in the dict tree (e.g. the
previous `LDAP.SERVER.REACH_DOMAIN` typo in web-app-odoo).

Touches three external references so cross-links keep resolving:
- docs/contributing/actions/testing/suppression.md (run-once, shared)
- roles/sys-svc-compose/tasks/utils/network/README.md (network-via-util)
- tests/lint/ansible/sql/test_no_inline_multiline_sql.py (self-path exclude)
2026-05-14 17:12:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 8055ad5d78 refactor(sys-svc-compose): centralise docker network creation under utils/network/
Splits the network-create helper out of the all-in-one wrapper so roles
can pick the right entry point:

  utils/network/create.yml   network-only, derives name + subnet from
                             `network_role_id` (use when the role still
                             needs to do work between the network create
                             and the compose-up, or when pre-creating
                             foreign networks in a loop).
  utils/network/routine.yml  former wrapper, now delegates to create.yml
                             and re-flushes sys-svc-compose right after
                             (use for self-contained single-container
                             services).

A new lint
`tests/lint/ansible/test_network_create_via_util.py` forbids ad-hoc
`community.docker.docker_network` calls or `docker network create` /
`container network create` shell-outs anywhere in `roles/` outside
`utils/network/create.yml`. The existing 5 callers (mariadb, postgres,
ollama, openldap, web-app-prometheus) move to the new layout and shed
the now-redundant `docker_network_name` / `docker_network_subnet`
caller-side vars; that retires 5 unused role constants
(MARIADB_NETWORK / MARIADB_SUBNET / POSTGRES_SUBNET / OLLAMA_NETWORK /
OPENLDAP_NETWORK).

The `web-app-prometheus` task gains a pre-create loop over
`lookup('native_metrics_apps')` so the deploy unblocks the bootstrap
deadlock observed on test-deploy-server CI run 25634428082 for
web-app-gitea and web-app-matrix (`network <X> declared as external,
but could not be found`): prometheus is loaded as a shared service
*before* the target role, so its `external: true` references to the
target's network needed pre-creation to satisfy compose-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2a023ca6a1 chore(lint): autoformat strict-failure semantics + claude.sh rename
* scripts/lint/autoformat.sh:
  - rename run_sort_claude_settings worker to run_claude (matches the
    renamed scripts/lint/claude.sh).
  - drop the EXIT trap that wiped status_dir; cleanup happens explicitly
    after `wait` returns, so a parallel worker that interrupts the
    parent never races a vanished status dir.
  - each worker now captures its tool's exit code and writes one of
    OK / SKIP / FAIL. Tolerated `expected violations remain`-style exits
    (markdownlint-cli2 rc≤1, ansible-lint rc≤2) still count as OK; only
    real failures count as FAIL.
  - all workers run to completion regardless of individual tool failures
    (so the user always sees the full picture); after the summary, the
    script exits 1 if any worker reported FAIL.
* scripts/lint/sort_claude_settings.sh -> scripts/lint/claude.sh
  (history preserved via git mv); update test_claude_settings_sorted.py
  fix-hint accordingly.

Plus working-tree drift across roles/, group_vars/, inventories/,
requirements/, and .pre-commit-config.yaml accumulated from the
make-test setup phase regenerating generated artefacts during recent
test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:30:49 +02:00
kevinveenbirkenbachandClaude Opus 4.7 951f6d36d2 chore(lint): unify autoformat/galaxy parallelism into one PARALLEL env var
* scripts/lint/autoformat.sh: rename AUTOFORMAT_PARALLEL to PARALLEL,
  declare it at file start with default `true`; still env-overridable.
  Reword the worker comment so the literal `# shellcheck` token no
  longer appears at start-of-line and trips SC1072/SC1073.
* scripts/lint/galaxy.sh: drop the GALAXY_FATAL / GALAXY_NAMESPACE /
  GALAXY_PARALLEL env vars (over-engineered for a single repo). Failure
  is always fatal. NAMESPACE is an internal constant. Add the same
  PARALLEL env var (default `true`, env-overridable); falsy demotes the
  xargs fan-out to one worker.
* docs/contributing/environment/variables.md: collapse the four
  AUTOFORMAT_*/GALAXY_* rows into one `PARALLEL` row covering both
  consumers.
* docs/contributing/tools/make.md: refer to `PARALLEL=0` everywhere;
  add the lint-galaxy parallelism note.

Plus working-tree drift across roles/, group_vars/, inventories/, and
tasks/ that accumulated from the make-test setup phase regenerating
generated artefacts during recent test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:59:24 +02:00
kevinveenbirkenbach a55c99ef62 refactor(compose): replace docker lookup with container and unify compose paths
- Rename lookup plugin from `docker` to `container`
- Switch all lookups from `lookup('docker', ...)` to `lookup('container', ...)`
- Normalize compose file keys:
  - docker_compose → compose
  - docker_compose_override → compose_override
  - docker_compose_ca_override → compose_ca_override
- Centralize compose path resolution via docker_paths_utils
- Update compose_file_args to always derive paths from DIR_COMPOSITIONS
- Align all roles, templates, and vars with container/compose naming
- Update and rename unit tests accordingly

This prepares the codebase for engine-agnostic container backends
(e.g. Docker ↔ Podman) and removes legacy docker-specific assumptions.

https://chatgpt.com/share/6987a7cc-d374-800f-99ad-bb4c8f92c59e
2026-02-07 21:59:39 +01:00
kevinveenbirkenbach 9fa9692d7d refactor(compose): replace docker compose calls with compose wrapper
- Switch all docker compose invocations to the compose wrapper
- Update comments, task names, and log messages accordingly
- Simplify handlers and tasks to rely on wrapper-managed flags and env files
- Align tests and documentation with compose-based workflow

https://chatgpt.com/share/69866ede-be2c-800f-94b0-bffcf8fed260
2026-02-06 23:44:39 +01:00
kevinveenbirkenbach 0a7b4efa22 Replaced docker compose pull by compose pull 2026-02-06 23:26:20 +01:00
kevinveenbirkenbach 9701eefe99 refactor(compose): rename compose_f_args lookup to compose_file_args
https://chatgpt.com/share/6985ed13-7c98-800f-bb5f-e5832f7c094c
2026-02-06 14:30:51 +01:00
kevinveenbirkenbach 7bb15d1b5e refactor(container): rename docker-container/docker services to sys-svc-container and update all references, schedules, tests, and templates
https://chatgpt.com/share/6985b09d-8ff8-800f-800c-7a694c0983e2
2026-02-06 10:12:31 +01:00
kevinveenbirkenbach 51239968fd refactor: rename docker-compose roles to sys-svc-compose and update paths
Renamed docker-compose and docker-compose-ca roles to sys-svc-compose and sys-svc-compose-ca, updated template includes, handlers, lookups, tests, and variables, and moved compose instances to /opt/compose.

https://chatgpt.com/share/6985ab2c-8344-800f-9e9a-768f562d138d
2026-02-06 09:49:42 +01:00