Commit Graph
16 Commits
Author SHA1 Message Date
kevinveenbirkenbach d2abd332f0 fix(web-app-suitecrm): let the external-auth provisioning path create its user
SuiteCRM 8.10.2 tightened User::hasSaveAccess() from return true to is_admin($current_user) for a record without an id, and restrictAdminOnlyFields() now forces status Inactive on such a record. CreateUser::create() runs while the browser is still anonymous, because it is the LDAP and SAML auto-create path, so every FIRST login of a directory account ends in RuntimeException: Not authorized and HTTP 500 on POST /login. The pre-existing administrator is unaffected, which is why one persona passes and one fails in both auth modes. 451f63cbdd carried the role from 8.9.1 to 8.10.2; 8.9.1 exempted a new record from both guards.

The patch raises the admin context around that one save and restores it in a finally, rather than relaxing the guards, which stay closed for every request-driven save. create() has a single caller in its own file, createExternalAuthUser, which sets is_admin 0 and external_auth_only 1 before calling, so the raised context cannot mint an administrator. patch runs with -F0 so a hunk upstream has moved fails the build instead of landing somewhere else.

The patch path is derived once in vars/main.yml and reaches the Dockerfile as a build arg, following the role's existing SUITECRM_ENTRYPOINT_SCRIPT_* shape, so the file name is written once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 495d23dbde)

# Conflicts:
#	roles/web-app-suitecrm/vars/main.yml
2026-10-16 13:29:07 +02:00
kevinveenbirkenbachandClaude Opus 5.5 3cefb01d71 fix(web-app-suitecrm): keep sessions in Redis so replicas share them
With three swarm replicas the PHP session files on the shared NFS volume
were read as empty under concurrent requests of one browser, so SuiteCRM
answered 403 on /api/graphql and dropped the session. Sessions now live in
Redis with session locking that waits like flock did; the phpredis version
is pinned in services.yml with its own update source, and the engine lookup
renders the save path.

Refs #879

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 18:31:19 +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 b53d207667 fix(suitecrm): fetch the IdP descriptor with curl, not wget
Every swarm task of the tor variant died 1.4s after start with exit 8, so the
stack never converged and the drill failed at sys-svc-compose's converge wait
(CI run 32119014832, job 95662863105). The task log held exactly one line: the
entrypoint's "Fetching the IdP signing certificate" - nothing after it.

GNU wget cannot parse a socks5h:// proxy. It aborts before sending anything
with "Error parsing proxy URL: Unsupported scheme.", which src/retr.c maps to
PROXERR and src/exits.c to WGET_EXIT_SERVER_ERROR, i.e. 8 - identical in 1.21
through 1.25. Under -qO- the message is suppressed, which is why the container
log ends mid-sentence. Reproduced locally: the same URL without a proxy exits
4, with the socks5h proxy exits 8.

The proxy was injected container-wide as http_proxy/https_proxy, gated on
Keycloak's domain but applied to every destination - and the CI Tor network
has no exit nodes, so that also dead-ended every clearnet call the app makes.
The three sibling roles that solved this (wordpress, nextcloud, joomla) each
gate per destination host instead. Here the only server-side onion call is the
descriptor fetch itself: the SAML bundle is configured statically, so it needs
no backchannel, and the healthcheck already forces --noproxy '*'. The proxy
now reaches exactly the one client that needs it.

curl's timeouts are pinned so a stalled onion cannot hang the boot: 21
attempts of at most 30s bound the fetch at ~10.5min, inside the 20m
start_period and the ~28min converge budget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:28:15 +02:00
kevinveenbirkenbachandClaude Opus 5 85e3e30659 fix(suitecrm): make the boot mutex survivable and stamp the seeded version
59de61ec4 sized the stale-lock window at 1800s but left the waiter's budget at
300 tries of sleep 5, i.e. 1500s. A replica that restarts promptly after a
leader was killed mid-install therefore gives up 300s before it is ever allowed
to break that leader's lock, and only recovers on the next restart. The budget
goes to 420 tries so it always outlives the window it is waiting on. The
`continue` after the stale reap also skipped the sleep, so a persistently
failing rmdir spun hot without ever reaching the timeout; the loop now falls
through to the sleep and retries the mkdir from there.

