Commit Graph
390 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Fable 5 1072ee27cc test(lint): catch argv-form container exec + mark legitimate bare-name uses
The container-exec-resolver lint only saw the inline shell form, so the
`command: argv: [container, exec, <name>]` list form (two separate lines)
slipped through, which is how checkmk's bare name reached swarm. Extend the
scanner to parse argv sequences via yaml.compose_all with node line numbers,
and annotate the nine legitimate bare-name execs (mariadb recovery container,
matomo compose-only debug, probe-gated nextcloud partner provisioning, pixelfed
runtime-resolved CID, wordpress launcher-managed discourse) with
# nocheck: container-exec-resolver.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:35:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 de95b5f240 fix(web-app-wordpress): gate the SSO env assertions behind isServiceEnabled
The playwright beforeEach required KEYCLOAK_BASE_URL, OIDC_ISSUER_URL and
the super-admin credentials unconditionally, but playwright.env.j2 sets
SUPER_ADMIN_* to "" when SSO is off, so non-SSO variants failed with
"SUPER_ADMIN_USERNAME must be set" at spec line 508. Wrap those checks
in isServiceEnabled("sso") (the same SSO_SERVICE_ENABLED flag the
template keys off), matching the dashboard role. The SSO variant still
runs them; both swarm and compose non-SSO variants were affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbachandClaude Fable 5 1aba5707f8 chore: quality fixups for the run-28721118458 fix series
Drop the now-unused TAIGA_INSTANCE_DIRECTORY (its only consumer left
with the orphaned compose-inits render), mark the two discourse
launcher execs with the container-exec-resolver nocheck (the launcher
container is named literally in both modes), and purge narration
comments from the dotenv quote tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:20:19 +02:00
kevinveenbirkenbachandClaude Fable 5 f5911e1ec4 fix(web-app-wordpress): repair discourse addon exec and gate keycloak env on sso
Three defects broke the wordpress jobs in both modes:

The wp-discourse shell tasks used folded scalars whose continuation
lines were indented deeper than the first, so YAML preserved the
newlines and the shell ran three separate commands instead of one
(--path=... reached the host shell as its own command). Align the
continuations so the fold collapses to a single line.

The discourse exec address came from container_address, which expands
to a resolve subshell under swarm; the argv-form command task passed it
verbatim to docker as a container name. Discourse is launcher-based and
its container is literally named 'discourse' in both modes, so use the
services.discourse.name config lookup instead.

The playwright env rendered keycloak credential lookups uncondition-
ally, crashing the sso-disabled variant; gate them on
lookup('service','sso').required like the sibling flags, and drop
narration comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 15:34:28 +02:00
kevinveenbirkenbachandClaude Fable 5 fa456576b6 refactor(web-app-wordpress): SPOT the ca-trust mu-plugin path
The infinito-http-ca-trust.php filename and its container path were repeated across the copy and chown tasks. Hoist WORDPRESS_CA_TRUST_MU_PLUGIN_FILE and _PATH into vars/main.yml (matching the existing CONFIG_FILE/CONFIG_PATH idiom) so the string lives once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:49 +02:00
kevinveenbirkenbachandClaude Fable 5 03ed52eea5 fix(wordpress): ship mu-plugins via exec stdin and unbreak OIDC eval fold
container cp needed the role checkout on the node running the docker
CLI, which does not exist on swarm workers; the mu-plugins now stream
through container exec -i tee with the file read controller-side. The
daggerhart WP-CLI eval line was indented deeper than its folded scalar
block, so YAML kept the newline and bash -lc received no script
argument; realigning the indent folds it back into one command.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:05:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 d4ee9a85ba refactor(compose): align templates to the service_name skeleton
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>
2026-07-04 02:36:58 +02:00
kevinveenbirkenbachandClaude Opus 4.8 96aa7784b6 feat(sys-svc-compose): scale all task timeouts via TIMEOUT_FACTOR lookup
Add a 'timeout' lookup plugin that returns round(base * TIMEOUT_FACTOR)
(group_vars default 1, so value-identical to the old literals). One knob
in group_vars stretches every wait in the stack for a slow uplink:
TIMEOUT_FACTOR: 2 doubles them all.

- plugins/lookup/timeout.py + unit tests (identity, factor scaling,
  string coercion, error paths).
- group_vars/all/17_resource.yml: TIMEOUT_FACTOR: 1.
- Convert all 34 literal 'timeout:' task keywords / module args across 25
  task+handler files to "{{ lookup('timeout', <base>) | int }}".
