Commit Graph
49 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 c35f2409a4 fix(web-app-odoo): make the OAuth addon work on Odoo 20
Nobody could sign in to Odoo through Keycloak. Odoo sent an http redirect
URI, and Keycloak refused it as "Invalid parameter: redirect_uri".

The addon auth_oauth_https builds that URI from web.base.url. Two things
kept it from doing so after the image moved to Odoo 20.

Its manifest declared version 19.0.1.8.0, and Odoo marks a module whose
version names another series as not installable. It only warns about that,
so the install task stayed green and the addon was missing. The manifest
now declares 1.8.0, which Odoo prefixes with its own series.

Once installed, the addon read the parameter with get_param, which Odoo 20
replaced by typed getters. The call raised AttributeError, a broad except
logged it and fell back to the URL of the request, which is http behind
the proxy. The addon reads the parameter with get_str now and no longer
catches the error.

A deploy now fails when the addon is not installed afterwards. The check
reads ir_module_module, because the output of "odoo -i" does not tell an
installed module from a skipped one.

The spec logged out by navigating to /web/session/logout. Odoo 20 accepts
POST only on that route and answered 405, so both SSO tests failed after
a successful login. The spec logs out through the user menu now.

A host that runs the suite with several workers still failed it. Odoo
computes the session token of an account from its OAuth access token, so
every new sign-in ends the other sessions of that account, and the spec
files share the administrator. The role caps its suite at one worker
with PLAYWRIGHT_MAX_WORKERS, which the central config now honours, and
the spec asserts the cap.

Closes #822

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 11:37:41 +02:00
kevinveenbirkenbachandClaude Opus 5 b3ce43a596 fix(security): require no_log on every task that carries a credential
At -vvv Ansible prints each module argument and result, so a password in
a command line, a request body, a header or a fact lands in the deploy
log. Nothing enforced masking outside the MCP task files, and no check
looked at parents at all.

A new lint, test_credential_tasks_are_masked, finds tasks whose printed
parts (module arguments, environment, loop) reference an identifier ending
in password, secret, token, api_key or private_key, or
secrets.credentials. A task passes when it or an enclosing block sets
no_log, when every file that pulls it in does so through include_tasks
with apply.no_log or import_tasks with no_log (transitively), when the
secret only reaches a parameter the module masks itself, or when the
module masks its values. no_log on an include_tasks task itself does not
count, because it does not reach the included tasks. The rule can be
suppressed per task with `# nocheck: credential-masking`, now listed in
the suppression catalog, and the lint tests its parent and suppression
logic on synthetic cases.

It found 39 unmasked tasks in 19 roles, all real: join tokens, client
secrets, API tokens in Cloudflare headers, database passwords in
command lines, the ChatGPT bot login body and more. Each now sets
no_log from MASK_CREDENTIALS_IN_LOGS. The sys-dns-hetzner-rdns alias
hetzner_no_log, which only restated that switch, is gone.

Not covered yet: template and copy tasks whose rendered file carries a
secret print it as a diff under --diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 21:52:18 +02:00
kevinveenbirkenbachandClaude Opus 5 575a5a87ac refactor(layout): sort shipped scripts by language and split the unit suite
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).

