Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 a0f0e3ac03 feat(swarm): SPOT lookup gates container_name emission per deploy mode
container_name is illegal in swarm mode when replicas > 1 (docker rejects
with "services.deploy.replicas: can't set container_name and X as container
name must be unique"). All compose.yml.j2 templates now route through a
single lookup that suppresses the key in swarm mode and emits it in compose
mode.

SPOT: plugins/lookup/compose_container_name.py
  Reads DEPLOYMENT_MODE from the templating context, returns
  'container_name: "<name>"' under compose and an empty string under swarm.
  Call sites read:

      {{ lookup('compose_container_name', MY_CONTAINER) }}

  No explicit DEPLOYMENT_MODE pass-through needed.

Sweep: 82 templates rewritten across roles/web-app-*, roles/web-svc-*,
roles/svc-*, plus 4 partial includes (sso_proxy, redis, mariadb, postgres).
For container names assembled from multiple Jinja fragments the args use
tilde concat (X ~ '-' ~ Y) so the lookup receives a single resolved string
and the call stays a single well-formed Jinja expression.

Lint: tests/lint/ansible/jinja/test_no_raw_container_name.py blocks
  (a) raw 'container_name:' YAML keys in any roles/*/templates/**/*.j2,
  (b) split-Jinja regression where the outer braces close mid-call:
      {{ lookup('compose_container_name', A }}-{{ B) }},
  (c) embedded-Jinja regression inside a string argument:
      lookup(..., "{{ X }}_suffix") (removed in ansible-core 2.23).
  The (b) and (c) regexes accept either single- or double-quoted lookup
  name and scan the full file so multi-line shapes also surface. Suppress
  per-site with '# nocheck: container-name' on the same or preceding line.

Tests: tests/unit/plugins/lookup/test_compose_container_name.py pins the
lookup contract: compose vs swarm output, whitespace tolerance, default-to
compose when DEPLOYMENT_MODE is absent, AnsibleError on missing or excess
terms, non-string name coercion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 02:24:50 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4fb8005f09 fix(svc-cache-registry): make role fully opt-in
Drop the required_by declaration on svc-cache-registry so the
post-deploy validator no longer enforces its execution for every
svc-docker / web-app deploy. The pull-through cache is a performance
optimization, not a correctness requirement; operators that omit it
from inventory should not see a deploy fail.

Compose-mode auto-place now scopes svc-prx-openresty to plays that
actually carry a web-* role (any group whose name matches ^web-),
so svc-only deploys do not get an orphan front-proxy. The match is
intentionally on the web-* prefix family so new web-<sub>-<entity>
categories pick up the auto-place rule without an allowlist edit.

The swarm-NFS test still wants the cache for the pull-through speedup,
so svc-cache-registry is listed explicitly in 07_provision_inventory.sh
alongside the swarm bootstrap roles.

Fixes test-deploy-server / Server web-app-mediawiki failing with
"required role(s) did not execute for deploy: svc-cache-registry".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 02:24:50 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bfed033544 fix(web-app-funkwhale): start api with gunicorn and bind port
The api service had no command, so the CA-trust entrypoint wrapper ran the image entrypoint with no argument and it crash-looped (parameter not set). Pass command: gunicorn and set FUNKWHALE_API_PORT so the entrypoint binds 0.0.0.0:5000, matching the compose port mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:06:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 dac52a7c42 fix(web-app-friendica): strip trailing slash from base URL
lookup('tls', ...).url.base keeps a trailing slash, so Friendica generated double-slash profile URLs (host//profile/<nick>) that broke local-follow contact resolution (no confirmation form rendered). Strip it via a shared FRIENDICA_BASE_URL var reused by env.j2 and the local.config.php patch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:06:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 738533622a fix(web-app-erpnext): bypass v16 setup wizard via per-app completion
Frappe v16 gates the desk setup wizard on Installed Application.is_setup_complete per app and on the desktop:home_page default, not System Settings.setup_complete alone. Set the per-app flag, reset the setup-wizard home page, and clear cache so the native administrator lands on the desk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:06:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 346e05692c fix(web-app-erpnext): drop separate assets volume for v16 startup
ERPNext v16 bakes built assets into /home/frappe/frappe-bench/assets and symlinks sites/assets to them on every container start via an entrypoint that runs 'rm -rf sites/assets' under 'set -e'. Mounting assets as a separate named volume nested inside the sites volume made that rm hit a mountpoint (Device or resource busy), crashing the backend and all dependent containers before gunicorn opened its TCP port.

Persist only sites and logs, matching upstream frappe_docker; assets are relinked from the image on each start.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:06:34 +02:00
kevinveenbirkenbach 9d5157c480 Merge branch 'main' into feature/swarm-nfs 2026-06-04 17:25:12 +02:00
kevinveenbirkenbachandClaude Opus 4.7 339f41c9bb chore(erpnext): autoformat description line wrapping
Long credential descriptions wrapped to fit the line-length limit. No
semantic change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2cdf1f1a31 chore: swarm-mode template lint, act tweaks, formatting
Add tests/lint/ansible/jinja/test_no_ansible_facts_in_templates.py: under
swarm-mode the rendered template lands once on the primary manager and is
slurp-distributed to every other node, so any per-host fact reference
(ansible_hostname, ansible_default_ipv4, ansible_fqdn, ansible_nodename,
ansible_machine_id, ansible_eth0, ansible_all_ipv*_addresses) would
silently produce wrong output on the other nodes. The lint blocks them
at PR time.

scripts/tests/deploy/act/workflow.sh forwards --action-offline-mode +
--action-cache-path so repeated local act runs do not refetch the actions
catalog.

roles/sys-ctl-cln-anon-volumes/tasks/01_core.yml quoting cleanup applied
by autoformat (single to double quotes, no semantic change).

.claude/settings.json adds Workflow to permissions.allow so the
multi-agent Workflow tool no longer prompts per invocation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 98dafc3f4e feat(ci): zip and upload generated inventory as a debug artifact
Both deploy workflows now ship the rendered inventory for the matrix
entry as a zip artifact so failed runs can be inspected without
re-running the provisioner. test-deploy-swarm.yml zips /tmp/inv directly;
test-deploy-server.yml docker-cps /root/inventories from every running
infinito_nexus_* container into a staging dir first because the
inventory lives inside the compose container, not on the runner.

Both steps are if: always() to capture artifacts on failure too, but
gated on \!env.ACT because act-local does not provide
ACTIONS_RUNTIME_TOKEN and would otherwise fail the upload step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 39e32f74a5 feat(test): harden swarm-NFS rig — DinD DNS, RUNTIME, mirrors, N/N replicas
DinD DNS chain reworked so the Playwright container run --rm npm install
no longer EAI_AGAINs on registry.npmjs.org:
- 03_start_swarm_nodes.sh runs each DinD with --dns 1.1.1.1/8.8.8.8 and
  pre-seeds /etc/docker/daemon.json. systemd-resolved still clobbers the
  outer resolv.conf at boot, so the daemon-level DNS is the only thing
  that survives for containers the inner daemon spawns.
- daemon.json.j2 omits the "dns" key when networks.internet.dns is empty
  (default), so the rendered config no longer overwrites the pre-seeded
  daemon.json with "dns": [""] and break inner-container resolution.
- 09_write_extras.sh sets networks.internet.dns to a public resolver for
  the test rig and bakes RUNTIME via cli.meta.runtime (so e2e-playwright
  actually runs in act/github instead of being skipped under "host").

07_provision_inventory.sh generates mirrors.yml via
cli.contributing.mirror.resolver and passes --mirror to the inventory
provisioner only when cli.meta.runtime == "github". act/local keeps
upstream pulls (no mirror infrastructure to point at).

12_wait_converge.sh and 15_assert_state.sh accept any N/N replica state
instead of hardcoded 1/1, so multi-host-replica deploys converge cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 411bcfe4fe refactor: cluster-wide shared-DB detection + openresty-host gating + load_app guard
sys-svc-rdbms now decides central-DB vs dedicated-DB mode from the provider
group's presence in the inventory (groups[provider_id] | length > 0)
instead of the per-host lookup('database', _, 'shared'). The old form
returned different values on different hosts (e.g. mariadb.shared =
'svc-db-mariadb' in group_names), so workers would silently disagree with
the manager about which mode is active and trip the central-init handlers
on the wrong host. The central-init block now also gates on the host
actually being in the provider group.

sys-stk-full guards the sys-stk-front-proxy include behind
"inventory_hostname in groups['svc-prx-openresty']", so under swarm only
the openresty host renders the per-app vhost. Combined with the
compose-mode auto-place from the previous commit, single-host compose and
swarm both pick exactly one host.

tasks/utils/load_app.yml skips loading a role for hosts that are not in
its target group when a group exists, matching the new gating semantic
upstream.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e61900e897 feat(swarm): multi-manager raft-quorum support
Allow more than one host in svc-docker-swarm-manager. The 01_core
assertion drops the exactly-one check and now requires >= 1 manager; the
init phase still bootstraps the first manager as leader, additional
managers join via the manager-join token (new 02_manager_join.yml), and
workers join via the worker token as before. With three or more managers
in the group, swarm gains raft quorum and survives the loss of a single
manager.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6f985dd810 feat(deploy): auto-place shared services via meta default_placement
Roles that declare default_placement: manager in meta/services.yml are now
auto-added to their own inventory group when the operator did not list them
explicitly. Both the runtime constructor add_host loop and the CLI
pre-flight inventory extender use the new roles_with_default_placement
lookup so swarm and CLI-validation stay consistent.

In compose-mode (no svc-docker-swarm-manager group), svc-prx-openresty
auto-joins the first play host. run_once + ansible_play_hosts | first
keeps that deterministic in multi-host compose deploys so the openresty
vhost view does not fragment.

The svc-cache-registry / svc-docker-registry entries previously hardcoded
in scripts/tests/deploy/swarm/08_extend_inventory.sh fall out of the new
dynamic loop; net inventory is unchanged.

ansible_play_hosts joins the magic-var allowlist in
test_variable_definitions so the constructor add_host passes integration
lint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3b7bbfa71d refactor(swarm): SPOT-define SWARM_MANAGER_ACTIVE + SWARM_PRIMARY_MANAGER
Collapse two repeated swarm-vs-compose expressions into single-source-of-truth
group vars and apply across sys-svc-compose, web-app-mediawiki:

- SWARM_MANAGER_ACTIVE: true when DEPLOYMENT_MODE \!= 'swarm' or this host is
  in svc-docker-swarm-manager. Used as the gate for "execute manager-only
  deploy work here" (dir creation, file render, compose up, handler tasks).

- SWARM_PRIMARY_MANAGER: first svc-docker-swarm-manager or self under
  compose-mode fallback. Used as delegate_to: for cluster-wide ops
  (compose pull/build/up, swarm stack deploy, post-deploy DB health probe,
  network create, MediaWiki one-shot install/admin).

Pure refactor — no semantic change. Subsequent edits to either expression
now live in one file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f83ff17b84 fix(tls): always redirect bare DOMAIN_PRIMARY so openresty has an SNI match
Drop the "web-opt-rdr-domains in deployed" gate in the
current_play_redirect_domains lookup. CURRENT_PLAY_REDIRECT_DOMAINS now
always carries DOMAIN_PRIMARY to DOMAIN_HOMEPAGE when both are set and
differ. This triggers sys-stk-front-base to auto-load web-opt-rdr-domains
so openresty actually renders a vhost matching its declared canonical, and
the bare apex stops hitting the default-server's ssl_reject_handshake.

CSP probing skips those redirect-source domains: pure 301 vhosts have no
CSP header to validate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2c57b4a0a0 refactor(web-app-mediawiki): one-shot install/admin + multi-host pilot
The install + update + admin maintenance commands all run as throwaway
container instances against the custom image, with the live service's
bind mounts attached. The previous container exec-against-static-name
pattern stops working in swarm where the container name is a
per-replica swarm task id and the task itself can be reaped by the
healthcheck mid-update.

container_name is gated under swarm because docker-compose's strict
validation rejects it once replicas > 1. Cross-host config replication
in 01_configuration.yml is removed entirely: the play now runs natively
on every host in the role's group, so the LocalSettings.php / oidc.php
/ debug.php templates render in place per host.

Test inventory adds the pilot role to all three simulated swarm hosts,
which is what flips DEPLOYMENT_MODE to swarm under the new
inventory-driven model.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9cacfbf959 refactor(swarm): inventory-driven topology replaces per-role swarm flags
DEPLOYMENT_MODE is now derived per role from group membership:
swarm iff len(groups[application_id]) > 1, else compose. The cluster
infrastructure flag SWARM_CLUSTER_AVAILABLE is true whenever any host
sits in svc-docker-swarm, so DB and proxy services render attachable
overlay networks even when their own group lists only one host.

All service_is_stateful flags are removed across svc-db-mariadb,
svc-db-postgres, svc-db-openldap, svc-prx-openresty, svc-docker-registry
and svc-db-mariadb's compose template; the inventory shape now expresses
the same intent. Cluster-wide handlers (swarm build/push/validate/deploy,
external network creation, DB healthcheck) gain run_once + delegate_to
the manager so they execute exactly once regardless of how many hosts
the role's play targets. CA-injection and compose validate are gated to
compose-mode only; their compose-config strictness breaks when the
deploy block coexists with container_name under swarm.

Dead per-host swarm-resolve infrastructure is removed: swarm_resolve_
container.sh, resolve_container.yml, resolve_self.yml, the MariaDB-
specific swarm_mariadb_cid.sh, the cross-host template replication
step, and the _swarm_retries / _swarm_delay defaults. With run_once +
delegate_to: manager replacing the old "render every template on every
swarm peer" pattern, every consumer becomes a noop on workers and
collapses to the manager's one-shot path.

The web-app-nextcloud memcache override mapping flips from the legacy
"single" sentinel to "compose" (semantically equivalent under the new
model). Requirement 023 is updated to reflect the new model and drops
the now-irrelevant per-role opt-out / placement-constraint clauses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a94dfe87e2 feat(svc-cache-registry): cluster-local Docker Hub pull-through cache
New role that runs registry:2 in pull-through proxy mode on the swarm
manager. Worker nodes' daemon.json gets a registry-mirrors entry plus
the corresponding insecure-registries entry so plain-HTTP traffic to
the cache is permitted. The role's inventory group lists exactly one
host (manager) so the new inventory-driven topology model resolves
DEPLOYMENT_MODE to compose for the cache itself.

Within a single deploy run, the three swarm workers all pull the same
base images via the cache instead of hitting registry-1.docker.io
three separate times. Measured speedup on iter26 vs the no-cache iter21
baseline: pkgmgr/apt-install steps 2.4-2.8x faster, Docker image pulls
1.8-2.8x faster.

Cross-iter cache persistence is intentionally out of scope: act
recycles the DinD nodes between runs so the cache volume is wiped each
time. Cross-run reuse would require either bind-mounting the cache
from the host or pointing daemon.json at the dev compose stack's
registry-cache; both are tracked separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:11:06 +02:00
infinito-nexus-updater[bot] 7762ce4372 update(images): update Docker image versions 2026-06-04 10:05:27 +00:00
kevinveenbirkenbach 19e8161c50 Merge pull request #298 from infinito-nexus/update/docker-image-versions-20260603-d1baba6
update(images): update Docker image versions
2026-06-04 11:17:41 +02:00
kevinveenbirkenbachandClaude Opus 4.7 20745cecbd test(lint): enforce parameterized FROM in role Dockerfiles
Add tests/lint/docker/dockerfile/test_from_parameterized.py to fail any
roles/*/{files,templates}/**/Dockerfile* whose FROM does not reference an
ARG (${VAR}) or Jinja variable ({{ var }}). FROM scratch and a
`# nocheck: from-parameterized` per-line marker are exempt.

Refactor web-app-bluesky login-broker Dockerfile to satisfy the rule:
surface the base image and version via services.broker.{base_image,base_version}
-> vars/main.yml -> docker-compose build args -> ARGs in the Dockerfile.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 20:43:18 +02:00
kevinveenbirkenbach 3c033451da Update services.yml 2026-06-03 17:39:10 +02:00
kevinveenbirkenbach a55a3c311f Update services.yml 2026-06-03 17:38:46 +02:00
kevinveenbirkenbach 3ac5dcf2b4 Update services.yml 2026-06-03 17:38:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a71c5a2de6 docs(requirements): add 025 web-app-matrix ansible flavor
New requirement for adding a second deployment flavor to web-app-matrix
that delegates to the upstream matrix-docker-ansible-deploy (MDAD)
playbook. The existing role-internal compose stack stays selectable as
the `compose` flavor for one deprecation window.

Captures 12 proposed design decisions for operator review before
implementation: flavor dispatch in services.matrix.flavor (ansible
default; compose deprecated), MDAD pulled in as a pinned git checkout at
deploy time (not vendored), MDAD inventory templated from
lookup('config', ...), every supported bridge + Jitsi + Element Call
enabled by default when no operator-issued third-party token is needed,
central svc-db-postgres + svc-db-redis + Mailu reuse, sys-svc-proxy
fronts MDAD's services (MDAD's bundled traefik disabled), four variants
(V1 ansible all-on, V2 ansible all-off, V3 ansible ldap-only, Vcompose-1
regression), two new Playwright specs (element-call, bridge-roster),
lifecycle: alpha on the new flavor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 12:01:19 +02:00
kevinveenbirkenbach d142431f9c Release version 9.2.0 v9.2.0 2026-06-03 10:37:49 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1324f26407 refactor(service_is_stateful): drop | default(false) now that the flag is SPOT-defined
`service_is_stateful` is set unconditionally to `false` in
group_vars/all/18_swarm.yml; per-role overrides in vars/main.yml win by
Ansible precedence. The `| default(false)` fallback is no longer reachable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 10:06:20 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fa4eea5a86 refactor(deployment-mode): SPOT-define DEPLOYMENT_MODE in group_vars
Replace the host-scoped set_fact resolvers in sys-svc-compose/tasks/01_core.yml
and tasks/stages/01_constructor.yml with a single group_vars definition driven
by group_names. Uppercase the variable to match the SPOT convention and drop
every `| default('compose')` fallback now that the resolution is unconditional.

Role-var opt-out on a swarm host still works via Ansible's role-vars > group_vars
precedence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 10:06:20 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c861a46c36 fix(web-app-erpnext): conform meta/volumes.yml + vars to mapping schema
The new meta/volumes.yml schema (enforced by
test_every_volumes_yml_entry_has_mandatory_name) requires each entry
to be a mapping with a mandatory `name:` string. erpnext was still
shipping the legacy plain-string form, which broke the lookup-schema
test in turn — consumer code referencing `volumes.sites` (no
sub-attribute) fails test_every_volumes_lookup_resolves_to_declared_attr.

Promote each volume entry to a mapping with `name:` and append `.name`
to the three matching `lookup('config', application_id, 'volumes.…')`
calls in vars/main.yml. The autoformat also normalized
roles/web-app-mediawiki/meta/volumes.yml's mode quoting from single to
double — included in the same commit because it landed in the same
autoformat sweep that preceded the test run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 09:53:56 +02:00
kevinveenbirkenbach cd14ded859 Merge branch 'main' into feature/swarm-nfs 2026-06-03 09:53:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 46286bae4e fix(dev-nix): purge script no longer dies when nixbld1/2 are missing
On a fresh Ubuntu (the swarm pilot's jrei/systemd-ubuntu base) neither
nixbld1 nor nixbld2 exist, so `getent passwd nixbld1` returns exit code
2. `set -euo pipefail` makes the whole script abort silently — ansible
reported `rc: 2 / stderr: '' / stdout: ''` with no useful output, and
iter13 spent ages tracing it to dev-nix being pulled in transitively
via pkgmgr → dev-git → dev-make → dev-nix on every compose-deploying
role.

Wrap both `getent` calls with `2>/dev/null | ... || true`. The
existing `[[ -z "$uid1" || -z "$uid2" ]]` guard then catches the empty
values and the script exits 0 with `[UNCHANGED] nixbld1/nixbld2 not
both present, nothing to purge` — the documented idempotent path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 09:53:56 +02:00
kevinveenbirkenbach 57322c300a Merge pull request #297 from kevinveenbirkenbach/feature/web-app-erpnext
feat(web-app-erpnext): ship ERPNext role with OIDC SSO, LDAP, outbound mail
2026-06-03 09:53:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 deefd6dff1 fix(sys-svc-compose): swarm task resolve works across nodes and skips stateful services
Three coupled fixes for the Controller≠Target topology the swarm pilot
exposed in iter12, iter15, and iter17/iter18:

1. `swarm_resolve_container.sh` now locates task containers via the
   swarm state API (`docker service ps`, then `docker inspect TASK
   --format '{{.Status.ContainerStatus.ContainerID}}'`) instead of a
   local `docker ps --filter label=...` on the manager. A replica that
   landed on a worker stayed invisible to the manager-side filter and
   the role-level wait spun out after 60 retries (iter17 / iter18).
   The state check accepts both `running` and `starting` so the
   resolve unblocks before the app's healthcheck has had to pass —
   first-deploy apps like mediawiki need their install_or_update.sh
   step BEFORE they can become healthy.

2. `resolve_container.yml` invokes the script via an absolute
   `{{ playbook_dir }}/roles/sys-svc-compose/files/...` path. When the
   include_tasks was pulled in from another role (e.g. svc-db-mariadb's
   02_init.yml), ansible's `ansible.builtin.script` file-lookup
   searched the calling role's `files/` directory and aborted with
   "Could not find or access 'swarm_resolve_container.sh'" (iter15).

3. `post_deploy.yml` gates the swarm task resolve on
   `not service_is_stateful`. Stateful services (svc-docker-registry,
   svc-db-mariadb) deploy as plain compose containers on the manager,
   not as replicated swarm services — there is no `STACK_SERVICE` task
   to resolve, so the role spun out for 60 retries waiting for one
   that would never exist (iter12).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 09:53:56 +02:00
kevinveenbirkenbach 320316b801 Merge branch 'main' into feature/web-app-erpnext 2026-06-03 09:53:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3476c20f0e revert(swarm-pilot): drop apt-cacher-ng bootstrap proxy
iter11/iter12/iter14 each hung nfs-server's `apt-get update` in TCP
CLOSE_WAIT against apt-cache:3142 for 5-60 minutes, even with
Pipeline-Depth=0 + No-Keep-Alive + Retries=3 + Timeout=30. Pulling once
into the cache and fanning out to four nodes saves ~4 minutes of
bootstrap when it works, but every run that hit the hang lost 20+
minutes — net negative.

Remove the workflow cache restore step, the 02b_start_apt_cache.sh
launcher, the apt.conf.d/01-cacher write in 05_bootstrap_nodes.sh, and
the apt-cache cleanup in 17_cleanup.sh. Each node now talks directly
to the upstream mirrors. Slower-but-reliable beats faster-but-stuck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 09:53:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 277ce98ca2 fix(web-app-erpnext): move subnet to 192.168.105.176/28 to avoid web-app-jitsi clash
The 192.168.105.160/28 slot is also claimed by the open web-app-jitsi PR
(#287). CI's network-uniqueness lint catches the overlap once both branches
are checked together. Move to .176/28 (next free slot in the 105.x band).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 09:53:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 39c990a7ad feat(web-app-erpnext): ship ERPNext role with OIDC SSO, LDAP, outbound mail
Implements requirement docs/requirements/024-web-app-erpnext.md.

Stack (per frappe_docker v15 single-image multi-role pattern):
- configurator (one-shot bench set-config: db_host/port, three redis URLs
  split by DB number 0/1/2 for cache/queue/socketio, socketio_port, host_name
  HTTPS)
- backend (gunicorn), frontend (nginx-entrypoint.sh), websocket (socketio),
  scheduler, queue-short (--queue short,default), queue-long
  (--queue long,default,short)

Central-service consumer pattern (Decision #7):
- ERPNEXT_DB_ROOT_PASSWORD branches on services.mariadb.shared: central
  svc-db-mariadb root pw when true, per-role MariaDB consumer pw when false
- Redis via in-compose `redis` alias, 3-DB-number split for Frappe's logical
  buses
- Explicit erpnext_assets volume mount on every container (image's
  VOLUME ['sites/assets'] otherwise creates anonymous volumes that fail the
  hlth-volumes check)

Bench bootstrap (Decision #9):
- bench new-site --install-app erpnext with 30-min hard timeout
- Post-install restart of all long-lived Frappe containers (otherwise the
  cached app set serves HTTP 500 on /login)
- TCP-port readiness probe (HTTP would 404 pre-site)
- API-bot user seed + setup_complete=1 via apply_api_bot.py

OIDC (Decisions #4 + #5):
- redirect_uris filter auto-includes ERPNext in the shared Keycloak client
- Social Login Key wired via apply_oidc_settings.py with
  social_login_provider=Keycloak (delete+recreate due to set_only_once),
  custom_base_url=1, relative authorize/token/userinfo paths,
  auth_url_data={response_type:code,scope:openid}, login_via_keycloak
  redirect path
- Group-to-Frappe-role mapping persisted as a frappe.db default; per-login
  reconciliation hook deferred to follow-up (documented in README)

LDAP (V3): LDAP Settings doctype applied via apply_ldap_source.py. End-to-end
LDAP login (auto-create Frappe user from LDAP bind) deferred to follow-up;
LDAP-login Playwright specs intentionally absent in v1 (documented).

Email (Decision #8, outbound only): outbound Email Account applied via
apply_email_account.py using db_insert/db_update to bypass Frappe's
deploy-time SMTP socket validation. IMAP inbound deferred to follow-up.

Variant matrix (Decision #11): V1 sso+ldap, V2 all-false, V3 ldap-only.
Three variants verified end-to-end via
`make compose-deploy mode=reinstall apps=web-app-erpnext full_cycle=true
purge=true` — 6 PLAY RECAPs, all failed=0, all Playwright runs
passed-or-skipped with no failures.

Playwright specs: test-landing, test-csp-headers, test-login-administrator
(OIDC), test-login-biber (OIDC), test-login-native-administrator
(break-glass local login, always runs), test-guest-persona. _shared.js
exposes signInViaErpnextOidc + signInViaErpnextLocal.

Repo-wide lint addition: tests/lint/ansible/modules/test_task_name_length.py
caps Ansible task `name:` strings at 120 chars (opt-out via
`# nocheck: name-length`). Fixed two pre-existing violations in
web-app-keycloak/tasks/update/ldap/per_app_mapper_one.yml.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 09:53:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b895322a63 docs(requirements): add 024 web-app-erpnext with OIDC SSO
New requirement for a `web-app-erpnext` role that integrates ERPNext
(Frappe Framework) into Infinito.Nexus with OIDC-direct SSO against
Keycloak, reusing central `svc-db-mariadb` and `svc-db-redis` (three
Redis logical roles split by DB number 0/1/2 for cache/queue/socketio).

Captures 13 proposed design decisions for operator review before
implementation: upstream `frappe/erpnext` image pinning, central-service
reuse (Decision #7), wizard-bypass `bench` bootstrap (Decision #9),
Keycloak-group → Frappe-role reconciliation (Decision #5), Mailu IMAP
inbound + SMTP outbound (Decision #8), and the standard
V1/V2/V3 variant matrix (Decision #11) mirroring web-app-kix /
web-app-zammad. Out of scope for v1: multi-site / multi-tenant Frappe
(Decision #12).

Modeled on the layout precedents from web-app-odoo (ERP shape,
dual HTTP + WebSocket vhost) and web-app-zammad (central-service
reuse pattern, three-variant matrix).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 09:53:55 +02:00
infinito-nexus-updater[bot] 92e997db09 update(images): update Docker image versions 2026-06-03 07:18:03 +00:00
kevinveenbirkenbach 0c4ee01ea2 Merge pull request #297 from kevinveenbirkenbach/feature/web-app-erpnext
feat(web-app-erpnext): ship ERPNext role with OIDC SSO, LDAP, outbound mail
2026-06-03 08:57:37 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c160ec45e3 fix(swarm-pilot): force one-shot apt connections to apt-cacher-ng
iter11 hung nfs-server's `apt-get update` for 16+ minutes in CLOSE_WAIT
against apt-cache:3142 even with Pipeline-Depth=0 — the cache closed
each TCP slot but apt's HTTP method held onto them via keep-alive and
never noticed the half-close.

Set `Acquire::http::No-Keep-Alive "true"` so every apt request opens a
fresh socket and tears it down on completion (no shared state to leak),
plus a 30 s `Acquire::http::Timeout` so any future stuck fetch errors
out fast enough for the Retries=3 backoff to actually kick in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 00:49:16 +02:00
kevinveenbirkenbachandClaude Opus 4.7 cb65e4e319 fix(svc-docker-registry): order after svc-docker-swarm via run_after
iter9 aborted at the registry deploy with
`failed to create network registry: ... This node is not a swarm manager`.
sys-svc-compose auto-sets `deployment_mode='swarm'` when the host is in
the svc-docker-swarm group, which makes networks.yml.j2 render the
registry's default network as `driver: overlay`. compose-up then tries
to create that overlay before `docker swarm init` has run, because the
generated tasks/groups/svc-docker-roles.yml orders roles alphabetically
(registry < swarm < swarm-manager).

Declare the dependency explicitly via run_after on svc-docker-swarm and
the matching `services.swarm` flag the registry's compose chain
genuinely consumes (it lives in a swarm overlay; the dependency is
hard, not cosmetic — hence the `# nocheck: dynamic-flag` literal `true`
in place of the usual `'<role>' in group_names` Jinja). The group-file
regenerator now emits registry after svc-docker-swarm, so overlay
creation hits an already-initialized cluster.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-03 00:23:14 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a21bf1ece5 fix(sys-ctl-cln-anon-volumes): resolve dockreap via systemd PATH, not controller-side lookup
`lookup('command_path', 'dockreap')` runs on the Ansible controller and
calls `shutil.which('dockreap')` against the controller's PATH. That
works for single-host deploys (controller == target), but in the
swarm-NFS pilot the controller is the GHA runner / act-runner and
dockreap is only ever installed on the target swarm nodes. Result:
`command not found in PATH: 'dockreap'` aborted every compose deploy
once iter6/iter7/iter8 got past the daemon.json fix and reached the
first sys-ctl-cln-anon-volumes invocation.

Write the systemd unit with a bare command name + explicit PATH= in
[Service]. systemd resolves dockreap at unit-start time from the
target's own filesystem (where sys-pip-install put it), so the
controller no longer needs the binary at all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 23:38:08 +02:00
kevinveenbirkenbachandClaude Opus 4.7 437a67bf39 fix(swarm-pilot): sudo the act-runner-side .deb build for non-root GHA runners
In CI the workflow runs as the unprivileged `runner` user, so
`scripts/install/package.sh`'s `apt-get update && apt-get install
build-essential debhelper dpkg-dev rsync` aborted with `E: Could not open
lock file /var/lib/apt/lists/lock - open (13: Permission denied)` before
the .deb was ever produced. Local act runs as root inside
catthehacker/ubuntu so we never hit it.

Detect non-root in 05_bootstrap_nodes.sh and prepend `sudo -E` to the
build invocation. Inside the swarm-node containers package.sh still
runs as root and never goes through sudo (those exec calls are
unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 23:38:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e0fc6c462e fix(svc-docker-registry): use sys-svc-compose, not sys-stk-backend
sys-stk-backend pulls the full backup chain (sys-svc-rdbms +
sys-bkp-container-2-local), and the latter's systemd unit renders via
`lookup('command_path', 'dockreap')` — which fails at registry-deploy
time because the `sys-pip-install` step that installs `dockreap` runs
later in the play. iter6 hit `command_path: command not found in PATH:
'dockreap'` here and the play aborted on swarm-mgr-01.

The registry is a stateless image cache for swarm workers — it has no
database, no backup target, and no SSO proxy. sys-svc-compose covers
exactly what it needs: compose render + handler flush.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 23:04:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 66954e45ff fix(swarm-pilot): scope apt-cacher-ng proxy to bootstrap only
Leaving the apt-cacher-ng proxy enabled past the .deb-deps install made the
later ansible-driven apt tasks (sys-svc-ssh, update, ...) hang on TCP
CLOSE_WAIT against the cache even with Pipeline-Depth=0: the connections
the proxy closed upstream were never picked up by ansible's apt method on
the next invocation. The cache only pays for itself during the parallel
4-node .deb-deps fetch anyway — afterwards the proxy is pure friction.

Remove /etc/apt/apt.conf.d/01-cacher at the end of provision_node so
every ansible apt task hits the upstream mirrors directly, undisturbed
by the cache's keep-alive semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 23:04:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 773579da5d fix(swarm-pilot): allow systemd init in jrei/systemd-ubuntu on GHA runners
jrei/systemd-ubuntu:24.04 has been exiting with code 255 in ~0.4s on
ubuntu-24.04 GHA runners since image build 20260525.161, producing zero
stdout/stderr (the docker-logs fallback from d7e859041 captured nothing).
`--privileged` alone no longer disables AppArmor + seccomp on the host
runner image, so systemd can't take ownership of its cgroup / signal mask
and PID 1 aborts before it logs anything.

Add the three flags that, in combination, restore the same isolation
posture systemd-in-docker has historically had:

  --cgroupns=host                    let systemd see the host cgroup tree
                                     (otherwise the unified hierarchy is
                                     remapped under /sys/fs/cgroup/system
                                     and pid 1 refuses to manage it)
  --security-opt seccomp=unconfined  drop the default seccomp filter that
                                     now blocks io_uring/clone3 calls
                                     systemd-255 uses during init
  --security-opt apparmor=unconfined explicit override; --privileged is
                                     supposed to imply this but the new
                                     runner image's docker.service-level
                                     profile re-applies it on top

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 22:39:28 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3b2e581b29 fix(swarm-pilot): disable apt http pipelining when proxying via apt-cacher-ng
iter4 hung swarm-wrk-02's `apt-get update` for 5+ minutes in TCP CLOSE_WAIT
against the apt-cacher-ng container: 3 sockets to apt-cache:3142 were closed
upstream, but apt's http method kept reusing pipelined slots and never
detected EOF.

`Acquire::http::Pipeline-Depth "0"` forces apt to one request per connection,
which is the documented workaround for apt-cacher-ng (see acng manual
section 4 "Common Apt Configuration Problems"). Bundled defaults:
* `No-Cache "false"` lets validated cache hits short-circuit IMS checks
* `Retries "3"` makes transient proxy errors recoverable

Switched the inline apt.conf write from a fragile single-line `printf
> file` to a heredoc piped through `docker exec -i`, since the new content
spans multiple lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 22:39:27 +02:00
kevinveenbirkenbachandClaude Opus 4.7 67f54ddca3 fix(swarm-pilot): include swarm.registry in extras + make registry-push script executable
The pilot's /tmp/swarm-nfs-extras.yml is passed to ansible via -e @file
which makes its top-level keys (including swarm:) fully *replace* the
group_vars/all/18_swarm.yml swarm dict — silently dropping swarm.registry
and breaking the daemon.json.j2 + handler chain with
"object of type 'dict' has no attribute 'registry'" once iter3 hit the
Configure docker daemon storage driver task on the workers.

09_write_extras.sh now emits swarm.registry.host=${MGR} (swarm-mgr-01,
docker-DNS-resolvable from the workers) and port=5000, matching the
group_vars defaults.

Bundled: the lint-python EXE001 violation on the new
roles/sys-svc-compose/files/swarm_registry_push.py — the shebang was
present but the file was not chmod +x.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 22:24:46 +02:00