Commit Graph
1953 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 39b5774b5a fix(cache): rebuild canonical volumes when the registry is empty
get_canonical_volumes returned {} when _CANONICAL_VOLUMES_BY_ROLE was empty even
though the defaults cache was warm (get_application_defaults no-ops on a warm
cache, so it never refilled the registry). That left nfs_prep's volume loop empty,
so the NFS subdir for shared/late-loaded services (svc-ai-ollama's ollama_models)
was never created and the swarm bind mount failed ENOENT. Force a real
_build_application_defaults on an empty registry. Adds two regression tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:46:14 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e0d2749e2a style(test-volume): parenthesize patched context managers (linter reformat)
Apply the formatter's preferred multi-line 'with (...)' grouping for the mocked context managers added with the volume-lookup registry guard. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:46:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 687f1243cd fix(volume-lookup): force application-defaults build on registry miss so nfs_prep pre-creates the NFS subdir
lookup('volume', role) read the lazily-populated canonical-volumes registry directly, so a lookup running before any consumer built that role (nfs_prep's pre-create loop for an early unpinned engine such as ollama) saw an empty registry and skipped the subdir; the swarm bind mount then failed with 'no such file or directory'. Force get_application_defaults() on a miss, guarded so the populated hot path skips the deepcopy. Adds two guard tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:46:13 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8e9062834e feat(cli): add string output format to applications complexity
complexity --format string prints only the role names, one per line, so the list feeds straight into make roundtrip apps=... A unit test covers the new format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 20:46:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b09598dc80 feat(engine): add central engine lookup + provisioning machinery
Foundation for the central svc-db-*/svc-dns-* engine roles: the engine lookup and engines descriptor, per-consumer engine credentials in the inventory manager, shared-net overlay rendering, container_depends_on, the sys-svc-engine provisioning dispatcher, and the compose_volumes NFS-placement split. base.yml.j2 gates the embedded redis sidecar on lookup('engine','redis',application_id,'local').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:29:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6796658d03 fix(install): let the lint package installer wait for a busy dpkg lock
system_pkg.py's apt-get update/install failed immediately ('Could not get lock /var/lib/dpkg/lock-frontend') when a concurrent deploy held the dpkg lock, so make autoformat/test could not run while a deploy was in progress. Pass -o DPkg::Lock::Timeout=600 to both apt-get invocations so they wait for the lock instead of failing, and update the unit test to assert the lock-wait. Scope is the dev/lint package installer only; the Ansible deploys use ansible.builtin.apt and are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:10:02 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e39d77c82a fix(compose-ca): keep CA-trust wrapper on probe timeout, don't degrade to env-only
The image probe (docker inspect + 'docker run --entrypoint /bin/sh') conflated a timeout/kill under load with 'image has no /bin/sh', skipping the CA-trust wrapper entrypoint and leaving only env vars (CURL_CA_BUNDLE/SSL_CERT_FILE) that PHP libcurl ignores -> cURL 60 on server-side OIDC. has_sh is now tri-state: a timeout (rc==124) maps to None ('assume shell present', apply the wrapper); only a clean non-zero rc disables it. Hang protection (timeout+killpg) unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:50:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a08ec88c03 feat(deploy): always run the full variant matrix (remove runtime time-cut)
Drop the RuntimeBudget guard and its INFINITO_MAX_RUNTIME /
INFINITO_VARIANT_TIME_OVERFLOW env handlers: the deploy round loop now runs
every variant unconditionally instead of skipping (cut) or aborting (fail)
when the projected runtime would exceed the budget. Removes runtime_budget.py,
both env handlers + their registration, the budget calls in deploy.py, the
obsolete unit tests + the TestHandlerBudget class, and the docs reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:50:56 +02:00
kevinveenbirkenbach 06c686a3bd fixup! fix(deploy): bound CA-trust inject so it can't hang the handler 2026-06-22 13:04:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5246c6b86d fix(env): shorten INFINITO_VARIANT_TIME_OVERFLOW comment under the .env lint
The handler COMMENT rendered a 188-char line in .env; the env-file-schema
lint caps non-nocheck comment lines at 140. Trim it. Also collapses two
ruff-format lines in the variant-split unit tests. Follow-up to the
runner-split variant feature; no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 12:49:19 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e029dccacc fix(deploy): bound CA-trust inject so it can't hang the handler
compose_ca.py ran ~3 docker calls per service sequentially; on a DiD
daemon saturated by a many-service app's compose up (e.g. BigBlueButton,
16 services) the inject blew the CA-trust handler's timeout and failed
the whole deploy.

- run(): Popen + kill the whole process group on timeout so a wedged
  docker/buildx grandchild cannot hold the captured pipe open and defeat
  the timeout (subprocess.run's own timeout was inert in that case).
- /bin/sh probe: DEVNULL + short bound, so a wedged probe has no pipe to
  block on.
- gather_image_meta(): read per-image (inspect + probe) CONCURRENTLY
  (8 workers) up front; the render loop then only consults the cache.
- unreadable image (missing, or inspect stalled under load) -> env-only
  injection (mount CA + set SSL_*/CURL_CA_BUNDLE env, skip the wrapper);
  never build or hard-fail, so the inject always completes.
- handlers/main.yml: timeout backstop on the "Generate CA trust override"
  handler.
- test_compose_ca.py: updated to the new run() signature + gather_image_meta.

Proven on the heaviest round: nextcloud variant-0 full_cycle went green
(failed=0, sync + async) with BigBlueButton + xwiki + all bonded partners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 12:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 35c2ff5f26 feat(deploy): runner-split variant bundles + unified variant selector
Thread a single `variant` selector (CSV) end-to-end so CI can split a
role's matrix deploy across runners.

- variant_select.py (SPOT, shared by init + deploy): one --variant taking
  a CSV (2 pins one round, 0,1,2 is a runner bundle, empty = full matrix);
  replaces the old separate --variant/--variants.
- planner.filter_plan_to_variants filters the plan to a round subset.
- CI discovery (utils/github/variant_bundles.py) splits roles with more
  than INFINITO_VARIANT_BUNDLE_SIZE (default 3) variants into bundles of
  consecutive indices, one matrix runner each (5 -> 0,1,2 + 3,4), threaded
  via the variant env. test-deploy-{local,universal,workstation,server}.yml
  switch to matrix.include with a comma-free variant_slug for artifact/job
  names (GitHub expressions have no replace()).
- runtime_budget honours INFINITO_VARIANT_TIME_OVERFLOW (cut|fail) when a
  runner cannot finish its slice within INFINITO_MAX_RUNTIME.

Unit tests + docs (variants.md, deploy.md) included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:52:50 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ae7e45dddb feat(deploy): runtime-budget guard stops the variant matrix before INFINITO_MAX_RUNTIME
Add INFINITO_MAX_RUNTIME (env handler: 6h on real GitHub Actions, 48h otherwise)
and a RuntimeBudget guard in the dev-deploy variant loop. Before each next
variant round the guard projects elapsed + longest round so far + 30m buffer;
if it would exceed the budget it stops and emits a CI warning annotation
(never fails — the rounds that ran stay deployed).

- utils/env/handlers/infinito_max_runtime.py: new handler (canonical SPOT for
  the var; uses ctx.on_gha, materialised into .env by the generator).
- cli/administration/deploy/development/runtime_budget.py: RuntimeBudget +
  duration parser (6h/48h/90m/3600/3600s), extracted to keep deploy.py lean.
- deploy.py: loop uses budget.exhausted()/start_round()/end_round().
- tests for both the handler and RuntimeBudget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fc9bb0dd9d refactor(ressources): move resource collection/aggregation to utils (single spot)
Move the parse/collect/aggregate logic out of the ressources CLI into
utils/roles/applications/services/resources.py so the CLI and the upcoming
variant resource-budget guard share one implementation instead of duplicating
it. The CLI's query/__main__ and the unit tests now import from utils; the
per-module CLI files (parse.py, collect.py, aggregate.py) are removed.

No behavior change: the 43 ressources unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 092a5c8888 refactor(cli): split + extend the role-resources tool
Rework cli/meta/roles/applications/ressources into single-responsibility
modules (parse, collect, aggregate, query, render) behind a thin __main__,
with mirrored unit tests, and extend it:

- resolve template-gated shared services to the provider role's entity via the
  service registry, so a role's full dependency footprint is listed
- bond + depth columns; entries without bond default to bond 1
- load each service once by default; --unshared lists every occurrence
- --order [asc|desc] FIELD, --filter 'bond<=0.5 & cpus>=1 & mem_limit>=512m'
- --depth N (0 = unlimited recursion over shared deps)
- --variant N applies the same meta/variants.yml overlay as inventory creation,
  reusing utils.cache.applications.get_variants (single source, no duplication)
- --sum [FIELD...] shows a SUM row over all or selected fields

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6fea999e93 fix(tests): make make test green (eslint + env order + autoformat)
- discourse-activity-pub, integration_matrix specs: drop unused catch
  binding and useless json init (eslint no-unused-vars/no-useless-assignment)
- openwebui playwright.env.j2: order OLLAMA_SERVICE_ENABLED after
  OIDC_ISSUER_URL (env keys must be ASC, test_alphabetical_sort)
- test_addon_env_flags.py: autoformat

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:16 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2b027bb64a feat(addon_env_flags): gate bridged addon flags on partner deployment
A bridged addon's <ID>_ADDON_ENABLED flag is now true only when one of its
declared bridge partners is in the deployed playwright role set
(TEST_E2E_PLAYWRIGHT_APPS): integration specs skip cleanly when the partner is
not deployed and assert the real coupling when it is. App-addons without
bridges are unaffected; an unresolvable set falls back to the prior
enabled-AND-required behaviour (no regression).

Adds TestBridgeDeploymentGating (7 cases): partner-deployed, not-deployed,
multi-bridge any-match, no-bridge passthrough, disabled stays false, fallback
without the set, and string-set splitting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d2f5efa7fe feat(test-e2e-playwright): parallel workers via plugin + tunable timeouts; split role vars by precedence
Add plugins/lookup/playwright_workers.py computing a conservative parallel-worker count (CPU/RAM-derived, app-under-test concurrency-capped, reduced on CI) and wire test_e2e_playwright_workers + test_e2e_playwright_fully_parallel + test_e2e_playwright_test_timeout into playwright.config.js (read from env) and 02_run_one.yml (passed via -e). fullyParallel stays false so parallelism is file-level only and intra-file ordering assumptions stay safe. Raise run_timeout to 3600 for addon-heavy roles (nextcloud ~50 specs).

Split the role's variables by Ansible precedence and intent: derived/fixed constants live in vars/main.yml (UPPERCASE, high precedence, not operator-tuned), operator-tunable knobs live in defaults/main.yml (lowercase, overridable). VARIANT_INDEX stays UPPERCASE with a single definition. Update references across tasks/templates/inventory/docs accordingly. Includes a unit test for playwright_workers and an autoformat pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:03:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 70786aa126 refactor(addons): emit playwright addon gate flags via addon_env_flags lookup
Replace the per-template Jinja loop (which mis-unpacked dict2items and rendered every flag as KEY_ADDON_ENABLED=false, silently skipping every addon spec) with a single lookup('addon_env_flags', application_id). The plugin is the single source of truth for the <ADDON_ID>_ADDON_ENABLED flags consumed by tests/addon-gating.js: env-key derivation matches addon-gating.js exactly, and a flag is true only when the addon is both enabled AND required (required:false addons skip). Switch all six role templates to the lookup, extend the env-file schema lint to recognise whole-line {{ ... }} expressions, and add the plugin unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:03:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fc398084f9 fix(compose_restart): quote the restart policy scalar
Bare policies like no/off render as 'restart: no' which YAML parses as a boolean, so 'docker compose config' rejects the file. Quote the scalar ('restart: "no"') to keep it a string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 04:35:15 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2c985b6dd6 fix(compose): strip service env_file for build/config subcommands
`/usr/bin/compose` (roles/sys-svc-compose/files/compose.py) auto-adds
`--env-file` and compose.yml carries service `env_file:`. `docker compose
build`/`config` parse+interpolate those env_file values and reject
$-bearing secrets (e.g. the dev keycloak password), whereas `docker stack
deploy` reads env verbatim and is unaffected. build/config need no runtime
env, so route them through env_file-stripped compose copies with no
--env-file; all other subcommands keep the original files + --env-file.

Adds `_subcommand`, `strip_env_files`, `_ENV_PARSING_SUBCOMMANDS` and unit
coverage for the new build/config path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 544e4b83fc fix(networks): make objstore providers (seaweedfs/minio) shared-net providers
The net refactor (366dc7b70/cb27ff375) migrated every shared-net provider (postgres/mariadb/openldap/ollama/openresty/keycloak) to the overlay registry but missed seaweedfs and minio, so an object-store consumer like baserow never joined the provider's docker network and its S3 client hit ECONNREFUSED on seaweedfs:8333 (the intended direct data-plane path).

Add networks.overlay to both engines so consumers attach, and complete the multi-service shared-net-provider contract: (1) render.py emits a bare auto-IPAM project default for an own shared_net provider (no name, no subnet) so the entity-named external net created by network/create.yml is not re-requested -- this caused 'invalid pool request: Pool overlaps' on the first attempt. (2) a provider_self_alias flag lets the provider's sidecars (proxy/sso-proxy) JOIN the shared net WITHOUT publishing the <entity> alias, so the name resolves only to the main container and baserow's :8333 never round-robins onto a port-less sidecar. (3) seaweedfs/minio 01_core pre-create the external <entity> net via network/create.yml (mirroring web-app-prometheus) so compose adopts it as the project default instead of building <entity>_default.

Verified: baserow compose e2e test-seaweedfs.js passes (upload stored in the bucket, 3.9s); new TestOwnSharedNetProvider locks the contract; full make test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:44:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b2a6cf11c1 feat(swarm): add stack_host_copy action wrapper and migrate compose-dir copies
stack_host_copy is the copy: analogue of stack_host_template: it folds the IS_STACK_HOST gate into the module so callers do not silently fail on hosts whose destination tree is owned only by the stack host. In swarm an app's compose / build-context directory (/opt/compose/<app>) exists only on the manager, so a bare copy: into directories.instance also ran on workers and aborted the play with 'Destination directory does not exist'.

Migrate the six compose-dir copy tasks (baserow, jenkins, bookwyrm, postmarks, yourls, openldap) to stack_host_copy and drop the now-redundant when: IS_STACK_HOST. A new drift-guard lint test (test_stack_host_copy_migration) flags any future bare copy: into the compose dir, and a unit test covers the wrapper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:44:37 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b4d52296ad refactor(seaweedfs): extract server command to a seaweedfs_command filter
The server args were duplicated across the standalone provider stack (compose.yml.j2) and the local sidecar (service.yml.j2), so the -ip fix had to be applied in two places. They now live in one filter (plugins/filter/seaweedfs.py); both templates keep command: inline and render the args via it, appending -s3.config only when the caller passes the mounted s3.json path (the sidecar omits it). Adds a unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:05:10 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e6e9c15bd2 feat(api): add centralized API SPOT lookup plugin and global dict
Add group_vars/all/18_api.yml defining the global API mapping (cloudflare, telegram, github, google, jira) and plugins/lookup/api.py — the single sanctioned reader that resolves a dotted 'provider.key' path against it and raises on unknown keys. Routing every proprietary/external credential access through one lookup keeps a single point to change how credentials are sourced. Includes the unit test covering nested resolution, subtree access, error paths, and templar handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 00:16:53 +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
kevinveenbirkenbachandClaude Opus 4.8 eb377d93aa fix(e2e): scope injection targets to deployed, live consumers
roles_with_service returns only consumer roles that are themselves deployed (in group_names) and excludes the service's own provider, so the logout/css/dashboard/matomo/prometheus injection specs no longer target undeployed or surface-less roles in a partial single-app deployment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c3f01a8640 feat(proxy): mode-aware proxy_pass SPOT lookup
Centralise the nginx proxy_pass directive in one source of truth (utils/networks/proxy.py + plugins/lookup/proxy_pass.py). Compose keeps the literal 127.0.0.1:<local> upstream; swarm resolves the service over the shared overlay at request time (variable upstream + http resolver) so a rescheduling backend never blocks openresty startup. Converts the html/media/upload/ws location partials, the keycloak oauth2 and login-iframe endpoints, and the logout endpoint to the lookup; adds the swarm resolver and the front-proxy service_key plumbing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7376d5bd4d feat(overlay): web_facing consumer + proxy_resolvable collect for openresty net
Extend the registry-driven overlay renderer so the central openresty proxy
reaches every web-facing backend over a shared swarm overlay:

- Add consumer kind `web_facing` (matches web-app-*/web-svc-*) so every
  web-facing app attaches its primary service to the openresty overlay in
  swarm mode; openresty resolves them by swarm service DNS.
- openresty's overlay declares `collect_proxy_resolvable`, harvesting the
  keycloak beacon's `auth.<domain>` alias onto its own attachment so each
  per-app sso-proxy resolves the auth host to openresty.
- Fix the default-net name collision: a shared_net provider rendering its own
  compose must not also emit `name:` on its default overlay (the external net
  already carries that name), which made `docker stack deploy` fail with
  "network <x> already exists".

Runtime-confirmed: openresty reached keycloak over the overlay (302, no more
connection-refused) once consumers attached.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f7603cc0c2 feat(matrix): migrate mautrix bridges to meta/addons (req 026)
Six mautrix network bridges declared as mechanism: bridge, enabled: false, with DB creds referenced from meta/schema.yml. MATRIX_BRIDGES derived from addons; vars/bridges.yml, filter_plugins/bridge_filters.py and its unit test removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e317f08ac9 feat(addons): unified meta/addons contract (req 026)
Per-file meta/addons/<id>.yml loader, schema + bridge-parity lints, shared update SPOT, unit/external tests, and layout.md/plugins README docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbach 601476f4e5 Merge branch 'main' into fix/ci-deploy-server-failures 2026-06-17 11:30:37 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c0582cd2d7 fix(web-app): restrict trusted-header SSO bridge to proxy-set headers
The baserow (infinito_sso.py) and bookwyrm (header_auth.py) trusted-header bridges now read identity only from the X-Forwarded-* headers nginx overwrites from the oauth2-proxy auth response. The client-injectable X-Auth-Request-*/Remote-User/X-Forwarded-Name variants are dropped, closing an authenticated-user header-injection escalation (including a groups->superuser path) in the claim-less edge case where X-Forwarded-* would be empty.

Add SSO security e2e tests (test-oidc-security.js) to both roles: forged-header gate bounce, trusted-header bridge endpoint gating, SSO-disabled inertness, and injected-header re-identification of an authenticated session. Wire the new spec into baserow's playwright.spec.js.

Update the bookwyrm header_auth unit test: precedence now uses two listed headers, plus a regression guard asserting proxy-uncontrolled headers are ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 942ede07a7 fix(mirror): enroll registry.opencode.de in the GHCR mirror (web-app-opentalk)
OpenTalk's images (web-frontend, controller, recorder) live on
registry.opencode.de, which was absent from both _MIRRORABLE_REGISTRIES and
_ALL_REGISTRY_PREFIXES in utils/docker/image/discovery.py. So is_mirrorable_image
returned False, iter_role_images skipped them, the GHCR mirror never copied
them, and the deploy pulled direct from registry.opencode.de -> TLS handshake
timeout (deploy failed).

Add the registry to both sets so the single iter_role_images gate enrolls the
3 images: the mirror-sync copies them to GHCR and the resolver rewrites the
deploy to pull from the GHCR mirror. The prefix entry strips the host so the
mirror destination is well-formed (no doubled registry). Add a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fc9d8b3b54 feat(web-app-baserow): trusted-header SSO bridge, drop LDAP
Baserow's native SSO is Enterprise-only, so gate the community image with the shared Keycloak oauth2-proxy and patch urls.py (via a custom Dockerfile) with a trusted-header bridge. The bridge provisions/repairs a local Baserow user from the oauth2-proxy identity headers and mints native refresh/access/user_session tokens, mirroring Baserow's own SSO redirect contract (?token=&user_session=).

proxy.conf.j2 performs the nginx handoff: an authenticated browser without a jwt_token cookie is 302'd to /api/infinito/sso/login/, which redirects back to /?token=&user_session= for the frontend middleware to consume. bootstrap_admin marks the superuser email_verified + completed_onboarding to skip onboarding.

Replace the LDAP service/variant with proxy-gating and rewrite the seaweedfs + persona Playwright tests to drive Baserow through its API using the SSO-minted JWT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c40d49dc96 feat(web-app-bookwyrm): trusted-header SSO bridge + seaweedfs object-store fix
BookWyrm core has no OIDC/LDAP login, so behind the oauth2-proxy a gated visitor only reaches BookWyrm's own login form. Add a trusted-header SSO bridge (files/sso/header_auth.py: RemoteUser middleware + backend) that turns the oauth2-proxy identity (X-Forwarded-Preferred-Username, set by Keycloak after OIDC or LDAP-federated login) into a real BookWyrm session, auto-provisioning the local user. Active only when PROXY_HEADER_SSO is truthy (= sso is_proxy_gated, centralised in BOOKWYRM_SSO_PROXY_GATED).

Supporting fixes: USE_S3_FOR_EXPORTS=false (exports stay local; SeaweedFS has no per-bucket ACL so private exports must not share the public media bucket) fixes the fresh-build settings crash / preferences 502; proxy_pass_request_body off on the keycloak sso_proxy /oauth2/ block so the auth_request subrequest no longer hangs 60s on gated POST bodies (504->500); split playwright.spec.js into per-test files; add positive (oidc/ldap -> authenticated session) + negative (forged header -> gated) SSO e2e tests; fix the avatar test (form-scoped Save locator + valid PNG); remove dead BookWyrm OIDC_* env; unit tests for the bridge + the gated settings patch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b0255bb6c4 fix(sso): scope oauth2-proxy cookie to a multi-domain app's shared parent
The oauth2-proxy sidecar set cookie_domains to the app's primary domain only. A role served under several sibling subdomains (e.g. SeaweedFS filer/master/api under one base) therefore could not read its session cookie on the non-primary hosts, looping the auth chain. Add the common_dns_suffix filter and use it so cookie_domains becomes the longest DNS suffix shared by all of the app's domains: a single-domain app keeps its exact host (unchanged), a multi-domain app collapses to the shared parent so one session cookie spans every subdomain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1b09580323 fix(changelog): use asterisk bullets in Older Releases index
The auto-generated "Older Releases" index in CHANGELOG.md emitted dash
bullets while the rest of the file uses asterisks, failing markdownlint
MD004 (ul-style, consistent) with 49 errors. Switch the archive index
generator to asterisk bullets, convert the existing entries, and align
the unit test assertions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:33 +02:00
kevinveenbirkenbachandClaude Opus 4.8 153bfb1898 feat(objstore): consume the object store across 14 web-app roles
Switch penpot, nextcloud, gitea, mattermost, openwebui, funkwhale, baserow, openproject, pixelfed, mastodon, bookwyrm, listmonk, peertube and opentalk to the engine-agnostic objstore lookup: the minio service entries are pinned off, a seaweedfs service (gated on the provider being in group_names, public for the media apps) replaces them, and the matrix variants pin the new flag. OpenTalk keeps object storage mandatory through an embedded fallback and provisions its bucket via weed shell.

Add objstore lookup SPOT keys consumed by these roles: endpoint (host:port), public_domain, public_bucket (public S3 host plus bucket) and public_url (scheme plus public_bucket, scheme derived from the provider TLS state). This removes the repeated lookup('config', lookup('objstore', ...), 'server.domains.canonical.api') / host:port concatenations from the consumer templates. Listmonk settings render their JSON values via the to_json filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:33 +02:00
kevinveenbirkenbachandClaude Opus 4.8 37f7d8e406 feat(objstore): engine-agnostic object-store service (SeaweedFS + MinIO)
Add a database-style object-store abstraction: the objstore lookup resolves the active engine (seaweedfs|minio) per consumer and yields a uniform S3 payload, objstore_secret_key is auto-generated like database_password, sys-svc-objstore delegates per-consumer provisioning to the resolved provider's objstore_init entrypoint (lazy-loading the provider when it was not preloaded), and compose/network/volume plumbing supports both a shared central instance and an embedded storage-only instance per consumer.

Add the web-app-seaweedfs provider role: Filer and Master UIs behind the admin-only oauth2-proxy via a role-local nginx and proxy.conf.j2 (S3 API domain stays ungated, admin UIs deny with 403 when SSO is disabled), per-consumer S3 identities from s3.json, idempotent bucket provisioning via weed shell, a /24 on the central-provider subnet range, Playwright coverage for admin/biber/guest plus consumer bucket existence, and unit tests for the objstore lookup and the per-engine compose volumes.

Mark web-app-minio lifecycle: unsupported, move its mc client image into a dedicated services.client entry, align it with the objstore credential contract, and make its consumer provisioning idempotent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:33 +02:00
kevinveenbirkenbachandClaude Opus 4.7 cb27ff3756 refactor(networks): replace Jinja includes with compose_networks/container_networks lookup plugins
Convert the two registry-driven networks templates into pure-Python
rendering functions plus thin Ansible lookup plugins:

  roles/sys-svc-compose/templates/networks.yml.j2  -> {{ lookup('compose_networks') }}
  roles/sys-svc-container/templates/networks.yml.j2 -> {{ lookup('container_networks') | indent(4) }}

The rendering logic lives in utils/networks/render.py as pure-Python
functions that take a registry + closure-style config/database lookups,
making the logic directly unit-testable without a Jinja stub harness.
The lookup plugins are thin wrappers that resolve application_id,
DEPLOYMENT_MODE, and swarm.network.encryption from the templar, build
the service_registry, and forward to the render functions.

Migrate 233 include lines across 85 production templates. Convert
web-app-matrix's hand-coded MDAD networks block (default + conditional
postgres + conditional openldap) to the generic lookup; this adds an
openresty attachment when services.sso.enabled (harmless extra
interface, pinned in test_matrix_mdad_consumer_attaches_postgres_ldap_sso).

Mark local-DB / redis sidecars (sys-svc-rdbms/templates/services/,
svc-db-redis/templates/service.yml.j2) with `# nocheck: networks-literal`
because they intentionally attach to default-only.

Extend the lint to scan ALL roles/*/templates/**/*.yml.j2 (not only
compose.yml.j2) and to forbid the legacy include form; the marker
above is the documented escape hatch.

Coverage:
  - tests/unit/utils/networks/test_render.py (30 cases incl. encrypted=false literal pin)
  - tests/unit/plugins/lookup/test_compose_networks.py (5 cases incl. lookup_loader arg-schema)
  - tests/unit/plugins/lookup/test_container_networks.py (5 cases incl. lookup_loader arg-schema)
  - tests/integration/infrastructure/compose/test_networks_render.py (10 snapshot scenarios)
  - tests/integration/infrastructure/compose/test_network_includes.py (3 lint rules)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 18:42:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 366dc7b705 refactor(overlay): registry-driven shared-overlay networks with implicit defaults
Replace the hand-coded if-chains for db/ldap/ollama/proxy shared overlays
with a single registry-driven loop in sys-svc-{compose,container}/networks.yml.j2.
Each provider declares its overlay metadata under meta/server.yml.networks.overlay;
discover_role_services passes the block through verbatim into the service_registry.

Schema (modes/topology/consumer/aliases/proxy_resolvable) with implicit defaults:
- consumer.kind defaults to services_flags
- consumer.key defaults to <provides or entity_name>
- consumer.flags defaults to [enabled, shared]
- aliases defaults to [entity_name] (shared_net) or [] (default_net)

Openresty becomes a generic alias-collecting proxy: web-app-keycloak declares
a proxy_resolvable beacon with its public auth.<domain> alias; openresty sweeps
the registry at render time and harvests all proxy_resolvable aliases onto its
default-net overlay. Openresty no longer hardcodes keycloak's domain.

Lock the new behaviour with 9 render-snapshot tests covering provider self-render,
consumer attachment, beacon sweep, default-derivation, and compose-mode fallback.
Replace the Ansible-only `_entry.get('canonical') is not defined` filter with
`'canonical' not in _entry` so the templates also render correctly under vanilla
Jinja2 (test harness).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 18:42:03 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3b07cb86f2 fix(swarm-deploy): inventory-filter closure + lint accepts dict-form include
* cli/administration/deploy/swarm/closure.py: drop dep-walked roles
  that are not present in the inventory's role groups so the
  `validate_application_ids` strict in-inventory check no longer
  collides with `disable=` (which removes provider roles from the
  inventory). Operator-supplied seed IDs and the
  `default_placement: manager` safety net are kept unchanged.
* tests/unit/cli/administration/deploy/swarm/test_closure.py: add
  `test_operator_ids_dep_walked_not_in_inventory_dropped` to pin
  the new behaviour and update the existing scenarios to put the
  filtered roles in their test inventories.
* tests/lint/ansible/shell/test_container_exec_is_stack_host_gated.py:
  recognise `include_tasks: { file: X, apply: { ... } }` dict form
  alongside the free-form string. Without it the lint's include
  graph dropped the apply-include edge and false-positived gated
  files reached by it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 02cf3c5e9e fix(users-cache): substitute ORGANIZATION + SOFTWARE_NAME placeholders
Two scalar group-vars are referenced verbatim in
`roles/user-*/meta/users.yml` (`lastname: "{{ ORGANIZATION }}"`,
`description: "owned by {{ SOFTWARE_NAME }}"`). Ansible 2.19's
TrustedAsTemplate gate silently leaves those untagged Jinja
literals unrendered, so downstream consumers received the raw
`{{ ORGANIZATION }}` string. Keycloak rejected the resulting user
updates with `error-person-name-invalid-character` because `{` /
`}` are not valid in a person name.

Add `substitute_scalar_placeholders` next to the existing
`substitute_primary_domain_placeholder`. Both helpers run from
`get_merged_users` immediately before `_render_with_templar`, so the
cache returns fully-resolved scalars regardless of the templar's
trust gate.

Also restructure: move the cache into a `utils.cache.users` package
so the placeholder helpers no longer need a redundant `users_`
prefix:

  utils/cache/
    users/
      __init__.py     was utils/cache/users.py (public API
                      `get_user_defaults` / `get_merged_users` is
                      unchanged; consumers using
                      `from utils.cache.users import ...` keep
                      working via package __init__).
      placeholders.py the substitution helpers, dropping the
                      redundant `users_` prefix.

  tests/unit/utils/cache/
    users/
      __init__.py
      test_module.py        was test_users_module.py
      test_placeholders.py  new: 15 unit tests covering both
                            helpers (nested-structure walk, URL /
                            port handling for DOMAIN_PRIMARY,
                            Jinja-in-raw-value resolution, empty /
                            missing skip paths, and a guardrail
                            pinning the current
                            `_SCALAR_USER_PLACEHOLDERS` tuple).

Update README links under `utils/cache/` and
`docs/contributing/artefact/files/plugins/lookup/users.md` to point
at the new package path.

Placeholders covered today: ORGANIZATION, SOFTWARE_NAME. Extend the
tuple in `utils/cache/users/placeholders.py` when a new scalar
group-var appears in user metas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 13414814af refactor(lookups): generalize address/service for swarm + application key
container_address + container_service are mode-aware lookups that
build a docker-exec-safe address from `application_id + service_key`.
Two adjustments to align them with how the rest of the project uses
service keys:

* Swarm-mode return value now uses the lookup's `service_key`
  argument directly as the swarm service-suffix instead of
  `services.<key>.name`. This matches the documented contract in
  `roles/sys-svc-compose/files/resolve_container_id.sh` (the helper
  composes `<stack>_<SERVICE_KEY>`) and what `docker stack deploy`
  names a service after (the compose-yaml service key, not the
  compose-side `container_name:` override).

* Compose-mode `_resolve_bare_name` falls back to
  `services[entity_name]` when `service_key == 'application'` and
  the meta has no top-level `application:` entry. 26 roles use
  `application:` as their compose-yaml service key while keying
  their meta/services.yml under the entity name (baserow, listmonk,
  wordpress, mobilizon, etc.). Without the fallback, every consumer
  of `container_address(application_id, 'application')` raised
  `service 'application' missing in '<role>' services config` at
  task-runtime. The fallback is intentionally narrow (only the
  literal 'application' key) so an actual typo in another
  service_key still raises clearly.

Unit-test contract update: test_container_address.py replaces
`test_swarm_mode_uses_resolved_service_name` with
`test_swarm_mode_uses_compose_yaml_service_key`, asserting the
resolver receives the service_key ('mattermost') rather than the
`services.<key>.name` value ('custom-mm-name'). Existing
`test_unknown_service_raises` still passes because the fallback only
fires for service_key='application'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 13:34:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0ffbb73d72 refactor(images): central container_image lookup + migrate all roles
Add plugins/lookup/container_image.py: returns the full `image: "<ref>"`
line so templates drop the redundant image: prefix and surrounding
quotes. Supports `custom=True` to auto-construct `<entity_name>_custom`
for locally-built images, plus explicit `image=`/`version=` overrides
for exotic cases (taiga back-auth/front-oidc variants, decidim infinito
suffix, etc.).

* 25 unit tests for the lookup cover compose vs swarm prefix, custom
  flag, overrides, tag_only=True, missing version warnings.
* New strict lint (tests/lint/.../test_no_raw_image_in_compose_templates)
  fails on any raw `image:` line in roles/*/templates/*.j2 unless
  nocheck-suppressed. Legacy `image: "{{ lookup(...) }}"` form no
  longer accepted.
* Migrate 159 image refs across ~80 roles to the standalone
  `{{ lookup('container_image', application_id, '<service_key>') }}`
  form. 26 roles use `custom=True` (mariadb, postgres, openldap,
  jenkins, jira, gitlab, nextcloud, mediawiki, shopware, wordpress,
  odoo, joomla, openproject, bookwyrm, espocrm, hugo, littlejs,
  fediwall, roulette-wheel, openresty, confluence and more).
* Fix svc-db-redis application_id leak: service.yml.j2 is included
  into consuming roles where application_id evaluates to the outer
  role, so the image lookup needs the hardcoded 'svc-db-redis'.
* Integration tests (test_templates_require_image,
  test_build_requires_image) recognise the standalone lookup as an
  implicit `image:` at the same indent.
* Mediawiki swarm tasks: add explicit `:{{ MEDIAWIKI_VERSION }}` to
  the registry-prefixed image string because the custom var is now
  bare.

Only 3 legitimate exotic nocheck-suppressed image lines remain:
matrix-mdad runner (nested services.matrix.runner.image path),
matrix mautrix bridges (per-iteration dynamic) and svc-registry-docker
(chicken-and-egg self-reference).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b0067512ba fix(volumes): render Jinja in mount source/target + coerce config/secret mode to int
Two related fixes in the volume-rendering filters, both surfaced by
`docker stack deploy` failing on the rendered compose.yml.

1) Render Jinja in mount source/target via the lookup-supplied
   render_jinja closure. `container_volumes` previously emitted the
   `meta/volumes.yml` `source:` / `mounts[].target:` strings unchanged;
   in swarm those flowed verbatim into compose.yml as
   `{{ lookup('nginx', ...) }}:/usr/local/openresty/nginx/conf/conf.d`
   and broke `docker stack deploy expanding mount target` with
   `function "lookup" not defined`. `compose_volumes` had the analogous
   bug for top-level `configs:` / `secrets:` source fields.
   Add `_maybe_render` to `compose_volumes` and the matching
   try/except-AnsibleUndefinedVariable render block to `container_volumes`
   for bind source + mount target, raising `AnsibleFilterError` with the
   offending expression so failures stay diagnosable.

2) Coerce config/secret `mode:` to int. The YAML/J2 idiom carries `mode`
   as a string ("0440"); `docker stack deploy` rejects that as
   "services.X.configs.N.mode must be a number". Add `_coerce_mode_int`
   (octal-string -> int, with bool + empty + non-octal guards) and
   convert before emission. Update the per-service config/secret unit
   tests to expect `0o440` / `0o400` instead of the legacy strings.

Validated by the t5 -> t8 act-swarm-zombie progression (mode-int gate
cleared at t6; bind source/target + config/secret source render cleared
at t7 -> t8, lifting ok=452 to ok=798).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbach 29519603ef Merge remote-tracking branch 'origin/main' into feature/swarm-nfs 2026-06-14 20:32:59 +02:00
kevinveenbirkenbachandClaude Opus 4.7 478d0679dd refactor(roles): drop compose-nocheck flags + extract include-gated sub-tasks
Refactors four roles that previously used `# nocheck: compose-chdir-in-task` /
`compose-verb-in-task` / `container-exec-resolver` markers to follow the
canonical swarm-aware pattern:

- web-app-bigbluebutton: adds BBB_POSTGRES_CONTAINER_ADDRESS /
  BBB_GREENLIGHT_CONTAINER_ADDRESS vars; 02a_postgres_password_sync.yml +
  03_administrator.yml use `container exec -i {{ ..._CONTAINER_ADDRESS | quote }}`;
  02_compose.yml's compose-build-phase tasks (generate-compose +
  prebuild etherpad) move into the new
  02b_bootstrap_compose_project.yml (>3 entries cannot live in a block)
  under a single `when: DEPLOYMENT_MODE \!= "swarm"` include guard.
- web-app-baserow: BASEROW_APP_CONTAINER_ADDRESS var + 02_manager_ops.yml
  converted to container exec + container restart canonical pattern.
- web-app-matomo: MATOMO_APP_CONTAINER_ADDRESS var; 07_patch_database.yml
  swaps MATOMO_LOCAL_CID for the proper resolver-backed address.
- web-app-matrix: 01_docker.yml's two consecutive `compose pull` /
  `compose up` tasks extract into 03_compose_pull_and_up.yml (with the new
  `# include-gated: when: DEPLOYMENT_MODE \!= "swarm"` header marker) and
  the previous 03_webserver.yml renames to 04_webserver.yml to keep the
  numeric file sequence contiguous.

Supporting lint infrastructure changes so file-level include-gated
sub-task lists survive the swarm-compat lints:

- utils/annotations/task_gate.py: `is_task_compose_only_gated` now walks
  up nested parent blocks (a single `when: DEPLOYMENT_MODE \!= 'swarm'`
  on `- block:` exempts every child task underneath) and a new
  `is_file_compose_only_by_header` recognises files declaring
  `# include-gated: when: DEPLOYMENT_MODE \!= "swarm"` in their first
  five lines as compose-only-by-include.
- tests/lint/ansible/shell/test_no_compose_chdir_in_shell_tasks.py and
  test_no_compose_verb_in_shell_tasks.py call the header helper before
  the per-line scan, so include-only sub-files are exempt.
- tests/unit/utils/annotations/test_task_gate.py covers both new
  behaviours (recursive parent walk plus file header) end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 19:08:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6e49fe74fb feat(volumes): canonical dict-of-dicts meta schema + lookup('volume')
Migrate every role's meta/volumes.yml from ad-hoc dict-of-named-volumes
to a canonical dict-of-dicts schema where the YAML key is the semantic
short name and the entry body carries type / name / source / mounts /
nfs / mode / read_only. The optional 'name:' field is the docker volume
name (defaults to the key); the previous 'docker_name:' field is gone.

What's in this commit (210 paths):

Infrastructure (23)
- New filter plugins/filter/{compose,container}_volumes.py — render the
  top-level volumes:/configs:/secrets: section and per-service mount
  blocks from the canonical meta. container_volumes uses the SEMANTIC
  name as per-service mount source so it resolves against compose_volumes'
  top-level dict key (docker_name as source produced 'undefined volume'
  on swarm).
- New lookup plugins/lookup/{compose,container,volume}_volumes.py.
  volume(application_id, name) returns a normalised single entry;
  volume(application_id) returns the whole canonical dict.
- utils/cache/applications.py keeps a sibling registry
  _CANONICAL_VOLUMES_BY_ROLE outside the merged applications payload so
  raw Jinja source: strings never enter the templar walk (root-cause of
  the 200x cache-miss + recursive get_merged_applications storm).
- utils/roles/applications/mounts.py — schema validator + normaliser;
  list-shape is rejected.
- plugins/lookup/nginx.py: _volume_path_by_name reads canonical directly.
- scripts/tests/deploy/swarm/_context.sh: NFS_VOLUMES extraction handles
  both legacy dict and canonical dict shapes.
- roles/svc-prx-openresty/handlers/restart_and_validate.yml: gate
  compose-only validate+reload behind a single when-block; swarm uses
  service-update instead (bind-mounts of a FILE pin source inode at
  task-create time, so atomic write-tmp+rename leaves the running task
  on a deleted inode).
- roles/sys-svc-compose/tasks/utils/nfs_prep.yml: switch to
  lookup('volume', application_id) | dict2items so NFS subdir
  pre-creation works under the new cache split.

Tests (12)
- New strict lints flagging inline mount sections in compose templates,
  single-file bind mounts, and legacy lookup('config', ..., 'volumes.X.Y')
  call sites — all volume-mount surfaces MUST flow through container_volumes
  / compose_volumes / volume lookups.
- Integration: test_volumes_compose_chain + test_volume_constants_are_lookups
  accept both the legacy and the new lookup('volume', app, 'X').name forms
  during the migration window.
- Unit tests for the new lookup, filter, mounts helper, plus updated
  nginx lookup test.
- Prometheus role-wiring test reads canonical meta for the
  alert_rules.yml bind mount.

Role migrations (175)
- 62 meta/volumes.yml converted to dict-of-dicts canonical shape
- 63 templates/compose*.yml.j2 replace inline volumes:/configs:/secrets:
  with lookup('container_volumes', application_id, '<service>') |
  indent(N) per service + a single trailing lookup('compose_volumes',
  application_id) for the top-level block
- 50 vars/main.yml drop dead volume constants and refactor 23 remaining
  legacy lookup('config', ..., 'volumes.X.Y') to lookup('volume',
  application_id, 'X').Y across 9 roles

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 19:08:46 +02:00