The seed stamp stored the literal "seeded", so bumping app_version produced a
new image whose tree was never copied into the volume - silently running old
code forever. It now stores the version, warns loudly when image and tree
disagree, and adopts a legacy "seeded" stamp without noise. It deliberately
does NOT re-seed: new code on an unmigrated schema is worse than old code that
works, and there is no suitecrm:app:upgrade step in this role yet. That needs
SUITECRM_APP_VERSION in the runtime image, which the final stage did not carry.

59de61ec4 also established that root-owned trees serve permanent 500s
(live-verified there: chown turned GET / from 500 to 200) but chowned only the
two cache dirs. The installer runs as root and leaves config.php, custom/ and
upload/ behind as well, so public/legacy gets the same treatment once, on the
boot that installed. Scoped to public/legacy on purpose: ${APP_DIR}/extensions
holds a read-only config mount that a recursive chown would abort the container
on under set -e.

healthcheck.html was written on every boot and requested by nobody - the curl
probe hits / because meta/services.yml declares no path - so it goes, and
exec "$@" replaces the hardcoded apache2-foreground that made the Dockerfile's
CMD a lie.

Verified by running the unmodified script in a throwaway php:8.2-cli image with
a stubbed bin/console, seven scenarios: first boot with a read-only config
mount below extensions/ (chown survives it, CMD is reached), restart, legacy
stamp adoption, version mismatch warning, stale lock reclaim, missing
SUITECRM_APP_VERSION aborting legibly, and the non-leader path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:02:27 +02:00
kevinveenbirkenbachandClaude Opus 5 3e03413dcb fix(suitecrm): scope the Composer superuser flag to the autoload dump
d87cb4a51 set COMPOSER_ALLOW_SUPERUSER=1 for the whole builder stage, which
enables plugins for all four composer invocations while only the autoload dump
needs one. Two consequences of that blast radius: public/legacy/composer.json
allow-lists wikimedia/composer-merge-plugin, so the legacy install would start
running a plugin it never ran before, and PluginManager throws
PluginBlockedException non-interactively for a plugin missing from
allow-plugins, so a future app_version bump could abort the build at the very
first composer install instead of never evaluating that condition at all.

The three installs go back to running with plugins disabled, exactly as they
did in the builds CI already proved green. Only dump-autoload gets the flag:
DumpAutoloadCommand calls setRunScripts(true) unconditionally, so it always
dispatches POST_AUTOLOAD_DUMP, which is the event symfony/runtime's plugin
writes vendor/autoload_runtime.php from.

Proven by an A/B build in the role's own shape (php image, composer binary
copied in, open_basedir set to defeat Composer's docker detection): after
`composer install` without the flag `test ! -f vendor/autoload_runtime.php`
passes, and a single `COMPOSER_ALLOW_SUPERUSER=1 composer dump-autoload` makes
`test -f` pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:02:27 +02:00
kevinveenbirkenbachandClaude Opus 5 55c89a619a fix(suitecrm): allow Composer plugins as root in the builder
Composer disables plugins when it runs as root without
COMPOSER_ALLOW_SUPERUSER; the env var is the first short-circuit of that
check in Application.php. The builder copies the composer binary out of
composer:2, and a binary copy leaves that image's own ENV behind, so every
composer call in the stage ran with plugins off. symfony/runtime writes
vendor/autoload_runtime.php from its ComposerPlugin, so the file was never
generated and the test -f guard after the seed COPY failed the build. Both
SuiteCRM swarm jobs of run 31927586060 died there, after the retry handler
and the no-cache rebuild.

