Commit Graph
28 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 96408f23a0 perf(healthcheck): let the start window outlast a real boot
start_interval is the cadence while start_period runs, and only while it runs.
Once the window closes the probe falls back to interval, so a service that
becomes ready just after it pays a whole tick anyway and the cadence never
reached it. Six of the seven flavors were shaped that way, Curl worst: a thirty
second window against a one minute interval.

Measured across 151 converge waits in four job logs, 63 sit exactly on a tick
boundary, 36 in the thirty second step and 27 in the sixty second one.
Attributed by application, web-app-seaweedfs is the largest at 1501s over
sixteen waits with a median of 57.7s: it probes with curl and fell straight into
the gap. web-app-zammad probes with connect, whose window is fifteen minutes,
and its 154s median is real boot time with nothing to recover.

So the window moves to ten minutes and the steady state interval to two, and the
flavor overrides that sat below them go: Curl, Wget and Connect lose interval
1m, Http its twenty second window, Nc its ten. Connect keeps fifteen minutes.

Neither number costs anything while a container works. start_period delays
nothing, it only stops failures counting, and interval no longer touches
convergence at all since that hangs on the window alone; it is steady state
load, which drops from 120 probes an hour per service to 30.

Forty six role files follow. Twenty seven declarations go that repeat the new
default or undercut it, an undercut window being the defect itself. Eight
windows longer than ten minutes keep their value and now say why. The literal
blocks and the three Dockerfiles this repository builds spell every timing out,
because outside the lookup an omission takes docker's value and not the
platform's.

A fourth lint rule keeps the shape: start_period must outlast one interval,
checked against the effective values, so raising interval without the window
cannot quietly bring the cliff back.

Two faults in the lint surfaced while building it and are fixed here. It
compared timings as strings, so 600s and 10m read as different values, and it
matched the healthcheck key exactly, so a line carrying a trailing nocheck was
skipped whole, which is how the two swarm-init partials went unexamined.

One regression, bounded. A container that starts and never turns healthy is
neither rejected nor errored, so stack_ready.sh only calls it fatal after three
killed tasks, and each cycle now spans the longer window. That reaches twenty
minutes rather than six, capped there by swarm_stack_converge_retries at 600
with a two second delay, and it lands only on runs that were failing anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 21:54:26 +02:00
kevinveenbirkenbachandClaude Opus 5 16be22007b fix(healthcheck): spell the timings out where the SPOT cannot reach
A healthcheck written outside the container_healthcheck lookup inherits nothing
from utils/docker/healthcheck, and docker offers nowhere to put a replacement.
daemon.json carries storage, logging, DNS, registries and runtimes, and no
health key at all, so an omitted timing lands on docker's own value rather than
the platform's. The two disagree where it matters most: docker starts a
container with no start period, which leaves start_interval no window to act in.
That is why the hand-written blocks never gained the cadence.

The three images this repository builds itself now carry it. openldap and
postgres also drop to the platform interval, and bluesky gains the start window
it never had:

  svc-db-openldap/files/Dockerfile         --interval=30s ... --start-interval=5s
  svc-db-postgres/templates/Dockerfile.j2  --interval=30s ... --start-interval=5s
  web-app-bluesky/.../Dockerfile           --start-period=30s --start-interval=5s

Six literal blocks move to the platform interval as well: magento, redis,
elasticsearch, matrix and both sys-svc-rdbms partials. Since start_interval
exists, interval no longer touches convergence and is steady state load alone.

Sixteen values that stay where they are now name the mechanism that keeps them
there. Two are worth reading. web-app-kix overrides start_period and nothing
else on purpose, letting the image's own HEALTHCHECK supply test, interval,
timeout and retries; filling those in would have overwritten what upstream tuned.
The memcached and redis swarm-init partials probe every second and deliberately
run with no start window, which is why start_interval is absent rather than
forgotten.

The rest name their probe: occ boots the whole Nextcloud stack, tor re-parses
its torrc instead of asking a running service, opensearch is still forming a
cluster, matrix waits on a file another step writes to a share.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 21:15:49 +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 5137465c6f style: delete every comment the comments-valid policy rejects
3529 invalid comments in 528 files, grandfathered in while the linter
only saw the working-tree diff. Removal ran through the linter's own
collectors, so detection and deletion cannot drift: a comment whose
opener line is blank before it and whose closer line is blank after it
loses all its lines, anything else loses only the comment region, and a
line left holding a bare delimiter goes with it.

