Give each reverse-proxied app a swarm-resolvable upstream: add services.<entity>.ports.internal.http to the role metadata and rename the main compose service from the generic 'application' to the entity name, so its short DNS alias on the shared overlay is unique. Thread the service name through compose, vars (*_SERVICE_NAME), volume mounts and the oauth2 origin host. Consolidate the split image/ports blocks in bridgy-fed and postmarks into the entity block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
* scripts/lint/autoformat.sh:
- rename run_sort_claude_settings worker to run_claude (matches the
renamed scripts/lint/claude.sh).
- drop the EXIT trap that wiped status_dir; cleanup happens explicitly
after `wait` returns, so a parallel worker that interrupts the
parent never races a vanished status dir.
- each worker now captures its tool's exit code and writes one of
OK / SKIP / FAIL. Tolerated `expected violations remain`-style exits
(markdownlint-cli2 rc≤1, ansible-lint rc≤2) still count as OK; only
real failures count as FAIL.
- all workers run to completion regardless of individual tool failures
(so the user always sees the full picture); after the summary, the
script exits 1 if any worker reported FAIL.
* scripts/lint/sort_claude_settings.sh -> scripts/lint/claude.sh
(history preserved via git mv); update test_claude_settings_sorted.py
fix-hint accordingly.
Plus working-tree drift across roles/, group_vars/, inventories/,
requirements/, and .pre-commit-config.yaml accumulated from the
make-test setup phase regenerating generated artefacts during recent
test runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements requirement 015. Both new roles join the central Keycloak
realm and consume the central OpenLDAP through the same patterns as
web-app-nextcloud, so users can move between Files (OpenCloud) and
Video (OpenTalk) without separate accounts.
OpenCloud (web-app-opencloud)
- Single-binary deploy of opencloudeu/opencloud:4.0.6 at open.cloud.<DOMAIN>
- External Keycloak as IdP (OC_EXCLUDE_RUN_SERVICES=idp,
PROXY_AUTOPROVISION_ACCOUNTS=true, PROXY_ROLE_ASSIGNMENT_DRIVER=oidc)
- Direct OpenLDAP user/group source via OC_LDAP_*
- Co-deployable with web-app-nextcloud (next.cloud.<DOMAIN> stays put)
- WEB_OPTION_OPENTALK_URL surfaces the OpenTalk meeting launcher into
the Files UI when web-app-opentalk is in scope
OpenTalk (web-app-opentalk)
- Controller + LiveKit + web-frontend + dedicated Redis + dedicated MinIO
(controller refuses to start without an S3 backend) at talk.<DOMAIN>
- Uses shared svc-db-postgres for relational data and shared
web-svc-coturn for STUN/TURN (no dedicated TURN provisioning)
- Native Keycloak integration via controller.toml [oidc.*] +
[user_search] backend = "keycloak_webapi"; LDAP is consumed
transitively through Keycloak's federation
- Frontend env exposes SHARED_FOLDER_OPENCLOUD_URL when web-app-opencloud
is in scope, wiring the in-meeting OpenCloud file picker
- Bucket bootstrap is idempotent: an Ansible task waits for MinIO and
creates the configured bucket via mc
Cross-cutting
- Extends PORT_BANDS public.relay to 65535 so LiveKit's media UDP range
(60000-60999) can coexist with web-svc-coturn (20000-39999),
web-app-bigbluebutton (40000-49999), and web-app-nextcloud's onboard
TURN (50000-59999)
- All credentials (OIDC client secrets, LDAP bind, TURN auth_secret,
MinIO root, LiveKit API, OpenCloud JWT/transfer) are sourced via
lookup('config', ..., 'credentials.*'); none are committed in plain text
- Each role ships IAM/LDAP docs mirroring web-app-nextcloud and
role-local Playwright specs that cover the administrator AND biber
personas through OIDC
Validated end-to-end via the Role Loop (deploy-fresh-purged-apps then
deploy-reuse-kept-apps with SERVICES_DISABLED=matomo,email): all five
OpenTalk containers + the OpenCloud container come up cleanly, the
controller successfully completes OIDC discovery against Keycloak, and
both canonical hostnames return HTTP 200 through the central reverse
proxy. Browser-validated assertions (admin role mapping, full SSO logout
propagation, OpenCloud<->OpenTalk cross-launch from a real LDAP user)
remain as TODOs in each role's TODO.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>