mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 18:51:32 +00:00
b767f579ef
Closes Bundle 4 of the 2026-05-02 deployment-target coverage audit
(see cowork/deployment-target-audit-2026-05-02/RESULTS.md). Pre-fix,
DeployCertificate called deploy.AtomicWriteFile twice — once for
cert at L123, once for key at L131 — instead of bundling both into
a single deploy.Plan and calling deploy.Apply. Three downstream
hazards:
1. If cert write succeeds and key write fails, the cert is already
on disk. The in-line best-effort cert rollback at L137-141 had
no error wrapping and the dedicated rollbackCertAndKey helper
only restored the cert.
2. Idempotency was per-file, not all-files. The verify gate
(if !certRes.Idempotent) skipped verify when cert was unchanged
but key was new — exactly the shape that produces a fresh key on
disk + a stale fingerprint served, and zero alarm.
3. Verify-failure rollback only handled the cert. Key was left in
whatever state the deploy reached.
This commit aligns Traefik with the canonical NGINX/Apache/HAProxy/
Postfix template:
- buildPlan() constructs deploy.Plan{Files: []{cert, key}}.
- deploy.Apply runs it all-or-nothing. SHA-256 idempotency is
all-files (Result.SkippedAsIdempotent).
- No PreCommit (Traefik has no validate-with-target command —
file watcher absorbs config errors).
- No PostCommit (file watcher auto-reloads on rename).
- runPostDeployVerify retained as-is (TLS handshake + SHA-256
fingerprint compare + retry/backoff).
- On verify failure, restoreFromBackups iterates
res.BackupPaths and rewrites each destination via
AtomicWriteFile{SkipIdempotent: true, BackupRetention: -1}.
Removed:
- The legacy rollbackCertAndKey helper (cert-only restore).
- The inline best-effort cert-rollback in DeployCertificate.
Tests added to traefik_atomic_test.go:
- TestTraefik_Atomic_KeyWriteFails_CertRollsBack — regression guard
for the original two-AtomicWriteFile bug. Pre-writes a sentinel
cert; sets the key path inside a read-only subdir so the key
write must fail; asserts the cert on disk still contains the
sentinel bytes (Apply's all-or-nothing rollback).
- TestTraefik_Atomic_AllFilesIdempotent — two subtests:
both_match_skips: pre-writes cert + key matching what Traefik
would write; asserts idempotent=true AND probe is never
called.
cert_match_key_new_runs_verify: pre-writes only the cert; key
is new; asserts idempotent=false AND probe IS called once.
Pre-fix per-file gate would have leaked through and skipped
the verify here.
- TestTraefik_Atomic_VerifyMismatch_BothFilesRollBack — pre-writes
sentinel cert + key; stub probe returns wrong fingerprint;
asserts BOTH files are restored to sentinel bytes after the
rollback fires. Pre-fix rollbackCertAndKey only restored the
cert; the key would still be the new bytes.
The pre-existing TestTraefik_Atomic_VerifyMismatch_Rollback (which
asserted only the cert restore) is left intact — it's a strict
subset of the new BothFilesRollBack assertion and serves as a
narrower regression guard.
docs/deployment-atomicity.md L84 unchanged — operator-facing claim
("atomic-write only; ValidateOnly returns sentinel") stays accurate.
Verified locally:
- gofmt -l ./internal/connector/target/traefik/ clean
- go vet ./... clean
- staticcheck ./internal/connector/target/traefik/... clean
- go build ./... clean
- go test -race -count=1 ./internal/connector/target/traefik/...
green (pre-existing tests + 3 new = 13 test functions; 14 with
the AllFilesIdempotent subtests)
- go test -short -count=1 ./internal/connector/target/... green
(no cross-connector regressions)
Audit reference: cowork/deployment-target-audit-2026-05-02/RESULTS.md
Bundle 4.