This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.

Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:

  * 7 `src:` values built from a role variable holding a bare filename
    (svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
    The variable doubles as the destination name inside the container, so the
    language segment belongs at the call site, not in the variable.
  * `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
    through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
    18 roles ship exactly that and have no template fallback: the play would
    have died on each.
  * `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
    interface now reads `files/javascript/javascript.js`.
  * `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
    path through `path_absolute(... 'files', ...)`.
  * 4 `ansible.builtin.script:` invocations naming a bare command
    (pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
  * `dev-python/files/shell/install.sh` pointed at a sibling role's script.

Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:31:09 +02:00
kevinveenbirkenbachandClaude Opus 5 a46348b6cf refactor(handlers): bind notify to a listen topic, not to a name
A handler name was doing two jobs at once: the line an operator reads in the
play output, and the identifier a `notify:` binds to. The two pull in opposite
directions -- the log wants "restart the SSH service, with diagnostics", the
binding wants a token that survives quoting, YAML flow style and a shell
round-trip -- and a name that changes for readability silently changes which
handler runs.

Every handler that a notify reached by name now carries a `listen:` with that
exact wording, so the binding is explicit before anything is renamed. The
topics themselves become identifiers: `compose up` reads `compose-up`,
`Restart NFS ganesha` reads `restart-nfs-ganesha`, 22 of them across both
sides of every notify. Where the topic is assembled at runtime, both branches
moved together: the conditional in sys-service carries two literals, and the
LDAP renderer interpolates `import-{{ folder }}-ldif-files`, which is what
makes those handlers reachable at all.

Freed from the binding, the names take the emoji prefix every other task line
already has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 03:38:42 +02:00
kevinveenbirkenbachandClaude Opus 5 673e994aa7 feat(lint): pin type and resolver on every container inspect
An untyped inspect searches every docker object kind, and a role's overlay network carries the same entity name as its main container, so the call can resolve the network and report a missing State key instead of naming the container. That is how the openresty assert died on every swarm job of run 30709070050.

Extend container-exec-resolver to cover inspect, stripping raw markers and quoted spans first so the flag walker survives Go template format strings. Add container-inspect-type, scoped to roles/ because the collision follows the entity naming convention and the outer act namespace carries no role networks. Sweep 40 call sites; suppress the delegate_to-resolved CIDs and the compose co-location probes with a stated reason.

Move the five container rules into tests/lint/ansible/shell/container/ and drop the container_ prefix from their filenames. Register both markers in the suppression docs, where the resolver marker was missing entirely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:51:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4039313266 refactor(tasks): enforce and apply a single task-file numbering scheme
Add tests/lint/repository/test_task_file_numbering.py: numbered task
files use exactly two leading digits, a numbered *_core.yml must be
00_core.yml, per directory the numbers are unique/consecutive and start
at 01 (00 with a core file), numbered includes appear in ascending
order, and shared helpers live in tasks/utils/ instead of the numbered
chain.

Apply it across every role: rename NN_core.yml to 00_core.yml with a
consecutive down-shift of the siblings, move letter-suffix children into
a subdirectory named after their parent task, relocate helpers included
from more than one place into tasks/utils/, and update every reference
(include_tasks, path_absolute, .ansible-lint-ignore, requirement docs).
Legacy narration comments in the touched files are deleted; the upstream
xwiki.properties reference block opts out via nocheck. The unused
MAILU_IP6_PUBLIC var (only ever named in now-removed comments) is dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:01:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 0b11346977 fix(odoo): drain to zero replicas for the swarm offline update
A forced rolling update keeps N-1 replicas' ORM sessions alive while the
replacement runs -u all, deadlocking on AccessExclusiveLock. Scale the
service to 0, wait until no running task remains, then scale back to the
topology replica count so the entrypoint's advisory-lock winner is the
sole database actor during the update window.

Renumber the task files to the dispatcher convention: 02_manager_ops
keeps its repo-wide name, its children follow as 03-07 in execution
order, and sub-steps move into a subdirectory named after their parent
task instead of letter suffixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:01:31 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e99d76e490 fix(web-app-odoo): run -u all offline in the entrypoint to end the migration deadlock
The upgrade ran via `container exec` into a live-serving odoo: serving backends hold RowShareLock while the migration DDL wants AccessExclusiveLock, so `-u all` and the workers deadlock. The install task now pauses crons, touches a marker and restarts; the entrypoint runs `-u all --stop-after-init` as the sole DB actor under a pg advisory lock, marker-gated, before serving. The marker path lives in one source (vars/main.yml), fed to the container env and the touch task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 05:45:26 +02:00
kevinveenbirkenbachandClaude Fable 5 40e553069d refactor(structure): nest prefix-grouped sibling files into subfolders
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>
2026-07-14 20:00:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fc0799883f refactor(structure): move prefix-grouped files into named subfolders
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>
2026-07-14 20:00:03 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b859afa625 fix(odoo): pause crons around the module install/update window
odoo -u all writes every ir_cron row while loading base data; live cron workers (N replicas x max_cron_threads) hold FOR NO KEY UPDATE row locks for the whole job, so the NOWAIT probe aborted with 'Record cannot be modified right now'. Snapshot active crons into infinito_ir_cron_pause and deactivate them (the UPDATE drains in-flight jobs), run all module operations inside a block whose always leg resumes from the snapshot (crash-safe: a killed run leaves the table and the next pause reuses it), and drop the redundant -u core update that ran outside the window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:37:26 +02:00
kevinveenbirkenbachandClaude Fable 5 e5a844cabb fix(web-app-odoo): run module ops on the resolved node and stop masking failures
Every `container exec {{ ODOO_CONTAINER_ADDRESS }}` (DB init, module install,
update) ran on the manager against the bare compose name. Under swarm the odoo
task sits on a worker, so the manager-local exec silently no-op'd behind
failed_when: false and auth_oauth was never installed, surfacing later as
"relation auth_oauth_provider does not exist". Resolve the hosting node + local
container id, delegate the execs there, drop failed_when: false in favour of
real error detection (rc + 'invalid module names'), and make the /web/health
wait SIGPIPE-safe with a per-attempt timeout. Restarts split into compose
(container restart) and swarm (service update --force); DB init extracted to
01a_bootstrap_schema.yml so the parent block stays within the size limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:30 +02:00
kevinveenbirkenbachandClaude Fable 5 913455c139 fix(web-app-odoo): initialize the pinned database on first boot
The odoo config pins db_name with list_db=False, so on an empty schema
odoo preloads that database at boot, crashes (KeyError 'ir.http',
relation ir_module_module does not exist) and never binds its port; the
swarm converge wait then deadlocks because the healthcheck can never
pass and 01_init_database only runs after convergence.

Wrap the image entrypoint: probe the schema via psycopg2 against the
mounted odoo.conf, install the core modules once when absent, then hand
off to the stock entrypoint so the server boots healthy on the first
attempt. Proven live: a container against a fresh postgres installs
base and serves /web/health 200. Compose is idempotent (schema present
-> skip). The entrypoint file/paths come from a single ODOO_ENTRYPOINT_
SCRIPT_FILE SPOT (host abs/rel + docker dest), built via the same
build-arg pattern as web-app-suitecrm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:41:43 +02:00
kevinveenbirkenbach 9ee4cd0fb9 Merge branch 'main' of github.com:infinito-nexus/core into feature/swarm-nfs
# Conflicts:
#	docs/agents/action/iteration/compose.md
#	roles/svc-db-openldap/vars/main.yml
#	roles/svc-runner/defaults/main.yml
#	roles/svc-runner/tasks/02_cleanup_instance.yml
#	roles/web-app-discourse/meta/server.yml
#	roles/web-app-mailu/tasks/01_core.yml
#	roles/web-app-odoo/tasks/main.yml
2026-07-04 18:16:12 +02:00
kevinveenbirkenbachandClaude Opus 4.8 02e7584f94 fix(web-app-odoo): restart after auth provisioning so workers drop stale config
The OIDC/LDAP provisioning writes ir.config_parameter rows (web.base.url,
its freeze flag, signup scope) via direct SQL, which the four running
workers never observe: get_param() is ormcached per worker, so a worker
that auto-detected its base url keeps building OAuth redirect_uris that
Keycloak rejects ("Invalid parameter: redirect_uri") while other workers
serve correct links — the partial addon-spec failures in CI. Restart the
container once after provisioning so every worker boots with the frozen
values, and give the web client the same 120s first-render budget that
openModule already documents for cold asset compilation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:22:12 +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 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 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 ffac55c1d5 fix(proxy): stop cross-role webserver_* var leak (config fields + task-scoping + front-proxy reset + lint)
A role's vars/main.yml is auto-loaded into play scope and persists, so proxy
override vars (webserver_*) leaked into the central front-proxy render of OTHER
roles (re-evaluated against the wrong application_id) — silently applying one
role's value elsewhere, or hard-failing when the lazy lookup hit a role lacking
the key (the seaweedfs<->matrix client_max_body_size crash).

- Templates (sys-svc-proxy html/upload/ws_generic, matrix synapse homeserver)
  resolve client_max_body_size from the role's own server config
  (lookup('config', application_id, 'server.client_max_body_size', '100m')).
- Move every role-level webserver_* out of vars/main.yml into task-scoped
  include params (9 roles); they apply only to that role's proxy and can't leak.
- sys-stk-front-proxy resets webserver_* to safe defaults via task-scoped vars
  on the render tasks, so a leaked value from any other source is neutralized;
  a role's own include params (#20) still override the reset (precedence-tested,
  direct + nested).
- Add test_no_leaky_proxy_role_vars lint forbidding webserver_* in vars/main.yml
  (suppressable via '# nocheck: leaky-proxy-var').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 06:03:18 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c61c68e903 test(lint): forbid >3-child blocks + ≥3-task same-when runs; refactor 5 roles
Adds two structural ansible lints under tests/lint/ansible/structure/:

- ansible-block-oversized: forbid `- block:` constructs with more than
  3 direct child tasks. Big block bodies hide intent, accumulate
  unrelated guards on the wrapper, and signal that the body wants to
  be its own include_tasks sub-file. Extraction keeps the parent
  scannable; the sub-file MAY carry the `# include-gated:
  when: …` header marker so swarm-compat lints exempt it.

- ansible-repeated-when-run: forbid 3+ consecutive sibling tasks
  carrying the SAME `when:` expression. Copy-pasted guards drift,
  silently disable each other over time, and read worse than a single
  `- name: …` + `when: <expr>` + `block:` wrapper. Per-line opt-out
  for runs where wrapping would alter semantics (cross-task register
  dependencies).

Both lints use the line-based scanner pattern from existing
tests/lint/ansible/jinja/* + `is_suppressed_at` from
utils.annotations.suppress for the per-line nocheck markers.

Refactors five roles into compliance:

- svc-prx-openresty/tasks/02_assert.yml: heavy rescue body extracted
  to 02a_assert_rescue_debug.yml.
- test-e2e-playwright/tasks/01_setup.yml: 3 install-needed siblings
  wrapped in a single when-block.
- web-app-erpnext/tasks/02_bench_bootstrap.yml: 3 site-probe-failed
  siblings wrapped likewise.
- web-app-odoo/tasks/02_install_modules.yml: 3 OIDC-enabled siblings
  wrapped. tasks/03_oidc.yml: dropped redundant single-task wrapper
  block (the original wrapper carried name only and would have
  required extraction for size — inlining the 23 tasks at file root
  keeps the file purpose-narrow and removes one layer of indirection).
- web-app-xwiki/tasks/04_extensions.yml: large block body + rescue
  body both extracted to 04a_extensions_rescue.yml +
  04b_extensions_execute.yml.

Mixed-scope offenders (web-app-keycloak/tasks/{02_manager_ops,05a,
assert/05_ldap,update/05_ldap_shared,02a} and
web-app-matomo/tasks/{03_manager_ops,05_bootstrap,05a}) deliberately
stay unstaged; they ship with their respective swarm-aware
restructure commits where they belong by topic, not bundled with the
structural-lint cleanup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 19:08:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a4cb5b89e1 feat(swarm): swarm-aware container exec via container_address lookup
Adds a mode-aware `container_address` lookup plugin and a
`resolve-container-id` helper script that resolve swarm task IDs at
exec time (compose mode returns bare names unchanged). Migrates ~25
roles + ~28 task files from `command:` to `shell:` so the lookup's
`$(...)` subshell evaluates correctly, and extends `database` lookup
with an `address` key for cross-role DB exec.

Three new lint tests enforce the contract: every container exec
target goes through the resolver, lives behind an IS_STACK_HOST gate
(walks the include graph one hop deep), and uses `shell:` rather than
`command:` when the target is a Jinja reference. Existing nocheck
suppressions on bare-name DB exec sites (nextcloud redis, listmonk,
moodle OIDC, mailu) are replaced with the lookup-based form.

Fixes the systemd Environment= injection in sys-ctl-cln-disc-space
(it never expanded $(...)) by passing STACK / SERVICE_KEY parts and
resolving inside the script. Extracts shopware admin and mediawiki
extension install bodies into dedicated .sh files invoked via env.
Rewrites password-bearing shell tasks to either `| quote` directly or
the `-e VAR=... | quote` + `sh -c '... ${VAR} ...'` env-bridge pattern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 03:46:28 +02:00
kevinveenbirkenbachandClaude Opus 4.7 05f6a97bd8 refactor(stack_host_template): generalise IS_STACK_HOST template wrapper across the codebase
Renames the local action plugin from `conf_template` to
`stack_host_template` so the name carries the actual semantics
(IS_STACK_HOST gate, not nginx config), broadens the lint guard to
every bare `template:` task, and migrates every consumer found in the
audit.

Plugin / tests / lint:
  * plugins/action/conf_template.py -> plugins/action/stack_host_template.py
    (drop nginx-specific docstring; trust_as_template gate unchanged)
  * test_conf_template.py            -> test_stack_host_template.py
  * test_conf_template_migration.py  -> test_stack_host_template_migration.py
    Scope expanded from `.conf.j2` to every `template:` task; marker
    renamed to `nocheck: stack-host-template`.

Migrations (~50 tasks now using stack_host_template:):
  * sys-svc-compose/04_files.yml: compose.yml, .env, Dockerfile,
    compose.override (every per-app compose-tree file)
  * web-app-keycloak: sso_proxy upstream-config (fixes the friendica
    CI failure that prompted this refactor) + realm-import
  * web-app-taiga, web-app-matrix, web-app-opentalk, web-app-pgadmin,
    web-app-moodle, web-app-listmonk, web-app-littlejs, web-app-fediwall,
    web-app-wordpress, web-app-discourse, web-app-bigbluebutton,
    web-app-jenkins, web-app-jitsi, web-app-mobilizon, web-app-suitecrm,
    web-app-decidim, web-app-dashboard, web-app-nextcloud, web-app-xwiki,
    web-app-mailu, web-app-magento, web-app-odoo, web-app-prometheus,
    web-app-matrix, web-app-peertube, web-app-mailu (+ existing `conf_template:`
    callers updated to the new name)
  * sys-front-tls, svc-db-openldap, sys-svc-webserver-core,
    sys-svc-rdbms (dedicated DB env + initdb), sys-front-inj-css,
    sys-front-inj-dashboard, sys-front-inj-all, sys-stk-front-proxy,
    web-svc-cdn-css, web-svc-xmpp, web-svc-legal, web-svc-simpleicons,
    web-opt-rdr-domains

Nochecks (per-host or already-structurally-stack-host):
  * sys-svc-mail-smtp/postfix, sys-svc-sshd, sys-svc-container/daemon.json
  * sys-timer, sys-service, sys-svc-mail-msmtp, sys-svc-journalctl,
    sys-daemon, sys-svc-compose/utils/_render_one_template (delegate_to
    stack host with peer slurp dance)
  * dev-locales, dev-nix, desk-*, user, user-backup, user-workstation
  * svc-storage-nfs-server/exports, test-e2e-playwright stage dirs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 15:45:38 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2921ab6f03 feat(ansible): conf_template action plugin enforces IS_STACK_HOST gate
Adds plugins/action/conf_template.py, a drop-in `template:` replacement
that returns `skipped: true` whenever IS_STACK_HOST evaluates to false.
Routes the rendered Jinja through ansible.module_utils.parsing.convert_bool
so both Python bool and string "True"/"False" templar outputs resolve
correctly. Inherits every standard template module argument.

Migrates seven nginx-tree consumers that previously aborted on workers
with "Destination directory does not exist": sys-front-tls,
svc-db-openldap, sys-svc-webserver-core (nginx.conf + default_server.conf),
web-app-matrix (synapse vhost), web-app-peertube (per-domain vhost),
web-opt-rdr-domains (redirect vhost), and web-app-prometheus.

Tags twelve legitimate bare `template:` callers with
`# nocheck: conf-template`: per-host system configs (locale, nix, systemd
manager defaults, journald, msmtprc) and per-app compose-dir configs
(magento x2, mailu x2, moodle, odoo, taiga) that intentionally bypass the
project's nginx tree.

