The deploy CLI shipped every mode as -e KEY=value, which ansible-playbook parses as a string at the highest precedence. MODE_DEBUG=false therefore arrived as the non-empty, and so truthy, string false. Every when: MODE_X | bool was unaffected because the filter maps it back, but four sites read a flag bare and silently inverted: openldap wrote LDAP attributes with state exact on every deploy instead of only under MODE_CLEANUP, SYS_SERVICE_DEFAULT_STATE was restarted where it should have been omit, SYS_SERVICE_RUNNER_STATE was never started, and mailu always logged at DEBUG.
Send the modes as one JSON extra-var, which keeps the declared type, and give those four sites the | bool they were missing so the meaning of a flag no longer depends on how its value reached Ansible. A lint pins the rule: a MODE_* read in a truthiness position carries the filter or a nocheck naming a genuine string comparison.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve conflicts, resolved by intent rather than by side.
Six shared one shape: the incoming side adds the new rescue_index.sh call but
drops this branch's `# nocheck: shell-or-true` markers. The markers carry a
lint contract here, so the incoming structure was taken and the markers put
back — dedicated.sh, guide/one.sh, guide/host_deploy.sh, swarm/routine/00_one.sh
and trigger_units.sh, where the incoming version additionally writes the full
journal to a file instead of only echoing a tail.
web-app-matrix keeps HEAD's _synapse_extra_args accumulator (the superset: CA
mount plus the onion case) but adopts the incoming improvement inside it — the
inlined regex_replace gives way to lookup('subnet_gateway').
handlers/__init__.py drops rescue_diagnostics_dir from the old import group
(the incoming side moved it under .infinito.rescue, keeping both would import
it twice) and keeps tor_socks_port, a HEAD-only handler ORDERED_HANDLERS still
needs.
svc-bkp-volume-2-local takes the incoming side throughout: both branches landed
the same feature, but swarm-nfs evolved it — the knob moved from the global
STORAGE_BACKUP_SNAPSHOT_MODE to services.volume-2-local.snapshot_mode, plus the
subvolume helper, 04_snapshot_subject.yml, meta/packages.yml and baudolo 3.2.2.
Three files that left behind were removed: files/baudolo_snapshot.py and the two
worktree tests at their pre-refactor paths, all byte-identical to their moved
counterparts.
Six follow-on failures the merge produced, fixed at the root:
- STORAGE_BACKUP_SNAPSHOT_MODE lost its last reader with the config-lookup
design and is gone from group_vars.
- utils/github/variant_bundle_limits.py sat next to variant_bundles.py, which
the prefix lint forbids; the pair now lives in utils/github/variant/ as
bundles.py and bundle_limits.py, with the -m entry point, the ruff
per-file-ignore, the design-doc link and the mirroring test path updated.
- The incoming WordPress addon spec navigated with a raw page.goto and carried
three bare timeout literals; both route through gotoOnion and resolveTimeout
now, which is what makes navigation survive onion transport.
- Six unmarked `|| true` got a concrete reason each: docker.socket is absent on
distros that ship docker without it, the loop device is already detached when
the probe failed before attaching, and modprobe zfs may fail because the
/dev/zfs check decides.
make quality green across all four targets. The cold-domains budget test that
had to be waived here passes on its own now — 19d56f07e raises it to 100s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The retrigger sent only whitelist and priority, so entry-manual.yml applied its
own distros default of 'debian'. A run that swept five distros came back as one,
and a failure that only reproduces on centos returned green with nothing fixed.
The dispatch inputs are not recoverable from the API - it answers inputs: null
for a finished workflow_dispatch - so they are read back from the run title.
The title format is not copied here: run_name.py parses the workflow's own
run-name declaration and derives the literals framing an input, and the tests
build their titles through the same helper instead of spelling one out.
Known gap: a run from another entry point carries an unrelated title and yields
no distros, leaving the retrigger on the workflow default. Job step names carry
the list for every entry point and arrive in the same gh call, so that is where
to look when it matters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docker info reports DockerRootDir as the DAEMON sees it, and
host_storage_constrained fed that string straight into shutil.disk_usage,
which statvfs()es it in the CALLING process's mount namespace. The two
coincide only when the caller is the daemon's host. In the nested act
container of the workspace jobs they do not, so os.statvfs('/var/lib/docker')
raised FileNotFoundError, matrix.py died, and because 00_one.sh invokes it
bare under set -euo pipefail the whole swarm drill went down with it. All
five workspace jobs of run 30560683608 failed this way, byte-identically,
across five base images and five nested distros.
docker_root_free_bytes() now takes the vantage point as a required keyword:
it measures the daemon's data root when that path resolves here, and the
caller's stated local path when it does not. matrix.py passes "/", which is
the same vantage it already trusts for its ENOSPC abort 270 lines earlier;
overlayfs passes statfs through to the upper filesystem, so a container of a
daemon measures that daemon's data-root filesystem through its own rootfs.
Nothing is caught and nothing is guessed. An unreachable daemon still raises
RuntimeError out of docker_data_root(), which is the guard this module exists
for, and local_vantage carries no default because only the caller knows where
it is running.
The compose-side twin loses its || true and its hardcoded /var/lib/docker
substitution, so an unresolvable root no longer measures a guessed filesystem.
Its compose.exec locality was already correct and is unchanged.
Verified against the pre-fix module under the CI condition: with the data root
stubbed to a path absent from this namespace, the old code raises
FileNotFoundError and the new code measures the local vantage; with the daemon
stubbed unreachable, the new code still raises RuntimeError.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three conflicts, none resolved by picking a side wholesale:
- Makefile: took the incoming ACT_INPUTS, which routes through SWARM_DISTROS
instead of inlining the same expression a second time.
- test-e2e-playwright/02_run_one.yml: took the incoming rc handling. An absent
rc now counts as failure; the previous default(0) let an aborted Playwright
run pass silently, contradicting the debug message right above it.
- web-app-peertube spec: combined both. Kept gotoOnion and resolveTimeout,
which this branch's lint enforces, and adopted the incoming retry-inside-poll
for the OIDC button plus its networkidle wait.
The flag was decided by a fixed free-space threshold (100 GiB by default), which fired on roomy hosts and stayed silent on a 145 GiB CI runner that a single deploy round could fill. It now compares free space on Docker's data root against what the deploy declares it needs: the summed min_storage of the selected applications and their transitive dependencies.
The need is variant-aware. utils/storage/constrained.py overlays each app's meta/variants.yml entry before walking the service registry, so a round that trims services down is measured at its own size instead of the base configuration's.
Both entry paths pass their per-round variant map: cli/administration/deploy/development/init.py decides inside the round loop (compose and host share this via ci/one.sh -> dedicated.sh), and utils/tests/swarm/matrix.py feeds the decision into the swarm host_vars payload. --threshold-gib is gone; --force-storage-constrained still overrides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The nested dockerd writes /mnt/docker as root, so shutil.rmtree from the runner user silently removed nothing and the previous distro's containers survived into the next distro run. They restarted under unless-stopped against a fresh container rootfs, failed with exit 127, and sys-ctl-rpr-container-soft exited 2 because their compose working_dir no longer existed, which failed the guide deploy on the second distro.
Escalate the wipe the same way dedicated.sh does and hard-fail when the root survives, so a silent no-op can no longer leak state across distros.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Parallel checkouts collided on the compose subnet, host ports and container
names, so a quality run in a worktree died at compose-up. Slot each checkout
via custom.env and layer two overrides on top: the shared cache network in
place of a second cache stack, and the shared .git directory without which
git aborts inside the container.
Both overlays gate on the resource they need, not on the slot number, so a
stray INFINITO_INSTANCE cannot make compose demand a value nobody set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
categories.yml moves from roles/ to the repository-root meta/, joining packages.yml. utils/roles/categories.py owns its location plus the tree-walking helpers that were stranded in entity/name.py, so no caller spells the path or duplicates the lookup.
meta/distros.yml becomes the SPOT for every distribution: os_family, Galaxy platform name, dev-environment base image, and the pkgmgr/pkgmgr-virgin/environment reference templates. It replaces seven scattered copies (DISTRO_FAMILY, VALID_DISTROS, _DISTROS, _CANONICAL_PLATFORMS, two shell arrays, a Makefile literal). INFINITO_DISTROS is no longer a static default but generated by utils/env/handlers/infinito/distros.py; the shell consumers read it through the env layer.
Drops dead CLI surface found along the way: --categories-file and --roles-dir on the provision CLI plus three unused parameters of generate_dynamic_inventory, and the positional roles_file of the invokable categories command.
tests/lint/repository/test_distros_spot.py pins the copies that cannot call the SPOT (workflow literals, docs table) and forbids composing a pkgmgr reference outside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
categories.yml moves from roles/ to the repository-root meta/, joining packages.yml. utils/roles/categories.py owns its location plus the tree-walking helpers that were stranded in entity/name.py, so no caller spells the path or duplicates the lookup.
meta/distros.yml becomes the SPOT for every distribution: os_family, Galaxy platform name, dev-environment base image, and the pkgmgr/pkgmgr-virgin/environment reference templates. It replaces seven scattered copies (DISTRO_FAMILY, VALID_DISTROS, _DISTROS, _CANONICAL_PLATFORMS, two shell arrays, a Makefile literal). INFINITO_DISTROS is no longer a static default but generated by utils/env/handlers/infinito/distros.py; the shell consumers read it through the env layer.
Drops dead CLI surface found along the way: --categories-file and --roles-dir on the provision CLI plus three unused parameters of generate_dynamic_inventory, and the positional roles_file of the invokable categories command.
tests/lint/repository/test_distros_spot.py pins the copies that cannot call the SPOT (workflow literals, docs table) and forbids composing a pkgmgr reference outside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Import ordering and one comprehension reflowed by make autoformat. No
behaviour change; kept out of the orphan-prune commit so that diff stays
readable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
init.py prunes the include set twice: it drops the roles named by disable
and then re-walks the dependency graph so providers that only existed to
serve a disabled role are dropped as well. deploy/cli.py filtered only
the named roles, so its deploy-id set stayed a superset of what the
inventory actually contains.
With disable=matomo,dashboard,prometheus,email,css the init side pruned
svc-db-redis, svc-db-mariadb, web-svc-cdn, web-svc-file, web-svc-asset
and web-svc-simpleicons; the deploy side still passed them to
validate_application_ids, which aborted the run with 'not present in
inventory' before any role deployed.
Both sides now call prune_orphans_after_disable with the same
round-merged services map, so the sets match. Document the two-sided
contract in the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The --failed selection treats anything not green (cancelled, timed-out, still-running) as a failure, so retriggering a partially-running or aborted run re-fires jobs that never actually failed. Add --failed --strict to select only roles with a hard failure conclusion, excluding cancelled/aborted and running. New aliases i8cifo (strict base, mirrors i8cifail) and i8pufo (sign-push then strict retrigger, mirrors i8pufail); i8pufail is unchanged. Verified against a live run: --failed matched 48 roles (8 failed + 40 still-running), --strict matched exactly the 8 hard failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
disable= now drops providers left with no surviving parent, not only
the named provider roles. Compose (init) and swarm (derive_includes)
share one reachability cut: BFS from the surviving primaries over
dependency + service edges with disabled roles as cut-nodes. No-disable
deploys stay byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ruff toolchain was unpinned, so host venvs, the lint container and CI
installed different releases over time. That drift is why lint-python only
failed on CI: locally ruff 0.15.x preview-gated ISC004/FURB192 and (falsely)
fired S310 on urllib.request.Request, while CI's 0.16.0 has ISC004/FURB192
stable and drops the S310-on-Request false positive.
Pin ruff==0.16.0 in pyproject.toml's dev extras (Dependabot keeps it current)
and have utils.install.lint.ruff read that pin and reinstall whenever the
found version drifts, so every environment resolves the same ruff. Then fix
what 0.16.0 flags: parenthesise the implicit string concatenations (ISC004),
use min() over sorted()[0] (FURB192), drop the now-unused S310 noqa on the
Request() builders (RUF100), and delete the narration comments that the
comment lint surfaces in the touched files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the runner Dockerfile to the lint-standard Acquire::Retries=3
and keeps the collabora TODO wording; hardens the merged tree for the
stricter lints (apt retries in the tor Dockerfile, shell-or-true
markers, comment cleanup).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The provisioned become password doubles as the Keycloak administrator realm password (user-administrator/meta/users.yml) and the guide flow generated it 64-char alphanumeric, so kcadm set-password failed the realm passwordPolicy specialChars(1) on every fresh single-app deploy. Delegate generate_random_password to ValueGenerator.generate_strong_password, the same class-guaranteeing generator every other realm user already gets via the strong_password filter; the realm policy stays strict. Also drop the commented-out Trusted Hosts block from the realm template.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ci trigger (--failed) and status commands matched deploy jobs only by
the compose/swarm glyph, so host-mode jobs (svc-storage-nfs-server etc.)
were invisible and re-triggering a failed run never picked them up. Add
host to the mode set and read the mode glyphs from the symbol glossary
(single source of truth) instead of hardcoding them. Align the host deploy
job titles to the glossary glyph 💻 (were a rogue 🖥️ absent from the
glossary) so they match, with a leading space like compose/swarm.
Priority (⭐) and regular (🔁) jobs were already both discovered (the match
ignores the line prefix); host now works in both. Past runs titled 🖥️ will
not match; a fresh run does.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parse_role_statuses collapsed a role's variant shards per mode with
last-write-wins, so one green shard could mask a failed sibling and
--failed retriggers silently skipped the role (gitlab, run 29599179461).
Aggregate worst-first (failure > cancelled > running > success) and let
parse_role_urls link the worst shard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
infinito administration deploy ci trigger --failed now dispatches the
failed roles via the priority field and omits the whitelist, so the
priority line reruns them first and the regular line covers the rest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ci trigger --run now takes a bare run id and resolves it against the current branch's tracking-remote repo; full URLs keep using the owner from the URL. The aliases no longer build run URLs from INFINITO_FORK_REPOSITORY_URL (stale values produced 404s against the wrong owner): a single i8cifail base passes the id or URL through, and i8cire, i8puci, i8manfail and i8manfails compose it (quality gate / sign-push / scope variants).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runner command joins its siblings (dedicated, development) under
cli/administration/deploy; cli/deploy is gone. Makefile, docs and the
requirement links now point at the new path, PROJECT_ROOT climbs one
level further, and the unit tests moved along.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One module per concern, mirroring the inventory/ subpackage layout:
run.py (single deploy pass + env forwarding SPOT), drill.py (recover
drill, host mode exempt, paths from CONTAINER_REPO_ROOT instead of a
hardcoded /opt/src/infinito), purge.py (inter-round cleanup) and
cli.py (arg parsing + round/pass loop, PASS 1/2 deduplicated).
Also extend the comment linter to flag bare string-expression
statements outside docstring position in Python: a triple-quoted
literal floating in a body is a comment in disguise and now runs
through the same validity rules; docstrings and PEP 224 attribute
docstrings stay allowed. Test narration comments moved into method
docstrings accordingly, and the design docs now link deploy/cli.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move files that share a delimiter-bounded name prefix within a folder (threshold 2) into a subfolder named after the prefix, dropping the prefix from the filename. Covers plugins/filter (docker, generate, has, merge, get/all), env handler subpackages, utils/roles/entity, utils/tests/swarm/write, role SQL/scripts/templates, keycloak/mailu task-includes, shared tasks/utils/load, and CI scripts. Update every import, query_file/script/include_tasks/src/lookup target, compose bind mount, and python -m path to the new locations.
Extend the prefix-grouped-files lint to threshold 2 with dir-level .nocheck and file-head nocheck opt-outs, and skip groups whose shortest member's stem equals the shared prefix. Keep flat only where a loader forbids nesting: WordPress mu-plugins and the docs/requirements namespace. Ansible filter discovery is recursive, so nested filters stay Jinja-resolvable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve every 3+-file same-prefix group flagged by the new prefix lint
by moving the files into a subfolder named after the shared prefix and
dropping the prefix from the file names, updating all references:
- utils/env/handlers/infinito_*.py -> infinito/ (dir/, package_cache/
nested a second level); registry imports and the four handler-dir
globs switched to iter_project_files
- utils/install/lint/ansible_*.py -> ansible/ (aliased imports)
- roles/sys-svc-compose/files/swarm_*.{py,sh} -> files/swarm/
- web-app-erpnext/web-app-zammad files/scripts/apply_*.{py,rb} -> apply/
- web-app-odoo files/sql/{select,update}-* and sql/oidc/{grant,insert,
select,update}-* -> matching subfolders
- web-app-keycloak tasks/update/05_ldap*.yml -> 05_ldap/ (internal
includes made role_path-absolute)
- plugins/filter/get_*.py -> plugins/filter/get/ (filter names are the
filters() dict keys, so Jinja usage is unaffected; ansible discovers
the one-level-deep subdir)
Ansible lookup dirs, addon dirs and .github/workflows keep their flat
layout (the filename is the public id / GitHub only discovers flat).
Unit tests mirror the new source layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add DIR_BACKUPS to utils.paths and INFINITO_DIR_BACKUPS /
INFINITO_DIR_SECRETS env handlers, derive the recover-console constants
(including NFS_EXPORT_STATE) from utils.paths and the nfs export SPOT,
and switch every remaining self-built /var/lib/infinito subpath - the
secrets recover targets, the swarm extras backup source, the users
token-store candidate chain, the CI recover drill, the swarm NFS
diagnostics and the playwright reports dir - to those constants. Tests
consume the env constants directly; narration comments in the touched
files are dropped per the comment lint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Node image now builds FROM the distro infinito image (python, ansible, CLI baked in) instead of installing a per-node .deb, so there is no per-node bootstrap or timeout; the swarm CI job depends on the distribution build. Move compose/swarm.yml to compose/swarm/compose.yml and relocate the swarm matrix orchestrator from cli.administration.deploy.swarm to utils.tests.swarm (test code out of the shipped CLI).
Back the NFS export root with a disk-backed named volume instead of a RUNNER_TEMP bind: local-act RUNNER_TEMP resolves onto tmpfs and ganesha FSAL_VFS cannot export a tmpfs, so it loaded zero exports and the controller mount got ENOENT. Add optional compose/swarm/cache.override.yml wiring swarm nodes to the package-cache frontend when present, leaving CI without a cache unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add scripts/tests/deploy/ci/recover_drill.sh: seed a backup root (nfs, volume, secrets, each with a marker) plus a docker volume, then run recover full <root> localhost to restore every backup-stored type into its live target and verify the markers. Runs between PASS 1 and PASS 2 of the compose development deploy.
Hook it via _maybe_recover_drill in cli.administration.deploy.development, gated by INFINITO_RECOVER_DRILL (default true).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add cli.administration.recover: one uniform type/source/target command wrapping each svc-bkp-* role files/recover.py (nfs, volume, secrets, device) plus full for whole-tree recovery. Colon-encoded sources are classified by shape; device supports an on-device subpath, a generation, and remote pull over ssh.
Replace the per-host swarm DR-drill scripts (03/06/07/08_recover_*.sh) with the recover CLI in backup/base.sh. Pass the device on-device subpath relative (the CLI reads absolute segments as the restore-root) and load the applications-free .deploy.yml extras twin on the step-10 redeploy so host_vars credentials survive the -e override.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The disable env only matched service/entity keys (email, prometheus);
naming the provider role (web-app-mailu) silently did nothing. A shared
expand_service_tokens (registry SPOT) now normalizes either spelling to
the provided service key(s), and every disabler resolver plus the
conflict check normalize at entry, so compose and swarm accept both
forms.
derive_includes additionally prunes the disabled providers from the
swarm include closure. Without it the provision-time disabler removed
the provider role but extend_inventory re-added it via the same dep
walk, so the deploy tripped the 'provider role still active' conflict.
Live-verified on a swarm-dashboard run: disable='web-app-mailu,...'
expands to email->web-app-mailu and deploy round 1/1 proceeds with no
conflict.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The backup node no longer rides in the cluster inventory: extend_inventory
writes a sibling backup.yml (same host_vars dir) holding only the
svc-bkp-remote-2-local and svc-bkp-local-2-device groups, and the matrix
deploys it as a second play after the cluster converges, right before the
DR drill. Without a svc-swarm-node group that play resolves
DEPLOYMENT_MODE=compose and IS_STACK_HOST=true on the backup host, so the
backup roles and their alarm/cleanup units (cln-faild-bkps, alm-compose)
deploy without cluster-membership special cases; the interim group-gate
on sys-ctl-cln-faild-bkps is gone again. A stack host that should also
pull backups joins the bkp groups of its own cluster inventory instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The DR-drill applications block in swarm-nfs-extras.yml reached the
deploy as extra-vars; -e replaces the whole inventory applications dict,
so every generated credential vanished and the first schema-checked
lookup (svc-db-mariadb credentials) killed all swarm deploys. The block
now enters through the provisioner's host_vars merge (_bake_overrides
base_overrides), the deploy loads the applications-free
swarm-nfs-extras.deploy.yml twin, and the full extras file stays for the
drill, which reads the device paths from it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the per-node docker-run bring-up with a single node image
(compose/swarm/Dockerfile bakes python3/dnsmasq/openssh/rsync/cryptsetup)
declared in compose/swarm.yml, one compose up, and one ansible play
(compose/swarm/playbook.yml over ansible_connection: docker) that waits for
systemd, captures IPs, wires lab DNS and installs the .deb per node. Reshape
the harness into topology/ (naming SPOT), routine/ (01_bootstrap + the
sequenced 02-08 steps) and utils/ (clean/, collect/, shared helpers); the
workflow, matrix.py and Makefile follow the new paths. swarm init/join was
already ansible-owned (svc-swarm-node), corrected in the README diagram.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- compose/noports.override.yml drops every published port via !override
- compose_file_args appends it when INFINITO_PUBLISH_PORTS=false
- runner instances set the flag; default true keeps other flows unchanged
- central yaml loader + ansible-lint config accept compose merge tags
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the meta/services.yml key across all roles, the roles_with_default_placement lookup (now roles_with_placement), the get_role_default_placement / iter_roles_with_default_placement helpers, and every consumer (swarm closure, compose_volumes filter, nfs_prep, container deploy template, constructor stage, extend_inventory) plus their tests and docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
sys-svc-compose-ca (required_by web) and sys-ctl-mtn-cert-deploy
(required_by web-app-mailu) were skipped at their include sites via a
TLS/self_signed `when:`. On a TLS-off node (e.g. .onion) the skipped
include_role leaves no role-named TASK block, so the post-deploy
required_by validator (cli/meta/roles/services/called) reports the role
as "did not execute" and exits 2.
Move the applicability decision into each role: an always-run debug marker
when not applicable (leaving the non-skip TASK block the validator needs)
plus the real CA/cert work gated behind the same condition. Clearnet
self_signed nodes still run the real work; TLS-off nodes pass with the
marker. Adds sys-svc-compose-ca/vars/main.yml (_compose_ca_applicable SPOT).
Also flush the deploy phase-header prints so they stream in order instead
of being buffered until after a failure (which misattributed the failing
pass).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn a NAT/edge node into a full Tor v3 onion node: DOMAIN_PRIMARY
becomes the node's .onion, so the whole stack (web, SSO, LDAP, CA)
resolves onion domains and is reachable over Tor without a public IP,
port forwarding, VPS or DynDNS.
- utils/tor_onion.py: offline v3 key mint (address + HiddenServiceDir
key files), verified against a known key->address test vector.
- cli .../inventory/onion: `init-env` pre-mints the node onion, writes
INFINITO_DOMAIN(+_RE) into .env and authoritative key files into
.onion-identity/hs (idempotent). The onion is a deploy-time input;
no in-deploy random minting (that would not match the pre-set domain).
- roles/svc-net-tor: Tor sidecar (Debian-built, network_mode: host),
installs the minted keys, torrc with one node HiddenService + a SOCKS
proxy restricted to loopback/DiD ranges. files/test.sh probes every
served onion domain over the Tor SOCKS.
- tls: .onion -> TLS off (onion v3 crypto is the transport auth; LE
cannot sign .onion) + app-level flavor override.
- harness/DNS: coredns resolves the onion zone via INFINITO_DOMAIN;
skip the clearnet DNS sanity check for onion; raise nginx
server_names_hash_bucket_size to 128 for long onion server_names.
- test-e2e-playwright: gated browser SOCKS proxy (PLAYWRIGHT_PROXY) +
navigation/action timeouts so specs reach .onion over Tor and fail
fast; the onion node points it at the Tor SOCKS.
- docs/requirements/031: full design.
Verified: dashboard + SSO (Keycloak) + 9 apps reachable over Tor
(HTTP 200 through the node hidden service). make test green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
derive_includes applies the same shared:true flip as force_shared_db, so
variant rounds that pin a consumer's DB to shared:false keep the central
provider (and its generated credentials) in the include set the deploy
will actually load. extend_inventory derives its manager-placed groups
from the same variant-aware closure instead of a base-config walk, which
re-added pruned provider groups without credentials. matrix.py passes the
round's variant map to the extend step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>