Commit Graph
92 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 b3ce43a596 fix(security): require no_log on every task that carries a credential
At -vvv Ansible prints each module argument and result, so a password in
a command line, a request body, a header or a fact lands in the deploy
log. Nothing enforced masking outside the MCP task files, and no check
looked at parents at all.

A new lint, test_credential_tasks_are_masked, finds tasks whose printed
parts (module arguments, environment, loop) reference an identifier ending
in password, secret, token, api_key or private_key, or
secrets.credentials. A task passes when it or an enclosing block sets
no_log, when every file that pulls it in does so through include_tasks
with apply.no_log or import_tasks with no_log (transitively), when the
secret only reaches a parameter the module masks itself, or when the
module masks its values. no_log on an include_tasks task itself does not
count, because it does not reach the included tasks. The rule can be
suppressed per task with `# nocheck: credential-masking`, now listed in
the suppression catalog, and the lint tests its parent and suppression
logic on synthetic cases.

It found 39 unmasked tasks in 19 roles, all real: join tokens, client
secrets, API tokens in Cloudflare headers, database passwords in
command lines, the ChatGPT bot login body and more. Each now sets
no_log from MASK_CREDENTIALS_IN_LOGS. The sys-dns-hetzner-rdns alias
hetzner_no_log, which only restated that switch, is gone.

Not covered yet: template and copy tasks whose rendered file carries a
secret print it as a diff under --diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 21:52:18 +02:00
kevinveenbirkenbachandClaude Opus 5 8d378b8280 fix(sys-svc-rdbms): realign a dedicated postgres the way the shared one is
383a59e9a1 gave the shared engine a way to be told a rotated superuser
password: swap pg_hba to scram, ALTER USER over the local socket, verify the
new password over TCP, put pg_hba back. An engine an app owns alone never got
that, so after the deploy rotates a credential the app holds the new password
and its own postgres still holds the old one, and every request answers 500
until someone recreates the volume.

The script is now told which role to alter instead of assuming postgres. Both
engines are reached the same way -- the container's local socket authenticates
by peer, which is the whole reason a password can be written into an engine
nobody can log into any more -- so one implementation covers both. The role
travels as an environment variable and is applied through psql's identifier
quoting rather than string interpolation.

MariaDB solves this at engine start with --init-file (b067d21ef5). Postgres has
no equivalent and /docker-entrypoint-initdb.d only runs on an empty PGDATA, so
this has to be an ansible step after the container is up. post_deploy.yml is
where it goes, right behind the health wait, and that gate is passable here for
a reason worth stating: the dedicated healthcheck is pg_isready, which does not
authenticate, so the container reports healthy on a stale password and leaves
the window this task needs.

Seven jobs of run 33279942812 fail this way -- mattermost, pretix, xwiki,
gitlab, opentalk, flowise, discourse -- each traced to the same rotation gate
that exists to prove a changed secret propagates.

Exercised against a stubbed container for both roles: postgres and an app role
behave identically, the role reaches all nine calls, ALTER USER fires. It still
refuses to run without a role, and still reports FAIL when authentication never
succeeds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 20:03:06 +02:00
kevinveenbirkenbachandClaude Opus 5 575a5a87ac refactor(layout): sort shipped scripts by language and split the unit suite
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).