Adds tests/lint/ansible/modules/test_conf_template_migration.py as the
drift guard: any future `template:` task whose `src:` ends in `.conf.j2`
must adopt conf_template or carry the nocheck marker. Adds seven unit
tests for the plugin covering skip semantics, dispatch, mode defaults,
mode overrides, full-arg passthrough, and the string-bool templar path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 22:25:13 +02:00
kevinveenbirkenbach 1da3c2eda7 fix(web-app): gate manager-only role bodies behind IS_STACK_HOST
Across roles/web-app-*, the typical body shape is:
  - include_role: sys-stk-full (or sys-svc-compose)   cluster prereqs
  - <host-fs writes, container exec, compose exec>    stack-host only

Under swarm-mode delegation the second group only makes sense on the
host that holds the compose stack (manager), but the role plays on
every host in groups[<role>] so workers abort on missing paths.

Two workflow sweeps (gate-manager-ops + gate-manager-ops-01core, 58
agents total, discover/plan/adversarial-verify per role) plus three
manual fixes surfaced 27 roles to refactor. Each role keeps
sys-stk-full ungated so worker-side prereqs (sys-svc-container, NFS
client) still bootstrap; the trailing manager-only block either moves
into a new tasks/02_manager_ops.yml included with
when: IS_STACK_HOST | bool (SUB_FILE pattern) or gets the gate
inlined on the one or two trailing tasks (INLINE pattern). In
compose mode IS_STACK_HOST stays true so single-host deploys are
unchanged.