Five files keep their comments behind a nocheck marker instead. Four are
upstream configs whose inline docs are upstream's own (mautrix, systemd
journald, nextcloud-oidc-login, MediaWiki). In littlejs the
commented-out entries ARE the deactivation mechanism for an example
tile, so deleting them would delete configuration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 03:22:02 +02:00
kevinveenbirkenbachandClaude Opus 5 04eac9d73d fix(svc-db-tests): report why the SOCKS connect failed instead of swallowing it
The Tor-path credential tests tunnel their client through a local SOCKS5 shim.
When the CONNECT fails, handle() closed the client socket and returned, so the
client library saw nothing but an empty socket and reported whatever its own
generic read-EOF error is.

For MySQL that is `(2013, 'Lost connection to MySQL server during query')` —
which claims a query was in flight when none ever was. Fediwall 1 printed that
twenty times and it sent the investigation looking for a server that had hung
up, when in truth no HiddenServicePort existed behind the dialled onion port.
Postgres and ldap3 are less misleading ("server closed the connection
unexpectedly", "Connection reset by peer") but equally silent about the cause.

Reproduced for all three against a SOCKS5 proxy that completes the no-auth
greeting and rejects CONNECT with rep=5, running the real python block out of
each test.sh: every one now prints `socks connect failed: SOCKS5 CONNECT failed
rep=5` ahead of its library error.

This does not change any verdict — the root cause is fixed by 2ba3a14b9, where
tor_reachable requires both `services.tor.enabled` and an `exposed: true` that
actually earns a forward. This only stops the next occurrence from lying.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:33:32 +02:00
kevinveenbirkenbachandClaude Opus 5 2ba3a14b98 refactor(svc-db): gate the Tor credential tests on one shared lookup
The three svc-db-* credential tests each hand-rolled the same transport
gate in their test.env.j2:

    (lookup('config', application_id, 'services.tor.enabled') | bool
     and lookup('config', application_id, 'services.<entity>.exposed') | bool)

Three copies of one predicate, each naming its own entity key, and each
free to drift from the onion mapping that actually decides reachability.

Extract it into plugins/lookup/tor_reachable.py, split the same way as
its sibling tor_ports.py: a plain is_reachable() for callers holding the
merged applications view, plus a thin LookupModule resolving it through
lookup('applications'). Because collect_exposed_ports() scans
services.values(), the entity name disappears from the templates
entirely -- they collapse to

    TOR_ENABLED={{ lookup('tor_reachable', application_id) | lower }}

Rework the integration guard accordingly: it now asserts that every
svc-db-* template delegates to the shared lookup, and drives its
base/variant agreement table through the real is_reachable() instead of
re-implementing the predicate. Add unit coverage for the new lookup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 00:50:47 +02:00
kevinveenbirkenbachandClaude Opus 5 00c02251c0 Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Every conflicted hunk resolves onto the swarm-nfs side, because on all six
files it is the strict superset: it carries the display-name codec
(utils/roles/display.py) the CI role selections now encode through, and the
sequencing glyph move from 910748ec4. svc-net-tor holds no unique content on
any of them -- runs.py even had non-conflicted hunks already calling `codec`,
so keeping its side would have left a NameError.

The lint fallout is settled in the same commit, as 4afc9b770 did for the
previous merge: the shell-or-true lint lives on svc-net-tor (c505ce746) and
swarm-nfs brings two unmarked `|| true` sites with it. Both get a nocheck
reason rather than a rewrite, because neither swallows anything -- the
slaptest call is the measured exception of c4bbbba93 and has_cn_config decides
right below it, and the dockerd restart is followed by an is-active poll that
fatals. In both the exit code was never the authority.

make quality green end to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:47:15 +02:00
kevinveenbirkenbachandClaude Opus 5 c4bbbba933 fix(svc-db-openldap): let the cn=config check decide, not slaptest's exit
slaptest converts slapd.conf into slapd.d and only then runs slap_startup,
which opens the database that this very entrypoint seeds a few lines later.
On a fresh volume that open fails, slaptest exits 1 with "Restore from
backup!", and under set -e the container dies. It heals by accident because
slapd.d is already written, so the task swarm starts next takes the
has_cn_config branch, but it costs a dead task and its startup delay on every
fresh volume.

Measured against slaptest 2.6: the failing run writes cn=config.ldif,
olcDatabase={-1}frontend, {0}config, {1}mdb and every schema, so the
conversion is complete and only the exit code is misleading. A parse error
behaves the opposite way and writes nothing at all, as does an unwritable
target directory, so the has_cn_config check right below still catches every
case where the conversion did not happen.

Not -u: that switch makes slaptest a pure dry run. It exits 0 and leaves the
target directory empty, which would turn a single dead task into a container
that can never start.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:09:53 +02:00
kevinveenbirkenbachandClaude Fable 5 62a46e472f Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Resolves the runner Dockerfile to the lint-standard Acquire::Retries=3
and keeps the collabora TODO wording; hardens the merged tree for the
stricter lints (apt retries in the tor Dockerfile, shell-or-true
markers, comment cleanup).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:27:25 +02:00
kevinveenbirkenbachandClaude Fable 5 f4e8930579 fix(svc-db): give the onion credential tests tor's 120s window
A cold onion connect alone measured 15.7s and tor holds SOCKS streams
up to 120s while fetching the descriptor; the 30s caps cut cold
circuits mid-handshake across all three forwarders and DB clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 15:37:19 +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 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 a4f423da70 build(deps): replace python-ldap with pure-python ldap3 for the Tor credential test
python-ldap is the only project dependency that compiles a C extension,
and its sdist build (Python.h, OpenLDAP/SASL headers) broke the image
build since it was added. The svc-db-openldap Tor credential test is its
sole consumer, so it now binds via ldap3 instead, keeping pyproject
compile-free like psycopg2-binary and PyMySQL. The Ansible ldap_* module
prerequisite handling inside the openldap role is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 01:57:14 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4e582c9d86 fix(svc-db): satisfy shellcheck + env-sort lint on the DB test scaffolding
test.sh: dispatch auth via a run_auth wrapper calling auth_tor/auth_local directly — shellcheck SC2317 flagged the indirect ${attempt_fn} invocation as unreachable. test.env.j2: sort keys ASC for the env alphabetical-order lint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:53:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 111e8cc7b2 feat(svc-db): per-DB exposed toggle + Tor/local credential tests
Add exposed: bool (default false) to svc-db-postgres/mariadb/openldap meta/services.yml with a variant-0 (exposed) / variant-1 (not) toggle, plus real credential e2e tests (files/test/test.sh + templates/test.env.j2): authenticate with actual credentials over the node Tor SOCKS proxy when the variant enables tor, locally in-container (password auth) otherwise. mariadb/openldap force the local publish (+ OPENLDAP_PORT var); secrets stay off the process argv (MYSQL_PWD, ldapwhoami -y /dev/stdin, PGPASSWORD env).

Depends on shared onion pieces landing in a follow-up commit: tor_ports exposed-port collection, svc-net-tor services.tor.node + services.tor.ports.local.socks SPOT, the tor_socks lookup, and test-e2e-cli 0600/stage-cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:53: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
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
kevinveenbirkenbach 8a81630391 Added newline at file and POSIX compatible 2026-02-04 19:49:56 +01:00
kevinveenbirkenbach 64b902fcbf Updated image and version to make it mirror compatible 2026-02-03 01:38:43 +01:00
kevinveenbirkenbach 0afaabc494 feat(openldap): make env vars mandatory and enable configurable slapd logging
- Enforce required LDAP_* environment variables (fail fast on startup)
- Add configurable LDAP_LOG_LEVEL for slapd runtime logging
- Enable detailed stats logging in debug mode, stats in normal mode
- Route slapd logs to stdout for docker logs visibility

https://chatgpt.com/share/69773341-01ac-800f-8d0e-429398b6dcc5
2026-01-26 10:26:07 +01:00
kevinveenbirkenbach 5e097c02f1 fix(shellcheck): address lint findings across scripts
- openldap: use find instead of ls for module path detection
- docker-volumes health: robust whitelist parsing + quote vars
- btrfs health: quote path
- docker CLI installer: silence SC2031 false-positive for /etc/os-release vars
- test-dns: use loop counter
- chess: require APP_KEY_FILE env
- espocrm: log normalized seed flags to avoid unused vars
- shopware: disable SC2016 for php -r blocks (PHP vars in single quotes)
- fix script: add shebang + strict mode

https://chatgpt.com/share/69712026-7288-800f-b1d0-701e31754362
2026-01-21 19:51:12 +01:00
kevinveenbirkenbach 7c10cb6fe5 fix(openldap,keycloak): allow user binds and enable LDAP debug logging
- allow simple bind for normal LDAP users (userPassword access rules)
- relax generic read access for directory browsing
- enable Keycloak LDAP and LDAPSDK debug logging
- normalize database config formatting

https://chatgpt.com/share/696cefa5-e308-800f-aef6-fc71cc9a811e
2026-01-18 15:35:10 +01:00
kevinveenbirkenbach 1c3b20cbad **svc-db-openldap: preload memberof/refint modules during bootstrap**
Load `memberof` and `refint` in `slapd.conf` before generating `cn=config` via `slaptest`, so the `memberOf` attribute is known and user entry updates no longer fail with “entry update failed”.

https://chatgpt.com/share/696be95b-68cc-800f-a883-fe2755a0a6cc
2026-01-17 20:55:57 +01:00
kevinveenbirkenbach 505af3e2ad cli: make --log require explicit directory and create parents automatically
- Change --log flag to require explicit log directory (--log <LOG_DIR>)
- Automatically create log directory with parents=True
- Harden permissions best-effort (0700)
- Update CLI help text accordingly
- Adjust Makefile test targets to pass explicit log directory
- Ignore logs/ via .gitignore and .dockerignore
- Refactor flag parsing to correctly detect deploy with --log
- Add dedicated logging unit tests
- Update existing tests for new log semantics
- Remove deprecated -i alias for --id in deploy command

https://chatgpt.com/share/6969680a-dbf8-800f-aafb-3d32d6212ba0
2026-01-15 23:19:31 +01:00
kevinveenbirkenbach 60a9fb7393 **fix(openldap): allow RootDN manage access to cn=config for schema tooling**
Grant `${LDAP_ROOT_DN}` manage rights on the `config` database (in addition to SASL EXTERNAL) so tools like `ldapsm` can read/write `cn=config` over LDAP and no longer fail with “No such object”.

https://chatgpt.com/share/6966da0f-b458-800f-8e40-97a919b976f5
2026-01-14 01:08:44 +01:00
kevinveenbirkenbach 9639496280 fix(openldap): add config backend to slapd.conf and read module DN from filesystem
- Add explicit `database config` backend so cn=config is accessible via ldapi
- Fix access rules for EXTERNAL SASL on cn=config
- Resolve module DN from /etc/ldap/slapd.d instead of ldapsearch
- Simplify docker-compose role inclusion and network wiring

https://chatgpt.com/share/6966da0f-b458-800f-8e40-97a919b976f5
2026-01-14 00:49:32 +01:00
kevinveenbirkenbach 77c3e6b9fa fix(openldap): robust bootstrap via slaptest, deterministic MDB init, and hardened runtime perms
- Rework Docker image to start from a clean slapd.d and data dir
- Bootstrap cn=config via slaptest from generated slapd.conf
- Deterministic MDB initialization (creates data.mdb on first start)
- Enforce correct ownership and permissions for slapd, config, and DB
- Add logging and fatal guards for broken bootstrap states
- Make memberof check optional in healthcheck
- Ensure base LDAP tree and OUs via Ansible (root, people, groups, roles)
- Cleanup Docker hardening vars

Result: stable, reproducible OpenLDAP container startup with clean volumes.

https://chatgpt.com/share/6966b019-d604-800f-86c2-d6ee04129130
2026-01-13 21:50:20 +01:00
kevinveenbirkenbach 01d625aad3 **feat(openldap): replace Bitnami OpenLDAP with custom Debian image + ldapi EXTERNAL config**
Migrate `svc-db-openldap` from `bitnamilegacy/openldap` to a locally built `infinito/openldap:local` image (Debian bookworm-slim) with a bootstrap `entrypoint.sh` that initializes `slapd.d` and an MDB backend from `LDAP_SUFFIX/LDAP_ROOT_DN/LDAP_ROOT_PW`.

* Add Dockerfile + entrypoint + healthcheck scripts
* Persist config + data in separate volumes (`/etc/ldap/slapd.d`, `/var/lib/ldap`)
* Switch cn=config operations to `ldapi:///` with SASL EXTERNAL (`-Q -Y EXTERNAL`)
* Simplify env template to only `LDAP_SUFFIX`, `LDAP_ROOT_DN`, `LDAP_ROOT_PW`
* Refactor docker init into `02_docker.yml` and update compose template (build context)
* Update LDAP tasks/handlers to use `OPENLDAP_BIND_DN/OPENLDAP_BIND_PW`
* Remove outdated Bitnami-specific docs and credential reset logic

https://chatgpt.com/share/6965c921-3304-800f-9e8f-d2f10d273f1b
2026-01-13 05:24:25 +01:00