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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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
- 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
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
- 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
- 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
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