e6fb78af4 removed the other blocker, where --no-scripts suppressed the
post-autoload-dump dispatch; this removes the one behind it. Composer skips
the root check entirely inside a container (Platform::isDocker), which is
why the defect only ever surfaces in a build layer and never in a run.

Proven by an A/B build in the same shape: php image, copied composer
binary, open_basedir set to defeat the docker detection. Without the
variable the build fails with "The symfony/runtime plugin was not loaded as
plugins are disabled"; with it the guard passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:02:26 +02:00
kevinveenbirkenbachandClaude Opus 5 e6fb78af49 fix(suitecrm): generate the Symfony runtime autoloader in the image
public/index.php requires vendor/autoload_runtime.php, which symfony/runtime
writes from its ComposerPlugin on the post-autoload-dump event. Composer's
AutoloadGenerator guards that dispatch behind `if ($this->runScripts)`, and
every composer install in this Dockerfile passes --no-scripts, so the event
never fired and the file was never written. The image shipped, then fataled
on the first request.

A dump-autoload without --no-scripts fires the event. It fires nothing else
here: SuiteCRM 8.9.1 declares no post-autoload-dump script, and its
auto-scripts (cache:clear, cache:warmup, assets:install,
scrm:copy-legacy-assets) plus Google\Task\Composer::cleanup hang off
post-install-cmd, which dump-autoload does not dispatch. The only listener
left is the runtime plugin.

It runs after the --no-dev prune rather than after the first install, so the
autoloader reflects the vendor tree that actually ships and nothing re-dumps
behind it.

The test -f after the seed COPY turns a future regression into a failed
build instead of an image that only breaks at runtime.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 02:21:47 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2f5cf82533 test(lint): require apt Acquire::Retries in Dockerfiles
A transient Debian/Ubuntu mirror fetch aborts a docker build layer with exit 100 (it killed the web-app-nextcloud swarm openldap build while the same image built fine elsewhere). New lint under tests/lint/docker/dockerfile/apt/ flags any RUN with apt-get update/install that omits Acquire::Retries; retrofit all 15 apt Dockerfiles with -o Acquire::Retries=3. Both apt lints move into the apt/ subpackage. dnf retries by default and apk has no equivalent flag, so both stay out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 03:01:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 97624ba1f5 fix(suitecrm): drop the frontend build caches so backups skip them
The Angular build cache and the Yarn dependency cache land under the app dir, which the first-run seed copies into the persistent suitecrm_data volume; baudolo backs up every docker volume with no per-volume exclude, so the remote-backup pull choked on the thousands of tiny .angular/cache files (4.2G peak, exit 1). Both caches are regenerable and unused at runtime once the frontend is built and node_modules is gone, so they are removed right after yarn build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 02:39:28 +02:00
kevinveenbirkenbachandClaude Opus 4.8 3b11f3f38b fix(suitecrm): prune composer dev packages after the scss build and pin APP_ENV=prod
scssphp (pscss) is a require-dev tool, so dev packages must survive the root install until the theme scss compile; a --no-dev re-install afterwards prunes them, dropping the Symfony web profiler whose /_wdt XHRs violate the CSP. APP_ENV is pinned to prod because the pruned image no longer ships the dev-scoped bundles from bundles.php and would crash the kernel in dev. The legacy composer install never needed dev packages and installs --no-dev directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:44:35 +02:00
kevinveenbirkenbachandClaude Fable 5 c3d6657311 fix(suitecrm): seed the app tree from the image instead of volume copy-up
Baking the multi-GB tree into /var/www/html made dockerd's volume
copy-on-first-use flood the shared NFS export from every swarm node in
parallel, so the stack never converged. The Dockerfile becomes a
multi-stage build (shared base with PHP/Apache setup, builder producing
the tree without node_modules, final stage with one COPY --chown layer
under /usr/src/suitecrm) and the entrypoint seeds the empty webroot
volume once, under the existing cross-replica boot lock; installed
volumes are only stamped, never overwritten. Ownership and the
writable-dir modes are baked at build, dropping the runtime chown pass
that crashed on the read-only ldap.yaml config mount. start_period
grows to 20m with the lock ceilings recoupled (live lab NFS measures
the seed at roughly a minute). App dir, seed dir and web user/group are
single-point role vars threaded through build args, entrypoint and
php.ini; the FROM lint now accepts file-internal stage references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:44:17 +02:00
kevinveenbirkenbachandClaude Opus 4.7 7779fea529 fix(dockerfiles): purge apt lists before update + lint + taiga rename
Move `rm -rf /var/lib/apt/lists/*` before `apt-get update` in every
Debian-based Dockerfile so a stale base-image-baked InRelease
(Valid-Until expired) cannot abort the build layer with exit 100. The
trailing rm at end-of-RUN never ran because `apt-get update` aborted
first; the leading rm forces a fresh fetch from the configured source.