Manual catches the workflow agents missed:
- web-app-fediwall: utils/once/flag.yml lives AT THE END of 01_core,
  not at the start; preserved verbatim outside the gate so workers
  still flip the run_once_<role> fact.
- web-app-xwiki: a pre-existing block(...) wraps sys-stk-full plus
  _flush_config.yml; gated only the _flush_config.yml include so
  sys-stk-full keeps running on workers.
- web-app-dashboard: 02_javascript.yml (creates /opt/compose/dashboard/
  JS dirs) ran BEFORE sys-stk-full and was ungated; added the gate.

Roles touched (INLINE): akaunting, jenkins, magento, mobilizon
Roles touched (SUB_FILE): baserow, dashboard, decidim, erpnext,
  fediwall, friendica, gitea, jitsi, keycloak, listmonk, littlejs,
  mailu, matomo, moodle, odoo, openproject, opentalk, pixelfed,
  shopware, suitecrm, taiga, xwiki, zammad
2026-06-05 12:18:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 19254a23b4 refactor(sys-svc-compose): rename docker_compose_flush_handlers → compose_handlers_flush
Pure 1:1 rename (69 insertions / 69 deletions across 66 files) to
finish the ``docker_*`` → ``compose_*`` alignment started in
8373468e9. The flag configures whether the compose include flushes
handlers right after run; the ``docker_`` prefix was misleading
(handlers are Ansible-side, not docker-side) and the variable is part
of the same ``sys-svc-compose`` SPOT cluster as ``compose_repository_*``.

Touches the role default, the implementation
(``sys-svc-compose/tasks/utils/up.yml``), all 60+ consumer roles,
the network-utility wrapper + its README, and the
``test_network_create_via_util.py`` lint (both the docstring guidance
and the asserted diff).

Breaking change for any external override that still sets the old
name — no alias is provided.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:43:43 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4b05105e17 feat(database_query): replace static no_log: True with caller-controlled mask_values param
- `library/database_query.py`: drop `no_log: True` from `config` /
  `named_args` argument_spec; add `mask_values` bool param (default
  True). When True, the module copies their string values into
  `module.no_log_values` at runtime — same effective redaction as
  before, but now toggleable per task.
