Commit Graph
28 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 045ef095f2 fix(web-app-matrix): serve the federation delegation on the server name
The LiveKit JWT service checks each OpenID token over federation
against the user's homeserver. Server discovery found no delegation:
https://<server_name>/.well-known/matrix/server hit the redirect vhost
of web-opt-rdr-domains, which answered 301 to the dashboard and then
404, so the lookup fell back to <server_name>:8448. In compose OpenResty
runs in the host network and answers there; in swarm it publishes 80
and 443 only, the dial was refused and Element Call's token exchange
returned 401 M_UNAUTHORIZED (main run 40017166046, Nextcloud#12).

The role also wrote the delegation to `<well-known>/matrixserver`,
because the file path joined the directory and `server` without a
separator.

The file now lands at `<well-known>/matrix/server`, and the redirect
vhost serves `/.well-known/matrix/` from the shared well-known
directory, so discovery delegates to the Synapse domain on 443 in both
modes. Both flavors share tasks/utils/well_known.yml, which also
includes web-opt-rdr-domains for the server name when no play-wide
redirect claims it, so the delegation has a vhost there too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 11:44:14 +02:00
kevinveenbirkenbachandClaude Opus 5.5 9b88fccfc6 fix(web-app-matrix): wire Jitsi, Element Call and Heisenbridge in the ansible flavor
An earlier commit on this branch turned the three off because the ansible flavor ran
them without a published port, proxy entry, domain or certificate. They
now stay on and are wired end to end.

Element Call: MDAD's LiveKit server and JWT service get their own
entities in meta/services.yml with resource limits, local ports and a
public media port (TCP and UDP). The runner container publishes them
through container_ports, and the Synapse vhost proxies /livekit-server/
(websocket) and /livekit-jwt-service/ to them. The JWT service checks
each OpenID token against the user's homeserver over federation, and
federation was not reachable in this flavor: Synapse served it only on
a plain-HTTP listener that the runner published directly on 8448.
Synapse now serves the client and federation resources on one listener,
OpenResty serves the Synapse vhost on 443 and as default_server on the
federation port, the direct 8448 publish is gone, and the role writes
/.well-known/matrix/server as the compose flavor does. With a
self-signed CA the JWT container mounts the CA bundle as Synapse does.

Jitsi: a new services.jitsi entity follows the shared-service contract.
It is enabled in the ansible flavor off tor nodes. When web-app-jitsi is
in the inventory, Element points at that shared Jitsi and MDAD's own
Jitsi stays off. Otherwise MDAD runs its own Jitsi on jitsi.<domain>
with a front-proxy vhost, its web port published to the runner and its
videobridge on a public UDP port. The entity lists the jitsi domain, so
the web health probe skips it whenever Matrix does not serve it. The
jitsi_*_container_network overrides were dropped: MDAD reads none of
them, Jitsi always ran on its own network. Variant 0 covers the shared
case, variant 1 the internal one. On an onion-primary deploy both call
stacks stay off, so Element never advertises a clearnet call surface.
MDAD's Prosody keeps an account's first password, so after a credential
rotation jicofo failed SASL with the new one. The role now derives the
jicofo and videobridge passwords from the macaroon secret and recreates
both accounts on every deploy.

Heisenbridge stays on by default. It runs in the nested host network,
where MDAD's container-name URLs do not resolve, so its homeserver URL
and the appservice URL in its registration point at 127.0.0.1.

A bootstrap that a previous deploy left failed (for example after a
Docker Hub rate limit) is now reset and restarted, so a mode=update
deploy converges instead of failing on the stale unit state.

Playwright proves each path: the RTC spec fetches a LiveKit token from
the JWT service and opens the SFU signalling websocket, the Jitsi spec
checks Element's preferred domain and joins a conference (signing in
through Keycloak first when the shared Jitsi sits behind SSO), and the
Heisenbridge spec opens a DM with the bot and waits for its answer to
"help".

Closes #1165

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 17:16:20 +02:00
kevinveenbirkenbachandClaude Opus 5.5 94115964b7 fix(web-app-matrix): pull MDAD images through the stack's registry proxy
The ansible flavor's runner starts its own docker daemon, and that daemon
pulled every MDAD image straight from Docker Hub and ghcr.io, past the
registry cache the stack's own daemon uses. MDAD pulls with
`pull: always`, so every bootstrap spent the shared anonymous Docker Hub
quota; locally a single run exhausted it and died with
"toomanyrequests" even for images already on disk.

