mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-14 04:09:06 +00:00
feat(haproxy): atomic deploy + post-deploy TLS verify + rollback + ValidateOnly + test-depth uplift to 36 tests
Phase 6 of the deploy-hardening I master bundle. HAProxy connector follows the canonical Phase 4 NGINX template with the HAProxy- specific quirk: combined PEM file (cert + chain + key in one file, in that order). Test count lifts 3 → 36. HAProxy specifics: - buildCombinedPEM concatenates cert, chain, key in HAProxy's required order. The combined file goes through deploy.Apply as a single File entry (vs NGINX/Apache's 2-3 separate File entries). - Default mode 0600 unconditionally (combined file contains the private key); operators rely on this back-compat behavior. PEMFileMode override is the supported escape hatch. - Validate command is `haproxy -c -f <config>`. Reload via `systemctl reload haproxy` (NOT `restart` — reload uses socket activation to drain in-flight connections). - Default user/group: haproxy (cross-distro consistent). DeployCertificate refactor: - Replaces the duplicated os.WriteFile flow with deploy.Apply. - PreCommit runs `haproxy -c -f` validation (gated on ValidateCommand being non-empty — HAProxy historically allowed empty validate). - PostCommit runs the operator's ReloadCommand. - Post-deploy TLS verify (frozen-decision-0.3 default ON when Endpoint is configured): probes the configured target, fingerprint-matches against the deployed cert (the leaf cert block from the combined PEM), retries with backoff for load- balanced targets. - Rollback wires identical to NGINX/Apache: backup restore + reload retry on PostCommit failure; verify-fail also triggers rollback. ValidateOnly real impl: returns sentinel when no ValidateCommand; otherwise runs the operator's command without touching the live combined PEM. Tests (36 total: 33 in haproxy_atomic_test.go + 3 pre-existing in haproxy_test.go): - Atomic invariants (happy, validate-fail, reload-fail-rollback, rollback-also-fail-escalation) - Combined PEM order (cert + chain + key — verified via PEM block headers, not base64 bodies) - Mode handling (default 0600 even when existing is 0640 — back-compat; PEMFileMode override; existing-mode unchanged when override matches) - Idempotency (full skip) - Verify (match, mismatch, dial-timeout, retries, disabled, no-endpoint, rollback-runs-reload) - ValidateOnly (happy, fails, no-command-sentinel, stderr-in-error) - Concurrency (same-paths-serialize) - Edge cases (no-chain, no-key, ctx-cancelled, no-validate-command, config-validation rejects missing pem_path / reload / shell-injection) Coverage: HAProxy 88.0% (above >=85% prompt bar). Race detector clean. golangci-lint v2.11.4 clean. Smoke test connectorsAtPhase3 list shrinks 11→10 (haproxy removed alongside nginx + apache). Phase 7 next: Traefik + Caddy + Envoy + Postfix — the remaining file-based connectors get the same treatment.
This commit is contained in:
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/shankar0123/certctl/internal/connector/target/caddy"
|
||||
"github.com/shankar0123/certctl/internal/connector/target/envoy"
|
||||
"github.com/shankar0123/certctl/internal/connector/target/f5"
|
||||
"github.com/shankar0123/certctl/internal/connector/target/haproxy"
|
||||
// haproxy removed Phase 6 — real ValidateOnly implementation now in haproxy.go.
|
||||
"github.com/shankar0123/certctl/internal/connector/target/iis"
|
||||
"github.com/shankar0123/certctl/internal/connector/target/javakeystore"
|
||||
"github.com/shankar0123/certctl/internal/connector/target/k8ssecret"
|
||||
@@ -69,7 +69,8 @@ var connectorsAtPhase3 = []struct {
|
||||
{"caddy", func() target.Connector { return &caddy.Connector{} }},
|
||||
{"envoy", func() target.Connector { return &envoy.Connector{} }},
|
||||
{"f5", func() target.Connector { return &f5.Connector{} }},
|
||||
{"haproxy", func() target.Connector { return &haproxy.Connector{} }},
|
||||
// haproxy removed Phase 6 — its ValidateOnly is now real;
|
||||
// tested in haproxy/haproxy_atomic_test.go.
|
||||
{"iis", func() target.Connector { return &iis.Connector{} }},
|
||||
{"javakeystore", func() target.Connector { return &javakeystore.Connector{} }},
|
||||
{"k8ssecret", func() target.Connector { return &k8ssecret.Connector{} }},
|
||||
@@ -85,7 +86,7 @@ var connectorsAtPhase3 = []struct {
|
||||
func TestEveryConnectorDefaultsToSentinel(t *testing.T) {
|
||||
// Expected list size shrinks as Phases 4-9 land their real
|
||||
// ValidateOnly implementations. Phase 4 removed nginx.
|
||||
const expectedAtCurrentPhase = 11
|
||||
const expectedAtCurrentPhase = 10
|
||||
if len(connectorsAtPhase3) != expectedAtCurrentPhase {
|
||||
t.Fatalf("connectors-at-phase list = %d entries, want %d (drift in the 13-connector inventory)", len(connectorsAtPhase3), expectedAtCurrentPhase)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user