From b93ceeb063c5dae257447e74161a4aa8ee6619a0 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 21 May 2026 09:57:06 -0400 Subject: [PATCH] fix: filter no-op creates in Hover plan --- internal/provider.go | 23 +++++++++++++++- internal/provider_test.go | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/internal/provider.go b/internal/provider.go index 6ba9da1..f9a606c 100644 --- a/internal/provider.go +++ b/internal/provider.go @@ -112,7 +112,28 @@ func (p *HoverProvider) ResourceDriver(resourceType string) (interfaces.Resource // Plan delegates to platform.ComputePlan which dispatches driver.Diff per-resource. func (p *HoverProvider) Plan(ctx context.Context, desired []interfaces.ResourceSpec, current []interfaces.ResourceState) (*interfaces.IaCPlan, error) { plan, err := platform.ComputePlan(ctx, p, desired, current) - return &plan, err + if err != nil { + return nil, err + } + filtered := plan.Actions[:0] + for _, action := range plan.Actions { + if action.Action == "create" { + driver, err := p.ResourceDriver(action.Resource.Type) + if err != nil { + return nil, err + } + diff, err := driver.Diff(ctx, action.Resource, nil) + if err != nil { + return nil, fmt.Errorf("hover: create preflight diff %q/%q: %w", action.Resource.Type, action.Resource.Name, err) + } + if diff == nil || !diff.NeedsUpdate { + continue + } + } + filtered = append(filtered, action) + } + plan.Actions = filtered + return &plan, nil } // Destroy invokes the per-resource driver Delete for each ref. diff --git a/internal/provider_test.go b/internal/provider_test.go index 5606450..9a46416 100644 --- a/internal/provider_test.go +++ b/internal/provider_test.go @@ -6,6 +6,8 @@ import ( "net/http" "sync/atomic" "testing" + + "github.com/GoCodeAlone/workflow/interfaces" ) func TestHoverProvider_Capabilities_IncludesDelegation(t *testing.T) { @@ -53,3 +55,59 @@ type roundTripperFunc func(*http.Request) (*http.Response, error) func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func TestHoverProvider_Plan_FiltersDriverNoopCreates(t *testing.T) { + p := &HoverProvider{ + drivers: map[string]interfaces.ResourceDriver{ + "infra.dns_delegation": &noopCreateDriver{}, + }, + } + plan, err := p.Plan(context.Background(), []interfaces.ResourceSpec{{ + Name: "example.com", + Type: "infra.dns_delegation", + Config: map[string]any{ + "domain": "example.com", + "nameservers": []any{"ns1.example.com"}, + }, + }}, nil) + if err != nil { + t.Fatalf("Plan: %v", err) + } + if len(plan.Actions) != 0 { + t.Fatalf("expected no actions after driver no-op create filter, got %+v", plan.Actions) + } +} + +type noopCreateDriver struct{} + +func (d *noopCreateDriver) Create(context.Context, interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { + return nil, nil +} + +func (d *noopCreateDriver) Read(context.Context, interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { + return nil, nil +} + +func (d *noopCreateDriver) Update(context.Context, interfaces.ResourceRef, interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { + return nil, nil +} + +func (d *noopCreateDriver) Delete(context.Context, interfaces.ResourceRef) error { + return nil +} + +func (d *noopCreateDriver) Diff(context.Context, interfaces.ResourceSpec, *interfaces.ResourceOutput) (*interfaces.DiffResult, error) { + return &interfaces.DiffResult{NeedsUpdate: false}, nil +} + +func (d *noopCreateDriver) HealthCheck(context.Context, interfaces.ResourceRef) (*interfaces.HealthResult, error) { + return nil, nil +} + +func (d *noopCreateDriver) Scale(context.Context, interfaces.ResourceRef, int) (*interfaces.ResourceOutput, error) { + return nil, nil +} + +func (d *noopCreateDriver) SensitiveKeys() []string { + return nil +}