- Roll out `mask_values: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"` to
  every `database_query` call (38 sites across web-app-{odoo,
  openproject, peertube}) so the playbook constant in
  group_vars/all/00_general.yml drives the redaction end-to-end. Set
  it to false on demand for debugging unredacted rc/stderr.

Bundled: roles/web-app-openproject/tasks/02_ldap.yml also drops the
`async:`/`poll:` lines from Update/Create LDAP auth source. The
combination of `async` + `database_query` local-action + `no_log`
masked the async wrapper's result fetch with "Action failed: Unknown
error" while the SQL had actually succeeded (verified: 1 row in
`ldap_auth_sources` after the failing run). Sync execution matches the
pattern used by every other `database_query` caller in the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:15:04 +02:00
kevinveenbirkenbachandClaude Opus 4.7 017f67cd53 feat(plugins): introduce database_query module + migrate all SQL query tasks
Replaces controller-side `community.postgresql.postgresql_query` /
`community.mysql.mysql_query` calls with a project-local `database_query`
Ansible module that wraps `container exec -i <container> <psql|mysql>`
and pipes SQL via stdin. The module routes via the DB container's own
network so the same task works identically in shared mode (central
`svc-db-*` container) and non-shared mode (per-role `<entity>-database`
container) — the `container` field of `lookup('database', …)` is
shared-aware and the module just reads it. SQL payloads come from a
file (`query_file:`, preferred) or as a raw string (`query:`, escape
hatch). `named_args:` substitutes `%(name)s` placeholders with engine-
appropriate escaping (postgres single-quote-doubling, mariadb backslash
escapes), so role tasks stop string-interpolating Jinja into SQL.

New backends extend the module's `_ENGINES` dispatch table; everything
else is engine-agnostic. The module ships at `plugins/modules/` (Ansible
collection-style layout) — the shebang lint test learned to recognise
that path as a valid module location alongside the existing per-role
`library/` layout.

Two lint tests enforce the convention:

* `tests/lint/ansible/modules/test_no_raw_db_query_module_usage.py`
  forbids `community.postgresql.postgresql_query` /
  `community.mysql.mysql_query` in role tasks. Opt out per-line with
  `# nocheck: database-query` (used by `svc-db-postgres/tasks/02_init.yml`
  where the role manages its own DB and the `database` lookup would be
  self-referential).
* `tests/lint/ansible/modules/test_no_raw_database_query_string.py`
  forbids inline `query:` on `database_query:` tasks. Opt out with
  `# nocheck: database-query-raw` (no opt-outs in the current tree —
  every SQL payload reaches the module through a `.sql` file under
  `files/sql/`).

Role migrations:

* `web-app-peertube` — 3 tasks migrated, 3 new `.sql` files,
  `templates/upsert-oidc-plugin.sql.j2` deleted in favour of
  `files/sql/upsert-oidc-plugin.sql` + `named_args:`.
* `web-app-openproject` — 3 tasks migrated, 3 new `.sql` files for
  the LDAP-auth-source check/update/insert chain.
* `web-app-odoo` — 33 tasks migrated, 9 new `.sql` files for the
  LDAP / admin / init-database flows; 17 pre-existing OIDC `.sql`
  files renamed snake_case → kebab-case and relocated from
  `files/oidc/` to `files/sql/oidc/` (via `git mv` so rename history
  stays intact).

Unit tests cover every dispatch-table entry, the named_args escaper
(strings, ints, floats, bools, None, single-quote and backslash
injection vectors), and the password-redactor that hides
PGPASSWORD/MYSQL_PWD assignments from the module's returned `cmd`
field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:08 +02:00
kevinveenbirkenbachandClaude Opus 4.7 06defc5f64 fix(web-app-odoo): drop res_company_ldap.user from INSERT/UPDATE
CI Run 25890032686 web-app-odoo job 76093401665 failed at
roles/web-app-odoo/tasks/04_ldap.yml:54 (Create new LDAP configuration)
with `invalid input syntax for type integer: "uid"`, LINE 12 of the
INSERT being the `user` column.

Odoo 19's `res.company.ldap.user` is a `Many2one('res.users', ...)` —
i.e. an integer FK to the *template user* to copy when auto-provisioning
LDAP-authenticated users. It is NOT the LDAP attribute name, which lives
inside `ldap_filter` (e.g. `(uid=%s)`). The role was sending the string
`'uid'` (from `ODOO_LDAP_USER_ATTR`) into the integer FK column.

Per `addons/auth_ldap/models/res_company_ldap.py` @ 19.0:

  - `user` has no `required=True` flag, so the column is nullable
  - `_get_or_create_user` falls back to `SudoUser.create(values).id`
    when `conf['user']` is falsy, materialising users with the defaults
    from `_map_ldap_attributes` (name, login, company_id, optional email)

Drop the column from both INSERT and UPDATE, drop the matching key from
`ODOO_LDAP_CONFIG`. `ODOO_LDAP_USER_ATTR` remains because it still backs
`ODOO_LDAP_FILTER`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:23:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 762586a03a fix: matrix docker-network label clash + odoo res.company.ldap column rename
* sys-svc-compose/templates/networks.yml.j2: declare the per-role
  `default` network as `external: true`. The network is pre-created
  by `sys-svc-compose/tasks/utils/network/create.yml` via
  `community.docker.docker_network` (no compose labels), so the
  second `compose up` after an inter-round purge refuses with
  `network <name> was found but has incorrect label com.docker.compose.network
  set to "" (expected: "default")` — observed on web-app-matrix in
  CI run 25868178119. External lifecycle keeps compose out of the
  label race and matches the actual architecture (we manage the
  network ourselves via Ansible).