This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.

Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:

  * 7 `src:` values built from a role variable holding a bare filename
    (svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
    The variable doubles as the destination name inside the container, so the
    language segment belongs at the call site, not in the variable.
  * `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
    through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
    18 roles ship exactly that and have no template fallback: the play would
    have died on each.
  * `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
    interface now reads `files/javascript/javascript.js`.
  * `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
    path through `path_absolute(... 'files', ...)`.
  * 4 `ansible.builtin.script:` invocations naming a bare command
    (pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
  * `dev-python/files/shell/install.sh` pointed at a sibling role's script.

Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:31:09 +02:00
kevinveenbirkenbachandClaude Opus 5 673e994aa7 feat(lint): pin type and resolver on every container inspect
An untyped inspect searches every docker object kind, and a role's overlay network carries the same entity name as its main container, so the call can resolve the network and report a missing State key instead of naming the container. That is how the openresty assert died on every swarm job of run 30709070050.

Extend container-exec-resolver to cover inspect, stripping raw markers and quoted spans first so the flag walker survives Go template format strings. Add container-inspect-type, scoped to roles/ because the collision follows the entity naming convention and the outer act namespace carries no role networks. Sweep 40 call sites; suppress the delegate_to-resolved CIDs and the compose co-location probes with a stated reason.

Move the five container rules into tests/lint/ansible/shell/container/ and drop the container_ prefix from their filenames. Register both markers in the suppression docs, where the resolver marker was missing entirely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:51:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4039313266 refactor(tasks): enforce and apply a single task-file numbering scheme
Add tests/lint/repository/test_task_file_numbering.py: numbered task
files use exactly two leading digits, a numbered *_core.yml must be
00_core.yml, per directory the numbers are unique/consecutive and start
at 01 (00 with a core file), numbered includes appear in ascending
order, and shared helpers live in tasks/utils/ instead of the numbered
chain.

Apply it across every role: rename NN_core.yml to 00_core.yml with a
consecutive down-shift of the siblings, move letter-suffix children into
a subdirectory named after their parent task, relocate helpers included
from more than one place into tasks/utils/, and update every reference
(include_tasks, path_absolute, .ansible-lint-ignore, requirement docs).
Legacy narration comments in the touched files are deleted; the upstream
xwiki.properties reference block opts out via nocheck. The unused
MAILU_IP6_PUBLIC var (only ever named in now-removed comments) is dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:01:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 020e866341 fix(postgres): embed the boot entrypoint via a heredoc COPY
The swarm pre-deploy build renders only the Dockerfile and env into the build context before any role task runs, so the file COPY of boot-entrypoint.sh failed with not-found. The script is now embedded at template render time through a quoted BuildKit heredoc, keeping the role file as the single source and making the build context-independent; the copy task is gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:11:05 +02:00
kevinveenbirkenbachandClaude Opus 4.8 af3e638e50 fix(postgres): clear an empty stale postmaster.pid at container boot
A SIGKILL (the observed swarm task exit 137) can leave a zero-byte postmaster.pid on the NFS-backed data volume; the stock entrypoint then refuses every restart with 'lock file postmaster.pid is empty', looping forever and taking keycloak, synapse and every other central-DB consumer down with it. A boot entrypoint wrapper removes the pid file only when it exists and is empty, which in a PID-1 container is deterministically stale, then execs the stock entrypoint. The script ships as a role file copied into the build context; PGDATA is asserted rather than defaulted since the upstream image always sets it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:26:30 +02:00
kevinveenbirkenbachandClaude Fable 5 40e553069d refactor(structure): nest prefix-grouped sibling files into subfolders
Move files that share a delimiter-bounded name prefix within a folder (threshold 2) into a subfolder named after the prefix, dropping the prefix from the filename. Covers plugins/filter (docker, generate, has, merge, get/all), env handler subpackages, utils/roles/entity, utils/tests/swarm/write, role SQL/scripts/templates, keycloak/mailu task-includes, shared tasks/utils/load, and CI scripts. Update every import, query_file/script/include_tasks/src/lookup target, compose bind mount, and python -m path to the new locations.

Extend the prefix-grouped-files lint to threshold 2 with dir-level .nocheck and file-head nocheck opt-outs, and skip groups whose shortest member's stem equals the shared prefix. Keep flat only where a loader forbids nesting: WordPress mu-plugins and the docs/requirements namespace. Ansible filter discovery is recursive, so nested filters stay Jinja-resolvable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 20:00:03 +02:00
kevinveenbirkenbachandClaude Fable 5 c51c0de9ba refactor(diagnostics): recursive workflow-level rescue snapshot, no in-play rescue blocks
Replace scripts/system/diagnostics/rescue.sh with utils/diagnostics/
container.py: one workflow call on failure captures the current level
(containers with logs+inspect, swarm services, journal, host resources,
in-play role dumps) and recurses itself through every DiD boundary via
self-copy, from the runner down to the innermost runtime (RESCUE_MAX_DEPTH).
compose and swarm now share an identical failure step; the swarm per-node
collector wrapper is gone. All 17 script-only Ansible rescue: blocks are
deleted and their bare block: wrappers inlined (plus a wordpress leftover);
the three justified blocks (postgres pg_hba, xwiki extensions, keycloak
cross-node recovery) keep their evidence tasks and now end in a fail task.
Two lints enforce the state: rescue: is forbidden without a
'# nocheck: rescue-<reason>' first-task marker, and a block without
rescue/always/when or another scoping key must be inlined.
sys-service-terminator hoists become to the include apply. Also move
routine/08_cleanup.sh to utils/clean/teardown.sh and topology/ to
utils/topology/ so routine/ holds only the sequenced flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 094c02a69d feat(diagnostics): single SPOT rescue script with CI-uploaded snapshots
Every Ansible rescue: block used to carry ad-hoc per-role failure dumps that
drifted apart and captured different subsets. Replace them all with one
diagnostics script (scripts/system/diagnostics/rescue.sh) that snapshots
containers (full logs plus inspect per container), swarm services (ps plus
logs per service), journal, host resources, runtime info and stats into a
per-run folder and always exits 1 so the deploy still fails. A new lint
enforces the shape: every rescue ends with the script, at most one
nocheck-justified extra task before it (postgres pg_hba dump, xwiki
extension-repo probe), and a nocheck rescue-recovery exemption for the one
block that must genuinely recover: keycloak object updates, where a
cross-node reschedule needs a swarm-wide node re-resolve plus a delegate_to
change that only a rescue can express; its recovery is kept and now also
captures diagnostics before the terminal failure.

The snapshot folder path is a group_vars SPOT: DIR_RESCUE_DIAGNOSTICS and
DIR_VAR_LIB live in group_vars/all/05_paths.yml as plain strings; new env
handlers derive INFINITO_RESCUE_DIAGNOSTICS_DIR and INFINITO_DIR_VAR_LIB
from it for the generated .env, replacing the default.env entries, and the
play-level environment forwards the value to every task host so rescue.sh
needs no second default. CI uploads the folder like the inventory and
playwright artifacts: compose copies it out of the deploy container in
dedicated.sh, swarm captures per node (manager and both workers) into
node-named subfolders.

Deleting the ad-hoc dumps orphaned helper files (postgres inspect_container,
terminator docker_diagnostics, peertube diagnostics, matomo bootstrap debug,
xwiki and openresty rescue includes) and 14 role vars; all removed. Generic
pieces of those dumps (runtime version and info, stats, dmesg oom evidence,
swapon) moved into the SPOT script instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 03:04:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c057b1a1c8 fix(roles): never quote container_address swarm resolver fragments
In swarm mode the container_address lookup returns a $(/usr/bin/resolve-container-id ...) fragment that must expand at exec time; piping it through Jinja quote hands docker the literal string and every exec fails with 'No such container' (broke all three snipe-it CI variants). Remove the quote at the seven snipe-it sites and the live matrix/openproject sites, convert openproject's argv exec to shell so the fragment can expand, split the matrix bridge restart per deployment mode with a manager gate, and mark the compose-only or eval-contract sites with nocheck. A new lint (container-address-quote) bans the pattern repo-wide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 17:30:36 +02:00
kevinveenbirkenbachandClaude Opus 4.8 01124a3238 fix(svc-db-postgres): run the readiness probe on the node hosting postgres
The probe execs into the postgres container, but the old container
address resolved the id on the local node only, so it failed with 'No
such container' whenever swarm scheduled postgres on a worker instead of
the primary. Resolve the hosting node and container id via
resolve_host_cid and delegate the probe there. Compose resolves to the
local host and container name, so it is byte-identical to before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f02da33128 fix(ci): clear compose deploy-gate deprecation warnings + tighten comment markers
The test-deploy-compose "Fail on deprecation warnings" gate failed ~10 apps
while the deploys themselves succeeded. Sources cleared:

- svc-db-postgres: community.postgresql `db` -> `login_db` (the `db` param
  alias is deprecated, removed in collection 5.0.0)
- assert_no_deprecation_warnings.sh: ignore ansible-core's legacy filter/test
  PluginLoader-aliases deprecation (needs a collection migration with
  meta/runtime.yml, not fixable in role YAML)
- joomla/wordpress: drop the `{{ }}` delimiters from task conditionals
  (when/until/changed_when), per ansible-core 2.23

Also drop `upstream`/`compat`/`security`/`bug` from the comment-policy markers
and delete those comments from the joomla install tasks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:48:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 23d1447566 refactor(ansible): add path_absolute lookup, replace playbook_dir joins
Introduce plugins/lookup/path_absolute.py as the single source for the
repo-root anchor: lookup('path_absolute', 'roles/...') returns an absolute
path under playbook_dir, joining every term. Replace all hand-built
playbook_dir path constructions across .yml/.yaml and .j2 templates with it:

  [playbook_dir, '<rel>'] | path_join   -> lookup('path_absolute', '<rel>')
  playbook_dir ~ '/<rel>'               -> lookup('path_absolute', '<rel>')
  {{ playbook_dir }}/<rel>              -> lookup('path_absolute', '<rel>')

Unrelated repo_path symbols in cli/meta/roles/applications/resolution and the
single bare discover_playwright_roles(playbook_dir) call are left untouched.

Add tests/lint/ansible/test_playbook_dir_via_lookup.py: fails the build if any
of the three construction forms reappears in .yml/.yaml/.j2, pointing callers
at the lookup. Unit test covers the plugin.

Touching these files pulled their pre-existing narration comments into
test_comments_valid scope; clean them (delete restatement/banners, keep real
trip-wires as Workaround/Upstream/Invariant markers). Drop caution, important
and pitfall from the comment-lint marker vocabulary and remove the comments
that used them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:36:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 cddb0dc59b feat(lint): require emoji-prefixed task/step/job names + convert existing
Add tests/lint/repository/test_name_emoji_and_length.py: every Ansible task
name and GitHub Actions workflow/job/step name must start with an emoji (first
char not 0-9/A-Za-z) and be <= 80 chars.

- emit emoji names at the source: cli/build/include generator now writes
  "🔧 setup <app>" / "🚿 flush handlers after <app>" so generated
  tasks/groups/*-roles.yml satisfy the convention too
- convert existing task/step/job/workflow names to emoji-prefixed, choosing a
  task-fitting icon per action (📦 install, ✅ ensure, 🔍 probe, 🥾 bootstrap,
  🚀 deploy, 🧩 service-module load, ⬇️ checkout, 📂 include-routines, ...)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:45:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 36ebf34b64 Merge branch 'main' into feature/swarm-nfs
Resolve 69 conflicts across ~70 files: union main's image-version bumps and new service fields (bond, resource limits) with the swarm branch's default_placement / NFS / IS_STACK_HOST config; retire the three per-deploy-type test workflows main still edited (consolidated into compose+swarm); adopt main's run-once 01_core dispatchers while re-applying each role's swarm guard. Fix the post-merge lint surfaced by main's stricter rules: bond + resource-budget keys, container-exec stack-host gating + resolver routing, leaky/dead proxy vars, suitecrm once-flag, xwiki variant nocheck, nextcloud oversized-block to gated include, matrix-mdad capped at 3g. make autoformat clean, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:13:55 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4bcbb0fbf1 fix(svc-db-postgres): run the readiness checks once on the stack host
The svc-bkp play loads svc-db-postgres on every swarm node, so the un-delegated container readiness waits and the superuser-password task ran on workers and failed with 'resolve-container-id: must run on a Swarm manager node'. Add delegate_to STACK_HOST + run_once to the three core tasks, mirroring svc-db-mariadb.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:01:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0bbd6c43ff fix(postgres): pin provider context on 02_init for nested/looped consumers
svc-db-postgres 02_init.yml asserts application_id == 'svc-db-postgres' AND database_consumer_id \!= 'svc-db-postgres'. When 01_core is skipped (run-once), the include_role application_id var does not reliably reach 02_init through a consumer's nested looped include (web-app-matrix's per-bridge DB seeding), so the assert fails. Pin application_id on the 02_init include so it holds for any caller. On the matrix side, freeze the consumer id eagerly before the provider include so database_consumer_id stays the matrix consumer instead of lazily resolving to svc-db-postgres once application_id is pinned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:17:59 +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 a4cb5b89e1 feat(swarm): swarm-aware container exec via container_address lookup
Adds a mode-aware `container_address` lookup plugin and a
`resolve-container-id` helper script that resolve swarm task IDs at
exec time (compose mode returns bare names unchanged). Migrates ~25
roles + ~28 task files from `command:` to `shell:` so the lookup's
`$(...)` subshell evaluates correctly, and extends `database` lookup
with an `address` key for cross-role DB exec.

Three new lint tests enforce the contract: every container exec
target goes through the resolver, lives behind an IS_STACK_HOST gate
(walks the include graph one hop deep), and uses `shell:` rather than
`command:` when the target is a Jinja reference. Existing nocheck
suppressions on bare-name DB exec sites (nextcloud redis, listmonk,
moodle OIDC, mailu) are replaced with the lookup-based form.

Fixes the systemd Environment= injection in sys-ctl-cln-disc-space
(it never expanded $(...)) by passing STACK / SERVICE_KEY parts and
resolving inside the script. Extracts shopware admin and mediawiki
extension install bodies into dedicated .sh files invoked via env.
Rewrites password-bearing shell tasks to either `| quote` directly or
the `-e VAR=... | quote` + `sh -c '... ${VAR} ...'` env-bridge pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 03:46:28 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3387aa344f refactor(svc-db-postgres): per-consumer extensions declared in meta/services.yml via service_extensions lookup
Each consumer (bookwyrm/mobilizon/discourse) now declares its required
postgres extensions in its own meta/services.yml under postgres.extensions
and pins shared: true. svc-db-postgres' 02_init.yml drops the hardcoded
postgis-gate and unconditional vector steps and loops over the new
lookup keyed by database_consumer_id. The lookup fast-fails if a role
declares extensions without pinning shared: true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:44:24 +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 017f67cd53 feat(plugins): introduce database_query module + migrate all SQL query tasks
Replaces controller-side `community.postgresql.postgresql_query` /
`community.mysql.mysql_query` calls with a project-local `database_query`
Ansible module that wraps `container exec -i <container> <psql|mysql>`
and pipes SQL via stdin. The module routes via the DB container's own
network so the same task works identically in shared mode (central
`svc-db-*` container) and non-shared mode (per-role `<entity>-database`
container) — the `container` field of `lookup('database', …)` is
shared-aware and the module just reads it. SQL payloads come from a
file (`query_file:`, preferred) or as a raw string (`query:`, escape
hatch). `named_args:` substitutes `%(name)s` placeholders with engine-
appropriate escaping (postgres single-quote-doubling, mariadb backslash
escapes), so role tasks stop string-interpolating Jinja into SQL.

New backends extend the module's `_ENGINES` dispatch table; everything
else is engine-agnostic. The module ships at `plugins/modules/` (Ansible
collection-style layout) — the shebang lint test learned to recognise
that path as a valid module location alongside the existing per-role
`library/` layout.

Two lint tests enforce the convention:

* `tests/lint/ansible/modules/test_no_raw_db_query_module_usage.py`
  forbids `community.postgresql.postgresql_query` /
  `community.mysql.mysql_query` in role tasks. Opt out per-line with
  `# nocheck: database-query` (used by `svc-db-postgres/tasks/02_init.yml`
  where the role manages its own DB and the `database` lookup would be
  self-referential).
