Uploads ride the NFS-shared named volume (Local uploader), auth is stateless Guardian JWT with a shared secret_key_base, and boot migrations serialize on Ecto's default migration_lock against shared Postgres. Removing replicas:1 lets compose_replicas pick the topology count; compose ignores it. Confidence ~95% (static + deep-dive; no live swarm run).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A recursive DNS resolver binds :53 udp/tcp on the host and has no horizontally-scalable swarm topology (the routing mesh cannot load-balance the resolver), so pihole opts out of swarm discovery and deploys compose-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds tests/lint/ansible/roles/test_single_replica_justified.py: a bare replicas:1 in meta/services.yml is flagged so a single-replica pin is never applied silently; services that can scale should carry no replicas key and let compose_replicas pick the topology default. Marks the pins on genuinely single-instance services (audit-verified as unscalable): checkmk OMD/RRD single-writer, erpnext websocket/scheduler/configurator sidecars, jenkins controller, listmonk campaign-manager singleton, mastodon db:prepare one-shot, minio single-node (no erasure), opencloud decomposedfs+NATS single-writer, seaweedfs single-node, taiga admin sidecar, ejabberd standalone Mnesia.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
act/workflow.sh: build local/act-runner-fixed:latest on demand when that tag is selected but the image is absent, avoiding an image-not-found failure. swarm/_context.sh: probe_app_reachable falls back to a bash /dev/tcp connect and finally the container's own Docker health status, so toolless images (e.g. fider's debian-slim without curl/wget/nc, which exit 127 on every in-container probe) are no longer reported unreachable. Both additions are additive: the curl/wget/nc path and the non-matching act tag are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Penpot 2.14+ relabelled the native-login button (Login -> Continue), so the /^Login$/ role-name locator no longer matched and the e2e login flow failed on the 2.16 upstream UI. Use the stable data-testid=login-submit locator instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stock install runs blocking in the entrypoint before apache serves, so no HTTP is answered during it and the only healthcheck lever is start_period. The entrypoint's follower-wait already tolerates a ~10min leader install (300x2s), but start_period was 5m, so in swarm the leader or a waiting follower was reaped mid-install, orphaning the mkdir lock and deadlocking the shared-NFS init. Raise start_period to 15m so it exceeds the follower-wait window and no task is reaped mid-install. Compose is unaffected (it does not reap on unhealthy).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A consumer that forces a per-role deploy mode (web-app-matrix ansible-flavor and web-app-bigbluebutton set compose_mode_force=compose) leaked that value through the include_role chain into the shared database provider (svc-db-postgres/mariadb). In a swarm cluster the provider then ran via 'docker compose up' (bridge) instead of 'docker stack deploy' (overlay), breaking the shared DB. Reset compose_mode_force to empty on the provider include, alongside the existing compose_repository_pull override, so the shared provider always follows the cluster DEPLOYMENT_MODE. Covers every consumer, not just matrix/bbb.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On disk-constrained sync runners (act/CI) a mirrored external image was double-stored: once in the docker engine and once in the local registry. After pushing, docker rmi the upstream and prefixed tags so only the registry copy remains; swarm re-pulls from the registry at deploy time. Locally-built images are never removed (recovery and base images depend on them staying in the engine store). Also skip the re-pull when the prefixed image already has a registry manifest. The compose image rewrite and registry push are unchanged; rmi is best-effort.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Roles whose get_entity_name(application_id) resolves to an empty string (e.g. svc-runner) rendered 'name: ' with no value in the compose networks block, producing an invalid/empty network definition. Guard both the swarm overlay and the compose bridge emissions so the name line is only written when own_entity is non-empty; docker then auto-names the network. Add regression tests for both modes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
make autoformat re-wrapped the hostname read_text over multiple lines and moved the # nocheck: cache-read to the closing .strip() line; test-lint stays green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The .env boilerplate set CACHE_DRIVER=file / SESSION_DRIVER=file, overriding the intended redis config (already wired via the engine lookup). That made sessions node-local and made the custom image's 'migrate --force --isolated' take a file-based lock, so concurrent swarm replicas did not coordinate the first migration. Drop the file overrides so cache, sessions and the isolated migration lock all use the shared redis. Extend start_period to 5m for the first-boot migrate and seed, and raise swarm converge retries to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure formatting from make autoformat (a folded long Jinja line in svc-runner vars; double->single space before # nocheck comments in jitsi/openproject/rdr-www services and mailu/openwebui/snipe-it/web-svc-file variants). No semantic changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the tor.enabled true/false rounds (shared:false + nocheck on the disabled round) so the dynamic tor flag is covered on these roles, and reorder each variant services-first so the tor nocheck registers. Each variant keeps its exact keys (credentials / addons / server.domains preserved, values unchanged) — mapping key order only; no cross-variant moves.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add meta/variants.yml with the tor.enabled true/false rounds (shared:false on the disabled round) so the dynamic tor flag is covered for the two embedded cache DBs. No exposed field — redis/memcached stay stubs (embedded per-consumer, no standalone host port).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve services.<key>.<field> with consumer-override -> provider-native fallback (the provider being the role whose entity name is <key>, e.g. svc-net-tor for tor). Keeps provider defaults like tor.exclusive/tor.primary in one place while consumers stay free to override per app; consumed by utils.cache.domains for onion-domain injection.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test-e2e-cli renders test.env at 0600 in a 0700 stage dir and always removes the stage tree (it holds base64 DB credentials). test-e2e-playwright + the e2e scripts resolve the node SOCKS from the single spot: playwright via lookup('tor_socks', 'host.docker.internal'), onion.sh takes it as a required arg, rerun-spec.sh composes it from INFINITO_TOR_SOCKS_PORT — no 9050 literal or backup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mint the node onion during provisioning and write its address straight into applications.svc-net-tor.services.tor.node, dropping the INFINITO_TOR_NODE_ONION env indirection: ensure_node_onion (was init_env) mints/reuses the .onion-identity key files + returns the address (no env write); identity_hs_dir/IDENTITY_DIRNAME move to utils.tor_onion so cli and utils share the path SPOT; provision.apply_tor_node_onion sets services.tor.node in host_vars when svc-net-tor is deployed (run by 'inventory provision' at deploy time, and the svc-net-tor role copies the same key files into the daemon); domains.py reads services.tor.node; nginx_vhosts reads the key file instead of os.environ; INFINITO_TOR_NODE_ONION removed from default.env/passthrough/compose + the inventory env-lookup. Bonus: the address now lives in the persistent key files, surviving .env regeneration by make test/autoformat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New env handler reads svc-net-tor services.tor.ports.local.socks (via ROLE_FILE_META_SERVICES) and emits INFINITO_TOR_SOCKS_PORT; registered in the handler registry + passthrough static keys + default.env, and forwarded to the container by compose.yml — so shell consumers (the Playwright rerunner) get the port without a literal. 08_networks.yml adds the local.socks PORT_BANDS band (9050).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
meta/services.yml gains tor.node (node onion address, filled per environment by the inventory) and tor.ports.local.socks (SOCKS port single source of truth). torrc renders SocksPort via the tor_socks lookup; the role test.sh takes TOR_SOCKS from the environment instead of a hard-coded 127.0.0.1:9050.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test.sh: dispatch auth via a run_auth wrapper calling auth_tor/auth_local directly — shellcheck SC2317 flagged the indirect ${attempt_fn} invocation as unreachable. test.env.j2: sort keys ASC for the env alphabetical-order lint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add exposed: bool (default false) to svc-db-postgres/mariadb/openldap meta/services.yml with a variant-0 (exposed) / variant-1 (not) toggle, plus real credential e2e tests (files/test/test.sh + templates/test.env.j2): authenticate with actual credentials over the node Tor SOCKS proxy when the variant enables tor, locally in-container (password auth) otherwise. mariadb/openldap force the local publish (+ OPENLDAP_PORT var); secrets stay off the process argv (MYSQL_PWD, ldapwhoami -y /dev/stdin, PGPASSWORD env).
Depends on shared onion pieces landing in a follow-up commit: tor_ports exposed-port collection, svc-net-tor services.tor.node + services.tor.ports.local.socks SPOT, the tor_socks lookup, and test-e2e-cli 0600/stage-cleanup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Provides the tor.enabled true/false matrix coverage (variant 0 on, non-baseline off) for the openresty proxy role, matching the per-role tor variant contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_tor_contract gains a '# nocheck: tor-contract' opt-out (it previously only excluded svc-net-tor by name). web-opt-rdr-domains, a redirect-only role with no onion surface, drops its tor block and marks the exemption; its variant-coverage stub is removed with it. The new rule is registered in docs/contributing/actions/testing/suppression.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-app PLAYWRIGHT_PROXY: an app whose canonical URL is a .onion (services.tor primary/exclusive) has its browser routed through the node Tor SOCKS proxy (socks5://host.docker.internal:9050); clearnet apps stay direct. A SOCKS readiness gate probes the hidden service and skips (not fails) the app's run when Tor cannot reach it. TEST_E2E_PLAYWRIGHT_PROXY is removed since the per-app logic supersedes the global knob. Non-overridable tuning constants moved defaults->vars (UPPERCASE); only genuinely overridable knobs (only_roles/skip_roles/keep/VARIANT_INDEX) remain in defaults.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Onion routing is transport-layer with no persona/UI surface, so tor cannot be gated from the per-persona Playwright specs. Mark the tor block with '# nocheck: playwright-service-flag' in the 60 domain roles that ship a playwright.env.j2, per test_env_services_match's documented exemption for services that legitimately cannot be Playwright-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each role's meta/variants.yml now pins the tor onion gate both ways so the matrix exercises it: variant 0 pins services.tor.enabled: true, every non-baseline variant pins enabled: false + shared: false (# nocheck: variants-static-override, since tor.shared is a static literal). This gives real test_coverage / test_bond_baseline coverage without the removed per-key code exemptions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Domain-serving roles reference the svc-net-tor provider via a tor: block. Consumers declare enabled (dynamic group_names gate) and shared: true (static — onion is always the shared node model, inline nocheck: dynamic-flag). The provider svc-net-tor additionally declares exclusive/primary defaults that consumers inherit via resolve_service_config unless overridden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>