Affected: moodle, bridgy-fed, bookwyrm (2 RUN), postmarks, taiga
backend, svc-db-openldap, svc-db-postgres, jenkins, joomla, mediawiki,
odoo, suitecrm (2 RUN), wordpress.

Add tests/lint/docker/dockerfile/test_apt_update_purge_lists.py to
enforce the contract repo-wide: every `RUN` containing `apt-get
update` MUST `rm -rf /var/lib/apt/lists/*` first in the same RUN, and
MUST NOT pass `Acquire::Check-Valid-Until=false` (which would silence
apt's replay-protection rather than refresh the list).

Rename roles/web-app-taiga/templates/Dockerfile.{back,front}.j2 to
Dockerfile.{backend,frontend}.j2 and update the matching tasks/
main.yml template refs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:43:42 +02:00
Environment TestandClaude Sonnet 4.6 1db58af7fd fix(docker): replace ENV-based ENTRYPOINT with RUN symlink in espocrm and suitecrm
The ENV approach failed at runtime because the ca-trust-wrapper does not
propagate container ENV variables when executing the wrapped ENTRYPOINT,
causing $ESPOCRM_ENTRYPOINT_SCRIPT_DOCKER / $SUITECRM_ENTRYPOINT_SCRIPT_DOCKER
to remain unexpanded ("not found" error in CI).

Replace with a RUN-time symlink (ln -sf) so the dynamic ARG path is baked
into the image filesystem layer. ENTRYPOINT now uses the fixed /entrypoint.sh
path, which is independent of ENV propagation and cache state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 21:48:18 +02:00
Environment TestandClaude Sonnet 4.6 1c026ce6ef fix(docker): remove quoted FROM and fix ENTRYPOINT ARG expansion in 4 roles
- Remove quotes from FROM instruction in confluence, jira, suitecrm, espocrm Dockerfiles
  (Docker does not strip quotes — quoted image references are invalid)
- Promote ENTRYPOINT_SCRIPT_DOCKER ARG to ENV in espocrm and suitecrm so the
  runtime shell can resolve the path (ARG is not expanded in ENTRYPOINT exec form)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:40:28 +02:00
kevinveenbirkenbachandClaude Sonnet 4.6 f5a81736cf refactor(roles): convert Dockerfile.j2 templates to plain files/Dockerfile
Replace Jinja2 Dockerfile templates with static Dockerfiles using Docker
ARG statements. Variables are now passed as build args in compose.yml.j2,
following the existing pattern from web-app-bridgy-fed. Affects 12 roles:

- svc-db-openldap
- web-app-bookwyrm
- web-app-confluence
- web-app-espocrm
- web-app-jira
- web-app-joomla
- web-app-mediawiki
- web-app-pretix
- web-app-shopware
- web-app-suitecrm
- web-app-xwiki
- web-svc-simpleicons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 00:28:37 +02:00