Commit Graph
26 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 c0b3ac9462 fix(svc-db-postgres): run the hba swap once, not twice
The script carried its procedure twice. A retry loop was added around the original
linear body, and the body below it stayed: a second pg_hba backup, a second
`trap ... EXIT`, a second prepended scram line, a second reload, two more auth_test
rounds and a second ALTER USER. Every invocation therefore rewrote the host-based
auth file twice and set the password twice.

It worked, because each step is idempotent, and it cost every call an extra pass
with two auth probes that wait up to thirty seconds each.

The subtle part is the output. The loop set `result` and never printed it; what the
caller read came from the second body, which echoed [CHANGED] or [UNCHANGED] from its
own pre_ok/post_ok. Deleting the duplicate alone would have left the script silent and
`changed_when: '[CHANGED]' in stdout` permanently false. The verdict now comes from
the pass that actually measured, through `echo "$result"`.

164 lines to 110. The contract is unchanged: [CHANGED] / [UNCHANGED] on stdout,
[FAIL] on stderr with exit 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 21:55:10 +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 5137465c6f style: delete every comment the comments-valid policy rejects
3529 invalid comments in 528 files, grandfathered in while the linter
only saw the working-tree diff. Removal ran through the linter's own
collectors, so detection and deletion cannot drift: a comment whose
opener line is blank before it and whose closer line is blank after it
loses all its lines, anything else loses only the comment region, and a
line left holding a bare delimiter goes with it.

Five files keep their comments behind a nocheck marker instead. Four are
upstream configs whose inline docs are upstream's own (mautrix, systemd
journald, nextcloud-oidc-login, MediaWiki). In littlejs the
commented-out entries ARE the deactivation mechanism for an example
tile, so deleting them would delete configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:22:02 +02:00
kevinveenbirkenbachandClaude Opus 5 04eac9d73d fix(svc-db-tests): report why the SOCKS connect failed instead of swallowing it
The Tor-path credential tests tunnel their client through a local SOCKS5 shim.
When the CONNECT fails, handle() closed the client socket and returned, so the
client library saw nothing but an empty socket and reported whatever its own
generic read-EOF error is.