The role now reads the proxy of the stack's docker.service. When there
is one, it mounts a docker.service drop-in into the runner with the same
HTTP_PROXY, HTTPS_PROXY and NO_PROXY, and pins the proxy host through
container_extra_hosts. When the stack's daemon trusts the registry
cache's MITM CA, the drop-in fetches that CA from the proxy and runs
update-ca-certificates before the inner daemon starts. Without a proxy
on the stack's daemon nothing changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 17:16:20 +02:00
kevinveenbirkenbachandClaude Opus 5 ceb7e9e628 fix(penpot,matrix,n8n,swarm-drill): repair four CI root causes
From triage of run 33923190220. Each fix is independently complete.

penpot: ansible templates delegate_to before the task's merged when, and AnsibleUndefinedVariable escapes get_delegated_vars_and_hostname. PENPOT_BACKEND_HOST_NODE is set only by the resolve include, so the bootstrap task hard-failed on every host that include skipped instead of skipping with it. A block-level when cannot prevent this; move the tasks into a dynamic include.

matrix: the MDAD reconfigure looked for volume matrix_matrix_mdad_state (real name matrix_mdad_state) and container matrix-mdad (real name matrix). Both if-bodies were skipped while the task still reported changed, so a rotated S3 secret never reached synapse and every media write got SignatureDoesNotMatch. Move the shell into its own script and pass the real names through the environment. Also add no_log to the vars.yml render, whose diff printed 16 secrets in cleartext.

n8n: when the settings file and N8N_ENCRYPTION_KEY disagree, oclif's findCommand throws for every command, so 'n8n export:credentials' is unreachable in exactly the state that would need it. The re-key is one-way; pin it rotatable: false.

swarm-drill: distros.sh sees only rc=124 and cannot tell a wedged deploy from one the sweep deadline cut short. Say so where the governor caps it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 12:46:34 +02:00
kevinveenbirkenbachandClaude Opus 5 a46348b6cf refactor(handlers): bind notify to a listen topic, not to a name
A handler name was doing two jobs at once: the line an operator reads in the
play output, and the identifier a `notify:` binds to. The two pull in opposite
directions -- the log wants "restart the SSH service, with diagnostics", the
binding wants a token that survives quoting, YAML flow style and a shell
round-trip -- and a name that changes for readability silently changes which
handler runs.

Every handler that a notify reached by name now carries a `listen:` with that
exact wording, so the binding is explicit before anything is renamed. The
topics themselves become identifiers: `compose up` reads `compose-up`,
`Restart NFS ganesha` reads `restart-nfs-ganesha`, 22 of them across both
sides of every notify. Where the topic is assembled at runtime, both branches
moved together: the conditional in sys-service carries two literals, and the
LDAP renderer interpolates `import-{{ folder }}-ldif-files`, which is what
makes those handlers reachable at all.

Freed from the binding, the names take the emoji prefix every other task line
already has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 03:38:42 +02:00
kevinveenbirkenbachandClaude Opus 5 29015311b6 feat(lint): derive container paths from meta/volumes.yml everywhere
meta/volumes.yml says where a volume lands inside its container. Wherever a
second file spelled the same path out, the two drifted independently: the mount
moves, the consumer keeps pointing at the old place, and nothing fails until the
process looks for content that is not there. e031b7f61 fixed exactly this for
web-app-moodle; this generalises it and makes the rule hold.

The lookup gains what a caller needs. `targets` maps each mounting service to
its container path, and `target` is the one path an entry mounts at -- present
only when every mount agrees. An entry that lands at different paths per service
omits `target` rather than picking one, and the caller names the service:

    "{{ lookup('volume', application_id, 'prometheus_config').target }}"
    "{{ lookup('volume', 'web-app-prometheus', 'host_sys').targets['node-exporter'] }}"