* web-app-odoo/tasks/04_ldap.yml: rename SQL column `company_id` to
  `company` in both the UPDATE and the INSERT. Odoo 19's
  `auth_ldap` model declares `company = fields.Many2one('res.company', ...)`
  (no `_id` suffix), so the actual DB column is `company`. CI run
  25868178119 failed with `column "company_id" of relation
  "res_company_ldap" does not exist`. Verified against
  https://github.com/odoo/odoo/blob/19.0/addons/auth_ldap/models/res_company_ldap.py.
  Python named-arg keys keep their `company_id` name (`ODOO_LDAP_CONFIG.company_id`
  in vars/main.yml stays as is).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:44:48 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2a023ca6a1 chore(lint): autoformat strict-failure semantics + claude.sh rename
* scripts/lint/autoformat.sh:
  - rename run_sort_claude_settings worker to run_claude (matches the
    renamed scripts/lint/claude.sh).
  - drop the EXIT trap that wiped status_dir; cleanup happens explicitly
    after `wait` returns, so a parallel worker that interrupts the
    parent never races a vanished status dir.
  - each worker now captures its tool's exit code and writes one of
    OK / SKIP / FAIL. Tolerated `expected violations remain`-style exits
    (markdownlint-cli2 rc≤1, ansible-lint rc≤2) still count as OK; only
    real failures count as FAIL.
  - all workers run to completion regardless of individual tool failures
    (so the user always sees the full picture); after the summary, the
    script exits 1 if any worker reported FAIL.
* scripts/lint/sort_claude_settings.sh -> scripts/lint/claude.sh
  (history preserved via git mv); update test_claude_settings_sorted.py
  fix-hint accordingly.

Plus working-tree drift across roles/, group_vars/, inventories/,
requirements/, and .pre-commit-config.yaml accumulated from the
make-test setup phase regenerating generated artefacts during recent
test runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:30:49 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ed04dad5d5 refactor(meta): land req-008/009/010 role-meta layout/networks/ports/run_after migration
Single atomic landing for three back-to-back role-meta refactor requirements
(see docs/requirements/008-role-meta-layout.md, 009-per-role-networks-and-ports.md,
010-role-meta-runafter-lifecycle-migration.md).

