Commit Graph
2780 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Fable 5 598b7c3a9a feat(svc-bkp-secrets-2-local): host secret material backup role
New svc-bkp-secrets-2-local role: a differential rsync --link-dest snapshot
of the host-generated secret material that lives in no docker volume, NFS
export or operator inventory - infinito secrets + tokens, the self-signed
CA, the Let's Encrypt account/certs, the ACME DNS credentials - plus the
node identity (ssh host keys + machine-id). files/recover.py restores each
present subtree to its fixed system path and gates the node-identity
restore behind --restore-node-identity. Wires the systemd unit + nightly
schedule (06_daemons, 07_schedule). Unit tests cover the domain/target map
and the main() subtree-selection + node-identity gating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:52 +02:00
kevinveenbirkenbachandClaude Fable 5 7b6ca22d75 feat(backup): recover.py framework for the svc-bkp-* role family
Add utils/recovery/base.py DirectoryRecovery: a service-backed pre-recover
(start the role's backup unit first) + rsync -a --delete mirror. Each
svc-bkp-* role gets a files/recover.py subclass (volume resolves the docker
mountpoint, nfs mirrors the export, local-2-device opens the newest LUKS
snapshot across machine hashes) or a documented one-way opt-out
(remote-2-local recover.py.nocheck). A lint guard enforces every svc-bkp-*
role ships a recover.py and a ## Recover README section; docs/architecture/
backup.md documents the end-to-end schema. Unit tests cover the base flow,
the per-role class logic and every recover.py main() CLI wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:52 +02:00
kevinveenbirkenbachandClaude Fable 5 ee18fb17ed test(lint): keep utils.cache.yaml out of env handlers
Env handlers feed the .env generation, which bootstraps fresh hosts
before PyYAML is initialised, so the YAML cache is unavailable at that
point. The guard fails on any utils.cache.yaml use under
utils/env/handlers/ and points to utils.cache.files.read_text with a
stdlib line parse (canonical pattern: utils/storage/nfs.py); per-line
nocheck suppression stays possible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:52 +02:00
kevinveenbirkenbachandClaude Fable 5 e1062a1555 test(lint): forbid absolute-path shell defaults outside a SPOT
A default like ${BACKUP_KEY_PATH:-/tmp/swarm-nfs-backup.key} duplicates
a path contract that belongs in a SPOT (default.env or a role vars
file) and drifts silently. The guard covers every ALL-CAPS variable
whose inline default starts with a slash, allows per-line nocheck
suppression, and its failure message forbids dodging the syntax via
conditional assignments. Legacy offenders carry explicit nocheck
reasons (act tooling runs before default.env exists, POSIX TMPDIR
convention).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:52 +02:00
kevinveenbirkenbachandClaude Fable 5 bb5f569d56 fix(env): fall back to sysfs minimum MTU when no default route is readable
Agent sandboxes run in a network namespace with an empty /proc/net/route
while /sys/class/net still exposes the host NICs, so dotenv generated the
static 1500 fallback although the host uplink is 1400 and every TLS
handshake from the DinD stack blackholed. Take the minimum MTU across UP
physical interfaces (device symlink present) before resorting to the
static default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 03:15:51 +02:00
kevinveenbirkenbachandClaude Fable 5 e2f8d4c304 refactor(container): drive exec timeout and CA paths from group_vars SPOTs
Two wrapper behaviours move onto single sources of truth:

Exec timeout: SYS_TIMEOUT_CONTAINER_EXEC (07_schedule.yml, default 7d so a
hard kill never beats a production migration; the dev/CI inventory
tightens it to 60min) reaches every task host via the play environment as
CONTAINER_EXEC_TIMEOUT. The wrapper prefixes only the exec subcommand with
coreutils timeout --kill-after=15. Ansible task timeouts are SIGALRM-based
and slept through the last wedged docker socket for 4.5 hours; the OS
level kill still fires there and returns 124, which until/failed_when and
the rescue diagnostics react to. Unset or 0 keeps todays behaviour.

CA container paths: the bind-mount targets for the injected root CA and
wrapper script lived as string literals in container.py, compose_ca.py
and the filter plugin. They now live once in the CA_TRUST group_vars SPOT
(inject_cert_container, inject_wrapper_container) and reach container.py
as env vars (play environment plus CA_TRUST_SYSTEMD_ENV) and compose_ca.py
as CLI arguments built by the compose_ca_inject_cmd lookup. A caller that
provides the host CA env but not the container paths is half-migrated and
fails loudly instead of silently dropping CA injection; a host without any
CA env keeps the soft fallback. All tests read the paths from the SPOT,
and a regression test asserts the runtime scripts carry no literal copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:58:28 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ede6225b2d chore(tests): drop narration comments from the lookup test setups
The three identical Exception-labelled comments over cache_base._reset()
restate what the call does; the reset itself is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 03:04:06 +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 1a908a6e5b perf(tests): parallelize suites, hoist fixtures, gate ansible-lint
Run the python suites through pytest-xdist when pytest is importable
(id-level collection parity with unittest discover verified per suite;
serial unittest stays the fallback), with -rP so warnings printed by
passing tests, like the update annotations, still reach the log. run.sh
self-provisions the dev extras through scripts/install/dev-extras.sh,
which stamps inside the venv and skips read-only venvs; make onboard
pre-warms the same path in the running container.

Hoist per-test cache resets and roles scans to setUpClass in the lookup
and resolvable suites (a per-test fingerprint-memo reset closes an
id()-reuse cache-poisoning flake the hoist exposed), inject generation
names into the nfs backup script test instead of sleeping, and narrow
ansible-lint scope to real ansible content. ansible-lint additionally
sits behind a content fingerprint gate that skips the run when nothing
it walks has changed; without a usable git (container bind mounts) the
gate disables itself and the full run remains.

Drop the import-linter layer and ignore for the deleted deps module;
the contract now locks compose -> common -> env. Measured on the host:
unit 101s to 20s, integration 90s to 24s, lint tests 43s to 19s,
repeated ansible-lint 53s to 0.4s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 21:20:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f76bfcf531 refactor(svc-bkp-volume-2-local): rename role from svc-bkp-container-2-local
Rename the role, its test tree, the primary entity key
(volume-2-local), the derived vars (BKP_VOLUME_2_LOCAL_*,
SYS_SERVICE_BACKUP_VOLUME_2_LOCAL), the run-once fact, and every
group_names consumer flag, include path, doc, and lint reference. The
container_backup service key and the on-disk backup-docker-to-local
tree keep their names: both are contracts with existing consumer
entries, snapshot trees, and the baudolo pull tooling.

Operators must rename the svc-bkp-container-2-local inventory group and
disable the previous versioned units on already-provisioned hosts; the
pre-deploy snapshot glob derives its base from the new role name and
will not find them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:52:02 +02:00
kevinveenbirkenbachandClaude Opus 4.8 adb8bcf696 feat(svc-bkp-nfs-2-local): scheduled differential NFS export backup
Add a systemd-driven rsync --link-dest backup of the NFS export into the
baudolo-compatible local snapshot tree, plus a pre-deploy snapshot hook in
the constructor. A missing export fails the nightly unit loudly, a failed
rsync removes its incomplete generation so it cannot poison the next
--link-dest, and rsync exit 24 from the live export is tolerated.

Both pre-deploy snapshot scripts (container and nfs) now resolve the
previous deploy's unit through a version glob, so the snapshot still runs
after a project version bump. The user-backup ssh wrapper whitelists the
new backup-nfs-to-local type for downstream pulls. The nfs-server role
declares the backup as a dynamic consumer with true/false pinning
variants; svc-bkp-container-2-local drops its variants.yml because
non-subject closure members resolve foreign round indices against it and
leak its companion pins into other rounds.

Unit tests execute the real script against tempdirs and prove hardlink
deduplication by inode, history isolation, delete semantics, loud failure
on a missing source, and cleanup of incomplete generations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:43:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f08d71534a feat(tooling): operator alias, shortcut, skill, cheat and onboard system
Add the i8* terminal aliases (make install-alias) and ai8* agent
conversation shortcuts (docs/.../agents/alias.md), move skill
distribution to the external INFINITO_SKILLS_REPOSITORY (make
install-skills), and add make cheat, make alias and make onboard. Introduce
custom.env operator overrides (win over defaults and handlers) and pin
INFINITO_ALIAS_MD / INFINITO_ALIAS_REPOSITORY / INFINITO_SKILLS_REPOSITORY
in default.env. Drop the in-repo skills-lock.json and its update-skills CI
job. AGENTS.md instructs agents to suggest the matching cli/prompt alias
when an operator writes something an alias covers. Lints enforce alias
sorting, i8/ai8 prefixes, no consonant clusters, full make-target
coverage, and terminal/agent shortcut non-overlap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:43:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b533863559 fix(env): keep the .env generation stdlib-only for fresh-host bootstrap
The nfs SPOT loaders pulled PyYAML into the .env generation, which bootstraps fresh hosts BEFORE python dependencies exist: the import crashed cli.meta.env, load.sh produced an empty environment and make install-system-python died on the required-var guard (environment/manjaro and pkgmgr/centos CI jobs). The loaders now line-parse the services.yml SPOTs via the stdlib-backed cached reader and the mapping path constant. The client's protocol version field becomes nfs_version, since services.yml reserves version for image tags (schema lint), and the image discovery casts versions defensively.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:54:33 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8519642373 refactor(storage): SPOT the nfs export base and client version in their roles
storage.nfs shrinks to the deployment-specific server address: export_base moves into svc-storage-nfs-server's services.yml (the server dictates where exports root) and the mount protocol version into svc-storage-nfs-client's services.yml (the client owns how it mounts). Both are read through utils.storage.nfs loaders by the nfs lookups, the env handler and 00_topology, eliminating the unrendered-variables class for these values; the group_vars assert shrinks accordingly and the swarm test extras stop restating role defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:54:32 +02:00
kevinveenbirkenbachandClaude Opus 4.8 22268974fc feat(services): let providers opt out of the loader preload
Registering svc-bkp-container-2-local as the container_backup provider made the constructor's service-loader preload it in the server bucket, ahead of mailu (web-app bucket) - but its alarm chain (sys-ctl-alm-email -> sys-svc-mail) hard-asserts the mailu no-reply token, and run_after cannot legally cross buckets. A provider can now declare preload: false: it stays resolvable in the registry (consumer flags, closures, variants) while ordered_primary_service_entries skips it, leaving the existing constructor group pass (which runs after the full preload) as its load path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:41 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ab53a8b847 feat(lint): recognize hadolint directives and nocheck-based raw-docker opt-outs
The comment lint learns hadolint as a directive class so 'hadolint ignore=' lines survive comment audits, and the compose raw-docker lint drops its path whitelist in favor of the same '# nocheck: raw-docker' head/inline suppression the docker variant already honors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:41 +02:00
kevinveenbirkenbachandClaude Opus 4.8 29184e38da fix(storage): resolve the nfs state subdir from the python SPOT
Lookup plugins receive ansible variables unrendered, so routing the state subdir through a group_vars jinja value handed state_path the literal '{{ lookup(...) }}' string and the controller mount asked the server for /srv/nfs/{{ lookup('nfs_state_subdir') }} (Connection refused, nextcloud swarm roundtrip). Both nfs lookups now import STATE_SUBDIR directly; the intermediate lookup, the group_vars indirection and the unit-test fixture var that masked the gap are gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c34dba8406 feat(lint): require the container_backup flag on stack roles declaring volumes
role_is_stack in the role-mapping SPOT recognises compose.yml.j2, *.compose.yml.j2 siblings and compose.*.yml.j2 variants (bigbluebutton's compose.override.yml.j2). The new lint walks every invokable stack role whose meta/volumes.yml declares a named volume and requires the container_backup consumer flag in meta/services.yml, so no stack role's data can silently stay outside the backup plan. Opt out per role with '# nocheck: backup-service-flag <reason>' on the first line of meta/volumes.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 63b5a60c94 feat(svc-bkp-container-2-local): provide the container_backup service
Register the role in the service registry under the abstract container_backup name (provides + enabled/shared/bond on the primary entry) so consumer roles can declare the backup dependency without naming the implementation. The run-once lint learns to find the 01_core.yml include inside a gated block, since this role wraps its core load in the IS_STACK_HOST block instead of duplicating the condition per task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 10a5725143 feat(lint): accept guard-in-when-list and dynamic-flag key suppression
The run-once guard lint required the when to be exactly the guard string, rejecting roles that legitimately combine it with further conditions (svc-bkp-container-2-local gates its whole core on IS_STACK_HOST). A when-list containing the guard now passes. The variants coverage test learns the same block-above '# nocheck: dynamic-flag' key suppression the services dynamic-flags test already honors, so infrastructure consumer flags can opt out of two-polarity variant coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:39 +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 cfa368ceff fix(opentalk): break the swarm bucket/client provisioning deadlock
The controller crash-looped on a missing S3 bucket and the recorder on an unprovisioned OIDC client while the swarm converge gate blocked before any post-flush task could create either. The embedded seaweedfs sidecar now bootstraps its consumer bucket itself at startup (new seaweedfs_sidecar_script filter: entrypoint-wrapped server, bounded readiness poll, idempotent weed shell bucket create, TERM/INT forwarded to the server), and the Keycloak clients are provisioned before the handler flush. Post-flush bucket tasks stay as an idempotent backstop; the obsolete restart tasks are dropped. Live-validated against chrislusf/seaweedfs:4.38: bucket created, stop behavior identical to the stock entrypoint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 17:30:36 +02:00
Alejandro Roman 9a16884f08 fix(sys-svc-compose-ca): never pull locally-built image tags in the CA override probe and honor .git/info/exclude in the project-tree walk 2026-07-09 16:54:23 +02:00
kevinveenbirkenbachandClaude Opus 4.8 00829757a0 feat(checkmk): pin to swarm manager so its node-local OMD volume survives
The checkmk OMD site cannot live on NFS (fcntl locks return EBADF) and must stay node-local, but an unpinned single-replica service loses that data on a swarm reschedule. Pin checkmk to the manager so the task never leaves the node holding its data, and teach the nfs-false-data-loss lint to accept a manager-pinned role as justification (dropping checkmk's now-redundant per-volume opt-out).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 02:19:09 +02:00
kevinveenbirkenbachandClaude Opus 4.8 09a131d007 refactor(placement): rename meta key default_placement to placement
Rename the meta/services.yml key across all roles, the roles_with_default_placement lookup (now roles_with_placement), the get_role_default_placement / iter_roles_with_default_placement helpers, and every consumer (swarm closure, compose_volumes filter, nfs_prep, container deploy template, constructor stage, extend_inventory) plus their tests and docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 02:19:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 22a391ac56 test(lint): require data-loss justification for nfs: false volumes
A node-local volume does not follow a swarm reschedule, so its data is lost on drain. Add a meta/volumes.yml lint that fails when a 'nfs: false' opt-out lacks both '# nocheck: nfs-false-data-loss' and a '# Reason:' arguing why that loss is acceptable. Justify the 10 existing entries (checkmk OMD site; gitlab repositories, logs, runtime state).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:03:05 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2c20e1598c fix(networks): move n8n + svc-runner off mailu's /24, gate overlaps
n8n (192.168.106.16/28) and svc-runner (192.168.106.0/28) sat inside mailu's 192.168.106.0/24, so co-deploying either with mailu aborted with "Pool overlaps with other one on this address space". Reassign both to free /24s (218, 219). The overlap test read meta/server.yml, which has no networks key, so it passed vacuously; point it at meta/networks.yml so it actually gates duplicate/overlapping subnets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 00:03:33 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c91df5de7 test(e2e): forbid raw page.goto, migrate all specs to Tor-resilient gotoOnion
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:59:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8b1110d571 style: apply autoformat reformatting (no functional change)
make autoformat normalised inline-comment spacing (`  #` -> ` #`) in role meta/services.yml + meta/variants.yml and re-wrapped a few Python files (ruff line-length). Pure formatting — verified via git diff -w (whitespace-only) and diff inspection (ruff line-wraps only, no logic change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:50:59 +02:00
kevinveenbirkenbachandClaude Opus 4.8 55ba9c0ffa feat(sys-front-inj-all): rewrite injected CDN origin to onion at request time
New to_onion_url filter swaps a clearnet host suffix for the tor_node and forces http (no-op without tor_node or off-primary hosts). body_filter.lua uses it in onionize() so that .onion requests receive the onion CDN origin for injected head/body snippets (e.g. universal-logout.js), which the Tor browser cannot reach on clearnet. Covered by 7 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:56:32 +02:00
kevinveenbirkenbachandClaude Opus 4.8 35dee215c8 test(playwright): route all timeouts through resolveTimeout for onion-compat
Add the global TIMEOUT_FACTOR and a shared timeouts.js helper: resolveTimeout(base) scales by TIMEOUT_FACTOR and, over a .onion target, an extra multiplier, so Tor's per-request latency no longer trips hard-coded timeouts. Migrate every raw timeout in the specs (timeout:/timeout=/setTimeout/waitForTimeout) to resolveTimeout, stage the helper in all three staging paths, and add test_no_raw_timeouts to forbid raw numeric timeouts. Clearnet behaviour is unchanged (factor 1: resolveTimeout(N) === N).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:38:06 +02:00
Alejandro Roman 29f5e2cba1 Merge remote-tracking branch 'upstream/main' into feature/role-stalwart-mail 2026-07-08 10:31:42 +02:00
Alejandro Roman f9319863cf removed guras and make local/CI DinD use .test isntead of .example for the whole stack 2026-07-08 10:11:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6741b8a7d4 feat(sys-ctl-hlth-webserver): deploy-aware onion web health checks
web_health_expectations swaps (exclusive) or extends (dual) each services.tor.enabled app's clearnet domains with their onion domains via _inject_onion_domains, so the probe checks exactly what the deploy serves; script.py probes .onion over http (plaintext-only) with a longer Tor timeout. Adds onion unit tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 09:45:01 +02:00
kevinveenbirkenbachandClaude Fable 5 1e3e3802a1 feat(meta/complexity): stack column and swarm-implies-stack
Add a `stack` column to the complexity matrix: true when a role renders
its own container stack (ships templates/*compose*.yml.j2), false for
host-only roles (backup crons, wireguard, swapfile), the swarm
infrastructure roles themselves, and central-webserver content
providers. Exposed as a sort key and filter field.

Make `swarm` imply `stack`: a role with no compose template of its own
is never a swarm stack-deploy target, so swarm is forced false when
stack is false. Because scripts/meta/resolve/apps.sh selects the swarm
deploy jobs via `--filter "swarm == true"`, this drops 17 stackless
roles (106 -> 89) from the swarm job list; all keep compose testing and
their swarm overlay coverage still runs through the webserver/proxy
role, so no real deploy coverage is lost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:37:28 +02:00
kevinveenbirkenbachandClaude Fable 5 bea9308ce5 fix(curl): timeout-harden the build-time Dockerfile curls
The curl-timeout lint scanned tasks, templates and .sh scripts but not
Dockerfiles, so four build-time downloads (composer for mediawiki and
joomla, wp-cli for wordpress, the Docker GPG key for svc-runner) could
still hang a build against a silent mirror. Pin --connect-timeout and
--max-time inline (Dockerfiles cannot render the filter) and extend the
lint scope to roles/*/files/**/Dockerfile so future ones are caught.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:37:28 +02:00
kevinveenbirkenbachandClaude Fable 5 15f95f0725 fix(curl): timeout-hardened curl SPOT filter, lint, and fleet-wide rollout
A curl against an accepted-but-silent peer hangs forever: task retries
never fire because the first attempt never returns, and the CI job dies
at the 6h runner cut (proven by the mailu admin-API hang that took the
suitecrm/peertube swarm legs down). The new `curl` filter renders the
hardened base command ("{{ 30 | curl }}" -> curl -s --connect-timeout 5
--max-time 30); scaling composes with lookup('timeout').

test_curl_via_filter enforces it: bare curl invocations in role tasks,
templates and shipped scripts fail the lint with a pointer to the
filter. Plain .sh files, which cannot render Jinja, pass by pinning
--max-time inline; Docker healthchecks are exempt (their timeout field
caps them); `# nocheck: curl-timeout` opts out (catalogued in
suppression.md).

Convert all 34 existing invocations: mailu (replacing the short-lived
MAILU_CURL var), elasticsearch, checkmk, yourls (300s for the
schema-creating installer POST), the telegram alert script, and the
plain scripts of jellyfin, svc-runner, apt_repo, moodle (600s tarball)
and the mattermost provisioner. Mailu's user/token tasks additionally
carry task-level timeouts so a wedged container exec cannot stall the
play.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:37:28 +02:00
kevinveenbirkenbachandClaude Fable 5 e35acc09bc feat(updater): support registry.gitlab.com images
Add registry.gitlab.com to the mirrorable registries so discovery
yields the GitLab CNG images to the version updater and mirror sync.

Seed the generic v2 tag scan with the OCI last cursor for v-prefixed
pins and raise the page size to 1000: CNG repos carry tens of
thousands of commit-sha tags that sort before the release tags, so an
unseeded scan exhausted the page cap without reaching any semver tag.

Pin every gitlab service version in meta/services.yml and drop the
GITLAB_VERSION template override so services.yml is the single source
for per-service versions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:37:28 +02:00
kevinveenbirkenbachandClaude Fable 5 aedb26eacc fix(bigbluebutton): pull prebuilt images and align with house patterns
The freeswitch build fetched libopusenc from launchpad.net, whose
librarian backend rate-limits CI (429s across 11 attempts); three more
build-time fetches carried the same risk. The maintainer publishes an
alangecker/* image for every buildable service, so compose_mods drops
build: wherever the service carries an image and compose pulls instead;
a guard task fails loudly if a future upstream service still builds.

Pin ref to the v3.0.23 release branch (the packaging repo has no tags)
and clone it without submodules via the new compose_repository_recursive
toggle: submodules only feed build contexts and DEV_MODE mounts, and
generate-compose falls back to the committed repos/tags file.

Declare the pulled images in meta/services.yml (updater-managed SPOT,
mirror pre-pull) and the named data volumes in meta/volumes.yml; the
override renders both through the standard container_image /
container_volumes / compose_volumes lookups with the service_name
pattern. Compose merges volumes by target, so the override's named
volumes verifiably replace the clone-local bind mounts (docker compose
config proven). This also heals a split-brain mount: apps-akka,
periodic and prometheus-exporter previously kept bind mounts while
their peers used the named volumes.

Teach container_image and compose_volumes to honour compose_mode_force
(the SPOT the other mode-aware lookups already read): bbb deploys as a
node-local compose stack in swarm, so it must get hub image refs and
plain local volumes there, not registry-prefixed refs and NFS
driver_opts. The unpinned-engine-volume lint learns the same two safe
conditions. Read the embedded Collabora port from web-svc-collabora's
services SPOT instead of hardcoding 9980.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:37:27 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0bf1e8ef8c refactor(gitlab): replace Omnibus with the Cloud Native GitLab composition
Deploy GitLab from the official CNG CE images as six services: webservice (puma) and sidekiq scale to the topology default, workhorse is the sole HTTP entry, gitaly and a one-shot migrations job are pinned singletons, shell runs gitlab-sshd. The Omnibus container, its Dockerfile and the GITLAB_OMNIBUS_* config groups are removed. All rails/workhorse/shell/gitaly configs and shared secrets are pre-rendered by Ansible and mounted read-only; eleven credentials land in meta/schema.yml including an rsa_pem_2048 OIDC signing key (new value_generator algorithm). Postgres, redis and S3 object storage ride the existing lookups; OIDC, SMTP, the public SSH port and the root password are preserved. compose_volumes gains a per-volume nfs:false opt-out so the gitaly repositories volume stays node-local in swarm (upstream forbids git repositories on NFS); nfs_prep skips volumes without driver_opts accordingly. The Nextcloud gitlab integration execs the CNG rails runner. ruby_single_line dies with its only consumer; ruby_dq keeps test coverage. Ports (8181/8080/2222/8075/3808/9122) and service names resolve from single SPOT declarations in meta/services.yml via vars constants; the version pin sits on the webservice image pair so the updater can bump it. Live-validated in compose up to all six services healthy and migrations completed; fresh-install-only semantics for existing Omnibus data are documented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:37:27 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9ecb2f8f5e test(lint): cover Dockerfiles in the comments lint
Dockerfiles have no suffix, so the comments lint never scanned them. Match the Dockerfile basename in the changed-targets collector and dispatch it to the hash-comment parser; changed-files-only semantics stay, so the legacy tree is unaffected until a Dockerfile is touched. Drop the remaining prose comment from the espocrm Dockerfile it immediately flagged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:37:27 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fbe50d10ed fix(svc-runner): make the runner role swarm-compatible
Workers failed 'docker build: no Dockerfile': the build context, compose file, .env and runner binary were staged on the stack host only while every node deploys its own node-local runner stack. Build and push the image once on the manager (RUNNER_IMAGE_REF via the swarm registry, 04_image.yml) and let workers pull it; render compose.yml (template with lstrip_blocks, matching the replaced stack_host_template action), .env and the extracted run.sh on every node. Add a node_local kwarg to the compose_networks/container_networks/compose_restart/compose_only lookups so this node-local compose stack renders a bridge default network (workers cannot create overlays), keeps restart: unless-stopped (ephemeral runners died after one job) and keeps container_name under swarm; default is unchanged for every other role. Treat build/install.stamp as stale when the venv is missing and exclude ./build from the nested-src copy, closing the nested-e2e ruamel failure class. Regression tests for the node_local render paths; the network include tests now accept lookup kwargs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:37:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 954da6d180 fix(tor): onion-aware CSP and oauth2-proxy (exclusive vs primary)
Over Tor a page served on an .onion still advertised a clearnet-only CSP and oauth2-proxy config, so onion origins were blocked (dashboard could not frame auth.<onion>; oauth2-proxy rejected the redirect back to the onion app). build_csp_header now mirrors the clearnet primary to the node onion per token; the keycloak oauth2-proxy config derives whitelist_domains and cookie_domains from the app resolved domains. Exclusive (onion-only) apps get onion tokens only, primary apps get both, non-tor apps stay clearnet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:25:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 21d76f922f fix(vault): preserve !vault tag on cache YAML round-trip
The !vault YAML constructor returned a plain str, so round-tripping a host_vars
file through utils.cache.yaml (e.g. apply_tor_node_onion writing
services.tor.node) re-emitted every credential as an untagged quoted string.
Ansible then never decrypted them and LDAP/DB admin binds failed with result 49.
Return a VaultScalar so SafeDumper re-emits the !vault literal block; add a
regression test asserting apply_tor_node_onion keeps existing vault credentials
tagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:22:00 +02:00
kevinveenbirkenbachandClaude Opus 4.8 283108f802 test(lint): require a single-replica nocheck marker on every replicas:1 pin
Adds tests/lint/ansible/roles/test_single_replica_justified.py: a bare replicas:1 in meta/services.yml is flagged so a single-replica pin is never applied silently; services that can scale should carry no replicas key and let compose_replicas pick the topology default. Marks the pins on genuinely single-instance services (audit-verified as unscalable): checkmk OMD/RRD single-writer, erpnext websocket/scheduler/configurator sidecars, jenkins controller, listmonk campaign-manager singleton, mastodon db:prepare one-shot, minio single-node (no erasure), opencloud decomposedfs+NATS single-writer, seaweedfs single-node, taiga admin sidecar, ejabberd standalone Mnesia.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 01:14:29 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6b07289d6b perf(swarm-registry-sync): drop mirrored externals from the engine after push
On disk-constrained sync runners (act/CI) a mirrored external image was double-stored: once in the docker engine and once in the local registry. After pushing, docker rmi the upstream and prefixed tags so only the registry copy remains; swarm re-pulls from the registry at deploy time. Locally-built images are never removed (recovery and base images depend on them staying in the engine store). Also skip the re-pull when the prefixed image already has a registry manifest. The compose image rewrite and registry push are unchanged; rmi is best-effort.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 00:18:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1f83ede271 fix(networks): omit the network name line when own_entity is empty
Roles whose get_entity_name(application_id) resolves to an empty string (e.g. svc-runner) rendered 'name: ' with no value in the compose networks block, producing an invalid/empty network definition. Guard both the swarm overlay and the compose bridge emissions so the name line is only written when own_entity is non-empty; docker then auto-names the network. Add regression tests for both modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 00:13:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 40a9eb5080 chore(format): autoformat wrap of test_tor_node read_text call
make autoformat re-wrapped the hostname read_text over multiple lines and moved the # nocheck: cache-read to the closing .strip() line; test-lint stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 00:11:55 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f6bed25b67 feat(services): resolve_service_config with provider fallback
Resolve services.<key>.<field> with consumer-override -> provider-native fallback (the provider being the role whose entity name is <key>, e.g. svc-net-tor for tor). Keeps provider defaults like tor.exclusive/tor.primary in one place while consumers stay free to override per app; consumed by utils.cache.domains for onion-domain injection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:53:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 005f649498 refactor(onion): the inventory provisioner is the SPOT for the node onion
Mint the node onion during provisioning and write its address straight into applications.svc-net-tor.services.tor.node, dropping the INFINITO_TOR_NODE_ONION env indirection: ensure_node_onion (was init_env) mints/reuses the .onion-identity key files + returns the address (no env write); identity_hs_dir/IDENTITY_DIRNAME move to utils.tor_onion so cli and utils share the path SPOT; provision.apply_tor_node_onion sets services.tor.node in host_vars when svc-net-tor is deployed (run by 'inventory provision' at deploy time, and the svc-net-tor role copies the same key files into the daemon); domains.py reads services.tor.node; nginx_vhosts reads the key file instead of os.environ; INFINITO_TOR_NODE_ONION removed from default.env/passthrough/compose + the inventory env-lookup. Bonus: the address now lives in the persistent key files, surviving .env regeneration by make test/autoformat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:53:18 +02:00