* `tests/lint/ansible/modules/test_no_raw_database_query_string.py`
  forbids inline `query:` on `database_query:` tasks. Opt out with
  `# nocheck: database-query-raw` (no opt-outs in the current tree —
  every SQL payload reaches the module through a `.sql` file under
  `files/sql/`).

Role migrations:

* `web-app-peertube` — 3 tasks migrated, 3 new `.sql` files,
  `templates/upsert-oidc-plugin.sql.j2` deleted in favour of
  `files/sql/upsert-oidc-plugin.sql` + `named_args:`.
* `web-app-openproject` — 3 tasks migrated, 3 new `.sql` files for
  the LDAP-auth-source check/update/insert chain.
* `web-app-odoo` — 33 tasks migrated, 9 new `.sql` files for the
  LDAP / admin / init-database flows; 17 pre-existing OIDC `.sql`
  files renamed snake_case → kebab-case and relocated from
  `files/oidc/` to `files/sql/oidc/` (via `git mv` so rename history
  stays intact).

Unit tests cover every dispatch-table entry, the named_args escaper
(strings, ints, floats, bools, None, single-quote and backslash
injection vectors), and the password-redactor that hides
PGPASSWORD/MYSQL_PWD assignments from the module's returned `cmd`
field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:08 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c8bd32c056 fix(svc-db-postgres): hoist resource vars to group_vars/all so app-only matrix-deploys resolve them
A set_fact run_once: true at role scope in svc-db-postgres/tasks/01_core.yml
did not survive the matrix-deploy round split: rounds whose include set did
not contain svc-db-postgres failed at template-render time with
'POSTGRES_ALLOWED_AVG_CONNECTIONS' is undefined.

