Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 72bb1f7fb0 test(swarm): fail the report collector on a 15-minute docker exec timeout
The collector piped 'docker exec tar | tar' with '|| true', so a hung
'docker exec' blocked the run indefinitely and failures were swallowed.
Bound it with 'timeout 900'; a producer exit of 124 (manager hung) now
fails the collector via PIPESTATUS, while other collection errors stay
best-effort so partial reports do not fail the job.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 de95b5f240 fix(web-app-wordpress): gate the SSO env assertions behind isServiceEnabled
The playwright beforeEach required KEYCLOAK_BASE_URL, OIDC_ISSUER_URL and
the super-admin credentials unconditionally, but playwright.env.j2 sets
SUPER_ADMIN_* to "" when SSO is off, so non-SSO variants failed with
"SUPER_ADMIN_USERNAME must be set" at spec line 508. Wrap those checks
in isServiceEnabled("sso") (the same SSO_SERVICE_ENABLED flag the
template keys off), matching the dashboard role. The SSO variant still
runs them; both swarm and compose non-SSO variants were affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0d9ca2dc5a fix(web-app-pixelfed): drop trailing env comments that leak into values in swarm
'docker stack deploy' reads an env-file value as everything after '=',
so 'BROADCAST_DRIVER=log  # ...' set the broadcast connection to
'log  # log driver for local development' and Laravel aborted with
"Broadcast connection [...] is not defined". Compose strips the comment,
so only swarm failed. Move the nocheck directives above their lines and
drop the inline comments so the values stay clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:41:43 +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 6ed44cb9be fix(web-app-taiga): don't let the manager re-apply migrations in compose
taiga-back applies migrations from its own entrypoint on every boot. The
manager sidecar then ran 'manage.py migrate' too, and in compose there
is no converge wait, so the two applied the same migrations at once and
aborted with a duplicate-object error ("column ... already exists").
Swarm passed because its converge wait means taiga-back is already
migrated before the manager runs. Make the manager only wait for the
migrations to be applied ('migrate --check', read-only, retried) instead
of applying them, so taiga-back stays the sole migrator. No-op in swarm
where the check passes immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d4e1103fc3 fix(web-app-joomla): serialize first-boot copy and install across replicas
The stock image copies /usr/src/joomla into /var/www/html and runs the
auto-installer on every boot. With more than one replica sharing the
NFS volume they corrupt the tree and race the install. Add a custom
entrypoint that takes an atomic mkdir lock on the shared volume so one
leader runs the setup while the others block on a ready marker, wired
through the same host-path build args as the odoo entrypoint. Verified
locally: 2 concurrent replicas on a shared volume copy and install
exactly once and both serve HTTP 200. Sessions are already stored in
the database and caching is off, so the replicas stay stateless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 819b5ecf82 fix(web-app-snipe-it): serialize replica migrations with a redis lock
The stock image runs 'php artisan migrate --force' on every boot, so
concurrent swarm replicas race the first migration and abort with
"Table 'migrations' already exists". Build a custom image that patches
the migration to '--force --isolated' and point cache, session and the
lock store at redis, so exactly one replica migrates while the rest
skip. Verified locally: 3 stock replicas race on 2 of 3; 3 patched
replicas apply all migrations once with zero errors and all serve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Fable 5 0719b69131 fix(web-svc-libretranslate): bake pinned models into a versioned image
Without persisted models a rescheduled swarm task re-downloads the argos
packages on a fresh node, which exceeds the reschedule reachability
probe window and fails the chaos test. Build a custom image that bakes
the configured language pairs (services.libretranslate.load_only, joined
into LT_LOAD_ONLY and the install_models build arg from a single SPOT)
so every node starts warm. Proven live: the baked image serves HTTP 200
within ~20s with no runtime download. Pin the base to v1.9.6 instead of
latest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Fable 5 913455c139 fix(web-app-odoo): initialize the pinned database on first boot
The odoo config pins db_name with list_db=False, so on an empty schema
odoo preloads that database at boot, crashes (KeyError 'ir.http',
relation ir_module_module does not exist) and never binds its port; the
swarm converge wait then deadlocks because the healthcheck can never
pass and 01_init_database only runs after convergence.