66 literals across 48 role files now derive. Replacement only happened where the
path stood alone -- /srv/app inside /srv/app/data is a different path -- and
every rewritten file was re-parsed, Jinja and YAML alike. Three sites the tool
refused rather than guess were done by hand: zammad's path sat in a Jinja list
literal, seaweedfs' inside a quoted string feeding a filter, and openresty's
/var/run/docker.sock is not a mount target at all but the socket on the host,
which only happens to spell the same string. That one carries the single
nocheck.

tests/lint/repository/spot/test_volumes_path_single_spot.py keeps it that way.
roles/*/files/ is out of scope by construction: it is copied verbatim into an
image and executed there, so a Dockerfile or entrypoint has no Ansible to
evaluate a lookup with.

One defect this found, and it was in the tooling rather than the roles: deciding
ambiguity AFTER filtering short paths made web-app-prometheus' host_sys look
single-valued -- it mounts /sys for cadvisor and /host/sys for node-exporter,
and /sys is below the length filter. The template would have rendered an
undefined `.target`. Both the converter and the rule now decide ambiguity over
every target and filter afterwards, and that call reads targets['node-exporter'].

Verified by resolving rather than by reading: all 70 accesses in the diff were
run against the real registry and every one yields a non-empty path equal to the
literal it replaced. make test-lint: 328 passed.

Not verified: no Ansible render, so a call site whose surrounding context cannot
build the applications registry would still fail at deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 02:02:29 +02:00
kevinveenbirkenbachandClaude Opus 5 7783d10025 fix(matrix): stage the whole runner build context, not a hand-kept list
The staging task listed three files by hand while the runner Dockerfile
COPYs five. 0a581f662 added hostpin.sh and hostpin.service together with
their two COPY lines and did not extend the list, so the build failed
deterministically:

  #18 ERROR: failed to calculate checksum of ref ...: "/hostpin.sh": not found
  failed to solve: failed to compute cache key

and compose pull retried it ten times -- a missing file no retry can heal.
Both files and the defect are branch-local: neither exists on origin/main.

Copy the directory instead of enumerating it. The per-file modes the list
carried were fiction anyway: every source is 100644 in git, including
bootstrap.sh which the list published as 0755, and the Dockerfile chmods
0755 on both scripts itself, so the staged mode never reached the image.

The point is not the two missing entries. A list that must be updated
whenever a file is added is silent exactly when it is forgotten -- here it
went wrong inside a single commit that touched the Dockerfile and the
directory at the same time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 13:57:24 +02:00
kevinveenbirkenbach 342de43111 Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
# Conflicts:
#	roles/svc-ai-ollama/tasks/01_manager_ops.yml
#	roles/web-app-decidim/templates/compose.yml.j2
#	roles/web-app-joomla/templates/compose.yml.j2
#	roles/web-app-matrix/files/flavor/ansible/runner/bootstrap.service
#	roles/web-app-matrix/templates/flavor/compose/services.yml.j2
#	roles/web-app-nextcloud/files/playwright/_shared.js
#	roles/web-svc-cdn/tasks/01_assets.yml
#	scripts/tests/deploy/swarm/utils/trigger_units.sh
#	scripts/tests/deploy/utils/filesystem/docker_dataroot.sh
2026-08-05 02:32:58 +02:00
kevinveenbirkenbachandClaude Opus 5 0f27e60d23 fix(matrix): name both bootstrap phases in the failure message
`ActiveState=failed` only says the oneshot exited non-zero, and the unit runs
two phases: `ansible-galaxy install -r requirements.yml --force` and then
`ansible-playbook setup.yml` (bootstrap.sh:39-44, under set -euo pipefail).
Claiming the playbook failed sends the reader looking for an inner PLAY RECAP.

In run 30781723102 the web-app-nextcloud 3 job died in the first phase, at role
21 of 31, on a git clone that got HTTP 503. The container journal logs
"starting ansible-galaxy install" and never reaches "galaxy install done" or
"starting ansible-playbook setup.yml", so there was no recap to find.

Wording only; the branch, its condition and the diagnostics are unchanged. The
cause of that 503 is still open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:00:40 +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 Opus 4.8 f5ddd58b5f fix(matrix): move the inner docker daemon off the 172.17 default bridge
Since the inner mdad dockerd actually starts (storage-driver fix), its
default docker0 claims 172.17.0.1 inside the container netns and
shadows exactly the resolver address the swarm inventory hands to every
container via networks.internet.dns, so the bootstrap's ansible-galaxy
died on 'Could not resolve host: github.com' one second after boot.

Declare the nested-docker bridge ranges as a network SPOT
(NETWORK_NESTED_DOCKER_BIP / _ADDRESS_POOLS, deliberately outside
172.16.0.0/12 and away from the 192.168.244.0/24 lab net) and render
the runner's daemon.json from it at staging time instead of shipping a
static file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:46:22 +02:00
kevinveenbirkenbachandClaude Opus 4.8 236117f6f1 fix(matrix): let the inner docker daemon start in nested overlayfs setups
The mdad runner pinned storage-driver overlay2, but on swarm test nodes
the container's /var/lib/docker sits on overlayfs and the kernel rejects
overlay-on-overlay upperdirs, so the inner dockerd exited, the bootstrap
oneshot failed by dependency and Synapse never came up (proven by the
rescue-diagnostics journal of run 29356373746). Drop the pin so dockerd
auto-detects and ship fuse-overlayfs as the nested fallback; real hosts
keep overlay2.

Replace the 30min blind HTTP poll with a bootstrap terminal-state wait:
fail fast with diagnostics when the inner docker daemon or the bootstrap
playbook fails, probe Synapse only after the oneshot completed, and run
the journal dump on every failure path instead of behind MODE_DEBUG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 04:25:51 +02:00
kevinveenbirkenbachandClaude Opus 4.8 76cc5db491 fix(deploy): tolerate first-install state in matrix reconfigure + cached read
- matrix 02_render_inventory: the mdad-state volume does not exist yet on a
  fresh install, so `container volume inspect` errored the reconfigure task
  (`no such volume`) on the row-1 baseline. Guard the marker drop behind the
  inspect succeeding; the runner bounce still runs if the container is up.
- test_coverage: read services.yml via utils.cache.files.read_text so the
  no-uncached-file-reads lint passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:34:00 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4e6ef25423 fix(test): repo-lint compliance + seaweedfs api-primary readiness
- _shared.js: wrap the boot-poll delay in resolveTimeout (raw-timeout lint)
- 02_render_inventory: docker -> container wrapper (engine-abstraction lint)
- seaweedfs: api becomes the primary canonical so the deploy readiness
  probe hits a served vhost when the frontend (filer/master) is disabled
- seaweedfs frontend: exempt from variant pinning — the onion gate is a
  hard invariant, no variant may force the frontend on a tor node
- test_coverage: add the `# nocheck: variants-coverage` exempt (same
  mechanism as bond-baseline) for such invariant flags

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:34:00 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0a581f6622 feat(matrix): full onion support — OIDC backchannel, MXIDs, Tor-stable specs
Synapse on an onion node could never complete SSO: it has no Tor, so the
http://auth.<onion> issuer was unresolvable server-side (discovery/jwks/
token), and Synapse hard-rejects non-https issuers on top.

- runner/hostpin.{sh,service}: pin the onion auth host to the runner's real
  default gateway (node OpenResty) from /proc/net/route, Before=docker.service
  so nested containers see it from their first request; host-gateway is
  unusable in nested DinD (resolves to the inner docker0)
- Synapse bind-mounts the runner's /etc/hosts; token iss stays the onion
  issuer the browser uses
- bootstrap.service: PassEnvironment (systemd units do not inherit container
  env; MATRIX_MDAD_PLAYBOOK_TAGS only worked via its fallback default)
- vars.yml.j2: skip_verification on onion (Synapse's escape hatch for
  issuers without https; Tor provides transport security per RFC 7686)
- 02_render_inventory: drop the MDAD bootstrap marker + bounce the runner on
  inventory changes — mode=update reconfigures the nested stack at all now
- MATRIX_SERVER_NAME follows matrix_domain onto the onion so MXIDs built from
  it (DM spec, bridge configs) reference users that exist
- specs: shared Firefox onion fixture (16 files), gotoElementApp reload-retry
  against Element's Tor-triggered init race, seaweedfs spec gates on the
  filer frontend, env uses canonical_url consumer=

Verified in-container: SSO 302 to onion Keycloak, admin+biber OIDC login,
E2EE DM admin<->biber, CSP — all green over Tor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 10:56:13 +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 c27b4e70ac refactor(matrix): merge the matrix-mdad service into the matrix entity
The DiD runner ran as a separate compose service matrix-mdad while the app entity is matrix, so the swarm overlay upstream needed a service_key override to reach matrix_matrix-mdad. Name the service, container_name and entity all matrix (MATRIX_MDAD_SERVICE, MATRIX_MDAD_CONTAINER, services.matrix.name) so the overlay DNS is matrix and resolve_upstream returns matrix:8008 by default. Drop the webserver_proxy_service_key override; keep proxy_internal_port (synapse 8008, element 8080).

Also drop the now-dead matrix-mdad meta entry (resource_filter resolves to the entity, so its cpus/mem were never read), flavor-gate the volume mounts (mdad to ansible, synapse/element to compose) so the ansible flavor no longer declares compose-only volumes/configs, and have diagnostics.sh read the container name from the MATRIX_CONTAINER env instead of hardcoding it. Renderer + unit validated: swarm matrix:8008/8080, compose 127.0.0.1:8021/8022.

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:45:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 3e1c49e4bf fix(web-app-matrix): create the runner build dir on STACK_HOST before copy
The ansible-flavor stages the matrix-mdad runner build context into MATRIX_MDAD_RUNNER_BUILD_DIR via stack_host_copy, but nothing created that directory first, so the copy failed 'Destination directory ... does not exist'. Ensure the dir (delegate_to STACK_HOST, run_once) before the copy loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:02:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b6e201e215 fix(web-app-matrix): stage the mdad runner build context on the stack host
The matrix-mdad runner image's compose build.context pointed at the Ansible controller's role tree ({{ application_id | abs_role_path_by_application_id }}/files/flavor/ansible/runner). On swarm the pre-deploy 'compose build' handler runs on the manager (delegate_to STACK_HOST run_once), where roles/web-app-matrix/files/ does not exist, so buildx failed: 'unable to prepare context: path ... not found'. Stage the four runner build-context files onto STACK_HOST via stack_host_copy and point the build context at that host-local instance dir, mirroring svc-db-openldap. sys-stk-backend's flush is deferred so the build runs after the stage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00
kevinveenbirkenbachandClaude Opus 4.8 51c3d41ede Merge origin/main into feature/swarm-nfs
Reconcile main's object-store feature and pre-swarm deploy tasks with the branch's compose/networks/volumes SPOT and swarm lints.

objstore (seaweedfs/minio): standalone compose + embedded service templates migrated to the container_image/container_volumes/container_networks/compose_volumes/compose_networks/depends_on lookups; meta/volumes.yml on the canonical dict schema with s3.json and nginx.conf as type:config; embedded services made swarm-compatible via compose_only/compose_restart.

deploy tasks (erpnext, opentalk, seaweedfs, shopware, snipe-it, wordpress, yourls): container exec routed through the container_address resolver and gated on IS_STACK_HOST, shell module plus stack_host_template, no chdir or compose-verb forms.

Also removed orphaned vars, restored MAILU_INSTANCE_DIR, freed the seaweedfs subnet (192.168.206.0/24), stripped literal quotes from the nextcloud/bookwyrm env templates, and routed main's objstore compose_volumes tests through dir_var_lib.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:08:49 +02:00
kevinveenbirkenbach 29519603ef Merge remote-tracking branch 'origin/main' into feature/swarm-nfs 2026-06-14 20:32:59 +02:00
kevinveenbirkenbach 257cfc0491 fix(web-app-matrix): widen Synapse readiness probe to 360 retries for cold MDAD bootstrap 2026-06-14 09:59:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2f4ef3cfad refactor(web-app-matrix): central postgres + shared MATRIX_POSTGRES_*
Both flavors integrate with Infinito's central postgres (no more MDAD
bundled DB inside the DiD container). The connection vars live in a
single set of MATRIX_POSTGRES_* constants in vars/main.yml and are
referenced by both flavors' templates so the source-of-truth is
explicit and identical.

- vars/main.yml: MATRIX_MDAD_POSTGRES_* renamed to MATRIX_POSTGRES_*
  (drop MDAD prefix — these are flavor-agnostic) + add
  MATRIX_POSTGRES_DATABASE.
- templates/flavor/ansible/vars.yml.j2 (was mdad-vars.yml.j2):
  postgres_enabled: false; wire postgres_connection_*/synapse
  database_* to MATRIX_POSTGRES_* + matrix_synapse_container_network:
  host so the inner Synapse inherits matrix-mdad's outer networks and
  resolves `database` / `openldap` by hostname. valkey_enabled: false
  (Synapse single-process; central redis path not used here).
