Commit Graph
22 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 3e7bf106a3 fix(web-app-moodle): keep the component cache in debug deployments
config.php set `$CFG->debug = 32767` whenever MODE_DEBUG was true, as
the development and CI inventories set it. Moodle 5.3's
`core_component::is_developer()` treats any debug bit defined in
config.php as developer mode, so `init()` skipped
`$CFG->cachedir/core_component.php` and ran `fill_all_caches()` on
every PHP request: a walk over every plugin directory and every
`classes/` tree. The autoload in setup.php triggers it before the
ABORT_AFTER_CONFIG return, so even font.php and requirejs.php paid it.

On local disk the walk is cheap. In swarm the code tree is an NFS4
mount, and the walk turns into GETATTR round trips: main run
39494396920 served `/theme/font.php` in no less than 1.6 s and
`/lib/requirejs.php` in no less than 2.0 s, the five FPM workers per
replica queued, and the CSP check timed out loading the academy page
after 20 s on both Moodle swarm jobs.

Write `debug` and `debugdisplay` into the site config with
`admin/cli/cfg.php` instead, on every deploy and as integers (32767/1
in debug deployments, 0/0 otherwise). `is_developer()` reads only
values defined in config.php, so the component cache is honoured,
while `setup.php` still applies the stored debug level once the
database is up. The assignments leave config.php entirely, because
cfg.php refuses to change a name config.php defines, and
MOODLE_DEBUG leaves env.j2 with them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 12:06:59 +02:00
kevinveenbirkenbachandClaude Opus 5.5 0c748f74ed fix(web-app-moodle): swap the code tree only after a complete copy
The entrypoint removed the old tree before it copied the new one. A
copy that failed left the volume without a working Moodle, and every
restart removed again instead of recovering. It now copies into a
staging directory inside the volume and swaps the trees once the copy
is complete, so a failed copy leaves the old tree in place.

An image that ships an older release than the volume records no longer
replaces it and exits with a message instead. Two instances on
different releases would otherwise overwrite each other's tree.

upgrade.php reports "No upgrade needed" on stderr, so the task counted
every run on a current site as a change. It reads stderr now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 10:45:29 +02:00
kevinveenbirkenbach 402aa2fe9d Merge remote-tracking branch 'origin/main' into update/version-sources-web-app-moodle-20261005-4f455e2 2026-10-07 02:10:13 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d1eee3e26f fix(web-app-moodle): run Moodle 5.3 and upgrade existing sites to it
The pin moved from 4.5.11 to 5.3.0, and the role fitted neither a new
install nor an existing one.

Moodle 5 serves from public/ and routes every other path through
r.php. nginx now uses public/ as its root, hands unknown paths to the
router and passes PATH_INFO for scripts with slash arguments, and
config.php declares the router as configured. The paths the role checks
and writes (version.php, auth, webservice, the OIDC plugin) moved below
public/, the URL of the OIDC plugin stays auth/oidc. The source archive
no longer contains vendor/, so the image installs the Composer
dependencies it declares. The OIDC plugin is pinned to v5.2.1, the
newest release.

An existing site kept its old code: the entrypoint filled the code
volume once and never again. It now records the release it copied and
replaces the tree when the image carries another one, and a new task
runs admin/cli/upgrade.php. Plugins that were installed through the web
interface are not carried over, the README says how to ship them.

A site installed with 5.3 forces login by default, which 4.5 did not,
so its start page answers 303 to the login page and the webserver
health check refused it. The role declares 200 and 303 as the codes
that check accepts: 200 is what an upgraded site keeps answering.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 02:10:13 +02:00
kevinveenbirkenbachandClaude Opus 5.5 0f7fb92e12 refactor(roles): take seven roles' memory parameters from the container limit
Memcached, Joomla, Moodle, Odoo, Shopware, Snipe-IT and SuiteCRM set
their cache size, PHP memory_limit, OPcache, worker limits and search
heap as literals next to a mem_limit they never read. They now ask
lookup('service_memory_mb') for a share of that limit. The shares are
chosen so every rendered value stays what it was.

Where the parameter lives in a file that cannot hold a template, the
value is handed in: Shopware's init script reads INIT_PHP_MEMORY_LIMIT
and refuses to start without it, SuiteCRM's image takes it as a build
argument. Shopware's worker gets --memory-limit as a command appended
to its entrypoint, and Moodle's OPcache size moves from the static ini
into the rendered one.

