Deploy GitLab from the official CNG CE images as six services: webservice (puma) and sidekiq scale to the topology default, workhorse is the sole HTTP entry, gitaly and a one-shot migrations job are pinned singletons, shell runs gitlab-sshd. The Omnibus container, its Dockerfile and the GITLAB_OMNIBUS_* config groups are removed. All rails/workhorse/shell/gitaly configs and shared secrets are pre-rendered by Ansible and mounted read-only; eleven credentials land in meta/schema.yml including an rsa_pem_2048 OIDC signing key (new value_generator algorithm). Postgres, redis and S3 object storage ride the existing lookups; OIDC, SMTP, the public SSH port and the root password are preserved. compose_volumes gains a per-volume nfs:false opt-out so the gitaly repositories volume stays node-local in swarm (upstream forbids git repositories on NFS); nfs_prep skips volumes without driver_opts accordingly. The Nextcloud gitlab integration execs the CNG rails runner. ruby_single_line dies with its only consumer; ruby_dq keeps test coverage. Ports (8181/8080/2222/8075/3808/9122) and service names resolve from single SPOT declarations in meta/services.yml via vars constants; the version pin sits on the webservice image pair so the updater can bump it. Live-validated in compose up to all six services healthy and migrations completed; fresh-install-only semantics for existing Omnibus data are documented.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Workers failed 'docker build: no Dockerfile': the build context, compose file, .env and runner binary were staged on the stack host only while every node deploys its own node-local runner stack. Build and push the image once on the manager (RUNNER_IMAGE_REF via the swarm registry, 04_image.yml) and let workers pull it; render compose.yml (template with lstrip_blocks, matching the replaced stack_host_template action), .env and the extracted run.sh on every node. Add a node_local kwarg to the compose_networks/container_networks/compose_restart/compose_only lookups so this node-local compose stack renders a bridge default network (workers cannot create overlays), keeps restart: unless-stopped (ephemeral runners died after one job) and keeps container_name under swarm; default is unchanged for every other role. Treat build/install.stamp as stale when the venv is missing and exclude ./build from the nested-src copy, closing the nested-e2e ruamel failure class. Regression tests for the node_local render paths; the network include tests now accept lookup kwargs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the inline/trailing comments added across the Tor egress-router, onion-domain, mailu-notls and vault round-trip changes, per the repo no-comments convention. Comment-only: no code behaviour changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The handler imported utils.cache.yaml (PyYAML) at module load, which broke python -m cli.meta.env in the stdlib-only .env bootstrap on fresh distro images (manjaro CI failed with ModuleNotFoundError: No module named yaml). Import lazily inside apply() and skip when PyYAML is absent; the SOCKS port is re-set on any later build that has PyYAML.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The !vault YAML constructor returned a plain str, so round-tripping a host_vars
file through utils.cache.yaml (e.g. apply_tor_node_onion writing
services.tor.node) re-emitted every credential as an untagged quoted string.
Ansible then never decrypted them and LDAP/DB admin binds failed with result 49.
Return a VaultScalar so SafeDumper re-emits the !vault literal block; add a
regression test asserting apply_tor_node_onion keeps existing vault credentials
tagged.
Co-Authored-By: Claude Opus 4.8 <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>
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>
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>
Two stacked defects broke keycloak object updates on the swarm-delegated
cross-node path:
- get_merged_users cached the templar-less raw_users payload (fetched by
get_merged_applications) under the same key as templar-backed lookups, so
every later lookup('users') in the process returned UNRENDERED values. The
literal '{{ 42 | strong_password }}' / '{{ ansible_become_password }}' then
reached kcadm and swarmkit, failing the realm password policy and the stack
env validation. Key the cache on templar presence so rendered and raw
payloads never share an entry.
- The cross-node recovery block re-included itself with a self-referential
'kc_update_attempt' template var, which Ansible 2.19 expands recursively
('Recursive loop detected: maximum recursion depth exceeded') on the second
attempt. Advance the counter with an eager set_fact so each level reads a
concrete int.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a generic Docker Registry v2 client (WWW-Authenticate bearer-token
challenge) so tag listing and reachability work for any registry (Docker
Hub, GHCR, MCR, Quay, GitLab/opencode), and route the version updater
through it so quay.io and opencode.de images auto-update like the rest.
Add an external test that hard-fails when a pinned image:version tag is
unreachable (404) and warns on incomplete image/version pairs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Checkmk's OMD site crashes at boot with fcntl.flock raising OSError
[Errno 9] Bad file descriptor: NFSv4 ignores the nolock mount option and
routes flock() to the server. Force flock() client-local on v4 so
single-writer apps (checkmk, openldap) boot; POSIX locks stay
server-side. NFSv3 keeps nolock (statd-free, flock already local).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The between-rounds vhost purge computed which `servers/{http,https}/<domain>.conf`
files to remove from the domain, falling back to the hardcoded default
`infinito.example` when the DOMAIN env was unset. On a node whose
DOMAIN_PRIMARY differs (e.g. a .onion node, where DOMAIN_PRIMARY is driven
by INFINITO_DOMAIN), the purge computed non-existent filenames and removed
nothing, leaving stale vhosts. The next matrix-deploy round's health probe
then hit those orphaned vhosts as HTTP 502.
Fall back to INFINITO_DOMAIN (the dev harness' domain SPOT) before the
default so the purge targets the real vhost files on any node. Precedence:
explicit arg > DOMAIN > INFINITO_DOMAIN > default. Covered by 4 new tests.
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>
extend_inventory adds the nfs-server host without host_vars, so the
extras file is its only var source; without the cluster domain it fell
back to the group_vars default and rendered unresolvable
*.infinito.localhost endpoints.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Role-local library/ and module_utils/ resolve only inside the owning
role's task scope, so kcadm update tasks invoked from other contexts
in the swarm flow could not find keycloak_kcadm_update. ansible.cfg
already declares ./library and ./utils as the global paths; the filter
plugin and unit tests follow the move. Helper narrows its parse guard
to ValueError.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Active Storage signs URLs against AWS_ENDPOINT, so it must be the
browser-reachable public endpoint instead of the internal service url.
Compose reaches the canonical s3 api domain via extra_hosts and the
host gateway; swarm resolves it through a new proxy_aliases beacon
field that puts only the canonical domain on the front proxy, keeping
the ambiguous seaweedfs entity alias off shared overlays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get() signalled a missing key in non-strict mode by returning False, so
every genuinely-False leaf was coerced to the caller's default and
lookup('config', ..., true) could never return False. Moodle's LDAP gate
therefore fired in the ldap-disabled variant round and crashed templating
against never-generated openldap provider credentials. Use a unique
_MISSING sentinel for traversal, keep the default semantics for genuinely
absent keys, and make the moodle gate strict since services.ldap.enabled
is always defined.
Co-Authored-By: Claude Fable 5 <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>
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>
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>
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>
Lifecycle envelope for test-deploy discovery is now driven by
INFINITO_LIFECYCLES (default: alpha beta rc stable) instead of a
hardcoded constant. apps.sh forwards it to the complexity CLI via a
new --lifecycles flag (comma- or whitespace-separated); the model
falls back to the built-in default when unset.
Manual CI (entry-manual) gains two override fields: 'lifecycles' to
override the envelope per run, and 'modes' (default 'swarm compose')
to pick which deploy jobs run. ci-orchestrator gates test-deploy-compose
and test-deploy-swarm on the selected modes and threads lifecycles into
both. The new key is registered in the passthrough handler so it lands
in the generated .env.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
compose_volumes emitted a top-level configs:/secrets: entry for every declared
config/secret, but container_volumes already gates each per-service reference on
its mount's when:. So a config whose only mount was gated off (e.g. LDAP
disabled) became an orphan: a top-level object no service references, whose file:
source points at a file the role never renders (same flag). Under docker stack
deploy that source is read at deploy time and the deploy fails; under compose it
was a harmless unused entry.
Gate the top-level emission on the same mount_when_passes container_volumes uses,
so a top-level config/secret exists iff a service actually references it. To share
the predicate, promote the private _mount_when_passes from container_volumes into
utils/roles/applications/mounts and import it in both filters; the bare except
moves with it (inline noqa), so container_volumes no longer needs its BLE001
per-file-ignore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
moodle mail failed two ways in a swarm cluster. (1) moodle's build_context
pulled the FULL sys-svc-mail-msmtp role early, which rendered /root/.msmtprc on
the manager before Mailu had created the no-reply token and tripped the role's
run-once flag, so the manager kept an empty-password msmtprc and the health
check failed 'authentication method PLAIN needs a password'. Pull only
tasks_from: ca_bundle so the manager re-renders on the normal post-token pass.
(2) msmtprc forced host=127.0.0.1 for any in-container host, but the NFS server
sits outside the swarm mesh and has no local Mailu, so it hit 'connect to
127.0.0.1:465 refused'. Narrow the 127.0.0.1 branch to compose or a real
svc-swarm-node AND to when Mailu is actually deployed (email.external); else use
email.host, and map the NFS node's test DNS to the manager IP. Also import the
shared self-signed Root CA for the relay's TLS trust.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In swarm the shared openresty runs as an overlay container, so 127.0.0.1 is its own loopback, not the node's. bbb deploys node-local (host network) and its nginx binds the bbb-net gateway 10.7.7.1, a host IP the overlay reaches via inter-bridge routing. resolve_upstream now honors an opt-in services.<entity>.upstream_host in the compose branch (default 127.0.0.1) and bbb sets 10.7.7.1; proxy_pass collapses compose_mode_force apps to the static compose form. Compose keeps working because host-network openresty reaches 10.7.7.1 too. Validated live: curl bbb.talk/b/ returned 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Model change: per-user 'reserved: true|false' becomes 'accounts', a list of
host/mailbox/identity account types. Default when absent derives from roles
(bot/administrator -> [mailbox, identity], else [identity]) both for role-file
users and — via _apply_account_defaults after the inventory merge — for
inventory-only users, so users like biber keep their LDAP/Keycloak accounts.
Auto-generated role-suffix reservations get accounts: [].
New 'forward' field: mailu creates <username>@domain -> forward user's email
via the new 04b_manage_alias.yml (create-only; no_log on the loop so user
dicts never hit the log). Mailbox creation and password rotation now gate on
'mailbox' in accounts instead of role names; the mailu rbac role is renamed
mail-bot -> bot (token gate included).
svc-db-openldap drops the provision.reserved flag; OPENLDAP_USERS is always
the identity subset. reserved_usernames/non_reserved_users now classify by
'identity' in accounts, keeping the Keycloak deny-list semantics. root/backup
pin accounts: [host], administrator [host, mailbox, identity], helpdesk
[mailbox].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mastodon on the current mastodon/mastodon:latest (Ruby 4.0 / Rails 8.1.3) aborts at Zeitwerk eager_load_all (task: non-zero exit 1) without a usable VAPID key. The two keys were declared 'plain', so under CI allow-empty-plain they render empty and the web+sidekiq services never converge. Omitting the env vars (0fc5408db) was not enough: empty and absent both crash identically, so a valid key must be present. Add 'vapid_private'/'vapid_public' credential algorithms that emit one linked P-256 keypair (private = unpadded base64url 32-byte scalar, public = unpadded base64url uncompressed point 0x04||X||Y), wire the mastodon schema to them, and render both keys unconditionally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Embedded per-app DBs (shared:false) resolve to mariadb-on-NFS with no placement pin in swarm, which InnoDB does not support (locking, split-brain); embedded DBs are a compose-only pattern. After provisioning each swarm-test round's inventory, force shared:true on every svc-db-* provider service so consumers route to the central provider.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive the test-deploy matrix discovery through the complexity report instead of the type CLI: apps.sh selects the mode's tested roles via the 'compose'/'swarm' column (which already bakes in invokable + tested-lifecycle + per-mode skip), orders them coverage-first, and hard-caps the cumulative per-mode runner budget.
- complexity gains --deploy-mode {compose,swarm} (makes 'bundles'/'jobs' the runner count for that mode: compose packs variants into size/storage bundles, swarm runs one per deployable variant) and --max-jobs N (keep only the leading rows whose cumulative jobs stay below N).
- apps.sh emits whole role names; variant_bundles/output_apps.sh expand them into the matrix unchanged. Manual INFINITO_WHITELIST becomes a 'name %% {…}' filter clause applied before the cut, so a manual run only counts the apps it actually invokes.
- INFINITO_DEPLOY_MODE is now required; INFINITO_MAX_JOBS, INFINITO_DISCOVERY_SORT and INFINITO_REQUIRED_STORAGE move to default.env (the SPOT) + the passthrough handler, so no shell-side defaults remain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the 'meta roles applications complexity' report:
- --sort takes comma-separated multi-key clauses with per-key direction (e.g. 'desc embeds, asc weight'); replaces --order.
- --variant lists one row per meta/variants.yml variant; --format adds yaml.
- New columns: weight (was total), random, id, row, variants, bundles, jobs, lifecycle, covered_by, compose, swarm.
- covered_by marks the first green (1-based id) different-role predecessor that embeds the row; variant>0 never covered.
- bundles is the compose bundle/job count (whole-role) or 1 (per variant); jobs is the running sum.
- compose/swarm reflect the CI test-deploy skip logic (invokable + tested lifecycle + per-mode skip + swarm deployable variant).
- --filter is a boolean expression DSL (%% == \!= < > <= >=, and/or/xor/not, parens, set literals) over row fields.
Add shared SPOTs: variant_bundles.compose_bundle_counts and variant_status.deployable_variant_indices, reused by the swarm/compose matrix. Update roundtrip.sh/md to --sort "desc weight".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In swarm mode (INFINITO_DEPLOY_MODE=swarm) variant_bundles emits one matrix entry per variant (bundle size 1) and drops variants whose meta/variants.yml override disables every service; the all-enabled variant already exercises the role so a bare all-off deploy adds no coverage. Absolute variant indices are preserved so the emitted index still maps to the deploy planner's round. Compose mode is unchanged. Adds the variant_disables_all_services predicate (utils.roles.applications.services.variant_status) plus unit coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the 80 open CodeQL/code-scanning alerts on this branch.
Security-sensitive fixes:
- cli/administration/deploy/development: break the unsafe import cycle by
extracting the env/arg helpers into a leaf module `env.py`, giving the
load-time DAG compose -> common -> deps -> env.
- web-app-bluesky login-broker server.js: prototype-pollution guard on
cookie parsing, CR/LF + control-char log sanitisation, HTML-escaped
exception text, and a ReDoS-bounded handle regex.
- web-app-baserow SSO: harden the open-redirect `next` handling.
- network diagnose probes: require TLS >= 1.2.
- actionlint installer: validate tar members against path traversal.
- decidim/apt-purge lint regexes: rewrite to avoid catastrophic backtracking.
- 6x Playwright URL regexes anchored; crs-k/stale-branches pinned to a commit SHA.
Mechanical cleanups: unused imports/globals/locals, empty-except comments,
import-and-import-from, implicit-string-concat, multiple-definition,
unreachable/useless-assignment.
False positives are intentionally left in place (Django settings patches,
Odoo __manifest__, side-effect imports, cross-module symbols).
Recurrence guards:
- import-linter contract (pyproject + tests/lint) locks the dev-deploy
package acyclic; a new cycle now fails CI instead of only CodeQL.
- new lint test requires every bandit (S###) `noqa` to carry a justification.
- BLE001 (blind-except) added to ruff as a ratchet (tests/roles exempt,
current production offenders grandfathered).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Conflicts resolved:
- .claude/.gitignore: union of ignore patterns
- plugins/lookup/rbac_group_path.py: drop both unused conflicting imports
(body fetches apps via lookup_loader)
- roles/sys-svc-compose/handlers/main.yml: keep HEAD's refactor (compose-up
handlers live in compose.yml, included per DEPLOYMENT_MODE)
- utils/cache/base.py: keep HEAD's merge_with_defaults/FILE_TOKENS imports
Post-merge fixes for main's newly-enabled lints:
- pyproject.toml: grandfather 5 files under the BLE001 ratchet
- justify 6 pre-existing security # noqa markers (S108/S311/S701)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the csp, domains, and networks blocks out of roles/*/meta/server.yml
into dedicated meta files, each mapping to its own top-level applications key
via the recursive meta-walk:
csp: -> meta/csp.yml -> applications.<app>.csp
domains: -> meta/domains.yml -> applications.<app>.domains
networks: -> meta/networks.yml -> applications.<app>.networks
Residual nginx/proxy keys (status_codes, client_max_body_size, locations,
ip6_enabled, config_upstream_url) stay in server.yml; an emptied server.yml is
deleted. 98 server.yml -> 249 new files, 74 deleted.
- Consumers repointed applications.<app>.server.{csp,domains,networks} ->
applications.<app>.{csp,domains,networks} across templates, vars, tasks,
lookups and filters.
- Infra readers migrated: canonical_domains_map, application_domain_index,
build_ldap_role_entries, service-registry overlay discovery,
roles_with_service, and the file-based readers utils/domains/list.py +
utils/meta/scan.py (read the now-deleted server.yml directly).
- Detection + mapping: _META_TOPICS gains csp/domains/networks;
ROLE_FILE_META_{CSP,DOMAINS,NETWORKS} constants + ROLE_FILES entries.
- New self-expiring lint guard (test_server_meta_no_split_fields) fails if a
server.yml still carries a split field; auto-fails from 2026-12-28 as a
delete reminder.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consumer lookup plugins now reach the merged config through the Ansible
lookup loader instead of importing get_merged_applications /
get_merged_domains from utils.cache. The merge stays one cached
implementation surfaced via the applications/config/domains/users lookups;
consumers call lookup_loader.get(...).run() (the compose_networks idiom).
- get_merged_applications walks the whole meta/ tree recursively
(meta/**/*.yml to applications.<app>.<path>, addons and any depth) and
folds the raw meta/volumes.yml subtree onto applications.<app>.volumes
after the templar render so the Jinja source: strings stay unrendered.
- 31 consumer lookups rerouted; providers (applications/config/
applications_current_play/users/domains) and the raw-volume accessors
(volume/nginx) keep the cache import behind # nocheck: lookup-cache-import.
- New lint test_no_cache_imports_in_lookups enforces the contract.
- nfs_flavor is now the flavor SPOT (svc-storage-nfs-server + nfs_client_src
resolve via lookup('nfs_flavor')); removed the unused STORAGE_NFS_FLAVOR
group_var. Extracted extract_default_credentials to
utils/cache/schema_credentials.py to keep applications.py under the cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve the 80 open CodeQL/code-scanning alerts on this branch.
Security-sensitive fixes:
- cli/administration/deploy/development: break the unsafe import cycle by
extracting the env/arg helpers into a leaf module `env.py`, giving the
load-time DAG compose -> common -> deps -> env.
- web-app-bluesky login-broker server.js: prototype-pollution guard on
cookie parsing, CR/LF + control-char log sanitisation, HTML-escaped
exception text, and a ReDoS-bounded handle regex.
- web-app-baserow SSO: harden the open-redirect `next` handling.
- network diagnose probes: require TLS >= 1.2.
- actionlint installer: validate tar members against path traversal.
- decidim/apt-purge lint regexes: rewrite to avoid catastrophic backtracking.
- 6x Playwright URL regexes anchored; crs-k/stale-branches pinned to a commit SHA.
Mechanical cleanups: unused imports/globals/locals, empty-except comments,
import-and-import-from, implicit-string-concat, multiple-definition,
unreachable/useless-assignment.
False positives are intentionally left in place (Django settings patches,
Odoo __manifest__, side-effect imports, cross-module symbols).
Recurrence guards:
- import-linter contract (pyproject + tests/lint) locks the dev-deploy
package acyclic; a new cycle now fails CI instead of only CodeQL.
- new lint test requires every bandit (S###) `noqa` to carry a justification.
- BLE001 (blind-except) added to ruff as a ratchet (tests/roles exempt,
current production offenders grandfathered).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the kernel-flavor swarm ENOENT: no export carried fsid=0, so the NFSv4 mount of the nested /srv/nfs/infinito-state sub-export had no pseudo-root anchor and the server returned NFS4ERR_NOENT on the PUTROOTFH+LOOKUP walk. crossmnt could not fix it (it only crosses a boundary mid-walk, never sets the starting filehandle). Kernel now exports the state dir itself at fsid=0 and clients mount the bare v4 root ':/' (zero LOOKUPs). ganesha keeps Pseudo=Path and v3 mounts the full path. The self-bind is kept so fsid=0 rides a real mount boundary (stable st_dev) under overlayfs DinD.
The NFS derivations move to per-value lookup plugins (nfs_flavor, nfs_client_src, nfs_fstype, nfs_mount_opts, nfs_state_path) backed by pure helpers in utils/storage/nfs.py with unit tests; group_vars STORAGE_NFS_* become thin lookup wrappers. The dev/act->ganesha flavor rule stays the single SPOT in the role config; nfs_flavor reads the merged value so client and server agree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>