- templates/flavor/ansible/hosts.j2 (was mdad-hosts.j2): renamed, no
  content change. mdad- prefix dropped since the file is already
  under flavor/ansible/.
- templates/flavor/ansible/services.yml.j2: matrix-mdad joins the
  central `postgres` network (conditional on DB shared) and
  `openldap` network (conditional on MATRIX_LDAP_ENABLED).
- templates/flavor/compose/synapse/homeserver.yaml.j2 and the six
  templates/flavor/compose/mautrix/*.config.yml.j2 templates: switch
  inline `lookup('database', application_id, 'host'/'username'/...)`
  calls to MATRIX_POSTGRES_* (same constants the ansible flavor uses).
- tasks/flavor/ansible/: renumber 01-05 cleanly
  - 03_database.yml (was 02a_database.yml) — sys-svc-rdbms include
  - 04_run_playbook.yml (was 03_run_playbook.yml)
  - 05_proxy_wiring.yml (was 04_proxy_wiring.yml)
- tasks/flavor/ansible/main.yml + 02_render_inventory.yml + README.md:
  update references to the renamed files.
- tasks/flavor/compose/02_create-and-seed-database.yml: switch host/port
  lookups to MATRIX_POSTGRES_* too.

Tests: 186/186 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 19:00:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 54b6649170 fix(web-app-matrix): unshadow systemd unit dir + realistic probe budget
Failing CI run 27010233874 (job 79718652049) and the local DiD test
both timed out at `probe Synapse client API on host loopback` with
`Connection reset by peer` for the full 140s retry window. Workflow
rootcause inspection of the matrix-mdad container surface identified
two compounding issues:

1. `matrix_mdad_systemd:/etc/systemd/system` named volume shadowed the
   image-baked `multi-user.target.wants/matrix-mdad-bootstrap.service`
   enable symlink. First-boot's empty named volume masks the image
   content so systemd never sees the unit as enabled — bootstrap never
   runs, MDAD never installs, Synapse never starts. Smoking gun.

2. 60×2s = 140s probe window was an order of magnitude too short for a
   cold MDAD bootstrap (galaxy install + image pulls + ansible-playbook
   setup-all on a fresh container). Even with the unit enabled the
   probe would race the bootstrap.

Changes:
- services.yml.j2: drop `matrix_mdad_systemd:/etc/systemd/system`
  mount; MDAD-written units live in the container's writable layer
  alongside the image's enable symlinks. Add `matrix_mdad_state:
  /var/lib/matrix-mdad` so the bootstrap marker survives container
  restarts. Normalize federation port publish to `DOCKER_BIND_HOST`.
- compose.yml.j2: rename declared volume `matrix_mdad_systemd` ->
  `matrix_mdad_state`. Inside the `matrix_flavor == 'ansible'` branch
  only — compose flavor untouched.
- bootstrap.service: drop `network-online.target` (never fires in a
  container without systemd-networkd-wait-online). Add
  `ExecStartPre=/usr/bin/docker info` for fail-fast if the inner
  dockerd never came up.
- bootstrap.sh: log markers + per-step timing so the systemd journal
  inside matrix-mdad shows a clear timeline.
- 04_proxy_wiring.yml: 60×2s -> 180×5s = 900s; on probe failure under
  MODE_DEBUG, run a new diagnostics.sh script (container logs +
  systemctl --failed + journalctl + docker.service status) so the next
  iteration has actionable output instead of a silent retry stack.

make test: 186/186 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 19:00:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 257d533989 fix(web-app-matrix): bootstrap PATH + lint clean
- bootstrap.sh / bootstrap.service: systemd doesn't inherit Dockerfile
  ENV PATH, so the oneshot couldn't find ansible-galaxy. Export PATH in
  the script and set Environment=PATH in the unit explicitly.
- 04_proxy_wiring.yml: rewrite the inner-docker admin-register exec as
  shell scalar with `# nocheck: raw-docker` (the inner docker CLI talks
  to matrix-mdad's DiD daemon; not host docker).
- vars/main.yml: drop now-unused MATRIX_MDAD_POSTGRES_* + MATRIX_REDIS_*
  vars (DiD uses bundled postgres + valkey).

make test: 186/186 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 12:26:19 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0aed4aaebe refactor(web-app-matrix): full DiD isolation for ansible flavor
matrix-mdad is now a long-running compose service with systemd PID 1
and its own bundled docker daemon. MDAD writes its systemd unit files
and runs matrix-synapse/coturn/element/bridges/jitsi/element-call
entirely INSIDE that container via the inner docker daemon. Nothing
leaks to the host beyond standard /opt/compose/matrix/ role state.

Container layout:
- /sbin/init (systemd) as PID 1
- bundled docker.io daemon (inner DinD)
- bootstrap.service oneshot: runs `ansible-playbook setup.yml` once on
  first boot; marker at /var/lib/matrix-mdad/bootstrap.done
- Named volumes: matrix_mdad_docker (inner /var/lib/docker),
  matrix_mdad_systemd (/etc/systemd/system), matrix_mdad_matrix (/matrix)
- /sys/fs/cgroup:rw + cgroup: host for systemd-in-container
- Outer compose publishes synapse/element/federation ports to host loopback

mdad-vars.yml.j2:
- postgres_enabled: true (MDAD bundled postgres inside DiD)
- valkey_enabled: true (bundled redis)
- exim_relay_enabled: true when no outbound Mailu
- host_bind_port: 0.0.0.0 inside container so outer compose port-publish
  can forward to host loopback

Dropped (no longer needed):
- 02a_database.yml — MDAD provisions postgres inside the DiD container
- 00_cleanup_mdad_residue.yml — no host residue to clean
- ensure rshared mount task — no host bind-propagation needed
- container wait matrix-mdad-runner — service is long-running not one-shot
- matrix_synapse_container_additional_networks_custom — DiD has its own
  internal docker network
- matrix_synapse_container_extra_arguments CA mount — bundled CA inside
- coturn_container_additional_volumes — anonymous volumes live in inner
  docker daemon, outer hlth-volumes can't see them

04_proxy_wiring.yml:
- admin user creation now uses two-hop exec: outer `container exec
  matrix-mdad` -> inner `docker exec matrix-synapse register_new_matrix_user`

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 10:39:01 +02:00
kevinveenbirkenbachandClaude Opus 4.7 70c1a03f13 feat(web-app-matrix): add ansible flavor via unified compose.yml.j2
Implements requirement 025 (docs/requirements/025-web-app-matrix-ansible-flavor.md).

Architecture:
- services.matrix.flavor: ansible | compose
- Single templates/compose.yml.j2 branches on matrix_flavor
- ansible flavor: defines matrix-mdad-runner one-shot compose service that
  DiD-executes upstream matrix-docker-ansible-deploy (pinned ref + migration
  validated version). MDAD writes /etc/systemd/system/matrix-*.service units
  on the host; post-up Ansible task container-waits the runner, then probes
  loopback ports + wires sys-stk-front-proxy.
- compose flavor: legacy in-repo stack (synapse/element/coturn/bridges) kept
  for regression coverage, marked deprecated.

Variant-switch ansible -> compose:
- 00_cleanup_mdad_residue.yml tears down MDAD-installed systemd units +
  leftover matrix-* containers before compose-flavor brings up its own
  matrix-synapse (else: name conflict, compose down doesn't touch
  systemd-managed state).

Reorganization:
- roles/web-app-matrix/{tasks,templates,files}/flavor/{ansible,compose}/
- Renamed legacy compose-flavor task files into flavor/compose/
- New runner Dockerfile (multi-stage, ARG-parameterized FROM via build-args)

Verified: 3 variants x 2 passes (sync+async) full_cycle green, all 6
PLAY RECAPs failed=0. V1 (ansible all-on), V2 (ansible no-auth), V3
(compose deprecated all-on). Playwright matrix tests pass per variant.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 10:21:41 +02:00