- tests/lint/repository/yaml/test_timeout_uses_lookup.py enforces the
  lookup in every task/handler timeout (opt-out: # nocheck: timeout-lookup).
- swarm.yml pre-deploy build handler: add the missing timeout (same
  network-build hang vector as compose.yml's Build compose).
- Drop 13 pre-existing narration comments in the touched files (comment
  lint enforces whole-file on any edited file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:57:30 +02:00
kevinveenbirkenbachandClaude Opus 4.8 674d26de3a feat(lint/role-readme): derive Credits author from meta author SPOT
Role README Credits now name galaxy_info.author (plain bold or wrapped
in a Markdown link), matched byte-for-byte against the role's own
meta/main.yml, instead of a hard-coded maintainer name. Regenerate all
245 role README Credits blocks to the new 'Implemented by ...' format.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:47:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 16eaeddad7 fix(swarm): render host-bind config files as docker configs
On swarm a 'type: bind' of a host-rendered file exists only on the node that rendered it; other nodes reject the service ('invalid mount config: bind source path does not exist') so it never converges. Convert the single-file binds to 'type: config' (swarm-distributed, content-hashed name rotates on change), mirroring the working nginx_conf sibling: moodle+wordpress msmtprc (0444), decidim allowed_hosts.rb (0444), bluesky pdsadmin (0555, executable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 03:45:59 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4752b0690e fix(web-app-wordpress): render the WP-CLI plugin command on one line
Over-indented continuation lines in the folded scalar made YAML insert a literal newline, so the shell ran 'docker exec <container>' with no command (rc 127). De-indent to fold to a single command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:28:41 +02:00
kevinveenbirkenbach 9b2f870777 Merge branch 'main' into feature/swarm-nfs 2026-07-01 05:37:36 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f02da33128 fix(ci): clear compose deploy-gate deprecation warnings + tighten comment markers
The test-deploy-compose "Fail on deprecation warnings" gate failed ~10 apps
while the deploys themselves succeeded. Sources cleared:

- svc-db-postgres: community.postgresql `db` -> `login_db` (the `db` param
  alias is deprecated, removed in collection 5.0.0)
- assert_no_deprecation_warnings.sh: ignore ansible-core's legacy filter/test
  PluginLoader-aliases deprecation (needs a collection migration with
  meta/runtime.yml, not fixable in role YAML)
- joomla/wordpress: drop the `{{ }}` delimiters from task conditionals
  (when/until/changed_when), per ansible-core 2.23

Also drop `upstream`/`compat`/`security`/`bug` from the comment-policy markers
and delete those comments from the joomla install tasks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:48:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c1c5230302 Merge branch 'main' into feature/swarm-nfs
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>
2026-06-29 22:41:39 +02:00
Alejandro Roman b85729297b Merge remote-tracking branch 'upstream/main' into feature/self-host-runners
# Conflicts:
#	cli/administration/deploy/development/down.py
2026-06-29 21:16:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 23d1447566 refactor(ansible): add path_absolute lookup, replace playbook_dir joins
Introduce plugins/lookup/path_absolute.py as the single source for the
repo-root anchor: lookup('path_absolute', 'roles/...') returns an absolute
path under playbook_dir, joining every term. Replace all hand-built
playbook_dir path constructions across .yml/.yaml and .j2 templates with it:

  [playbook_dir, '<rel>'] | path_join   -> lookup('path_absolute', '<rel>')
  playbook_dir ~ '/<rel>'               -> lookup('path_absolute', '<rel>')
  {{ playbook_dir }}/<rel>              -> lookup('path_absolute', '<rel>')

Unrelated repo_path symbols in cli/meta/roles/applications/resolution and the
single bare discover_playwright_roles(playbook_dir) call are left untouched.

Add tests/lint/ansible/test_playbook_dir_via_lookup.py: fails the build if any
of the three construction forms reappears in .yml/.yaml/.j2, pointing callers
at the lookup. Unit test covers the plugin.

Touching these files pulled their pre-existing narration comments into
test_comments_valid scope; clean them (delete restatement/banners, keep real
trip-wires as Workaround/Upstream/Invariant markers). Drop caution, important
and pitfall from the comment-lint marker vocabulary and remove the comments
that used them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:36:57 +02:00
Alejandro Roman a6b9081dcb fix(ci): jinja-safe secrets, runner count/distro + clean state, update docs 2026-06-29 11:20:29 +02:00
kevinveenbirkenbachandClaude Opus 4.8 cb1c6e2eac style(meta): normalize csp/domains list indent
Block-sequence items under canonical/connect-src reindented two spaces to the autoformat-canonical style after the server.yml domains/csp split. Whitespace-only; git diff -w is empty, parsed YAML unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 02:16:50 +02:00
kevinveenbirkenbachandClaude Opus 4.8 cddb0dc59b feat(lint): require emoji-prefixed task/step/job names + convert existing
Add tests/lint/repository/test_name_emoji_and_length.py: every Ansible task
name and GitHub Actions workflow/job/step name must start with an emoji (first
char not 0-9/A-Za-z) and be <= 80 chars.

- emit emoji names at the source: cli/build/include generator now writes
  "🔧 setup <app>" / "🚿 flush handlers after <app>" so generated
  tasks/groups/*-roles.yml satisfy the convention too
- convert existing task/step/job/workflow names to emoji-prefixed, choosing a
  task-fitting icon per action (📦 install, ✅ ensure, 🔍 probe, 🥾 bootstrap,
  🚀 deploy, 🧩 service-module load, ⬇️ checkout, 📂 include-routines, ...)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 22:45:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6ce6780575 refactor(meta): split server.yml into csp/domains/networks.yml
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>
2026-06-28 22:45:05 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fc77d9d265 fix(security): resolve all open code-scanning alerts and guard against recurrence
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>
2026-06-28 15:42:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 36ebf34b64 Merge branch 'main' into feature/swarm-nfs
Resolve 69 conflicts across ~70 files: union main's image-version bumps and new service fields (bond, resource limits) with the swarm branch's default_placement / NFS / IS_STACK_HOST config; retire the three per-deploy-type test workflows main still edited (consolidated into compose+swarm); adopt main's run-once 01_core dispatchers while re-applying each role's swarm guard. Fix the post-merge lint surfaced by main's stricter rules: bond + resource-budget keys, container-exec stack-host gating + resolver routing, leaky/dead proxy vars, suitecrm once-flag, xwiki variant nocheck, nextcloud oversized-block to gated include, matrix-mdad capped at 3g. make autoformat clean, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:13:55 +02:00
kevinveenbirkenbachandClaude Opus 4.8 deafbad941 fix(networks): widen every role local subnet from /28 to a unique /24
The per-role swarm overlay local subnet was a /28 (14 usable IPs). A role's swarm stack plus its sidecars and per-task network attachments can need more than that, which surfaces as 'node is missing network attachments, ip addresses may be exhausted' and leaves services (sso-proxy, app frontends) unresolvable mid-deploy. Re-allocate all 74 /28 roles to a unique /24 (254 usable) inside 192.168.0.0/16, chosen via the network address suggester so the assignments stay non-overlapping. Validated by tests/integration/infrastructure/networks/test_unique_valid_and_mapped.py and a full make test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 08:46:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 594109018a refactor(compose): publish every container port through the container_ports lookup
Take the host IP as an optional third element of each term
([service, protocol, host_ip]) instead of an ip= kwarg, and build the whole
ports: block from declared ports.local/public (host) + ports.internal
(container). Convert every compose template across 84 roles, folding the
remaining raw var/numeric/specific-ip mappings (including mailu's per-interface
mail ports bound to MAILU_IP4_PUBLIC). Multi-term calls render one term per line.

Add the compose-ports-must-use-container-ports lint: it forbids any raw
"<host>:<published>:<container>" publish item under a ports: block and requires
every term to declare ports.internal. Remove
tests/lint/roles/test_swarm_internal_ports.py, now subsumed.

Fix a container_depends_on rendering regression. The lookup self-indented
(default indent=4), so the added 4-space placement + | indent(4) triple-indented
the block. Flip the lookup default to column 0 (consistent with
container_networks), make every call use the 4-space + | indent(4) form, and add
a lint enforcing it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c8a1ca59d refactor(compose): source published container ports from ports.internal
Rewrite every hardcoded literal container port in a compose.yml.j2 publish
mapping (- "<host>:<...ports.local|public.kind...>:<LITERAL>") to read the
declared ports.internal.<kind> via lookup, across 22 roles, so the published
(local/public) port and the container (internal) port are a single declared
source of truth. Mappings whose container already references the lookup or a
variable are left as-is.

This surfaced and fixed a real swarm bug: web-app-openproject declared
ports.internal.http: 8080 while the container actually listens on 80, so the
swarm resolve_upstream pointed openproject at the wrong port. Reconciled to 80
(the mapping's real container port).

Extend tests/lint/roles/test_swarm_internal_ports.py with a second check: a
published mapping's container segment must come from ports.internal via a lookup
(or a variable referencing it), never a hardcoded literal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:30:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bd2d652e83 fix(wordpress): trust the injected internal CA via env in the WP HTTP API
The earlier mu-plugin pointed http_request_args['sslcertificates'] at the OS bundle (/etc/ssl/certs/ca-certificates.crt), which does NOT carry the internal root CA unless update-ca-certificates ran in the container — so the OIDC token exchange to the self-signed internal Keycloak still failed with 'cURL error 60: SSL certificate problem' in CI run 28044348006. Read the env-provided bundle first (CURL_CA_BUNDLE / CA_TRUST_CERT / SSL_CERT_FILE), which sys-svc-compose-ca injects into every container pointing at the bind-mounted /tmp/infinito/ca/root-ca.crt (compose_ca.py), falling back to the OS bundle.

Verified chain: daggerhart-openid-connect-generic issues its token/userinfo requests via wp_remote_post/wp_remote_get (the WP HTTP API), so the http_request_args filter applies and WP's curl uses CURLOPT_CAINFO = the internal root CA, which verifies the Keycloak cert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 01:22:51 +02:00
kevinveenbirkenbachandClaude Opus 4.8 117d246a32 fix(wordpress): trust the internal CA in the WP HTTP API for OIDC back-channel
The OIDC login redirected to Keycloak and back with a valid code, but the server-side token exchange (daggerhart-openid-connect-generic wp_remote_post to the self-signed internal Keycloak) failed with 'cURL error 60: SSL certificate problem: unable to get local issuer certificate' (60x in CI run 28018750880), so the user was never logged in and never redirected to /wp-admin. WordPress's HTTP API (WpOrg\Requests) pins CURLOPT_CAINFO to its own bundled wp-includes/certificates/ca-bundle.crt and ignores the OS trust store and the CURL_CA_BUNDLE/SSL_CERT_FILE env vars, so the earlier ca-certificates Dockerfile change had no effect.

Add a mu-plugin that filters http_request_args to set sslcertificates to the OS CA bundle (/etc/ssl/certs/ca-certificates.crt), which the compose CA-injection populates with the internal root CA, so wp_remote_* TLS verifies the internal cert. Not a green-wash: no assertion weakened, no_sslverify left false (verification stays ON), the test still requires a real OIDC round-trip to /wp-admin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 19:23:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9829342483 fix(wordpress): install ca-certificates so the CA-trust wrapper can trust the internal CA
The wordpress (php-apache/Debian) image lacks ca-certificates, so the sys-ca-selfsigned with-ca-trust.sh wrapper's update-ca-certificates path is skipped and the internal self-signed CA never lands in /etc/ssl/certs/ca-certificates.crt — the bundle PHP libcurl verifies against. The daggerhart OIDC plugin then fails server-side TLS with 'cURL error 60: unable to get local issuer certificate' (confirmed in the Playwright artifact). Add ca-certificates so update-ca-certificates runs and the CA is trusted. compose_ca.py alone is insufficient because PHP libcurl uses the OS bundle, not the CURL_CA_BUNDLE/SSL_CERT_FILE env vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:17:58 +02:00
kevinveenbirkenbachandClaude Opus 4.8 3fa781004d feat(roles): add bond integration-importance factor to shared services
`bond` is a 0..1 factor for how strongly a service is tied to the overall
integration: bond: 1 = always required, bond: 0 = omittable. Groundwork for
tooling that later scores service importance and decides what can be dropped.
Named `bond` (not `bound`) to avoid clashing with the resource bounds/limits.

Add test_service_bond.py (lint): every enabled + shared service must declare a
valid `bond` (number in 0..1). Seed `bond: 1` as a placeholder on all 638
enabled+shared service entries across 88 roles, to be refined later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 51fbe1a52a test(web-app-wordpress): wp-discourse spec asserts the WordPress->Discourse partner coupling
Replace the settings-page check with the real bridge: the wp-discourse connection
tab carries the partner Discourse base URL (distinct host), and the provisioned
Discourse API credentials authenticate against the partner instance
(/session/current.json resolves the publishing user on the Discourse host).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.8 dbb4f5f96f test(web-app-wordpress): infinito-oidc-rbac-mapper spec asserts the OIDC bridge to Keycloak
Replace the users-list-renders check with the real cross-partner bridge: wp-login
redirects to the Keycloak partner authorize endpoint (distinct host) with
response_type=code, the provisioned client_id and a scope carrying the groups claim
the RBAC mapper consumes, then lands on the wp-admin role-mapping surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c01c9331db test(web-app-wordpress): activitypub spec asserts WordPress is a discoverable Fediverse actor
Replace the wp-admin settings-page check with a real federation probe: WebFinger
resolves the author acct to a rel=self ActivityStreams actor, which is fetched and
asserted to carry the AS @context, an actor type, id, inbox/outbox on this host, and
an RSA publicKey — the surface remote Fediverse partners federate with.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.8 70786aa126 refactor(addons): emit playwright addon gate flags via addon_env_flags lookup
Replace the per-template Jinja loop (which mis-unpacked dict2items and rendered every flag as KEY_ADDON_ENABLED=false, silently skipping every addon spec) with a single lookup('addon_env_flags', application_id). The plugin is the single source of truth for the <ADDON_ID>_ADDON_ENABLED flags consumed by tests/addon-gating.js: env-key derivation matches addon-gating.js exactly, and a flag is true only when the addon is both enabled AND required (required:false addons skip). Switch all six role templates to the lookup, extend the env-file schema lint to recognise whole-line {{ ... }} expressions, and add the plugin unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:03:06 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8e92f15f6a test(addons): require a Playwright spec for every addon; add 87 per-addon specs
Add tests/lint/ansible/services/test_addons_playwright_spec.py requiring roles/<role>/files/playwright/addons/<id>.spec.js for EVERY addon of EVERY role — no mechanism auto-exempt and no # nocheck escape. Ship the 87 per-addon specs (gated via skipUnlessAddonEnabled so they skip cleanly when the addon is disabled in the current variant), the test-e2e-playwright addon-gating harness (addon-gating.js, playwright.config.js, recursive 02_run_one.yml), the shared odoo helper, and the ADDON_<ID>_ENABLED flags in the six playwright.env.j2 templates. Relax test_env_file_schema to skip Jinja-templated keys in the ADDON loop. Doc updates (suppression.md, requirements/026) follow in a separate commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 00:16:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9a4701370a refactor(addons): move wordpress addon tasks to tasks/addons; remove bogus xwiki matomo addon
Relocate the wordpress per-addon task files from tasks/plugins/ to tasks/addons/ (pure renames) and repoint tasks/main.yml, giving every role a uniform tasks/addons/ layout. Remove roles/web-app-xwiki/meta/addons/matomo.yml: matomo is provided as a service via services.matomo, not an Extension-Manager extension, and the org.xwiki.contrib:matomo extension does not exist in the nexus catalog (install plan failed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 00:16:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 51c3d41ede Merge origin/main into feature/swarm-nfs
Reconcile main's object-store feature and pre-swarm deploy tasks with the branch's compose/networks/volumes SPOT and swarm lints.

objstore (seaweedfs/minio): standalone compose + embedded service templates migrated to the container_image/container_volumes/container_networks/compose_volumes/compose_networks/depends_on lookups; meta/volumes.yml on the canonical dict schema with s3.json and nginx.conf as type:config; embedded services made swarm-compatible via compose_only/compose_restart.

deploy tasks (erpnext, opentalk, seaweedfs, shopware, snipe-it, wordpress, yourls): container exec routed through the container_address resolver and gated on IS_STACK_HOST, shell module plus stack_host_template, no chdir or compose-verb forms.

Also removed orphaned vars, restored MAILU_INSTANCE_DIR, freed the seaweedfs subnet (192.168.206.0/24), stripped literal quotes from the nextcloud/bookwyrm env templates, and routed main's objstore compose_volumes tests through dir_var_lib.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:08:49 +02:00
kevinveenbirkenbachandClaude Opus 4.8 25f286d2bf feat(swarm): per-service internal ports and entity-named services
Give each reverse-proxied app a swarm-resolvable upstream: add services.<entity>.ports.internal.http to the role metadata and rename the main compose service from the generic 'application' to the entity name, so its short DNS alias on the shared overlay is unique. Thread the service name through compose, vars (*_SERVICE_NAME), volume mounts and the oauth2 origin host. Consolidate the split image/ports blocks in bridgy-fed and postmarks into the entity block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 226d5b5366 feat(wordpress): migrate plugins to meta/addons (req 026)
plugin/mu_plugin addons declared; daggerhart OIDC + rbac-mapper bridge sso, wp-discourse bridges discourse. Settings absorbed from vars/oidc.yml + vars/discourse.yml (deleted); wp-discourse task uses include_vars for the runtime vault key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1783799e40 refactor(web-app-wordpress): extract the WP Discourse API-key revoke rails-runner Ruby to files/ruby/revoke_wp_discourse_api_keys.rb fed via file lookup
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:41 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1ed0afcd61 test(objstore): require seaweedfs for database-backed web-app roles
Add a meta lint asserting every web-app role with postgres or mariadb declares a seaweedfs object-store service, exemptable via the seaweedfs-required nocheck marker. Mark the 33 database-backed roles that do not use object storage, and register the rule in the suppression catalog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:33 +02:00
kevinveenbirkenbachandClaude Opus 4.7 cb27ff3756 refactor(networks): replace Jinja includes with compose_networks/container_networks lookup plugins
Convert the two registry-driven networks templates into pure-Python
rendering functions plus thin Ansible lookup plugins:

  roles/sys-svc-compose/templates/networks.yml.j2  -> {{ lookup('compose_networks') }}
  roles/sys-svc-container/templates/networks.yml.j2 -> {{ lookup('container_networks') | indent(4) }}

The rendering logic lives in utils/networks/render.py as pure-Python
functions that take a registry + closure-style config/database lookups,
making the logic directly unit-testable without a Jinja stub harness.
The lookup plugins are thin wrappers that resolve application_id,
DEPLOYMENT_MODE, and swarm.network.encryption from the templar, build
the service_registry, and forward to the render functions.

Migrate 233 include lines across 85 production templates. Convert
web-app-matrix's hand-coded MDAD networks block (default + conditional
postgres + conditional openldap) to the generic lookup; this adds an
openresty attachment when services.sso.enabled (harmless extra
interface, pinned in test_matrix_mdad_consumer_attaches_postgres_ldap_sso).

Mark local-DB / redis sidecars (sys-svc-rdbms/templates/services/,
svc-db-redis/templates/service.yml.j2) with `# nocheck: networks-literal`
because they intentionally attach to default-only.

Extend the lint to scan ALL roles/*/templates/**/*.yml.j2 (not only
compose.yml.j2) and to forbid the legacy include form; the marker
above is the documented escape hatch.

Coverage:
  - tests/unit/utils/networks/test_render.py (30 cases incl. encrypted=false literal pin)
  - tests/unit/plugins/lookup/test_compose_networks.py (5 cases incl. lookup_loader arg-schema)
  - tests/unit/plugins/lookup/test_container_networks.py (5 cases incl. lookup_loader arg-schema)
  - tests/integration/infrastructure/compose/test_networks_render.py (10 snapshot scenarios)
  - tests/integration/infrastructure/compose/test_network_includes.py (3 lint rules)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 18:42:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d612b98ff5 fix(images): correct service_keys + decidim double-tag
Adversarial audit of a98357418 surfaced five roles whose container_image
lookup passed a service_key that does not exist in the role's
meta/services.yml, plus one role producing a double-tagged ref.

* baserow / listmonk / mobilizon: service_key 'application' is the
  compose-internal service name, not the meta key. Correct to the
  entity-name key ('baserow', 'listmonk', 'mobilizon').
* wordpress: same fix with custom=True preserved.
* web-svc-xmpp: 'ejabberd' is the image name, not the meta service
  entry. The meta key is 'xmpp'; the image lookup returns
  'ejabberd/ecs:latest' as before, but via the correct service entry.
* web-app-decidim: DECIDIM_CUSTOM_IMAGE already embeds the tag
  ('<image>:<version>-infinito'), so passing it as image= AND version=
  produced 'image:tag:tag'. Drop the override and pass
  version=DECIDIM_VERSION ~ '-infinito' so the lookup composes the ref
  cleanly. Remove the now-orphan DECIDIM_CUSTOM_IMAGE var.

Empirically verified all five service_keys resolve via
get_merged_applications. Without these fixes every deploy that
includes any of these roles would have raised
'container_image: service ... missing in ...' at compose render time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0ffbb73d72 refactor(images): central container_image lookup + migrate all roles
Add plugins/lookup/container_image.py: returns the full `image: "<ref>"`
line so templates drop the redundant image: prefix and surrounding
quotes. Supports `custom=True` to auto-construct `<entity_name>_custom`
for locally-built images, plus explicit `image=`/`version=` overrides
for exotic cases (taiga back-auth/front-oidc variants, decidim infinito
suffix, etc.).

* 25 unit tests for the lookup cover compose vs swarm prefix, custom
  flag, overrides, tag_only=True, missing version warnings.
* New strict lint (tests/lint/.../test_no_raw_image_in_compose_templates)
  fails on any raw `image:` line in roles/*/templates/*.j2 unless
  nocheck-suppressed. Legacy `image: "{{ lookup(...) }}"` form no
  longer accepted.
* Migrate 159 image refs across ~80 roles to the standalone
  `{{ lookup('container_image', application_id, '<service_key>') }}`
  form. 26 roles use `custom=True` (mariadb, postgres, openldap,
  jenkins, jira, gitlab, nextcloud, mediawiki, shopware, wordpress,
  odoo, joomla, openproject, bookwyrm, espocrm, hugo, littlejs,
  fediwall, roulette-wheel, openresty, confluence and more).
* Fix svc-db-redis application_id leak: service.yml.j2 is included
  into consuming roles where application_id evaluates to the outer
  role, so the image lookup needs the hardcoded 'svc-db-redis'.
* Integration tests (test_templates_require_image,
  test_build_requires_image) recognise the standalone lookup as an
  implicit `image:` at the same indent.
* Mediawiki swarm tasks: add explicit `:{{ MEDIAWIKI_VERSION }}` to
  the registry-prefixed image string because the custom var is now
  bare.

Only 3 legitimate exotic nocheck-suppressed image lines remain:
matrix-mdad runner (nested services.matrix.runner.image path),
matrix mautrix bridges (per-iteration dynamic) and svc-registry-docker
(chicken-and-egg self-reference).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 00:17:11 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8bded8fdae refactor(web-app-wordpress): extract the WP Discourse API-key revoke rails-runner Ruby to files/ruby/revoke_wp_discourse_api_keys.rb fed via file lookup
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 02:42:48 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a212ed9df2 refactor(roles): swarm-aware exec routing in 7 roles
Replace compose-mode-specific `{{ BIN_COMPOSE }} exec ... {{ SERVICE }}`
+ `args.chdir: lookup('container', ..., 'directories.instance')`
invocations with `container exec -i {{ ..._CONTAINER_ADDRESS | quote }}`
(plus `--workdir` flag where chdir was load-bearing). The container
exec wrapper resolves the actual runtime container name on both
compose (compose-suffixed name) and swarm (stack-task-id-suffixed
name) via the `container_address` lookup, so the task no longer
assumes the compose-mode service-name address.

Pure-syntax adaptation only — no behavioural change, same register /
when / failed_when / no_log semantics preserved on every task.

Roles touched (12 task files):
- web-app-espocrm (1)
- web-app-friendica (2)
- web-app-listmonk (1)
- web-app-openproject (2)
- web-app-snipe-it (3)
- web-app-taiga (1)
- web-app-wordpress (2)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 19:08:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6e49fe74fb feat(volumes): canonical dict-of-dicts meta schema + lookup('volume')
Migrate every role's meta/volumes.yml from ad-hoc dict-of-named-volumes
to a canonical dict-of-dicts schema where the YAML key is the semantic
short name and the entry body carries type / name / source / mounts /
nfs / mode / read_only. The optional 'name:' field is the docker volume
name (defaults to the key); the previous 'docker_name:' field is gone.

What's in this commit (210 paths):

Infrastructure (23)
- New filter plugins/filter/{compose,container}_volumes.py — render the
  top-level volumes:/configs:/secrets: section and per-service mount
  blocks from the canonical meta. container_volumes uses the SEMANTIC
  name as per-service mount source so it resolves against compose_volumes'
  top-level dict key (docker_name as source produced 'undefined volume'
  on swarm).
- New lookup plugins/lookup/{compose,container,volume}_volumes.py.
  volume(application_id, name) returns a normalised single entry;
  volume(application_id) returns the whole canonical dict.
- utils/cache/applications.py keeps a sibling registry
  _CANONICAL_VOLUMES_BY_ROLE outside the merged applications payload so
  raw Jinja source: strings never enter the templar walk (root-cause of
  the 200x cache-miss + recursive get_merged_applications storm).
- utils/roles/applications/mounts.py — schema validator + normaliser;
  list-shape is rejected.
- plugins/lookup/nginx.py: _volume_path_by_name reads canonical directly.
- scripts/tests/deploy/swarm/_context.sh: NFS_VOLUMES extraction handles
  both legacy dict and canonical dict shapes.
- roles/svc-prx-openresty/handlers/restart_and_validate.yml: gate
  compose-only validate+reload behind a single when-block; swarm uses
  service-update instead (bind-mounts of a FILE pin source inode at
  task-create time, so atomic write-tmp+rename leaves the running task
  on a deleted inode).
- roles/sys-svc-compose/tasks/utils/nfs_prep.yml: switch to
  lookup('volume', application_id) | dict2items so NFS subdir
  pre-creation works under the new cache split.

Tests (12)
- New strict lints flagging inline mount sections in compose templates,
  single-file bind mounts, and legacy lookup('config', ..., 'volumes.X.Y')
  call sites — all volume-mount surfaces MUST flow through container_volumes
  / compose_volumes / volume lookups.
- Integration: test_volumes_compose_chain + test_volume_constants_are_lookups
  accept both the legacy and the new lookup('volume', app, 'X').name forms
  during the migration window.
- Unit tests for the new lookup, filter, mounts helper, plus updated
  nginx lookup test.
- Prometheus role-wiring test reads canonical meta for the
  alert_rules.yml bind mount.

Role migrations (175)
- 62 meta/volumes.yml converted to dict-of-dicts canonical shape
- 63 templates/compose*.yml.j2 replace inline volumes:/configs:/secrets:
  with lookup('container_volumes', application_id, '<service>') |
  indent(N) per service + a single trailing lookup('compose_volumes',
  application_id) for the top-level block
- 50 vars/main.yml drop dead volume constants and refactor 23 remaining
  legacy lookup('config', ..., 'volumes.X.Y') to lookup('volume',
  application_id, 'X').Y across 9 roles

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 19:08:46 +02:00
kevinveenbirkenbach 3a74897a19 fix(env-templates): make dotenv_quote swarm-aware (compose strips, swarm preserves)
docker stack deploy passes env-file values verbatim, including outer
double quotes - the container reads KEY="value" instead of KEY=value
and DB auth, URL parsers, and int(os.environ['KEY']) consumers break.
Compose keeps stripping the quotes as before.

- plugins/filter/dotenv.py: read DEPLOYMENT_MODE from the render
  context via @jinja2.pass_context. compose mode keeps the old quoting
  (escape $$, \, "). swarm mode passes the value through verbatim.
  Missing or unknown mode falls back to compose quoting so a typo
  never silently leaks unquoted values into compose. Direct Python
  callers without a Jinja context keep the compose-style default.
- tests/unit/plugins/filter/test_dotenv_quote.py: keep the 12
  direct-call cases, add 8 TestDotenvQuoteModeAware cases rendering
  the filter through a real jinja2.Environment.
- 4 OIDC_TITLE templates (bookwyrm, confluence, jira, pretix): replace
  the manual "{{ X | replace('\"','\\\"') }}" Compose-only escape
  with {{ X | dotenv_quote }}.
- 3 env.j2 templates (nextcloud, taiga, wordpress): drop literal
  outer quotes that were Compose-only decoration; keep | dotenv_quote
  on the password / token assignments.
- sys-svc-rdbms/templates/env/mariadb.env.j2: strip literal quotes
  unconditionally - the file name is not env.j2 so the dotenv-quote
  integration test does not require the filter and the generated DB
  passwords are alphanumeric.
2026-06-14 19:08:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e7460890d9 refactor(tasks): drop redundant compose-instance chdir from container exec tasks
Across 12 task files (gitea x5, shopware x5, wordpress install, mattermost
setup), strip the leftover `chdir: "{{ lookup('container', application_id,
'directories.instance') }}"` arg from shell tasks that already drive the
container via `container exec` through the wrapper. The chdir points at
the compose project root, which is only useful when calling docker-compose
CLI verbs. With `container exec` the cwd is the container's own working
directory; the host-side chdir does nothing. Empty `args:` blocks left
behind by the removal are collapsed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 19:08:45 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4f681d9c27 refactor(web-app-wordpress): plugin ops via container wrapper + IS_STACK_HOST gate
Replace BIN_COMPOSE exec + instance-dir chdir with container exec
against WORDPRESS_CONTAINER_ADDRESS so plugin install/configure
tasks resolve correctly under swarm. Gate plugin block tasks on
IS_STACK_HOST to keep them single-execution. Drop the blanket
failed_when:false on the OIDC option-add task (the || in the shell
script already handles the idempotent case).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 19:08:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bc075906d7 test(objstore): require seaweedfs for database-backed web-app roles
Add a meta lint asserting every web-app role with postgres or mariadb declares a seaweedfs object-store service, exemptable via the seaweedfs-required nocheck marker. Mark the 33 database-backed roles that do not use object storage, and register the rule in the suppression catalog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:00:13 +02:00