- Hoist POSTGRES_VCPUS, POSTGRES_TOTAL_RAM_MB, POSTGRES_MAX_CONNECTIONS,
  POSTGRES_ALLOWED_AVG_CONNECTIONS, POSTGRES_SUPERUSER_RESERVED_CONNECTIONS,
  POSTGRES_SHARED_BUFFERS*, POSTGRES_WORK_MEM*, POSTGRES_MAINTENANCE_WORK_MEM*,
  POSTGRES_DELAY to group_vars/all/17_resource.yml (lazy Jinja evaluation
  per round).
- Drop the set_fact from tasks/01_core.yml; the value is now derived
  globally on every play.
- Relocate the split_postgres_connections filter plugin from
  roles/svc-db-postgres/filter_plugins/ to plugins/filter/ so it is always
  loaded via the global filter_plugins = ./plugins/filter declaration in
  ansible.cfg.
- Move the matching unit test from
  tests/unit/roles/svc-db-postgres/filter_plugins/ to
  tests/unit/plugins/filter/ to mirror the plugin's new location.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 04:25:06 +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 b1b73bc1df refactor(lint): tighten inline-script lint to 11 lines + extract overflows
Drops `MAX_LINES` from 12 to 11 in `test_inline_script_block_size.py`,
catching the five blocks that were sitting exactly on the previous
threshold. Pure-bash blocks move to dedicated `.sh` files invoked via
`ansible.builtin.script:` or `run: bash …`; the keycloak heredoc with a
templated JSON payload splits into a `set_fact` plus a folded
`printf | kcadm … -f -` one-liner.

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 c52e4269bf chore(tooling): integrate mbake (Makefile linter/formatter) + working-tree drift sync
Makefile lint integration:
* scripts/install/lint.sh: replace the prior checkmake binary install with
  ensure_mbake (mbake is pip-installable, parses our Makefile correctly
  whereas checkmake bailed on .ONESHELL: / .SHELLFLAGS: / ifneq).
