fix(fm-watch): read certsync health off host files, drop docker exec - #75
Closed
Freudator86 wants to merge 1 commit into
Closed
fix(fm-watch): read certsync health off host files, drop docker exec#75Freudator86 wants to merge 1 commit into
Freudator86 wants to merge 1 commit into
Conversation
The certsync health check ran `docker compose exec -T certsync certsync status`. On this fleet's own accounts - deliberately not in the docker group - that exec is denied at unix:///var/run/docker.sock, so since the honesty fix (PR #73) the check honestly but permanently reports "cannot run: status command failed", giving no read at all on whether certsync is well. Read certsync's status directly off the host instead. certsync now exposes its heartbeat JSON and sqlite state DB under a readable host bind mount (see the certsync repo's docs/deploy.md, "State host path"); `certsync status` computes healthy/reason purely from those two files plus the daemon-state argument, so certsync_health_reason runs certsync's own build_status via python3 against them (PYTHONPATH=$FM_CERTSYNC_SRC, default $FM_CERTSYNC_PROJECT/src). No docker socket, no exec, no docker-group membership, no socket proxy - the read needs no docker access at all, and reproduces the exact healthy/reason JSON the exec produced. Reading frozen files loses the one liveness signal exec gave for free: exec failed when the container was down. Reinstate it as a heartbeat-freshness bound (FM_CERTSYNC_HEARTBEAT_MAX_AGE, default 7200s = 2x the 3600s max sync interval, 0 disables): a healthy:true reading whose heartbeat has gone stale reads as unhealthy, never quiet, so a stopped container or a run of failing syncs can never read as healthy off stale files. This also closes a pre-existing blind spot where failing syncs read healthy under the exec path. Preflight now checks python3/jq and the certsync source tree; each failure keeps its own distinct "cannot run: ..." reason. Tests rewritten to the file-read model with added coverage for the freshness gate and the source-unavailable preflight. docs/configuration.md updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
Author
|
Superseded by #78, raised through the sanctioned no-mistakes channel. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Since the honesty fix (#73), the certsync health check honestly reports that it
cannot run rather than silently reading as healthy. Reproduced live on
2026-08-05:
docker compose execagainstprojects/hlr-certsyncreturnspermission denied on
unix:///var/run/docker.sock, because this account isdeliberately not in the
dockergroup. Net effect today: no read at all onwhether the certificate sync service is well.
Adding the account to the
dockergroup, or fronting the socket with anexec-capable proxy, was explicitly out of scope — exec through a proxy is
root-equivalent on the host and is not a scoped read however narrowly the proxy
is named.
What this does
certsync statusalready computeshealthy/reasonpurely from the sqlitestate DB and the heartbeat JSON, plus a caller-supplied
--daemon-stateargument (which this check has always passed as the literal
running). There isno container-only input to that computation. So instead of exec-ing into the
container,
certsync_health_reasonnow runs certsync's ownbuild_statusdirectly against those two files on the host, via
python3withPYTHONPATH=$FM_CERTSYNC_SRC(default$FM_CERTSYNC_PROJECT/src).This removes the need for docker access entirely — no socket, no
exec, nodocker-group membership, no proxy — and reproduces the exacthealthy/reasonJSON the exec path produced (verified byte-for-byte against real certsync state
files).
certsync exposes the two files on a readable host path in a paired PR in the
certsync repo (
heavyliftrental/hlr-certsync, branchfm/hlr-certsync-health-read-access): it backs/var/lib/hlr-certsyncwith ahost bind mount instead of a docker-managed named volume. Deploy order: that
compose PR lands and is deployed first (see its
docs/deploy.md→ "State hostpath"); this check reads the exposed files.
Liveness: heartbeat freshness gate (engineering decision — please confirm)
Reading frozen files loses the one liveness signal
execgave for free:execfailed when the container was down. Without replacing it, a stopped container
whose last-written files still say
successwould read as healthy off stalefiles — the exact silent-healthy failure #73 exists to prevent.
The heartbeat timestamp is in the exposed files, so I reinstated liveness as a
freshness bound:
FM_CERTSYNC_HEARTBEAT_MAX_AGE(default 7200s = 2× the3600s worst-case sync interval;
0disables). Ahealthy:truereading whoseheartbeat has gone stale now reads as
unhealthy: heartbeat stale (...), neverquiet. The daemon rewrites the heartbeat on every successful sync pass
(including no-op passes), so a live service never trips the bound.
Two consequences worth a reviewer's eye:
failing syncs (heartbeat frozen at the last success,
last_run_statealwayssuccess, no poison) read as healthy. It now surfaces once stale.FM_CERTSYNC_HEARTBEAT_MAX_AGE=0to restore the raw
build_statusverdict with no freshness gate.Operator step (not done here — deployment is out of scope)
After both PRs merge, on
hlr-1, provision the readable state dir andrecreate the container so the bind mount takes effect:
(Full migration notes in the certsync PR's
docs/deploy.md.)Tests
tests/fm-watch-triage.test.shcertsync cases rewritten to the file-read model(a hermetic fake
hlr_certsyncpackage stands in for the source tree), with newcoverage for the freshness gate (
healthy+ stale heartbeat → unhealthy) andthe source-unavailable preflight. All 7 certsync cases plus the run-bounded
regression pass locally.
Merge note
This repo forbids squash merges — merge with a merge commit or rebase, not
squash.