The performance.php.memory_limit keys of Joomla and Moodle are gone
from services.yml; nothing else read them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 23:24:22 +02:00
kevinveenbirkenbachandClaude Opus 5 1cd9578b83 fix(moodle): fetch the source from a host that answers the runners
download.moodle.org returns 403 to GitHub-hosted runners while serving the identical artefact elsewhere: 12 failures across three runners over twelve hours, each 40-66ms after TLS, with curl's four retries and Ansible's three exhausted. The GitHub tag archive is content-equivalent - both tarballs extracted and diffed over 39312 entries, the only delta being githash.php, which nothing in the 4.5.11 tree includes - and needs --strip-components=1 because it roots at moodle-4.5.11/ rather than moodle/.

auth-oidc.sh was the next landmine, two Dockerfile lines behind the fixed one: an anonymous api.github.com tag query, 60 requests per hour per runner IP, no retry, -s swallowing the rate-limit body under set -e, and a floating plugin version as the build input. It now reads a new meta/addons/auth_oidc.yml, which declares bridges: [sso] and update.monitored so the version is watched by the update pipeline instead of frozen.

MOODLE_BRANCH and MOODLE_TARBALL_FILENAME die with the old URL; the latter was already dead, demanded only by its own :?required guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 13:46:35 +02:00
kevinveenbirkenbachandClaude Opus 5 0e98fd4259 fix(moodle): let the entrypoint fail on a config it could not write
Four unmarked `|| true` remained in this file after c505ce7466 grandfathered
the corpus; they entered later and left the file in violation of shell-or-true.
Handling them beats marking them.

cp -f writing config.php is the one that mattered: swallowing its failure let
Moodle boot against a missing or stale configuration instead of stopping. It
now fails.

The chown and chmod calls can only fail for lack of privilege, so the root
check states that condition instead of discarding every error. The image
declares no USER over php, and the compose template sets no user:, so this
runs as root today; the check guards a future USER rather than an observed
failure.

moodle_refresh_config inverts its guard to an early return so the copy sits one
level shallower.

The five grandfathered `|| true` keep their nocheck and their TODO, including
the chown three lines above the one hardened here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 13:22:26 +02:00
kevinveenbirkenbachandClaude Opus 5 7162b7bf4c Merge fork/feature/svc-net-tor into feature/ai-agent-employees
834 commits from the Tor onion-service branch against 299 of ours, from merge
base 7b646bdece: 127 conflicted files, 3506 files changed.

