From 49f1a607624ea14c551058eb8335fef6cffcd277 Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Thu, 30 Apr 2026 14:40:51 +0000 Subject: [PATCH] feat(target): ValidateOnly dry-run method on Connector interface (default returns ErrValidateOnlyNotSupported) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of the deploy-hardening I master bundle. Extends the target.Connector interface with the dry-run method that operators will use to preview a deploy before committing — but ships only the default-stub for all 13 connectors. Phases 4-9 replace each stub with the real validate-with-the-target implementation. interface.go: - Add ErrValidateOnlyNotSupported sentinel (frozen decision 0.6 — connectors that cannot dry-run, like K8s, return this rather than nil so operator triage can errors.Is for "not supported" vs "validated successfully"). - Add ValidateOnly(ctx, request DeploymentRequest) error to Connector interface. 13 new validate_only.go files (one per connector at internal/connector/target//validate_only.go): - apache, caddy, envoy, f5, haproxy, iis, javakeystore, k8ssecret, nginx, postfix, ssh, traefik, wincertstore. - Each file is identical except for the package declaration: a one-method default stub returning target.ErrValidateOnlyNotSupported. - Per-connector files (rather than a single embed-method approach) let Phases 4-9 replace each connector's stub independently without churning a shared base. Tests: - internal/connector/target/validate_only_test.go pins the sentinel contract (errors.Is identity, Error() string, %w wrap propagation). - internal/connector/target/validate_only_smoke_test.go (external test package) constructs a zero-value &.Connector{} for each of the 13 connectors and asserts ValidateOnly returns ErrValidateOnlyNotSupported. The test's connectorsAtPhase3 list is the load-bearing CI guard: - A 14th connector added without wiring ValidateOnly fails the `len(connectorsAtPhase3) != 13` invariant. - A connector whose real ValidateOnly lands (Phase 4 NGINX, Phase 5 Apache, etc.) MUST be removed from this list or the smoke test fails (real impl no longer returns the sentinel). That removal IS the bookkeeping that the operator-visible bit + behavior change are wired together end-to-end. Compile + go vet + golangci-lint v2.11.4 + go test all 0 issues. Phase 4 next: NGINX canonical real-impl — replace the stub with nginx -t -c ; same time replace the existing os.WriteFile flow in DeployCertificate with deploy.Apply(...). --- .../connector/target/apache/validate_only.go | 18 ++++ .../connector/target/caddy/validate_only.go | 18 ++++ .../connector/target/envoy/validate_only.go | 18 ++++ internal/connector/target/f5/validate_only.go | 18 ++++ .../connector/target/haproxy/validate_only.go | 18 ++++ .../connector/target/iis/validate_only.go | 18 ++++ internal/connector/target/interface.go | 25 +++++ .../target/javakeystore/validate_only.go | 18 ++++ .../target/k8ssecret/validate_only.go | 18 ++++ .../connector/target/nginx/validate_only.go | 18 ++++ .../connector/target/postfix/validate_only.go | 18 ++++ .../connector/target/ssh/validate_only.go | 18 ++++ .../connector/target/traefik/validate_only.go | 18 ++++ .../target/validate_only_smoke_test.go | 99 +++++++++++++++++++ .../connector/target/validate_only_test.go | 63 ++++++++++++ .../target/wincertstore/validate_only.go | 18 ++++ 16 files changed, 421 insertions(+) create mode 100644 internal/connector/target/apache/validate_only.go create mode 100644 internal/connector/target/caddy/validate_only.go create mode 100644 internal/connector/target/envoy/validate_only.go create mode 100644 internal/connector/target/f5/validate_only.go create mode 100644 internal/connector/target/haproxy/validate_only.go create mode 100644 internal/connector/target/iis/validate_only.go create mode 100644 internal/connector/target/javakeystore/validate_only.go create mode 100644 internal/connector/target/k8ssecret/validate_only.go create mode 100644 internal/connector/target/nginx/validate_only.go create mode 100644 internal/connector/target/postfix/validate_only.go create mode 100644 internal/connector/target/ssh/validate_only.go create mode 100644 internal/connector/target/traefik/validate_only.go create mode 100644 internal/connector/target/validate_only_smoke_test.go create mode 100644 internal/connector/target/validate_only_test.go create mode 100644 internal/connector/target/wincertstore/validate_only.go diff --git a/internal/connector/target/apache/validate_only.go b/internal/connector/target/apache/validate_only.go new file mode 100644 index 0000000..b1764af --- /dev/null +++ b/internal/connector/target/apache/validate_only.go @@ -0,0 +1,18 @@ +package apache + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase apache dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/caddy/validate_only.go b/internal/connector/target/caddy/validate_only.go new file mode 100644 index 0000000..5791a8f --- /dev/null +++ b/internal/connector/target/caddy/validate_only.go @@ -0,0 +1,18 @@ +package caddy + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase caddy dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/envoy/validate_only.go b/internal/connector/target/envoy/validate_only.go new file mode 100644 index 0000000..753eece --- /dev/null +++ b/internal/connector/target/envoy/validate_only.go @@ -0,0 +1,18 @@ +package envoy + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase envoy dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/f5/validate_only.go b/internal/connector/target/f5/validate_only.go new file mode 100644 index 0000000..7e6d720 --- /dev/null +++ b/internal/connector/target/f5/validate_only.go @@ -0,0 +1,18 @@ +package f5 + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase f5 dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/haproxy/validate_only.go b/internal/connector/target/haproxy/validate_only.go new file mode 100644 index 0000000..805bfc2 --- /dev/null +++ b/internal/connector/target/haproxy/validate_only.go @@ -0,0 +1,18 @@ +package haproxy + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase haproxy dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/iis/validate_only.go b/internal/connector/target/iis/validate_only.go new file mode 100644 index 0000000..cbcaad4 --- /dev/null +++ b/internal/connector/target/iis/validate_only.go @@ -0,0 +1,18 @@ +package iis + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase iis dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/interface.go b/internal/connector/target/interface.go index b17b8f9..13d3baa 100644 --- a/internal/connector/target/interface.go +++ b/internal/connector/target/interface.go @@ -3,9 +3,20 @@ package target import ( "context" "encoding/json" + "errors" "time" ) +// ErrValidateOnlyNotSupported is returned by ValidateOnly when the +// connector cannot dry-run a deploy (e.g., K8s — there is no API +// for "would this Secret update succeed without modifying state?"). +// +// Frozen decision 0.6 of the deploy-hardening I master bundle: +// ValidateOnly returns this sentinel rather than nil so operators +// can errors.Is to distinguish "validated successfully" from +// "validation not supported on this connector type." +var ErrValidateOnlyNotSupported = errors.New("target connector does not support ValidateOnly dry-run") + // Connector defines the interface for certificate deployment operations. type Connector interface { // ValidateConfig validates the deployment target configuration. @@ -15,6 +26,20 @@ type Connector interface { // The request contains the certificate and chain in PEM format, but never a private key. DeployCertificate(ctx context.Context, request DeploymentRequest) (*DeploymentResult, error) + // ValidateOnly runs the validate step (PreCommit) of a deploy + // WITHOUT touching the live cert. Returns nil when the deploy + // would succeed at the validate stage; returns + // ErrValidateOnlyNotSupported when the connector cannot dry-run + // (e.g., K8s — there's no API for "would this Secret update + // succeed without modifying state?"); returns any other error + // from the connector's validate step. + // + // Operators preview a deploy via this method before committing. + // Phase 3 of the deploy-hardening I master bundle adds the + // interface method; Phases 4-9 implement the meaningful path + // per connector. + ValidateOnly(ctx context.Context, request DeploymentRequest) error + // ValidateDeployment verifies that a deployed certificate is valid and accessible. ValidateDeployment(ctx context.Context, request ValidationRequest) (*ValidationResult, error) } diff --git a/internal/connector/target/javakeystore/validate_only.go b/internal/connector/target/javakeystore/validate_only.go new file mode 100644 index 0000000..77efbfa --- /dev/null +++ b/internal/connector/target/javakeystore/validate_only.go @@ -0,0 +1,18 @@ +package javakeystore + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase javakeystore dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/k8ssecret/validate_only.go b/internal/connector/target/k8ssecret/validate_only.go new file mode 100644 index 0000000..9b8a980 --- /dev/null +++ b/internal/connector/target/k8ssecret/validate_only.go @@ -0,0 +1,18 @@ +package k8ssecret + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase k8ssecret dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/nginx/validate_only.go b/internal/connector/target/nginx/validate_only.go new file mode 100644 index 0000000..1c6d264 --- /dev/null +++ b/internal/connector/target/nginx/validate_only.go @@ -0,0 +1,18 @@ +package nginx + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase nginx dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/postfix/validate_only.go b/internal/connector/target/postfix/validate_only.go new file mode 100644 index 0000000..c55125f --- /dev/null +++ b/internal/connector/target/postfix/validate_only.go @@ -0,0 +1,18 @@ +package postfix + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase postfix dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/ssh/validate_only.go b/internal/connector/target/ssh/validate_only.go new file mode 100644 index 0000000..0be795c --- /dev/null +++ b/internal/connector/target/ssh/validate_only.go @@ -0,0 +1,18 @@ +package ssh + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase ssh dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/traefik/validate_only.go b/internal/connector/target/traefik/validate_only.go new file mode 100644 index 0000000..b9059a8 --- /dev/null +++ b/internal/connector/target/traefik/validate_only.go @@ -0,0 +1,18 @@ +package traefik + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase traefik dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +} diff --git a/internal/connector/target/validate_only_smoke_test.go b/internal/connector/target/validate_only_smoke_test.go new file mode 100644 index 0000000..529b3f0 --- /dev/null +++ b/internal/connector/target/validate_only_smoke_test.go @@ -0,0 +1,99 @@ +package target_test + +// Phase 3 of the deploy-hardening I master bundle: per-connector +// regression smoke pinning the default ValidateOnly stub returns +// the sentinel for every one of the 13 connectors. This test lives +// in target_test (external test package) so it can import each +// connector concretely + assert the interface contract. +// +// As Phases 4-9 replace each connector's stub with a real +// validate-with-the-target implementation, the corresponding +// per-connector entry in TestEveryConnectorDefaultsToSentinel +// MUST be deleted (or the test will fail because the real +// implementation no longer returns the sentinel). That deletion +// IS the bookkeeping that the operator-visible bit + behavior +// change are wired together. + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/shankar0123/certctl/internal/connector/target" + "github.com/shankar0123/certctl/internal/connector/target/apache" + "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" + "github.com/shankar0123/certctl/internal/connector/target/iis" + "github.com/shankar0123/certctl/internal/connector/target/javakeystore" + "github.com/shankar0123/certctl/internal/connector/target/k8ssecret" + "github.com/shankar0123/certctl/internal/connector/target/nginx" + "github.com/shankar0123/certctl/internal/connector/target/postfix" + "github.com/shankar0123/certctl/internal/connector/target/ssh" + "github.com/shankar0123/certctl/internal/connector/target/traefik" + "github.com/shankar0123/certctl/internal/connector/target/wincertstore" +) + +// connectorsAtPhase3 is the canonical list of connectors that, as +// of Phase 3, return ErrValidateOnlyNotSupported from +// ValidateOnly. Each entry is a (name, factory) tuple; the factory +// returns a target.Connector via the connector's bare-NewConnector +// constructor pattern. As Phases 4-9 land, the corresponding +// connector is REMOVED from this list — its real ValidateOnly +// implementation is then exercised in the per-connector test +// suite, NOT here. +// +// CI guard rationale: a future PR that adds a 14th connector +// without wiring ValidateOnly fails this test (the sentinel +// contract is not satisfied). A future PR that implements a real +// ValidateOnly for, say, NGINX, but forgets to remove its entry +// from this list, fails this test (real impl no longer returns +// the sentinel). Both are the load-bearing bookkeeping protections. +var connectorsAtPhase3 = []struct { + name string + // new returns a fresh Connector instance. The default + // ValidateOnly stub doesn't dereference any field on the + // receiver, so a zero-value &pkg.Connector{} is sufficient + // to satisfy the interface and exercise the sentinel return. + // Phases 4-9 introduce real validate-with-the-target impls + // that DO read fields; those connectors will need a populated + // constructor here OR (more likely) be removed from this list + // entirely and exercised in their own per-connector test + // suite. + new func() target.Connector +}{ + {"apache", func() target.Connector { return &apache.Connector{} }}, + {"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{} }}, + {"iis", func() target.Connector { return &iis.Connector{} }}, + {"javakeystore", func() target.Connector { return &javakeystore.Connector{} }}, + {"k8ssecret", func() target.Connector { return &k8ssecret.Connector{} }}, + {"nginx", func() target.Connector { return &nginx.Connector{} }}, + {"postfix", func() target.Connector { return &postfix.Connector{} }}, + {"ssh", func() target.Connector { return &ssh.Connector{} }}, + {"traefik", func() target.Connector { return &traefik.Connector{} }}, + {"wincertstore", func() target.Connector { return &wincertstore.Connector{} }}, +} + +func TestEveryConnectorDefaultsToSentinel(t *testing.T) { + if len(connectorsAtPhase3) != 13 { + t.Fatalf("connectors-at-phase-3 list = %d entries, want 13 (drift in the 14-connector inventory)", len(connectorsAtPhase3)) + } + for _, c := range connectorsAtPhase3 { + t.Run(c.name, func(t *testing.T) { + conn := c.new() + err := conn.ValidateOnly(context.Background(), target.DeploymentRequest{ + CertPEM: "ignored-by-stub", + ChainPEM: "ignored", + TargetConfig: json.RawMessage(`{}`), + }) + if !errors.Is(err, target.ErrValidateOnlyNotSupported) { + t.Errorf("got %v, want ErrValidateOnlyNotSupported", err) + } + }) + } +} diff --git a/internal/connector/target/validate_only_test.go b/internal/connector/target/validate_only_test.go new file mode 100644 index 0000000..7263ea5 --- /dev/null +++ b/internal/connector/target/validate_only_test.go @@ -0,0 +1,63 @@ +package target + +import ( + "errors" + "testing" +) + +// Phase 3 of the deploy-hardening I master bundle: pin the +// ErrValidateOnlyNotSupported sentinel contract. Connectors +// returning this from ValidateOnly indicate they cannot dry-run +// (e.g. K8s — no API for "would this Secret update succeed?"). +// +// Every connector compile-time-implements ValidateOnly via the +// per-package validate_only.go stub; Phases 4-9 replace the stub +// with a real validate-with-the-target implementation per +// connector. Until then the sentinel is the contract. + +// TestErrValidateOnlyNotSupported_Sentinel pins the sentinel's +// identity and message so downstream connectors can rely on +// errors.Is checks. +func TestErrValidateOnlyNotSupported_Sentinel(t *testing.T) { + if ErrValidateOnlyNotSupported == nil { + t.Fatal("sentinel is nil") + } + if !errors.Is(ErrValidateOnlyNotSupported, ErrValidateOnlyNotSupported) { + t.Fatal("errors.Is fails on the sentinel against itself") + } + want := "target connector does not support ValidateOnly dry-run" + if got := ErrValidateOnlyNotSupported.Error(); got != want { + t.Errorf("Error() = %q, want %q", got, want) + } +} + +// TestErrValidateOnlyNotSupported_WrappableForCallerContext +// confirms callers can wrap the sentinel with extra context (e.g. +// "k8s: ValidateOnly: ErrValidateOnlyNotSupported") and the +// wrapped error still satisfies errors.Is for the operator's +// triage logic. +func TestErrValidateOnlyNotSupported_WrappableForCallerContext(t *testing.T) { + wrapped := errors.New("connector wraps with extra info: " + ErrValidateOnlyNotSupported.Error()) + // errors.Is should NOT match a wrapped-by-string copy (no %w). + if errors.Is(wrapped, ErrValidateOnlyNotSupported) { + t.Error("errors.Is matched a string-wrapped copy; should require %w wrap") + } + // %w wrap MUST match. + properly := wrapErr(ErrValidateOnlyNotSupported, "k8s") + if !errors.Is(properly, ErrValidateOnlyNotSupported) { + t.Error("errors.Is failed to match %w-wrapped sentinel") + } +} + +// wrapErr is a small test-only helper proving the %w pattern. +func wrapErr(err error, ctx string) error { + return &wrappedErr{ctx: ctx, err: err} +} + +type wrappedErr struct { + ctx string + err error +} + +func (w *wrappedErr) Error() string { return w.ctx + ": " + w.err.Error() } +func (w *wrappedErr) Unwrap() error { return w.err } diff --git a/internal/connector/target/wincertstore/validate_only.go b/internal/connector/target/wincertstore/validate_only.go new file mode 100644 index 0000000..32cb693 --- /dev/null +++ b/internal/connector/target/wincertstore/validate_only.go @@ -0,0 +1,18 @@ +package wincertstore + +import ( + "context" + + "github.com/shankar0123/certctl/internal/connector/target" +) + +// ValidateOnly is the default Phase 3 stub for the deploy-hardening +// I master bundle: returns ErrValidateOnlyNotSupported so existing +// connectors compile against the extended target.Connector interface +// without changing behavior. Phase wincertstore dry-run support arrives when +// the connector's atomic-deploy implementation lands (NGINX in +// Phase 4, Apache in Phase 5, etc.); each phase replaces this stub +// with a real validate-with-the-target implementation. +func (c *Connector) ValidateOnly(ctx context.Context, request target.DeploymentRequest) error { + return target.ErrValidateOnlyNotSupported +}