req-008 — role meta layout
  * Move every role's metadata under meta/<topic>.yml with file-root
    convention (no wrapping key matching the basename):
      - schema/main.yml + config/main.yml.credentials → meta/schema.yml
      - users/main.yml                                → meta/users.yml
      - config/main.yml.server                        → meta/server.yml
      - config/main.yml.rbac                          → meta/rbac.yml
      - config/main.yml.compose.services + every other top-level key
        of config/main.yml inlined under <entity>.<key>
                                                      → meta/services.yml
      - config/main.yml.compose.volumes               → meta/volumes.yml
  * Drop the legacy schema/, users/, config/ directories from every role.
  * meta/schema.yml: support nested credential keys, optional `default:`
    field (Jinja literal, not rendered/validated/regenerated), implicit
    `algorithm: plain`.
  * Materialised paths drop the `compose.` prefix:
      compose.services.<e>.<…> → services.<e>.<…>
      compose.volumes.<k>      → volumes.<k>
      <former-top-level>.<…>   → services.<entity_name>.<former-top-level>.<…>
  * Update every Python consumer (utils/cache, utils/manager, utils/applications,
    plugins/lookup, plugins/filter, cli/*) plus every Jinja template, vars/*.yml
    and task file to the new paths.
  * No legacy fallback. Repository-wide grep for `compose.services.` /
    `compose.volumes.` / `schema/main.yml` / `users/main.yml` / `config/main.yml`
    is empty outside docs/requirements/ and tasks/utils/migrate_meta_layout.py.
  * Add tests/lint/repository/test_role_meta_layout.py guarding against the
    old paths returning, and an explicit lint guard that meta/variants.yml
    files do not carry a `compose:` wrapper.

req-009 — per-role networks/ports
  * Move group_vars/all/08_networks.yml.defaults_networks.local.<role>.{subnet,
    dns_resolver} into roles/<role>/meta/server.yml.networks.local.{subnet,
    dns_resolver}.
  * Delete group_vars/all/09_ports.yml entirely; ports now live on the
    service entity that exposes them at
    roles/<role>/meta/services.yml.<entity>.ports.{inter,local,public}.
  * Rename + int-cast every former top-level `services.<entity>.port`
    field to `services.<entity>.ports.inter` (single int).
  * Always-category-keyed maps for ports.local / ports.public; relay
    spans use {start, end} integers.
  * Add PORT_BANDS map to group_vars/all/08_networks.yml — single source
    of truth for the per-<scope>.<category> band ranges, consumed by both
    the new CLI helpers and the lint test.
  * New CLI helpers (cli meta ports suggest, cli meta networks suggest)
    walk the live role tree gap-first then increment-fallback, with
    deterministic output and non-zero exit on capacity overflow.
  * cli create role consumes both helpers when scaffolding a new role.
  * Lint extensions: bare-int port shape rejection, host-bound port
    collision check (single ints + relay span integers, inter excluded),
    band-membership check with documented allow-list for the legacy
    web-app-bigbluebutton.bigbluebutton.local.http: 48087 entry.

req-010 — run_after / lifecycle migration
  * Move galaxy_info.run_after and galaxy_info.lifecycle out of
    meta/main.yml into meta/services.yml.<primary_entity>.{run_after,
    lifecycle}; <primary_entity> is the value returned by
    get_entity_name(role_name).
  * For multi-entity roles whose primary entity has no compose service
    (web-app-bluesky → bluesky, web-app-matrix → matrix, desk/dev/drv/
    gen roles), create a dedicated metadata-only top-level entry under
    <primary_entity>.
  * Introduce utils/roles/meta_lookup.py with get_role_run_after /
    get_role_lifecycle that resolve <primary_entity> once and read from
    meta/services.yml; degrade gracefully when meta/services.yml is
    absent OR the field is absent; raise MetaServicesShapeError when the
    file exists but is malformed.
  * Update every consumer (utils/service_registry, utils/invokable,
    cli/meta/applications/resolution/run_after/__main__, cli/meta/roles/
    lifecycle_filter/__main__, cli/meta/applications/resolution/combined,
    cli/build/{tree,graph,role_include}, cli/deploy/development/{common,
    init,deps,deploy}, plugins/filter/canonical_domains_map, etc.) to
    use the helper.
  * Lint guards: no run_after/lifecycle in meta/main.yml; lifecycle
    values restricted to {planned,pre-alpha,alpha,beta,stable,deprecated};
    run_after/lifecycle never on a non-primary entity; empty run_after
    lists rejected.

Also folded into this change set
  * services_disabler now writes `services.<svc>.{enabled,shared}` into
    host_vars instead of the old `compose.services.<svc>.{...}` path; the
    discover/find pair walks meta/services.yml and only treats entries
    carrying enabled/shared/provides as provider declarations (so
    metadata-only entries from req-010 do not create spurious provider
    rows).
  * service_registry skips `run_after` deps whose target was filtered
    out via SERVICES_DISABLED instead of failing the whole load.
  * cli/mirror/resolver and cli/create/inventory/mirror_overrides emit/
    consume the new applications.<role>.services.<svc>.image shape
    (mirror.md updated to match).
  * utils/domains/list.build_applications_from_roles now reads
    meta/server.yml directly (it had been silently returning {} since
    config/main.yml stopped existing).
  * utils/roles/resource_validation.filter_roles_by_min_storage reads
    meta/services.yml.<entity>.min_storage instead of the legacy
    compose.services path.
  * web-app-bluesky pre-clones the social-app build context BEFORE
    sys-svc-compose so the build context is on disk by the time
    sys-stk-front-proxy/tasks/main.yml flushes the docker build handler.
  * 4 meta/variants.yml files (svc-ai-ollama, web-app-phpmyadmin,
    web-app-wordpress, web-app-keycloak) lose their `compose: services:`
    wrapper so deep-merge over meta/services.yml lands on the new
    file-root shape.

Validation
  * Unit tests:        1571 passed.
  * Lint tests:        50 passed.
  * Integration tests: 161 passed (1 skipped).
  * Full-cycle fresh-purged-apps deploy succeeded for the 8-app validation
    set: web-app-{nextcloud,wordpress,yourls,bigbluebutton,bluesky,gitea,
    discourse,mattermost}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:58:26 +02:00
kevinveenbirkenbach 4e9d4b53db refactor(web-app-odoo): extract OIDC SQL to files/oidc/*.sql
Splits all 23 inline SQL bodies in `roles/web-app-odoo/tasks/03_oidc.yml`
into 17 standalone `.sql` files under `roles/web-app-odoo/files/oidc/`,
referenced from the tasks via `lookup('file', 'oidc/<name>.sql')`.

Why:
- Inline `query: >` heredocs hide SQL from grep, syntax highlighters,
  and SQL-aware tooling. Extracting them to dedicated files makes the
  queries trivially diffable + parseable + reviewable.
- The two tasks that previously interpolated Jinja straight into SQL
  (`'{{ ODOO_OIDC_AUTHORIZE_URL }}'` etc. for the Keycloak provider
  upsert) move to `named_args` with `%(name)s` placeholders, removing
  the SQL-injection surface those interpolations carried.

What also changes (kept tight to stay in scope):
- The five-line DB connection block (db / login_user / login_password /
  login_host / login_port) is deduplicated via a `module_defaults` block;
  the one task hitting the Keycloak DB instead of Odoo overrides them
  inline.
- Seven `INSERT INTO ir_config_parameter ... ON CONFLICT (key) DO UPDATE`
  tasks now share `upsert_ir_config_parameter.sql` (key + value via
  named_args). The two specialised template-user-id upserts (which need
  a subquery for the value) share
  `upsert_ir_config_parameter_template_user_id.sql`.

Net effect: tasks file shrinks from 389 to 192 lines, every SQL body
sits in its own annotated `.sql` file with a header comment describing
intent, and the deploy-time behaviour is unchanged (same statements,
same ordering, same idempotency guards, just better-organised).
2026-04-25 14:01:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d97ed7a9e8 feat(deploy): per-role matrix-deploy variants with folder-per-round model
Introduces per-role `meta/variants.yml` for declaring multi-variant
deploys (e.g. WordPress Single-Site vs. Multisite) and reshapes the
development deploy CLI to materialise one inventory folder per variant
and iterate them with co-located sync+async passes.

Why:
- The previous single-inventory + Pass-1-then-Pass-2 split deployed each
  variant twice and ran the async pass against a host left in a
  different variant's state, masking real bugs.
- Inventory and runtime concerns were tangled: variant selection lived
  as a runtime extra-var and required cross-stage coordination.

How:
- `roles/<role>/meta/variants.yml` (file format docs at
  docs/contributing/artefact/files/role/variants.md): YAML list of
  deep-merge overrides on top of `config/main.yml`. Empty entry `{}` is
  variant 0 (= legacy payload). 6 roles get their dev overrides moved
  here from inventories/dev.yml; WordPress declares 2 variants.
- `utils/runtime_data.py` gains `_build_variants` / `get_variants` and
  drops the retired `_active_variants` runtime selector. Loader always
  returns variant 0 as the default; non-zero variants reach
  `get_merged_applications` as inventory-level `applications.<app>`
  overrides baked at init time.
- New `cli/deploy/development/inventory.py`: `DevInventorySpec`,
  `plan_dev_inventory_matrix` (pure planner shared between init and
  deploy), `build_dev_inventory_matrix` (materialises one folder per
  round at `<dir>-<R>`, or `<dir>` when single-variant). Single-folder
  paths preserve full backward compat.
- `cli/deploy/development/deploy.py`: matrix loop with per-round
  cleanup (only apps whose variant changed AND that are about to
  re-deploy), `--variant <idx>` (or `VARIANT=` env) to pin a single
  round, and `--full-cycle` (or `FULL_CYCLE=true`) which interleaves
  sync + async per round so the async re-deploy targets exactly the
  host state the matching sync deploy just produced.
- `cli/deploy/development/exec.py` gains repeatable `--env KEY=VAL` so
  bash callers can inject per-run context into in-container helpers
  without inlining heredocs.
- `scripts/meta/env/inventory.sh` exports `INVENTORY_VARS_FILE`
  (Python mirror in `cli.deploy.development.common.DEV_INVENTORY_VARS_FILE`;
  test_inventory_vars_file_spot.py locks them against drift).
- `make deploy-fresh-purged-apps` and `scripts/tests/deploy/ci/dedicated.sh`
  switch to a single init + single deploy `--full-cycle`. The
  `make deploy-reuse-*` bash scripts honour `VARIANT=` by suffixing
  `INVENTORY_DIR`/`INVENTORY_FILE`.
- The 5 `scripts/tests/deploy/local/deploy/*.sh` scripts now delegate
  their in-container heredocs to dedicated helpers in
  `scripts/tests/deploy/local/utils/`, removing the bash-in-bash
  quoting hell.

Tests:
- `tests/unit/utils/test_variants.py` (renamed from
  `test_application_variants.py`): loader edge cases, variant-zero
  default, retired-`_active_variants` guard.
- `tests/unit/cli/deploy/development/test_inventory.py` (new): planner,
  variant baking, builder, `filter_plan_to_variant`, `DevInventorySpec`.
- `tests/unit/cli/deploy/development/test_matrix_deploy.py` (new):
  per-round deploy filter, cleanup decisions, `--variant` pinning,
  `--full-cycle` per-round interleave.
- `tests/unit/cli/deploy/development/test_inventory_vars_file_spot.py`
  (new): drift guard for the bash <-> Python literal pair.
- `tests/unit/cli/deploy/development/test_exec.py` extended for `--env`.

Docs:
- `docs/contributing/artefact/files/role/variants.md` (new): file format.
- `docs/contributing/design/matrix-deploy.md` (new): runtime mechanism
  (folder layout, --variant, --full-cycle, cleanup logic).
- `docs/contributing/actions/deploy.md` (new): contributor dev-deploy
  flow (make targets, --variant pinning).
- `docs/administration/deploy.md` slimmed to admin-only (was mixed
  admin+dev), cross-links to the new contributor doc.
- `docs/agents/action/iteration/role.md` keeps only agent-iteration
  recipes; mechanism explanation moved out and linked.
- `docs/contributing/documentation.md` adds the "no self-SPOT
  declaration" rule; `applications.md` renamed its SoT header for
  consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:47:48 +02:00
kevinveenbirkenbachandClaude Opus 4.7 84ceb34249 fix(web-app-odoo): migrate direct users.* access to lookup('users', ...)
Replaces legacy `users.KEY.field` and `users['KEY'].field` references with
the runtime `lookup('users', 'KEY').field` pattern across vars, templates,
and tasks so the role aligns with the new users lookup plugin.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 02:44:12 +02:00
evangelostsak 453ce89343 fix(web-app-odoo): update Keycloak DB query to use administrator username instead of ID 2026-04-20 20:26:47 +02:00
evangelostsak 733b5453f4 feat(web-app-odoo): auto-create OAuth users with Internal User group
Addresses review feedback: biber and other LDAP users are now
auto-created on first SSO login via Keycloak OAuth signup rather
than hardcoded in Ansible.

OAuth template user mechanism:
- Sets website.auth_signup_uninvited=b2c to allow OAuth signup
- Creates _oauth_template user with Internal User group
- Sets base.template_portal_user_id (key param Odoo actually uses)
- Fallback grants Internal User to existing OAuth users

Adds biber Playwright test to verify OAuth auto-creation works.
2026-04-20 20:26:47 +02:00
evangelostsak 35cfcb834d fix(web-app-odoo): run admin setup before OIDC user linking
OIDC user linking failed because it ran before admin user was
renamed from 'admin' to configured username. Reorder tasks so
05_admin.yml runs before 03_oidc.yml.
2026-04-20 20:26:47 +02:00
evangelostsak c328cdca48 fix(main.yml): update task name and simplify Odoo addons directory creation 2026-04-20 20:26:47 +02:00
evangelostsak ce46ca21ab fix(oidc): remove biber user linking logic from Keycloak integration 2026-04-20 20:26:47 +02:00
evangelostsak 4bb5f5fce4 refactor(odoo): remove inline LDAP configuration variables from task file 2026-04-20 20:26:47 +02:00
evangelostsak 4a2b31085d fix(odoo): address Copilot review comments
- Remove unused BIBER env assertions from playwright.spec.js
- Pin pip package versions in Dockerfile for reproducible builds
- Use parameterized SQL queries in 04_ldap.yml to prevent injection
2026-04-20 20:26:47 +02:00
evangelostsak 6380b666f9 feat(odoo): enable OAuth signup and link existing users to Keycloak in Odoo 2026-04-20 20:26:47 +02:00
evangelostsak b595737fe8 feat(odoo): add auth_signup module installation for OAuth user auto-creation 2026-04-20 20:26:47 +02:00
evangelostsak 7c23c68874 feat(odoo): add main tasks for Odoo deployment including directory creation, configuration, and module installation 2026-04-20 20:26:46 +02:00
evangelostsak 98d439678f feat(odoo): add tasks to create or update Odoo administrator user and partner details 2026-04-20 20:26:46 +02:00
evangelostsak 18fa1a2c05 feat(odoo): add LDAP configuration tasks for Odoo authentication 2026-04-20 20:26:46 +02:00
evangelostsak 886eaae75a feat(odoo): add OIDC configuration for Keycloak provider in Odoo 2026-04-20 20:26:46 +02:00
evangelostsak f2c78526ef feat(odoo): add task to install Odoo modules including auth_oauth, auth_oauth_https, and auth_ldap 2026-04-20 20:26:46 +02:00
evangelostsak 7a47c8ed95 feat(odoo): add task to initialize Odoo database and install core modules 2026-04-20 20:26:46 +02:00