The two sides are largely orthogonal. Theirs adds svc-net-tor and a `tor` service
across the role tree, plus two sweeping refactors: shipped scripts sorted by
language (files/ruby/, files/python/, files/shell/, scripts/lint/*.sh) and the
unit suite moved under tests/unit/python/. Ours is requirement 036, the native AI
gateway, plus the MCP role work. Most conflicts were both sides appending to the
same service map, resolved as unions.

Resolutions that were not unions:

Credentials moved namespace. Theirs renamed meta/schema.yml to meta/secrets.yml
and re-addressed credentials as secrets.credentials.<name>. Git carried our
litellm_api_key blocks through the rename, but every consumer we added still read
'credentials.<name>', which no longer resolves. All 45 call sites are migrated.

plugins/filter/container_volumes.py is deleted by theirs and relocated to
plugins/lookup/container_volumes.py, where it carries the same tmpfs defect our
f85996b43 had just fixed: a hard-coded {"size": ...} that silently drops a per
mount `mode`. The fix is re-applied in the relocated file, as that commit's merge
note anticipated.

compose.yml keeps only INFINITO_PLAYWRIGHT_REPORTS_BASE_DIR. Theirs also forwards
INFINITO_PLAYWRIGHT_STAGE_BASE_DIR; that line is deliberately dropped, per the
analysis recorded in 45804e9ad -- STAGE has a drift-proof role-vars fallback,
while forwarding it promotes a setdefault-pinnable .env value above that SPOT.

tasks/stages/01_constructor.yml takes their include, ./tasks/utils/setup/group.yml,
because their refactor removed tasks/groups/*-roles.yml, leaving our include
target nonexistent. Our task name is kept, since svc-virt is in the loop.

Makefile swarm-zombie is a union: their validate plus teardown, then our
lab_subnet cleanup, which their side dropped while leaving lab_subnet.sh as the
only orphaned reference in the file.

01_bootstrap.sh keeps our deterministic --ip pinning, which distinguishes an
already-attached node from a real failure, over their `|| true`.

Their language-sorted layout orphaned references our side still held: zammad's
two scripts/apply/*.rb, jenkins' java-with-ca-trust.sh, and the notify topics
their handlers renamed to hyphenated form. All corrected.

roles/sys-svc-compose/files/shell/swarm/stack_ready.sh gains our latched-update
block: paused, rollback_paused, rollback_started and rollback_completed exit 2
rather than polling a state that cannot resolve without another deploy.

config_from_run is unioned to (title, logged, jobs); the production caller in
trigger/__main__.py now passes jobs, without which our suite suppression would
have been inert.

READMEs are regenerated rather than hand-merged: 15 LiteLLM rows and 91 Tor rows
now derive from the merged metadata, which is itself evidence the meta merge is
coherent.

Verified: 5161 unit tests pass. The single failure, test_query.py TestSortSpec,
also fails on fork/feature/svc-net-tor at b51d2f9919, measured in a baseline
worktree -- so this merge introduces no new unit-test failure. Two modules are
excluded from that run because `baudolo`, a dependency their branch introduces,
is not installed on this host.
Not verified: no deploy has run against the merged tree, and make quality has not
been run end to end here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:32:30 +02:00
kevinveenbirkenbachandClaude Opus 5 5e810c4a5a fix(moodle): accept MCP requests under the internal service name
Moodle 303-redirects every request whose Host is not $CFG->wwwroot, and an MCP
client dialling the internal service name cannot follow that redirect from the
container network. The previous accommodation was on the wrong side: the probe
sent -H 'Host: <public domain>' and -H 'X-Forwarded-Proto: https' and pinned
mcp_probe_host to MOODLE_DOMAIN, so it asserted the endpoint works when spoofed
and said nothing about how a real client reaches it.

Both headers and the probe host are gone. Acceptance moves into config.php,
which reads MOODLE_INTERNAL_HOST -- rendered from MOODLE_MCP_SERVICE_KEY -- and,
when the incoming HTTP_HOST matches it, rewrites HTTP_HOST and SERVER_NAME to
the wwwroot host before Moodle's redirect logic runs. The comparison splits on
':' so a port cannot defeat the match, and a request from any other host is
untouched, leaving the canonical-host protection intact for real traffic.

None of that could have taken effect on a deployed instance. The bootstrap seeds
the code directory with `cp -an`, which never clobbers, so an existing volume
keeps its first-run config.php forever and every later config change is silently
discarded. moodle_refresh_config now copies the deployment-managed config.php on
every start, which is what makes the rewrite above reachable at all.

Not verified: no MCP call has been observed succeeding against the internal name
since the change. This is also the same failure class that ended the last
deploy on another role, where the client already shared the provider's network
and had the right alias registered, yet the DNS search suffix still resolved the
single-label name to the public ingress. If that applies here too, the Host fix
is correct and the request still never arrives -- and removing the spoofing
headers will surface it as a failure where the probe previously passed.

Two further consequences worth stating: config.php is now overwritten on every
start, so an operator edit inside the container is discarded; and the HTTP_HOST
rewrite runs in the hot path of every page load, narrow though its condition is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 14:15:01 +02:00
kevinveenbirkenbach fd6bc6e7f4 fix(lint): read the mirrored-test scope from the suffix, not the directory
A role-files-layout exemption frees a tree from files/<lang>/ because
its import paths are fixed from outside. The mirrored-test lint derived
its scope from that same directory, so every layout exemption silently
became a test exemption nobody ever argued for.

The language now comes from the file suffix. Test code is excluded by
scope rather than exempted: a test-* role and a files/playwright/ or
files/test/ tree are the tests, so demanding a test of them is circular.
The eleven files this newly reaches are framework-instantiated and carry
a stated reason each.
2026-08-22 22:32:31 +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 0be527abb0 fix(web-app-moodle): give the local cache a node-local home
Moodle kept everything under dataroot, which is the shared NFS export in swarm
mode - including localcachedir. Upstream is explicit that this is the one
directory that must not be shared: config-dist.php marks tempdir and cachedir
as 'MUST BE SHARED by all cluster nodes' and localcachedir as 'Intended for
local node caching'.

The path is declared once, as the mount target in meta/volumes.yml, and every
other place derives from it: vars/main.yml reads it back through
lookup('volume', ...), compose passes it as a build argument, the Dockerfile
exports it, the entrypoint requires it with :? and creates it with the runtime
owner, env.j2 hands it to config.php as MOODLE_LOCALCACHEDIR. The two mounts of
the same volume share one YAML anchor rather than repeating the literal.

The cron service mounts it too, because it runs the same code and would
otherwise write its cache somewhere else than the web service reads it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 22:05:43 +02:00
kevinveenbirkenbachandClaude Opus 5 f2fe5e94b1 feat(moodle): serve MCP through the build-time webservice plugin
Install the protocol plugin during the image build, because a Moodle
plugin must match the pinned release and installing it at runtime would
leave the two free to drift.

Provision the webservice and its token through PHP, and attach only
read-only functions, so the read-only tool policy is enforced by the
service definition rather than declared in metadata.

Address the endpoint as its canonical origin. Moodle compares every
request against wwwroot, so a probe over the internal HTTP origin was
answered with an HTML redirect instead of JSON and every retry hit the
same wall; the verify step now sends the canonical Host and the
forwarded-proto header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 22:21:04 +02:00
kevinveenbirkenbach 1a0af0ce09 Merge remote-tracking branch 'fork/feature/swarm-nfs' into feature/svc-net-tor
# Conflicts:
#	roles/sys-ctl-mtn-cert-deploy/tasks/main.yml
#	roles/sys-svc-compose-ca/tasks/main.yml
#	roles/web-app-dashboard/templates/javascript/oidc.js.j2
#	roles/web-app-mastodon/tasks/01_overrides.yml
#	roles/web-app-peertube/templates/compose.yml.j2
#	scripts/github/guide/compose_deploy.sh
#	scripts/github/guide/host_deploy.sh
#	scripts/make/alias.sh
#	tests/lint/ansible/roles/test_service_core_first_task_run_once.py
2026-07-24 04:30:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 07d31d9001 fix(ci): retry-harden every egress path (curl filter, shell downloads, package managers)
Transient CDN/mirror TLS resets are a recurring hard-fail class (docker.com, PyPI). Harden all three egress paths at their SPOT: the curl filter now emits --retry-all-errors (retries=0 opts a call out), so every ansible curl retries; a new lint requires --retry-all-errors on file-downloading curl and --tries on wget in verbatim files/*.sh (which cannot use the filter), fixing the moodle/alias downloads it flagged; and the update role configures native retries for every package manager - apt Acquire::Retries, dnf retries=, pacman XferCommand over retrying curl - covering all five CI distros (ubuntu shares apt, centos shares dnf).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:01:31 +02:00
kevinveenbirkenbachandClaude Fable 5 d88f272f9f Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:31:08 +02:00
kevinveenbirkenbachandClaude Fable 5 c505ce7466 test(lint): forbid unmarked '|| true' in shell scripts; grandfather the corpus
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:39:16 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7bc5822421 fix(moodle): survive flock EIO on the NFS bootstrap lock and widen the converge budget
Six replicas race the bootstrap lock across three NFS clients; the truncating open invalidated the holder's lock state, so flock died with EIO and the task crash-looped with exit 65 until the winner finished the ~6min code copy. The lock file is now opened append-only and flock failures fall back to a sentinel recheck loop instead of killing PID 1. The converge budget gets the same 600-retry override the other slow-bootstrap roles already carry; the default 150 missed this run's recovery by six seconds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 19:53:20 +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 Fable 5 15f95f0725 fix(curl): timeout-hardened curl SPOT filter, lint, and fleet-wide rollout
A curl against an accepted-but-silent peer hangs forever: task retries
never fire because the first attempt never returns, and the CI job dies
at the 6h runner cut (proven by the mailu admin-API hang that took the
suitecrm/peertube swarm legs down). The new `curl` filter renders the
hardened base command ("{{ 30 | curl }}" -> curl -s --connect-timeout 5
--max-time 30); scaling composes with lookup('timeout').

test_curl_via_filter enforces it: bare curl invocations in role tasks,
templates and shipped scripts fail the lint with a pointer to the
filter. Plain .sh files, which cannot render Jinja, pass by pinning
--max-time inline; Docker healthchecks are exempt (their timeout field
caps them); `# nocheck: curl-timeout` opts out (catalogued in
suppression.md).

Convert all 34 existing invocations: mailu (replacing the short-lived
MAILU_CURL var), elasticsearch, checkmk, yourls (300s for the
schema-creating installer POST), the telegram alert script, and the
plain scripts of jellyfin, svc-runner, apt_repo, moodle (600s tarball)
and the mattermost provisioner. Mailu's user/token tasks additionally
carry task-level timeouts so a wedged container exec cannot stall the
play.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:37:28 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5c3749c437 fix(web-app-moodle): make the code-volume bootstrap race-safe on shared NFS
moodle runs replicas:3 sharing one NFS code volume. The entrypoint guarded its cp -a with a version.php TOCTOU check, so several replicas copied concurrently; losers hit 'File exists' and, under set -euo pipefail, aborted before chown + php-fpm, leaving an incomplete/unreadable tree -> install_database.php failed Permission denied (rc 255). Serialize the bootstrap under flock, gate on an atomic .bootstrap.done sentinel, cp -an || true so a benign collision can't abort, make the tree world-readable before the install (root_squash neuters the in-container chown), and make losing replicas wait for the sentinel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 00:50:53 +02:00
kevinveenbirkenbach 63ddd24aaf feat(req-015): self-built Moodle image, OIDC+LDAP hybrid + LDAP-only variants 2026-05-04 18:37:03 +02:00