For MySQL that is `(2013, 'Lost connection to MySQL server during query')` —
which claims a query was in flight when none ever was. Fediwall 1 printed that
twenty times and it sent the investigation looking for a server that had hung
up, when in truth no HiddenServicePort existed behind the dialled onion port.
Postgres and ldap3 are less misleading ("server closed the connection
unexpectedly", "Connection reset by peer") but equally silent about the cause.

Reproduced for all three against a SOCKS5 proxy that completes the no-auth
greeting and rejects CONNECT with rep=5, running the real python block out of
each test.sh: every one now prints `socks connect failed: SOCKS5 CONNECT failed
rep=5` ahead of its library error.

This does not change any verdict — the root cause is fixed by 2ba3a14b9, where
tor_reachable requires both `services.tor.enabled` and an `exposed: true` that
actually earns a forward. This only stops the next occurrence from lying.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:33:32 +02:00
kevinveenbirkenbachandClaude Opus 5 2ba3a14b98 refactor(svc-db): gate the Tor credential tests on one shared lookup
The three svc-db-* credential tests each hand-rolled the same transport
gate in their test.env.j2:

    (lookup('config', application_id, 'services.tor.enabled') | bool
     and lookup('config', application_id, 'services.<entity>.exposed') | bool)

Three copies of one predicate, each naming its own entity key, and each
free to drift from the onion mapping that actually decides reachability.

Extract it into plugins/lookup/tor_reachable.py, split the same way as
its sibling tor_ports.py: a plain is_reachable() for callers holding the
merged applications view, plus a thin LookupModule resolving it through
lookup('applications'). Because collect_exposed_ports() scans
services.values(), the entity name disappears from the templates
entirely -- they collapse to

    TOR_ENABLED={{ lookup('tor_reachable', application_id) | lower }}

Rework the integration guard accordingly: it now asserts that every
svc-db-* template delegates to the shared lookup, and drives its
base/variant agreement table through the real is_reachable() instead of
re-implementing the predicate. Add unit coverage for the new lookup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 00:50:47 +02:00
kevinveenbirkenbachandClaude Fable 5 f4e8930579 fix(svc-db): give the onion credential tests tor's 120s window
A cold onion connect alone measured 15.7s and tor holds SOCKS streams
up to 120s while fetching the descriptor; the 30s caps cut cold
circuits mid-handshake across all three forwarders and DB clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:37:19 +02:00
kevinveenbirkenbachandClaude Fable 5 d88f272f9f Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:31:08 +02:00
kevinveenbirkenbachandClaude Fable 5 c505ce7466 test(lint): forbid unmarked '|| true' in shell scripts; grandfather the corpus
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:39:16 +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 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 4e582c9d86 fix(svc-db): satisfy shellcheck + env-sort lint on the DB test scaffolding
test.sh: dispatch auth via a run_auth wrapper calling auth_tor/auth_local directly — shellcheck SC2317 flagged the indirect ${attempt_fn} invocation as unreachable. test.env.j2: sort keys ASC for the env alphabetical-order lint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:53:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 111e8cc7b2 feat(svc-db): per-DB exposed toggle + Tor/local credential tests
Add exposed: bool (default false) to svc-db-postgres/mariadb/openldap meta/services.yml with a variant-0 (exposed) / variant-1 (not) toggle, plus real credential e2e tests (files/test/test.sh + templates/test.env.j2): authenticate with actual credentials over the node Tor SOCKS proxy when the variant enables tor, locally in-container (password auth) otherwise. mariadb/openldap force the local publish (+ OPENLDAP_PORT var); secrets stay off the process argv (MYSQL_PWD, ldapwhoami -y /dev/stdin, PGPASSWORD env).

Depends on shared onion pieces landing in a follow-up commit: tor_ports exposed-port collection, svc-net-tor services.tor.node + services.tor.ports.local.socks SPOT, the tor_socks lookup, and test-e2e-cli 0600/stage-cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:53:17 +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 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
kevinveenbirkenbach 8a75959c11 Merge branch 'main' into feature/makefile-wsl2
# Conflicts:
#	.github/workflows/test-development.yml
#	Makefile
#	roles/svc-db-postgres/files/set_postgres_superuser_password.sh
#	scripts/administration/apparmor/restore.sh
#	scripts/administration/apparmor/teardown.sh
#	scripts/dns/setup.sh
#	scripts/install/package.sh
#	scripts/install/venv.sh
#	scripts/tests/development.sh
2026-03-24 20:20:27 +01:00
Alejandro Roman 954807e195 fix lint issues 2026-03-21 16:08:28 +01:00
Alejandro Roman 5018ecf145 fix all windows issues with the app + more robust for all 2026-03-21 16:08:28 +01:00
Alejandro Roman da6e87191b fix(makefile-wsl): Fix the tesl app error and the ca trust 2026-03-21 16:08:28 +01:00
kevinveenbirkenbach 19c72c6b25 fix(postgres): make EXIT trap shellcheck-compliant 2026-03-20 16:12:04 +01:00
kevinveenbirkenbach e3a16cbe8a refactor: switch docker CLI usage to container wrapper
- Replace direct docker calls with container wrapper across scripts, roles, and templates
- Add support for cp, start, stop, and restart passthrough commands
- Align handlers and notifications to container manager restart semantics
- Fix systemd ExecStartPost to use container wrapper without hardcoded paths
- Silence ShellCheck SC2016 false positive for container exec password injection
- Update tests and documentation strings to reflect container-based workflow

https://chatgpt.com/share/69866958-c88c-800f-9b4f-3a972615bf42
2026-02-06 23:21:02 +01:00
kevinveenbirkenbach 59b2d09eb7 fix(shellcheck): suppress false-positive for trap-invoked function
SC2329 is a known ShellCheck limitation for functions invoked via trap.

https://chatgpt.com/share/69755296-2d74-800f-8675-261a283e3782
2026-01-25 00:18:35 +01:00
kevinveenbirkenbach db2a9a6539 fix(shellcheck): address unused loop var and trap false positive
- Replace seq-based loop with brace expansion to avoid unused variable warning
- Use quoted trap handler so ShellCheck detects restore_hba invocation

https://chatgpt.com/share/69755296-2d74-800f-8675-261a283e3782
2026-01-25 00:15:27 +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