da729cecec reads .Options.device when a volume declares a bind, on the premise that only compose volumes carry no .Options. A swarm node can hold the volume object without them anyway: in run 33923190220 the DR drill probe reported 'exposes no backing store for kix_kix_config' on the node that held it. The script then falls back to .Mountpoint, the node-local _data that is empty while the stack is down, finds no config there and exits 0.
Pass the inventory's own path as PATCH_DATA_DIR, the same DIR_VAR_LIB/<volume> that compose_volumes.py bakes into driver_opts.device, and try it when the volume-derived path holds nothing.
The second half is the asymmetry that hid this: a config present but unmatched fails loudly since 2c4e0e18b1, while a config found nowhere passed silently. The silent case is the dangerous one. KIX kept the pre-rotation password, its backend crash-looped on exit 255, and the converge gate only gave up 44 retries later. A total miss now prints SKIPPED with both paths it tried, and stays non-fatal so a first install still works.
The suite covered three of the seven consuming roles; it now covers all seven, plus the fallback and the named miss.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docker mounts a bind-backed volume's device only while a container holds it, so .Mountpoint is an empty _data whenever the stack is down - which is exactly when the credential realign runs. The script then failed its 'test -f' and exited 0, reporting ok instead of changed, so a rotated password never reached the app's persisted config.
Read .Options.device when .Options.o declares a bind; compose volumes carry no .Options and keep the .Mountpoint path. Affects every role shipping tasks/utils/patch/db_credentials.yml: friendica, joomla, kix, matomo, moodle, nextcloud, suitecrm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The script ran sed and printed PATCHED unconditionally, so an expression that
matched no line reported success and the deploy believed a rotated password had
reached the application's own config. Nothing downstream could tell the difference.
Adding the check found two expressions that could never have worked:
friendica sed: -e expression #1, char 44: invalid reference \2 on 's' command
kix sed: -e expression #1, char 59: Invalid content of \{\}
Both spell their groups as (...) with \1 \2, which is extended-regex syntax, while
the script called basic-regex sed. Under sed -E all five declarations match a sample
of the line they target -- friendica, matomo, moodle, nextcloud and kix -- so the fix
is the flag, not the expressions.
That makes 06f5905d64 incomplete: it moved friendica's patch in front of the health
gate, but the patch itself aborted sed on every run and said PATCHED anyway.
The dry pass uses the same expression the write will use, so a matching line is
proven before the file is touched rather than after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The body was a Jinja-interpolated shell string inside the task. As a file it
can be read, shellchecked and run, and the three parameters travel through the
environment the way the password already did.
That forces one thing into the open. The old expression carried
${PATCH_PASSWORD} and the shell expanded it while assembling the command; a
value arriving in an environment variable is not expanded a second time, so the
substitution has to be explicit. The consumers now write @PASSWORD@ and the
script replaces it, which is also the version a reader can follow without
knowing that rule.
The script refuses to run without its parameters rather than treating an unset
one as empty, because an empty volume name would look exactly like a node that
does not hold the volume: a silent success on every host.
Tests move with it, to the files/ segment every other test of a shipped script
uses. They run it against a stubbed `container` in three states per consumer -
no volume, volume without the config, both present - and assert the marker the
task reads as a change. Five mutations of the script were checked against them:
dropping the substitution, the marker, the existence test, the parameter guards
and the volume filter each turn the suite red.
Layout follows what the linters already require: files/shell/ is enforced by
role-files-layout, and 65e34746fc names ansible.builtin.script as the sanctioned
way to run one from a task.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>