Wrap the image entrypoint: probe the schema via psycopg2 against the
mounted odoo.conf, install the core modules once when absent, then hand
off to the stock entrypoint so the server boots healthy on the first
attempt. Proven live: a container against a fresh postgres installs
base and serves /web/health 200. Compose is idempotent (schema present
-> skip). The entrypoint file/paths come from a single ODOO_ENTRYPOINT_
SCRIPT_FILE SPOT (host abs/rel + docker dest), built via the same
build-arg pattern as web-app-suitecrm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Fable 5 699008a168 fix(web-app-nextcloud): mount the infinito config dir as a bind, not a swarm config
infinito_conf's source is a DIRECTORY (volumes/infinito, rendered from
config/*.j2), but a Docker Swarm config object is a single file, so
docker stack deploy aborted the whole nextcloud stack with
'read .../infinito: is a directory'. Compose only tolerated it by
emulating configs: as bind-mounts. Declare it as type: bind read_only
so a directory payload mounts correctly in both modes; the single-file
nginx_conf and nextcloud_includes stay config objects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:41:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 61adfd094e style(meta): autoformat role meta YAML (--- markers, line wrap)
Byproducts of `make autoformat`: add YAML document-start markers to
svc-runner meta info/schema and wrap the jellyfin admin-password
description line. No semantic change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 16:39:41 +02:00
kevinveenbirkenbachandClaude Opus 4.8 266a37d965 feat(svc-net-tor): onion mail/SSH ports + TLS-off compat (CA CN, HSTS, msmtp)
- svc-net-tor: expose SSH (22) + Mailu SMTP/submission/IMAP (25/465/587/143/993)
  as HiddenServicePorts on the node onion (TOR_ONION_SSH_ENABLED, 19_tor.yml).
- email lookup: relay plaintext (tls off, port 25) for .onion mail domains —
  no public TLS cert for a .onion; onion transport is already authenticated.
- CA root CN: drop the " Root CA" suffix when a 62-char v3 onion + suffix would
  overflow the X.509 commonName 64-char cap (02_tls.yml).
- Playwright onion compat: HSTS is HTTPS-only (RFC 6797), absent on the http
  onion vhost. New shared helper expectHstsWhenTls() gates the assertion on the
  reached scheme; wired into web-svc-cdn spec.
- rerun-spec.sh: route the inner-loop browser through the Tor SOCKS proxy when
  the staged env targets a .onion, mirroring the deploy-time runner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 16:39:40 +02:00
kevinveenbirkenbachandClaude Fable 5 1aba5707f8 chore: quality fixups for the run-28721118458 fix series
Drop the now-unused TAIGA_INSTANCE_DIRECTORY (its only consumer left
with the orphaned compose-inits render), mark the two discourse
launcher execs with the container-exec-resolver nocheck (the launcher
container is named literally in both modes), and purge narration
comments from the dotenv quote tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:20:19 +02:00
kevinveenbirkenbachandClaude Fable 5 6b1c5db186 fix(web-app-opentalk): declare sso as the hard dependency it is
The controller refuses to start without an [oidc] (or legacy
[keycloak]) section (upstream settings validation:
OidcConfigurationMissing), so an sso-disabled OpenTalk cannot exist.
The all-off variant only ever passed because the old include resolver
ignored variant pins and pulled keycloak in via run_after; once
2f9a54cea enforced the pins, variant 1 deployed without keycloak and
the unguarded kcadm provisioning crashed in compose while controller
and recorder crashlooped on the unreachable issuer in swarm.

Pin services.sso to enabled/shared true (mirroring the hard ldap:false
next to it), drop the sso pins from both variants, and move the
keycloak client provisioning into 03_keycloak_client.yml behind an
OPENTALK_OIDC_ENABLED include gate as a defensive skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:20:19 +02:00
kevinveenbirkenbachandClaude Fable 5 c34bca4c9c fix(sso-proxy): skip OIDC discovery so sidecars start before keycloak
oauth2-proxy fetched the .well-known document at startup and
crashlooped with DiscoveryFailed whenever keycloak was not up yet; the
web-svc phase converges per role before the web-app phase deploys
keycloak, so web-svc consumers with an oauth2 sidecar (libretranslate)
deadlocked their converge wait deterministically.

Pin the deterministic keycloak endpoints from the OIDC.CLIENT SPOT
(AUTHORIZE_URL/TOKEN_URL/CERTS, byte-identical to the discovery values)
with skip_oidc_discovery. Verified on the production image v7.15.3:
the proxy passes config validation, serves /ping 200 and maps its
upstream even with the issuer unreachable; the jwks fetch happens
lazily on first login. Existing installations talk to the same URLs
discovery returned before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:20:19 +02:00
kevinveenbirkenbachandClaude Fable 5 eed8444f1c refactor(web-app-moodle): source php limits from services.yml performance block
Move the hardcoded memory_limit/upload_max_filesize/post_max_size vars
onto services.moodle.performance.php.* config lookups, matching the
nextcloud and joomla performance blocks. Values are unchanged; the
limits become operator-overridable through the merged config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:29 +02:00
kevinveenbirkenbachandClaude Fable 5 c6ed8234b4 fix(swarm-tests): flavor-agnostic NFS assert mount and busybox-safe probe
The post-reschedule assert mounted the state export with vers=3, which
ganesha does not serve ('Protocol not supported'), so the marker check
false-negatived and killed the gitea roundtrip; reproduced and the
v4 mount shown to read the marker on the live cluster. Replace the
single mount with a cascade covering every production client_src form:
nfs4 vers=4 against the full path (ganesha), against :/ (kernel fsid=0
root), then the previous vers=3 full-path arm (kernel v3). The block
also returned the umount status instead of the grep result, letting a
missing marker pass silently; propagate the grep rc explicitly.

The in-container reachability probe's last arm used bash /dev/tcp, but
e.g. the ejabberd image ships neither bash nor curl; use busybox
nc -z instead and move the probe chain into probe_app_reachable() in
_context.sh alongside a NFS_CHECK_MOUNTPOINT SPOT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:29 +02:00
kevinveenbirkenbachandClaude Fable 5 8304e7e48a fix(web-app-suitecrm): one-time permission pass and cache gate for NFS boots
The entrypoint walked the whole /var/www/html tree on EVERY boot
(chown -R plus two find/chmod passes over 100k+ inodes, one NFS
roundtrip each) and re-ran cache:clear/warmup unconditionally, all
before the healthcheck file exists. On the NFS-backed swarm volume each
deploy pass burned the full converge budget and the jobs hit the time
limit.

Gate the permission block behind a .permissions-applied stamp written
only after a completed pass (kill-mid-walk safe: an interrupted walk
reruns fully), batch the chmods, drop the recursive TMPDIR chown, run
cache:clear/warmup only on fresh installs or when cache/prod is
missing (Symfony rebuilds a missing prod cache itself), and give the
first boot a 15m healthcheck start period. The stamp lives on the
shared volume, so a rescheduled task skips the walk too. Known edge:
an image upgrade on a persisted volume keeps the old cache until a
manual clear; CI reinstall/update paths are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:29 +02:00
kevinveenbirkenbachandClaude Fable 5 4948a62538 fix(web-app-joomla): survive the NFS first boot (512M php memory + 5m start period)
The web SAPI ran at PHP's 128M default and OOMed during the first-boot
install tree scans over NFS, so the healthcheck flapped through kill
cycles until the swarm jobs hit the time budget. Raise memory_limit to
512M, declared in services.joomla.performance.php.memory_limit and
consumed via a vars constant like the nextcloud performance block, and
give the healthcheck a 5m start period since the observed first boot
already exceeds the 30s default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:29 +02:00
kevinveenbirkenbachandClaude Fable 5 5a0dfaa2e8 fix(web-app-yourls): point the sso upstream at the real 8080 listener
The yourls apache only listens on 8080 (upstream ports.conf has never
shipped a port-80 vhost), so the oauth2 sso proxy's upstream port 80
answered connection-refused and every authenticated request 502'd.
Derive the origin port from services.yourls.ports.internal.http like
web-app-gitea does, yielding yourls:8080 in compose and
yourls_yourls:8080 in swarm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:29 +02:00
kevinveenbirkenbachandClaude Fable 5 d9ebf47485 fix(web-app-jenkins): probe /login instead of the 403 root
The JCasC authorization is roleBased with no anonymous grant, so the
healthcheck's GET / received 403 in every variant and the swarm service
never became healthy. /login is served anonymously by core (200 for
local/LDAP realms, 302 for the OIDC realm; curl -f passes both),
matching the login/health endpoint idiom used by keycloak, checkmk and
others. Same probe in compose, where it upgrades a tolerated-unhealthy
container to healthy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:29 +02:00
kevinveenbirkenbachandClaude Fable 5 8db5e1fcdc fix(web-app-odoo): tcp healthcheck so the stack converges before the db init
/web/health answers 500 until the database is initialised, but the init
runs only after the converge wait triggered by the handler flush, so
curl -f kept the replica unhealthy and the deploy deadlocked (and burned
the whole converge budget in the cancelled variants).

Switch to the tcp healthcheck: it accepts any HTTP/1.1 status line, so
the pre-init replica counts as running, converge passes, the init runs
and the periodic check then sees 200. A crashed container still fails
the probe (closed port), so genuine failures are not masked. The odoo
image ships bash for the /dev/tcp probe; compose has no wait gate and
is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 53d74dc81c fix(web-app-pixelfed): gate the worker healthcheck on the bootstrap marker
The worker healthcheck required a running horizon, but horizon only
starts after the app bootstrap which runs AFTER the swarm converge
wait: the worker was killed unhealthy (exit 137) in a loop and the
stack never converged.

Pass the check while /var/www/storage/.docker.init is absent; the
bootstrap touches that marker on the shared pixelfed_data volume the
worker mounts at the same path, after which the real horizon:status
verification applies. The test string runs via CMD-SHELL in both modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 97b1ffddb4 fix(dotenv): quote whitespace values in the swarm env passthrough
The swarm branch passed values through raw, so a value with interior
whitespace (MAIL_FROM_NAME='Infinito.Nexus by ...') landed unquoted in
the rendered .env and broke the env parser: the snipe-it service
crashlooped at 0/3 until the converge wait failed in every variant.

Quote (and escape backslash/double-quote) only when the value contains
space/tab/newline; everything else stays raw, so the swarm invariants
hold: no compose $$-doubling, raw $ and double quotes preserved,
and the playwright --env-file path (docker CLI keeps quotes literal)
is unaffected because no playwright env value can contain whitespace.
Compose quoting is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 f5911e1ec4 fix(web-app-wordpress): repair discourse addon exec and gate keycloak env on sso
Three defects broke the wordpress jobs in both modes:

The wp-discourse shell tasks used folded scalars whose continuation
lines were indented deeper than the first, so YAML preserved the
newlines and the shell ran three separate commands instead of one
(--path=... reached the host shell as its own command). Align the
continuations so the fold collapses to a single line.

The discourse exec address came from container_address, which expands
to a resolve subshell under swarm; the argv-form command task passed it
verbatim to docker as a container name. Discourse is launcher-based and
its container is literally named 'discourse' in both modes, so use the
services.discourse.name config lookup instead.

The playwright env rendered keycloak credential lookups uncondition-
ally, crashing the sso-disabled variant; gate them on
lookup('service','sso').required like the sibling flags, and drop
narration comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 d7b1260aec fix(web-app-taiga): deploy the manager sidecar from the real compose file
The manager exec-sidecar (migrations/admin via container exec) was only
defined in compose-inits.yml.j2, which was rendered to disk but never
passed to docker: compose deploys failed with 'No such container:
taiga-manager' and swarm burned the full converge wait polling the
never-created taiga_manager service in all three variants.

Move the service block into compose.yml.j2 (joining the taiga overlay
like every sibling service, pinned to a single replica) and drop the
orphan template, its render task and the unused path var. The sidecar
is a sleep-infinity shell; ops still run only via the targeted ansible
exec, so nothing self-executes regardless of scheduling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 1e50e4adeb fix(web-app-nextcloud): replace volumes_from with an explicit ro data mount on the proxy
docker stack deploy rejects volumes_from ('compose file contains
unsupported options'), so every nextcloud swarm variant died before the
stack was even created. Mount the shared data volume on the proxy
read-only via meta/volumes.yml instead; the render pipeline emits
data:/var/www/html/:ro and the proxy nginx only reads the tree
(try_files/static/fastcgi_pass; temp dirs live in the container fs).
Compose behaviour is equivalent, matching upstream fpm+nginx examples.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 5d867c5dcc fix(sys-svc-proxy): include prometheus locations in ws_generic vhosts
The ws_generic vhost flavour (mastodon, espocrm, odoo) never rendered
the prometheus nginx locations that basic.conf.j2 includes, so those
apps served 404 on /metricz and /healthz/* and emitted no app= metric
label; the prometheus e2e spec failed for every ws_generic consumer.

Mirror the basic.conf.j2 include inside the ws_generic server block.
The include self-gates on prometheus_integration_active, so vhosts
without prometheus render byte-identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 b0f3e6e1ff fix(web-app-bigbluebutton): map the keycloak domain to host-gateway in the bbb compose project
Greenlight's server-side OIDC discovery failed with 'Connection refused'
against auth.<domain>: inside the isolated bbb-net the domain resolves
to 127.0.0.1, i.e. the greenlight container itself, while openresty
publishes 443 in host mode on the node. Playwright's OIDC login specs
timed out on greenlight's 404 page as a result.

compose_mods now takes an extra_hosts kwarg and injects the mapping into
every service except host-networked ones (docker rejects the combination
there). Existing list or dict extra_hosts entries are merged, not
replaced. The bootstrap task passes '<keycloak domain>:host-gateway'
when web-app-keycloak is in the play, so both compose (DinD host) and
swarm (manager node) route the discovery through the local openresty.

Verified live: curl from bbb-net with the mapping reaches the issuer
(200), and the full bbb swarm roundtrip including the OIDC specs passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 3b8ee74fa3 fix(web-svc-collabora): opt the WOPI health root out of the matomo tracker e2e
Collabora's canonical url serves the plain-text coolwsd health probe, so
the tracker spec navigating there can never observe an injected _paq
script. The matomo declaration stays: the vhost keeps the matomo CSP
allowance and the lua head injection for the real HTML surfaces
(/browser/<ver>/cool.html editor UI, admin console) that are loaded via
iframe. track: false only drops the role from MATOMO_TARGET_ROLES_JSON,
mirroring the scrape: false prometheus opt-out in the same file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:27 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e6538e4f61 feat(svc-net-tor): Tor onion-service provider (full onion node)
Turn a NAT/edge node into a full Tor v3 onion node: DOMAIN_PRIMARY
becomes the node's .onion, so the whole stack (web, SSO, LDAP, CA)
resolves onion domains and is reachable over Tor without a public IP,
port forwarding, VPS or DynDNS.

- utils/tor_onion.py: offline v3 key mint (address + HiddenServiceDir
  key files), verified against a known key->address test vector.
- cli .../inventory/onion: `init-env` pre-mints the node onion, writes
  INFINITO_DOMAIN(+_RE) into .env and authoritative key files into
  .onion-identity/hs (idempotent). The onion is a deploy-time input;
  no in-deploy random minting (that would not match the pre-set domain).
- roles/svc-net-tor: Tor sidecar (Debian-built, network_mode: host),
  installs the minted keys, torrc with one node HiddenService + a SOCKS
  proxy restricted to loopback/DiD ranges. files/test.sh probes every
  served onion domain over the Tor SOCKS.
- tls: .onion -> TLS off (onion v3 crypto is the transport auth; LE
  cannot sign .onion) + app-level flavor override.
- harness/DNS: coredns resolves the onion zone via INFINITO_DOMAIN;
  skip the clearnet DNS sanity check for onion; raise nginx
  server_names_hash_bucket_size to 128 for long onion server_names.
- test-e2e-playwright: gated browser SOCKS proxy (PLAYWRIGHT_PROXY) +
  navigation/action timeouts so specs reach .onion over Tor and fail
  fast; the onion node points it at the Tor SOCKS.
- docs/requirements/031: full design.

Verified: dashboard + SSO (Keycloak) + 9 apps reachable over Tor
(HTTP 200 through the node hidden service). make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:38:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d549c2eaec docs(req): add 031 svc-net-tor full onion node requirements
Requirements for a Tor v3 onion-service provider role that turns a
NAT/CGNAT node into a full onion node: DOMAIN_PRIMARY becomes the node
.onion (whole stack shifts, no domain-transform), key minted offline at
inventory build, Tor sidecar restores it, per-domain TLS keeps optional
public domains, egress torification off by default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:42:52 +02:00
kevinveenbirkenbach 3cb0b947ae Merge branch 'main' into feature/swarm-nfs 2026-07-05 00:02:23 +02:00
kevinveenbirkenbachandClaude Fable 5 7613b3508b fix(web-app-mastodon): run cleanup and admin execs on the container's node
Under swarm MASTODON_CONTAINER_ADDRESS expands to a resolve subshell
that can return the container id of a task living on a worker; the
manager then runs 'container exec <cid>' against its local daemon and
fails with 'No such container' (seen in the fediwall variant-2 job).

Reuse the resolve pattern 03_setup already proved in the same run:
exec MASTODON_LOCAL_CID delegated to MASTODON_HOST_NODE. The delegation
for 06_administrator is hoisted onto the include via apply. Compose is
unchanged: the resolver returns the compose container name and the
inventory host there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 a80f8a51a7 fix(web-app-mediawiki): 5m healthcheck start period so swarm converges before the schema install
On a fresh database the mediawiki tasks answer GET / with 500 until the
schema install runs, but 03_install only executes after the swarm
converge wait. With the default 30s start period the healthcheck reaped
every task before convergence, so the play died with 'not converged:
mediawiki_mediawiki' and the install never ran.

A 5m start period keeps the tasks Running through converge + install;
the next periodic check then turns them healthy. Even if the window is
exceeded the replacement task starts against the installed schema, so
the deadlock cannot recur. Compose is unaffected (no wait gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 979f60a68b fix(web-app-erpnext): stop publishing the websocket port on the frontend
The frontend nginx only listens on 8080; container port 9000 belongs to
the separate websocket service, which the frontend reaches internally
via its /socket.io proxy (SOCKETIO env). Publishing 4004->9000 on the
frontend therefore pointed at a port with no listener, and the swarm
reachability probe failed with 'manager port 4004/tcp unreachable'.

Publish only the http port. Realtime traffic is unaffected: it enters
through the http vhost and is proxied to websocket:9000 inside the
stack, matching upstream frappe_docker which publishes the frontend
alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 0ed87c9380 fix(web-app-checkmk): pin the stateful OMD site to a single replica
Without service_replicas the swarm deploy falls back to the topology
default of one replica per host, so three checkmk containers raced for
the same OMD site directory on the shared volume and crashlooped
(exit 2/1/137, replicas stuck at 1/3) until the converge wait failed
with 'not converged: checkmk_checkmk'.

Checkmk's container model is one container per site; scaling happens
via distributed monitoring with independent sites, never via replicas
on shared state. Mirrors web-app-jenkins and web-app-suitecrm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 1851fa46ae fix(web-app-seaweedfs): use the service key as sso upstream origin host
The oauth2 sso proxy builds its swarm upstream as <entity>_<origin.host>.
With origin.host set to the nginx container_name 'seaweedfs-proxy' the
upstream resolved to 'seaweedfs_seaweedfs-proxy', which is not a swarm
service DNS name, so every filer/master/buckets request through the
sidecar returned 502 and broke the shared seaweedfs Playwright suite for
fider, funkwhale and gitea.

Point origin.host at the bare service key 'proxy' so swarm yields
'seaweedfs_proxy' and compose resolves 'proxy' via the service-key alias,
matching web-app-kix and web-app-openproject.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 65e34746fc feat(lint): forbid role_path in shell/command task bodies
role_path expands to the controller's checkout path while shell/command
strings execute on the (possibly delegated) target host, so
{{ role_path }}/files/x references break on hosts without the checkout.
lookup()/query() expressions and ansible.builtin.script are exempt: they
evaluate on the controller respectively copy the file themselves.

Migrate the one real offender, the sys-service-terminator docker
diagnostics, from a shell wrapper to ansible.builtin.script with the
exports moved into the task environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 fc90a4781c fix(espocrm,zammad): feed container-exec scripts via stdin lookup
container exec ... < {{ role_path }}/files/X reads the file on the
executing host. Under swarm the manager-ops execs are delegated to the
node hosting the container, where the controller checkout does not
exist, so the redirect failed with 'No such file or directory'.

Read the script on the controller via lookup('file') and pass it as
module stdin instead, matching the existing set_siteurl.php pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 9f55afdc26 fix(sys-service-terminator): skip units not installed on the host in swarm
A swarm host can carry a unit in its final run-list that was never
installed there: the nfs-server node routes the compose health services
through the service loader, which appends the unit to the per-host
run-list, but the systemd unit itself is only installed on the stack
host. Restarting such a not-found unit is a permanent error, so the
apply-state task burned all retries and hard-failed the deploy.

Probe the unit with systemctl cat first and only apply state when it
exists on this host. Compose mode keeps the unconditional apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 1371d01a4f fix(service-lookup): resolve out-of-play keys via the static role registry
The sys-service-loader iterates the role-wide static service order, so it
asks the service lookup about keys (e.g. the css alias of cdn) that the
play-local registry built from applications does not contain. The lookup
raised and killed the bootstrap play on every distro.

Fall back to the registry read from roles_dir, the same source the
loader iterates, so every loader-visible key resolves with its provider
role and play-computed flags (false when no consumer is deployed).
Canonical aliases aggregate their primary key's consumer flags. A term
unknown to both registries still raises.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 a09e069ea4 chore(settings): allowlist WebFetch domains used for limit research
Adds frappe.io, discuss.frappe.io, docs.zammad.org, www.kixdesk.com, web.archive.org, amble.blog to the WebFetch allowlist (surfaced while researching upstream memory requirements for the container-worker resource limits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 9c84ae2240 feat(engine): shared-aware container field + SPOT container names via services.<x>.name
Container-name vars derived their value inconsistently (hard-coded literals, entity_name composites) instead of the role-owned SPOT. Add a lint requiring every *_CONTAINER var to resolve through lookup('config', app, 'services.<x>.name'), the bare entity_name form, or the engine lookup; give each multi-container worker a name: in meta/services.yml (with realistic resource limits) and switch its var to the lookup. Extend the engine lookup with a shared-aware 'container' field (central provider container when shared, embedded <entity>-<engine> otherwise) and gate zammad's embedded memcached sidecar on not-shared so its container_name never collides with the central memcached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 fa456576b6 refactor(web-app-wordpress): SPOT the ca-trust mu-plugin path
The infinito-http-ca-trust.php filename and its container path were repeated across the copy and chown tasks. Hoist WORDPRESS_CA_TRUST_MU_PLUGIN_FILE and _PATH into vars/main.yml (matching the existing CONFIG_FILE/CONFIG_PATH idiom) so the string lives once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 ba6ecba64b feat(lint): flag redundant compose_handlers_flush=false + manual flush
sys-svc-compose already flushes compose handlers (and runs post-deploy) when the including role passes compose_handlers_flush: true. Roles that set it false and then immediately hand-roll their own meta: flush_handlers just reimplement that flush. Add an adjacency-scoped lint that catches the back-to-back form (a deferred flush with real work between the include and the flush stays legitimate and keeps false), and collapse the five offenders (checkmk, chess, jellyfin, pihole, semaphore) to compose_handlers_flush: true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 94e8671d61 refactor(container): route docker ops through the container CLI
community.docker.docker_* module calls in role tasks bypassed the CA-aware container wrapper and split lifecycle handling across two mechanisms. Migrate lifecycle sites (rm/stop/start/run/build, network create) to the container CLI, keep read-only *_info modules with an inline nocheck, and add a lint that rejects new raw module usage. The mariadb recovery flow gains a stale-container pre-remove so an aborted reset stays re-runnable, discourse teardown keeps its graceful stop, and removals guard on 'No such container' instead of masking every failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2932fce411 fix(svc-storage-nfs-server): emit numeric NFSv4 owners on ganesha
The ganesha (dev/act) test backend sent name@domain owner attributes, but the swarm DinD clients mount with nfs4_disable_idmapping=Y (AUTH_SYS numeric passthrough) and decode names to nobody (65534). A named non-root service (matomo runs as www-data/33) then read its own files as 65534, so PHP session_start aborted with a 500 and matomo crashlooped, stalling the swarm deploy at converge. Set Only_Numeric_Owners so ganesha emits numeric ids matching the client; scoped to the ganesha flavor, so the production kernel-nfsd path is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:00:48 +02:00
kevinveenbirkenbach 2d7512c013 Merge pull request #379 from infinito-nexus/update/docker-image-versions-web-app-prometheus-20260704-fa62dcb
update(images): update Docker image versions (web-app-prometheus)
2026-07-04 23:28:02 +02:00
infinito-nexus-updater[bot] 68bdfe20f0 update(images): update Docker image versions (web-app-prometheus) 2026-07-04 20:13:39 +00:00