* scripts/lint/makefile.sh: new runner — `mbake format --check Makefile`
  reports formatting drift and missing .PHONY declarations.
* scripts/lint/autoformat.sh: add a two-pass `mbake format Makefile`
  (mbake's formatter is not strictly idempotent on the first pass).
* Makefile: split the multi-line `.PHONY:` block into 18 single-line
  `.PHONY:` declarations so mbake's parser can validate it; add the
  previously-missing `install-python-dev` and `build-dependency` PHONY
  targets; add `lint-makefile` aggregate-and-direct target.
* .pre-commit-config.yaml: new make-lint-makefile hook scoped to
  `^Makefile$`.
* .github/workflows/lint-makefile.yml: new CI workflow (pip-install mbake
  + make lint-makefile).
* .github/workflows/ci-orchestrator.yml: wire lint-makefile into
  lint-gate.needs.

Misc:
* .gitignore: add roles/importer_result.json (tooling artefact written by
  galaxy-importer when it runs in roles/).

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:32:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1aa1b084d5 refactor(svc-db-postgres): extract grant-schema SQL + lint inline multiline SQL
The schema-privileges block in svc-db-postgres/tasks/02_init.yml mixed
GRANT/ALTER DEFAULT PRIVILEGES SQL with Jinja interpolation in a 4-line
inline `query: |` scalar. Move the statements into
`roles/svc-db-postgres/templates/sql/grant_schema_privileges.sql.j2` and
load via `lookup('template', ...)` so the SQL stays reviewable as plain
SQL (Jinja stays only for identifier interpolation, since Postgres
prepared-statement bindings cannot substitute identifiers).

Also adds tests/lint/ansible/test_no_inline_multiline_sql.py that fails
when more than 3 SQL-leader lines (INSERT/UPDATE/SELECT/GRANT/ALTER/…)
appear in a single inline YAML scalar under roles/, tasks/, or
playbooks/. The error message points at the extraction-via-lookup
pattern so future blocks land in files/<name>.sql or
templates/sql/<name>.sql.j2 by default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 07:55:34 +02:00
kevinveenbirkenbach 262e1b43fc Merge branch 'main' into fix/users-applications.yml
# Conflicts:
#	docs/contributing/design/services.md
#	plugins/lookup/applications_current_play.py
#	plugins/lookup/service.py
#	roles/sys-stk-backend/lookup_plugins/service_should_load.py
#	roles/sys-stk-backend/tasks/_load_service.yml
#	roles/sys-stk-backend/vars/main.yml
#	roles/sys-svc-compose/templates/base.yml.j2
#	roles/sys-svc-mail/tasks/01_core.yml
#	roles/sys-utils-service-loader/tasks/load_frontend_service.yml
#	tests/unit/plugins/lookup/test_applications_current_play.py
#	tests/unit/plugins/lookup/test_service.py
#	tests/unit/roles/sys-stk-backend/lookup_plugins/test_service_should_load.py
2026-04-15 23:59:54 +02:00
kevinveenbirkenbachandClaude Opus 4.6 b70a783423 refactor(lookups): migrate to runtime applications/users lookups
Replaces inline `applications`/`users` merging with dedicated lookup
plugins and runtime hydration. Introduces `add_csp_hash` filter,
integrates sort into `find_dock_val_by_bkp_entr`, splits the Keycloak
one-shot realm import into `05a_realm_import_run.yml`, and fixes the
Mastodon administrator task YAML. Removes obsolete `csp_hashes` filter,
`sys-token-store` Jinja filter, and empty CLI setup modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 23:44:19 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 be6a310d59 refactor(services): implement role-local service registry (req-001)
- Delete group_vars/all/20_services.yml; all service metadata now lives
  in each role's config/main.yml under compose.services.<name>
- Rename generic 'database' service key to explicit 'mariadb'/'postgres'
  across all app roles; remove legacy type: field
- Add utils/service_registry.py and plugins/lookup/service_registry.py
  as the single source of truth for service discovery
- Add utils/database_service.py to centralize DB service resolution
- Delete service_load_enabled filter and service_should_load lookup;
  replaced by service lookup plugin
- Update sys-utils-service-loader to run in constructor stage and use
  generic load_service.yml; remove load_frontend_service.yml
- Add _service_loader_preloading guard to sys-svc-compose,
  sys-svc-webserver-core and sys-ctl-alm-compose to prevent
  health-checks/alarms from firing during preload phase
- Replace manual load_app.yml calls for shared DB/Mailu with assert-
  based preload validation in sys-svc-rdbms and sys-svc-mail
- Update networks.yml.j2 templates to resolve LDAP/AI networks via
  service_registry instead of hardcoded role names
- Migrate all tests to new service discovery API; delete obsolete tests
- Update docs/contributing/code/services.md to reflect role-local model

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 19:33:20 +02:00
kevinveenbirkenbach 64b0506947 refactor: replace raw docker usage with container wrapper across roles
Replace direct `docker` command usage, references, and documentation with
the `container` wrapper to ensure engine abstraction and future portability
(e.g. Docker → Podman).

This includes:
- Shell scripts, Ansible tasks, handlers, and Python helpers
- Documentation, comments, help texts, and meta descriptions
- Network, volume, system, and image operations
- Test expectations and comments

All runtime behavior remains unchanged while enforcing the repository-wide
rule to avoid raw Docker invocations.

https://chatgpt.com/share/698721c3-e270-800f-be7a-e4435fb54506
2026-02-07 12:57:26 +01:00
kevinveenbirkenbach c1d50d78da refactor(container): replace BIN_CONTAINER usages with system-wide container wrapper
https://chatgpt.com/share/698660b7-4c78-800f-b25f-af5408cff034
2026-02-06 23:00:21 +01:00
kevinveenbirkenbach ba35d7c5e0 refactor(container): replace docker ps usages with container wrapper and move binaries to /usr/bin
https://chatgpt.com/share/698660b7-4c78-800f-b25f-af5408cff034
2026-02-06 22:44:14 +01:00
kevinveenbirkenbach 2147690da0 Use container wrapper for docker logs across roles
https://chatgpt.com/share/698659d9-62e4-800f-89a1-5d44f309bde4
2026-02-06 22:28:38 +01:00
kevinveenbirkenbach 46238d4c89 refactor: use BIN_CONTAINER wrapper for container exec commands
https://chatgpt.com/share/698657ee-e708-800f-ba5d-4c7c5198a5fe
2026-02-06 22:06:42 +01:00
kevinveenbirkenbach 63926b23b9 chore(container): replace direct docker inspect/run calls with container wrapper
https://chatgpt.com/share/698647e2-7b94-800f-814d-5eb63fe4e537
2026-02-06 20:58:17 +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
kevinveenbirkenbach 2d2e218fe5 Solved templating warnings 2026-02-05 11:00:34 +01:00
kevinveenbirkenbach 3e8abe3cb4 fix(rdbms): preserve database_consumer_id across role boundaries
- Introduce database_consumer_id_passed to avoid fact/vars collisions
- Explicitly pass consumer context into svc-db-postgres via include_role
- Strengthen context assertion in postgres init tasks
- Move once-flag include to the beginning of postgres core tasks
- Make POSTGRES_EXEC_SHELL non-interactive by injecting PGPASSWORD
- Improve test coverage to detect facts overridden by include-time vars

https://chatgpt.com/share/69833e56-93a0-800f-8e3d-d970cf87daef
2026-02-04 13:40:44 +01:00
kevinveenbirkenbach 77a0bc5508 svc-db-postgres: unify docker exec shell + tidy yaml formatting
- Align database config key spacing and add missing newline in config/main.yml
- Run socket readiness check as OS user postgres
- Introduce POSTGRES_EXEC_SHELL helper and use it for TCP/auth debug psql calls
- Update sys-svc-rdbms comment (behavior note seems outdated)

https://chatgpt.com/share/6982a0b9-be28-800f-b5dc-d489b1e4eed8
2026-02-04 02:28:01 +01:00
kevinveenbirkenbach 152a576957 Make PostgreSQL startup wait idempotent by replacing init log markers with container state and SQL readiness checks
https://chatgpt.com/share/697e4312-610c-800f-977c-2946fdea48f8
2026-01-31 18:59:40 +01:00
kevinveenbirkenbach 9e3f4b9187 svc-db-postgres: fix race condition during initial Postgres startup
Wait explicitly for the Docker entrypoint init phase to complete by
checking the log marker before running any SQL commands. Then verify
database readiness with a real SQL roundtrip instead of pg_isready.

This avoids a race condition where pg_isready reports success while
the temporary init server is already shutting down, which caused
intermittent failures during initial deployment (especially with
PostGIS enabled).

https://chatgpt.com/share/697e3a68-e56c-800f-8438-6edb2a956802
2026-01-31 18:22:24 +01:00
kevinveenbirkenbach 383a59e9a1 feat(svc-db-postgres): robust superuser password rotation via script with explicit auth verification
- replace Python helper with Bash script executed via ansible.script
- wait for postgres readiness inside container (pg_isready)
- explicitly verify password auth via temporary pg_hba override
- distinguish CHANGED / UNCHANGED / ALREADY OK states
- map script result deterministically to Ansible changed/failed logic
- remove legacy postgres_set_superuser_password.py

https://chatgpt.com/share/69754a74-9d18-800f-89a6-304b083fbe50
2026-01-24 23:40:47 +01:00
kevinveenbirkenbach 9b567a2f0e feat(postgres): ensure superuser password idempotently via helper script
Add a Python helper script to ensure the postgres superuser password inside
the Docker container without shell/SQL quoting issues.

The script:
- checks whether the password already works
- updates it only if necessary
- signals change state via exit codes (0 = unchanged, 2 = changed)

The role now calls the script via ansible.builtin.script and maps rc to
changed/failed correctly.

https://chatgpt.com/share/697528b6-79f4-800f-96c9-da993feff9b9
2026-01-24 21:16:47 +01:00
kevinveenbirkenbach 737adaa567 fix(shell): quote all password variables in shell tasks
Ensure all password usages in shell commands are safely quoted using the
`| quote` filter to prevent shell injection issues and test failures.

Affected roles:
- svc-db-postgres
- web-app-gitea
- web-app-mediawiki
- web-app-moodle
- web-app-shopware
- web-app-xwiki

https://chatgpt.com/share/6974dd33-9470-800f-b469-d31fabcd59e1
2026-01-24 15:54:29 +01:00
kevinveenbirkenbach b6c2dd01c8 fix(postgres): replace pg_isready with real TCP auth wait and add log dump on failure
- Replace pg_isready with psql TCP password authentication check
- Eliminate Postgres init race condition in CI
- Add rescue handler dumping last 200 container log lines on failure

https://chatgpt.com/share/696f4386-fbbc-800f-bdde-f1e6c4145c84
2026-01-20 11:38:15 +01:00
kevinveenbirkenbach 6c50831456 fix(postgres): avoid jinja/go-template collision in debug rescue block
Replace inline lookup('pipe', 'docker info -f {{.DockerRootDir}}') with a raw shell task
to prevent Jinja parsing errors ("unexpected '.'") during rescue debugging.

https://chatgpt.com/share/696f4386-fbbc-800f-bdde-f1e6c4145c84
2026-01-20 11:22:23 +01:00
kevinveenbirkenbach 7272c3d56c fix(postgres): escape docker inspect go-templates in rescue debug
- Replace jinja-colliding docker inspect formats with raw-escaped go-templates
- Keep POSTGRES_CONTAINER rendered by Ansible while preserving Docker templates
- Improve reliability and readability of rescue debug output

https://chatgpt.com/share/696f4386-fbbc-800f-bdde-f1e6c4145c84
2026-01-20 10:40:01 +01:00