From 853eac19f990e8ff5aed129cd6f04f6a83547ea9 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Wed, 20 May 2026 03:47:21 -0400 Subject: [PATCH] fix(wfctl): isIaCNotFound falls back to string match for gRPC errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adoption ("look up, fall back to create" for adopt_existing resources) calls driver.Read and expects to skip on ErrResourceNotFound. For remote gRPC-bridged plugins the typed adapter loses sentinel identity across the wire — `errors.Is` returns false even though the wrapped string is the canonical ErrResourceNotFound.Error() ("iac: resource not found"). Result: workflow-plugin-digitalocean v2 database adoption fails every apply with "rpc error: code = Unknown desc = database \"multisite-pg\": iac: resource not found". Add a substring fallback to isIaCNotFound that matches the canonical sentinel message. Native typed-sentinel + platform-typed paths still handled first. 4 tests cover all branches. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/wfctl/infra_apply.go | 11 +++++- cmd/wfctl/infra_apply_isiacnotfound_test.go | 41 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 cmd/wfctl/infra_apply_isiacnotfound_test.go diff --git a/cmd/wfctl/infra_apply.go b/cmd/wfctl/infra_apply.go index 460339e9..b0ea009d 100644 --- a/cmd/wfctl/infra_apply.go +++ b/cmd/wfctl/infra_apply.go @@ -843,7 +843,16 @@ func isIaCNotFound(err error) bool { return true } var platformNotFound *platform.ResourceNotFoundError - return errors.As(err, &platformNotFound) + if errors.As(err, &platformNotFound) { + return true + } + // gRPC fallback: typed adapter loses sentinel identity across the + // wire. The message survives as a wrapped string. Match on the + // literal ErrResourceNotFound.Error() value so adoption can still + // detect "not present yet, fall back to create" against remote + // plugin drivers (workflow-plugin-digitalocean v2+ database + // adoption is the original repro path). + return strings.Contains(err.Error(), interfaces.ErrResourceNotFound.Error()) } func resourceStateFromLiveOutput(spec interfaces.ResourceSpec, providerType string, live *interfaces.ResourceOutput) (interfaces.ResourceState, error) { diff --git a/cmd/wfctl/infra_apply_isiacnotfound_test.go b/cmd/wfctl/infra_apply_isiacnotfound_test.go new file mode 100644 index 00000000..ccbebbef --- /dev/null +++ b/cmd/wfctl/infra_apply_isiacnotfound_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "errors" + "fmt" + "testing" + + "github.com/GoCodeAlone/workflow/interfaces" +) + +// TestIsIaCNotFound_TypedSentinel confirms native wrapping still works. +func TestIsIaCNotFound_TypedSentinel(t *testing.T) { + err := fmt.Errorf("database %q: %w", "multisite-pg", interfaces.ErrResourceNotFound) + if !isIaCNotFound(err) { + t.Error("expected typed sentinel to be detected") + } +} + +// TestIsIaCNotFound_GRPCStringFallback is the regression test for the +// gocodealone-multisite deploy: the typed gRPC adapter strips sentinel +// identity, leaving only the wrapped error string. Adoption is meant +// to be "look up, fall back to create" — without this fallback the +// remote plugin's not-found returns made apply-prereq fail every run. +func TestIsIaCNotFound_GRPCStringFallback(t *testing.T) { + grpcErr := errors.New(`rpc error: code = Unknown desc = database "multisite-pg": iac: resource not found`) + if !isIaCNotFound(grpcErr) { + t.Error("expected gRPC-flattened not-found to be detected via string match") + } +} + +func TestIsIaCNotFound_NilSafe(t *testing.T) { + if isIaCNotFound(nil) { + t.Error("nil err must not be reported as not-found") + } +} + +func TestIsIaCNotFound_OtherErrorsIgnored(t *testing.T) { + if isIaCNotFound(errors.New("permission denied")) { + t.Error("non-matching error must not be reported as not-found") + } +}