Inventory include sets ignored the active variant: swarm
derive_includes walked the variant-free base config (dynamic service
flags count as enabled) and the compose CombinedResolver followed
run_after as an include edge. Providers a variant pins to false
(e.g. mobilizon variant 1 sso) were still deployed.
- derive_includes: swap every app listed in INFINITO_APP_VARIANTS to
its variant config, index 0 included (nextcloud variant 0 disables
11 partner services)
- CombinedResolver: follow_run_after flag; include resolution passes
False so run_after stays a pure ordering hint
- drop dead deps.py chain (resolve_deploy_ids_for_*) and unused
resolve_with_edges; rewiring them would resurrect variant-blind
run_after-following includes
- inventory.md: document the real include pipeline and
is_explicit_truth semantics
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The INFINITO_CA_CERT_HOST handler pulled PyYAML into the handler chain via utils.cache.yaml, but python -m cli.meta.env runs before any dependency is installed (the .env bootstraps the environment that would provide yaml). Line-parse SOFTWARE_NAME from group_vars with stdlib only and fail loud when the key is missing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
odoo.conf routed all logging into /var/log/odoo/odoo-server.log, so the swarm task that sat 5 minutes in Starting produced an empty docker service logs dump and the actual blocker stayed invisible. Drop the logfile key: odoo falls back to stdout like the upstream image default; compose behavior only changes the log destination.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The swarm converge check derives its service name from meta services.<entity>.proxy.service_key and fell back to the non-existent bluesky_bluesky, failing the job after a fully green e2e. pds is the always-on core service; every bluesky vhost overrides its proxy key explicitly, so the declaration only feeds the monitor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
01_admin.yml redirected controller-side files ({{ role_path }}/files/*.sh) into container exec; delegated to a swarm node that path does not exist ('/home/runner/...: No such file or directory'). Resolve the web service's hosting node + local cid first, delegate the include there via apply, and feed the scripts through the shell module's stdin so the controller reads them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ActiveStorage pointed AWS_ENDPOINT at the public seaweedfs vhost, which resolves to 127.0.0.1 inside the container: every avatar upload died with Seahorse::Client::NetworkingError (Connection refused :443). Use the objstore lookup's internal url (http://seaweedfs:8333) like snipe-it does; presigned access stays server-side, AWS_PUBLIC is untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docker stack deploy does not append the image CMD when a service overrides only the entrypoint, so the backend's gated entrypoint ran /usr/local/bin/entrypoint.sh without a command: it linked assets and exited 0 in a ~5s replace loop (every other erpnext service carries an explicit command and ran fine). Declare the upstream gunicorn invocation as the service command; in compose it overrides the image CMD with the same value, in swarm it reaches the gate as $@.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Declare the LDAP and SSO auth plugins as proper meta/addons entries
(mechanism/source/version, github-releases update monitoring, url/tag/
md5/guid under config:) and resolve the install parameters in vars from
the addon config instead of hardcoding them.
Move biber's login journeys into the mandated per-addon Playwright specs:
ldap-authentication tests the LDAP manual login, sso-authentication the
OIDC round-trip. Because a Jellyfin user binds to exactly one auth
provider, the LDAP spec skips while SSO is active — the ldap-only matrix
variant owns that path — so a single test user suffices and the specs
stay parallel-safe. playwright.spec.js keeps guest + native admin;
biber's generic persona is opted out via PERSONA_BIBER_BLOCKED.
Also fill the optional meta/info.yml fields (homepage, video, display).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every compose.yml.j2 now declares services as '{{ service_name }}:' with
its '{% set %}' knobs on two-space lines above the key, includes at four
spaces and if/for tags aligned to the content they wrap. The layout
renders correctly because stack_host_template enables lstrip_blocks for
compose.yml.j2; indent(4, True) in the container base and '{%+' guards
in whitespace-sensitive scalars keep the shared fragments identical
under both render modes. A new lint test enforces the skeleton.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before every redeploy that follows a failed deploy, require one explicit
line: either verified-in-container or a named post-deploy-state-exception.
Forbid treating the redeploy itself as the verification, and reject
state-reset friction, logical inference from a prior deploy, and
"looks correct" as reasons to skip. Playwright-spec failures must reset
any polluted state and be green via make compose-playwright first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the full matrix (full-auth / no-auth / ldap-only) green:
- healthcheck: use curl (the image ships no wget)
- install the LDAP plugin from its GitHub release (repo.jellyfin.org is
unreachable from the deploy network); version/url/md5 live in
meta/addons/ldap-authentication.yml and reach the script via env
- configure the SSO/OIDC provider through the plugin config API as JSON
(order-agnostic) instead of hand-written XML, which silently produced
an empty provider map; set DisablePushedAuthorization + https
SchemeOverride so Keycloak accepts the redirect_uri
- inject the SSO login button via the branding login-disclaimer + CustomCss
- seed the break-glass admin unconditionally and retry the first-run
wizard until the admin token resolves (fixes the no-auth variant race)
- Playwright: role-based authenticated-chrome selector for JF 10.11,
localStorage-clearing logout, and LDAP-before-OIDC ordering (a Jellyfin
user binds to exactly one auth provider)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first install (composer, schema) exceeds the default 150x2s converge window; 600 retries matches collabora's precedent for slow first boots.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The horizon worker started in parallel with the app and hit the storage volume before the bootstrap (env setup, oauth keys, storage:link) finished; swarm ignores depends_on, so ordering needs an in-band gate. A wrapper entrypoint polls for /var/www/storage/.docker.init (touched by the bootstrap as its last step, visible through the shared pixelfed_data volume in both modes) and then execs the upstream /worker-entrypoint.sh; start_period 300s keeps the healthcheck from replacing the waiting task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first boot initializes the database for minutes while the curl healthcheck counted from the 30s default start_period; swarm replaced the task after ~3.5min (clean Shutdown/Complete in the task history), the replacement restarted the init and the stack never converged. start_period 5m gives the first task an 8min window and a replaced task resumes against the already-initialized database. longpolling_port is dead since Odoo 16; gevent_port is the key the 19.0 image actually reads (same value as the gevent default, so no behavior change).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The metricz spec asserted app=<role> labels once; a label only exists after the vhost logged a request (server-level log_by_lua into the shared dict), so quiet roles like web-svc-xmpp raced the 30s blackbox scrape and an openresty reconverge wipes the dict entirely. Warm every target via its unauthenticated /healthz/ready before scraping and re-warm+re-scrape under a 60s expect.poll per target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
meta/volumes.yml stages six type:bind config directories on the stack host; a worker-scheduled task lacks the source paths and the stack never converges. Unpinning requires migrating them to nfs-backed named volumes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OpenCloud is a single binary with embedded NATS, decomposedfs metadata and search index: mmap/lock-based stores that corrupt over NFS and lose state when swarm reschedules a node-local volume to another node. default_placement: manager opts the volumes out of the NFS driver and makes the node deterministic; one replica because the stores are single-writer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One entrypoint wrapper imports the project CA into the JVM truststore for penpot and jenkins: env-driven (CA_TRUST_CERT from sys-svc-compose-ca, set in both modes), fail-open without a cert (production), fail-loud with one; writable cacerts are patched in place, read-only layouts fall back to /tmp + JAVA_TOOL_OPTIONS. Replaces penpot's inline compose command and jenkins' role-local entrypoint, which silently skipped the import when cacerts were not writable. ca_cert_container becomes a plain constant since its only Jinja consumer moved into the wrapper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New env handler exports the ca_trust_paths SPOT value (SOFTWARE_NAME read from group_vars) into .env; trust/linux.sh and trust/wsl2.sh consume the variable instead of carrying the path literal and fail loud when it is unset. Stale .env files need a regeneration (make dotenv) before the trust scripts run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ca_cert_host derives the host cert path from SOFTWARE_DOMAIN; ca_cert_container carries the fixed in-container bind-mount target. CA_ROOT.cert_host and the test fixtures consume the filter; the standalone runtime scripts (compose_ca.py, container.py) cannot import it and are pinned to the SPOT by the unit test instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'login source already exists' was matched inline in both the LDAP and the OIDC setup; the filter is the single marker source and normalizes case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two handler tasks carried the identical four-marker stderr chain inline and matched case-sensitively; the filter is the single marker source and normalizes case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The discourse frontend loads data: script elements; the playwright persona run failed on the CSP violation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The five docker network connect/disconnect tolerances were inline stderr substring chains, inconsistent between sites (the handler variants matched case-sensitively without a default). The docker_network_noop filter is the single marker source per operation; disconnect additionally tolerates absent networks/containers, which is the proven swarm failure ('No such container: postgres' when the central DB is a swarm service). Drop the now-orphaned DISCOURSE_ERROR_NOT_CONNECTED.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolve_host_cid/wait_converged/guard_instance_dir took a run_host parameter every caller had to pass; STACK_HOST already encodes the correct host in both modes (first swarm manager, degrading to inventory_hostname in compose), so the manager-only queries now delegate to it directly and the parameter plus all caller declarations are gone. Also repairs the swarm guard include: apply/delegate_to are invalid on a HandlerTaskInclude and the previous commit carried duplicate run_once/delegate_to keys, which failed the ansible syntax-check. The keycloak handler drops its masking default; the 02b conditional collapses into the SPOT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pre-flush made the preload block exceed the 3-child lint limit; move the block body (mark, pre-flush, load, post-flush) into preload_service.yml so the block keeps only the include plus the always-clear.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The login broker made its server-side XRPC calls (createAccount/createSession/resolveHandle/updateAccountPassword) against the public PDS_URL, which routes through the front proxy and fails TLS verification under self-signed mode. Split public/internal: PDS_INTERNAL_URL http://pds:<port> for in-stack calls (falls back to PDS_URL when unset), PDS_URL stays browser-facing. All service ports in the compose template now come from vars constants reading services.*.ports.internal.http; drop the unconsumed broker port scalar from the services meta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Without proxy.service_key the vhost target falls back to the entity name 'funkwhale', but the compose stack has no such service; the webserver container is 'front'. In swarm the upstream DNS funkwhale_funkwhale resolved to nothing and the vhost answered 502.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
delegate_to templates eagerly, before when: is evaluated; with central redis the resolve include was skipped and MAILU_REDIS_HOST_NODE undefined, so the probe task died on templating despite its false condition. Move resolve+probe+rescue into 03a_probe_local_redis.yml and gate the include itself: with central redis the file is never loaded, with embedded redis the ungated resolve always defines the variable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every chdir-dependent compose/swarm handler assumed the instance dir of the flush-time application_id exists; a drifted notification then died with a cryptic 'Unable to change directory'. Guard the flush with a shared stat+fail (tasks/utils/guard_instance_dir.yml, included first in both handler files) that names the app, the missing dir and the drift cause, and instructs to fix the notifying role.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A shared-service preload switches application_id mid-consumer-pass; the preloaded service's front-proxy pipeline flushes handlers (sys-stk-front-proxy:113), firing the consumer's pending compose-up notifies under the service's binding whose instance dir may not exist. Drain them under the still-correct consumer binding first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The consumer re-render loop included sys-front-inj-all under web-svc-* application_ids in the middle of the matomo pipeline; the shared sys-svc-compose handlers rebind to the most recent role instantiation, so matomo's compose-up notifies from 06/07 flushed against a consumer's instance dir (/opt/compose/logout) that does not exist when that service is outside the play's whitelist. Move the loop into 10_reinject_consumers.yml as the last step after 08_config, behind the final flush.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
compute_external_network_roles() + compose_external_networks lookup expose which foreign provider networks a compose project references as external, so the swarm deploy path can pre-create them. network/create.yml probes for an existing network instead of unconditional state:present (the module's delete+recreate on attached networks crashed redeploys) and hard-fails on driver drift instead of silently skipping it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Without LDAP the fallback only seeded the administrator user and no RBAC groups, so oauth2-proxy's allowed_groups check 403'd every group-gated app (prometheus/seaweedfs, matomo/css downstream) and non-admin logins hit user_not_found (biber). 08_users_realm now seeds every non-reserved user with a usable password (parity with the LDAP path, which federates the same set); new 08b_rbac_groups_realm materialises the /roles/<app>/<role> group tree + memberships directly in the realm: build_realm_rbac_groups mirrors build_ldap_role_entries and restricts members to the users 08 actually seeds (by username attribute); ensure_group_path_members.sh walks the path via kcadm (children endpoint, exact user lookup, idempotent membership PUT) and hard-fails on any missed group or membership instead of masking. Both routines are gated to KEYCLOAK_LDAP_ENABLED false; LDAP-enabled deploys are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
oidc/enable.yml ran the plugin check/install/enable and the two mem_limit container updates against PEERTUBE_CONTAINER_ADDRESS on the manager, but peertube lives on a worker in swarm -> "No such container". Worse, the two updates used command:, which never expands the $(...) swarm address (passed literally). Resolve the hosting node + local CID (resolve_host_cid) and delegate_to it for all five container ops, and convert the two command: tasks to shell:. Compose unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
03_wait_for_redis ran container exec redis-cli ping and the admin HTTP probe on the manager, but mailu_redis/mailu_admin can be scheduled on a worker -> "No such container", exhausting all 30 retries. Resolve each service's hosting node + local CID (resolve_host_cid, prefixes MAILU_REDIS / MAILU_ADMIN) and delegate_to it for the probes and their log-collection rescues. Compose unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
02_manager_ops ran container exec for the config check, upgrade and flag-setter against ESPOCRM_CONTAINER_ADDRESS on the manager, but in swarm the espocrm container lives on a worker -> "No such container" (silently swallowed by failed_when on the earlier steps). Resolve the hosting node + local CID via resolve_host_cid and delegate_to it (also for the config-patch include), matching the erpnext pattern. Drop the now-unused ESPOCRM_CONTAINER_ADDRESS. Compose unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
email.py's environment folds TLS_ENABLED into its "external" base, so a non-external relay (no mailu deployed) under TLS resolved host to the mailu domain while auth/tls/from stayed in localhost mode: an unreachable, inconsistent msmtprc that aborted the sys-ctl-hlth-msmtp unit and the deploy (chess/bridgy-fed/hugo/legal). Gate host on external directly, like tls/auth/from, so a mailu-less deploy targets the loopback postfix that sys-svc-mail-smtp deploys under the same email.external gate. Adds a guarding unit test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mdad runner bind-mounts a manager-only host path (MATRIX_MDAD_DIR), but the ansible-flavor service template emitted no deploy/placement block, so swarm scheduled it onto workers where the path is absent and every task was Rejected ("bind source path does not exist"). Include deploy.yml.j2 (as the compose-flavor already does) so the role's default_placement: manager renders. Swarm-only; compose unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>