From 42439cc5f85abfbaa744a494f2a75966e20b7b6c Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Tue, 21 Apr 2026 08:57:24 -0400 Subject: [PATCH 1/2] feat(plugin): full ResourceDriver dispatch in InvokeMethod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add dispatch cases for Create, Read, Delete, Diff, Scale, and SensitiveKeys in doModuleInstance.InvokeMethod. Each case follows the existing Update/HealthCheck pattern (invokeDriver, refFromArgs, specFromArgs, resourceOutputToMap). New helpers: - invokeDriverCreate/Read/Delete/Diff/Scale/SensitiveKeys - currentFromArgs — decodes current_* prefixed args into *ResourceOutput - diffResultToMap — serialises DiffResult to map[string]any - intArg — tolerates both int and float64 (JSON unmarshal) - resourceOutputToMap now includes the Sensitive field Tests cover all six new dispatch paths plus float64 replicas coercion and Sensitive passthrough. All existing tests continue to pass. Co-Authored-By: Claude Sonnet 4.6 --- internal/module_instance.go | 193 +++++++++++++++++++++- internal/module_instance_test.go | 265 ++++++++++++++++++++++++++++++- 2 files changed, 448 insertions(+), 10 deletions(-) diff --git a/internal/module_instance.go b/internal/module_instance.go index 2c125fa..17213b1 100644 --- a/internal/module_instance.go +++ b/internal/module_instance.go @@ -54,9 +54,23 @@ func (m *doModuleInstance) InvokeMethod(method string, args map[string]any) (map case "ResourceDriver.HealthCheck": return m.invokeDriverHealthCheck(args) - case "ResourceDriver.Create", "ResourceDriver.Read", "ResourceDriver.Delete", - "ResourceDriver.Scale", "ResourceDriver.Diff": - return nil, fmt.Errorf("digitalocean plugin: %s is not yet supported via remote invocation — use wfctl infra apply", method) + case "ResourceDriver.Create": + return m.invokeDriverCreate(args) + + case "ResourceDriver.Read": + return m.invokeDriverRead(args) + + case "ResourceDriver.Delete": + return m.invokeDriverDelete(args) + + case "ResourceDriver.Diff": + return m.invokeDriverDiff(args) + + case "ResourceDriver.Scale": + return m.invokeDriverScale(args) + + case "ResourceDriver.SensitiveKeys": + return m.invokeDriverSensitiveKeys(args) default: return nil, fmt.Errorf("digitalocean plugin: unknown method %q", method) @@ -88,6 +102,113 @@ func (m *doModuleInstance) invokeDriverUpdate(args map[string]any) (map[string]a return resourceOutputToMap(out), nil } +// invokeDriverCreate decodes args and calls ResourceDriver.Create. +func (m *doModuleInstance) invokeDriverCreate(args map[string]any) (map[string]any, error) { + resourceType, _ := args["resource_type"].(string) + if resourceType == "" { + return nil, fmt.Errorf("ResourceDriver.Create: missing resource_type arg") + } + driver, err := m.provider.ResourceDriver(resourceType) + if err != nil { + return nil, fmt.Errorf("ResourceDriver.Create: %w", err) + } + spec, err := specFromArgs(args) + if err != nil { + return nil, fmt.Errorf("ResourceDriver.Create: %w", err) + } + out, err := driver.Create(context.Background(), spec) + if err != nil { + return nil, err + } + return resourceOutputToMap(out), nil +} + +// invokeDriverRead decodes args and calls ResourceDriver.Read. +func (m *doModuleInstance) invokeDriverRead(args map[string]any) (map[string]any, error) { + resourceType, _ := args["resource_type"].(string) + if resourceType == "" { + return nil, fmt.Errorf("ResourceDriver.Read: missing resource_type arg") + } + driver, err := m.provider.ResourceDriver(resourceType) + if err != nil { + return nil, fmt.Errorf("ResourceDriver.Read: %w", err) + } + out, err := driver.Read(context.Background(), refFromArgs(args)) + if err != nil { + return nil, err + } + return resourceOutputToMap(out), nil +} + +// invokeDriverDelete decodes args and calls ResourceDriver.Delete. +func (m *doModuleInstance) invokeDriverDelete(args map[string]any) (map[string]any, error) { + resourceType, _ := args["resource_type"].(string) + if resourceType == "" { + return nil, fmt.Errorf("ResourceDriver.Delete: missing resource_type arg") + } + driver, err := m.provider.ResourceDriver(resourceType) + if err != nil { + return nil, fmt.Errorf("ResourceDriver.Delete: %w", err) + } + if err := driver.Delete(context.Background(), refFromArgs(args)); err != nil { + return nil, err + } + return map[string]any{}, nil +} + +// invokeDriverDiff decodes args and calls ResourceDriver.Diff. +func (m *doModuleInstance) invokeDriverDiff(args map[string]any) (map[string]any, error) { + resourceType, _ := args["resource_type"].(string) + if resourceType == "" { + return nil, fmt.Errorf("ResourceDriver.Diff: missing resource_type arg") + } + driver, err := m.provider.ResourceDriver(resourceType) + if err != nil { + return nil, fmt.Errorf("ResourceDriver.Diff: %w", err) + } + spec, err := specFromArgs(args) + if err != nil { + return nil, fmt.Errorf("ResourceDriver.Diff: %w", err) + } + current := currentFromArgs(args) + result, err := driver.Diff(context.Background(), spec, current) + if err != nil { + return nil, err + } + return diffResultToMap(result), nil +} + +// invokeDriverScale decodes args and calls ResourceDriver.Scale. +func (m *doModuleInstance) invokeDriverScale(args map[string]any) (map[string]any, error) { + resourceType, _ := args["resource_type"].(string) + if resourceType == "" { + return nil, fmt.Errorf("ResourceDriver.Scale: missing resource_type arg") + } + driver, err := m.provider.ResourceDriver(resourceType) + if err != nil { + return nil, fmt.Errorf("ResourceDriver.Scale: %w", err) + } + replicas := intArg(args, "replicas") + out, err := driver.Scale(context.Background(), refFromArgs(args), replicas) + if err != nil { + return nil, err + } + return resourceOutputToMap(out), nil +} + +// invokeDriverSensitiveKeys calls ResourceDriver.SensitiveKeys. +func (m *doModuleInstance) invokeDriverSensitiveKeys(args map[string]any) (map[string]any, error) { + resourceType, _ := args["resource_type"].(string) + if resourceType == "" { + return nil, fmt.Errorf("ResourceDriver.SensitiveKeys: missing resource_type arg") + } + driver, err := m.provider.ResourceDriver(resourceType) + if err != nil { + return nil, fmt.Errorf("ResourceDriver.SensitiveKeys: %w", err) + } + return map[string]any{"keys": driver.SensitiveKeys()}, nil +} + // invokeDriverHealthCheck decodes args and calls ResourceDriver.HealthCheck. func (m *doModuleInstance) invokeDriverHealthCheck(args map[string]any) (map[string]any, error) { resourceType, _ := args["resource_type"].(string) @@ -141,13 +262,77 @@ func resourceOutputToMap(out *interfaces.ResourceOutput) map[string]any { if out == nil { return map[string]any{} } - return map[string]any{ + m := map[string]any{ "provider_id": out.ProviderID, "name": out.Name, "type": out.Type, "status": out.Status, "outputs": out.Outputs, } + if len(out.Sensitive) > 0 { + m["sensitive"] = out.Sensitive + } + return m +} + +// currentFromArgs decodes the "current_*" prefixed args into a *ResourceOutput +// for use in Diff calls. Returns nil if no current state is provided. +func currentFromArgs(args map[string]any) *interfaces.ResourceOutput { + providerID, _ := args["current_provider_id"].(string) + name, _ := args["current_name"].(string) + typ, _ := args["current_type"].(string) + status, _ := args["current_status"].(string) + if providerID == "" && name == "" && typ == "" { + return nil + } + out := &interfaces.ResourceOutput{ + ProviderID: providerID, + Name: name, + Type: typ, + Status: status, + } + if outputs, ok := args["current_outputs"].(map[string]any); ok { + out.Outputs = outputs + } + if sensitive, ok := args["current_sensitive"].(map[string]bool); ok { + out.Sensitive = sensitive + } + return out +} + +// diffResultToMap converts a DiffResult into a map[string]any for transport. +func diffResultToMap(d *interfaces.DiffResult) map[string]any { + if d == nil { + return map[string]any{"needs_update": false, "needs_replace": false, "changes": []any{}} + } + changes := make([]any, len(d.Changes)) + for i, c := range d.Changes { + changes[i] = map[string]any{ + "path": c.Path, + "old": c.Old, + "new": c.New, + "force_new": c.ForceNew, + } + } + return map[string]any{ + "needs_update": d.NeedsUpdate, + "needs_replace": d.NeedsReplace, + "changes": changes, + } +} + +// intArg extracts an integer from args, tolerating both int and float64 +// (JSON numbers unmarshal as float64 in map[string]any). +func intArg(args map[string]any, key string) int { + switch v := args[key].(type) { + case int: + return v + case float64: + return int(v) + case int64: + return int(v) + } + return 0 } func stringArg(args map[string]any, key string) string { diff --git a/internal/module_instance_test.go b/internal/module_instance_test.go index 51481ed..3ea1b31 100644 --- a/internal/module_instance_test.go +++ b/internal/module_instance_test.go @@ -212,32 +212,285 @@ func TestDoModuleInstance_InvokeMethod_HealthCheck_Unhealthy(t *testing.T) { } } +func TestDoModuleInstance_InvokeMethod_Create_DispatchesToDriver(t *testing.T) { + stub := &stubResourceDriver{createOutput: &interfaces.ResourceOutput{ + ProviderID: "do-123", Name: "my-app", Type: "infra.container_service", Status: "active", + }} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.container_service": stub}} + mi := &doModuleInstance{provider: provider} + + result, err := mi.InvokeMethod("ResourceDriver.Create", map[string]any{ + "resource_type": "infra.container_service", + "spec_name": "my-app", + "spec_type": "infra.container_service", + "spec_config": map[string]any{"image": "nginx:latest"}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + if !stub.createCalled { + t.Error("Create was not called on the driver") + } + if result["provider_id"] != "do-123" { + t.Errorf("expected provider_id=do-123, got %v", result["provider_id"]) + } + if result["status"] != "active" { + t.Errorf("expected status=active, got %v", result["status"]) + } +} + +func TestDoModuleInstance_InvokeMethod_Create_MissingResourceType(t *testing.T) { + mi := &doModuleInstance{provider: &DOProvider{}} + _, err := mi.InvokeMethod("ResourceDriver.Create", map[string]any{}) + if err == nil { + t.Fatal("expected error when resource_type is absent") + } +} + +func TestDoModuleInstance_InvokeMethod_Read_DispatchesToDriver(t *testing.T) { + stub := &stubResourceDriver{readOutput: &interfaces.ResourceOutput{ + ProviderID: "do-456", Name: "my-db", Type: "infra.database", Status: "running", + Outputs: map[string]any{"host": "db.example.com"}, + }} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.database": stub}} + mi := &doModuleInstance{provider: provider} + + result, err := mi.InvokeMethod("ResourceDriver.Read", map[string]any{ + "resource_type": "infra.database", + "ref_name": "my-db", + "ref_type": "infra.database", + "ref_provider_id": "do-456", + }) + if err != nil { + t.Fatalf("Read: %v", err) + } + if !stub.readCalled { + t.Error("Read was not called on the driver") + } + if result["provider_id"] != "do-456" { + t.Errorf("expected provider_id=do-456, got %v", result["provider_id"]) + } + outputs, _ := result["outputs"].(map[string]any) + if outputs["host"] != "db.example.com" { + t.Errorf("expected host in outputs, got %v", outputs) + } +} + +func TestDoModuleInstance_InvokeMethod_Delete_DispatchesToDriver(t *testing.T) { + stub := &stubResourceDriver{} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.vpc": stub}} + mi := &doModuleInstance{provider: provider} + + result, err := mi.InvokeMethod("ResourceDriver.Delete", map[string]any{ + "resource_type": "infra.vpc", + "ref_name": "my-vpc", + "ref_type": "infra.vpc", + "ref_provider_id": "do-789", + }) + if err != nil { + t.Fatalf("Delete: %v", err) + } + if !stub.deleteCalled { + t.Error("Delete was not called on the driver") + } + if result == nil { + t.Error("expected non-nil result map") + } +} + +func TestDoModuleInstance_InvokeMethod_Diff_DispatchesToDriver(t *testing.T) { + stub := &stubResourceDriver{diffOutput: &interfaces.DiffResult{ + NeedsUpdate: true, + Changes: []interfaces.FieldChange{ + {Path: "image", Old: "nginx:1.0", New: "nginx:2.0", ForceNew: false}, + }, + }} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.container_service": stub}} + mi := &doModuleInstance{provider: provider} + + result, err := mi.InvokeMethod("ResourceDriver.Diff", map[string]any{ + "resource_type": "infra.container_service", + "spec_name": "my-app", + "spec_type": "infra.container_service", + "spec_config": map[string]any{"image": "nginx:2.0"}, + "current_provider_id": "do-abc", + "current_name": "my-app", + "current_type": "infra.container_service", + "current_status": "running", + "current_outputs": map[string]any{"url": "https://app.example.com"}, + }) + if err != nil { + t.Fatalf("Diff: %v", err) + } + if !stub.diffCalled { + t.Error("Diff was not called on the driver") + } + if result["needs_update"] != true { + t.Errorf("expected needs_update=true, got %v", result["needs_update"]) + } + changes, _ := result["changes"].([]any) + if len(changes) != 1 { + t.Errorf("expected 1 change, got %d", len(changes)) + } +} + +func TestDoModuleInstance_InvokeMethod_Scale_DispatchesToDriver(t *testing.T) { + stub := &stubResourceDriver{scaleOutput: &interfaces.ResourceOutput{ + ProviderID: "do-777", Status: "scaling", + }} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.k8s_cluster": stub}} + mi := &doModuleInstance{provider: provider} + + result, err := mi.InvokeMethod("ResourceDriver.Scale", map[string]any{ + "resource_type": "infra.k8s_cluster", + "ref_name": "my-cluster", + "ref_type": "infra.k8s_cluster", + "ref_provider_id": "do-777", + "replicas": 3, + }) + if err != nil { + t.Fatalf("Scale: %v", err) + } + if !stub.scaleCalled { + t.Error("Scale was not called on the driver") + } + if stub.scaleReplicas != 3 { + t.Errorf("expected replicas=3, got %d", stub.scaleReplicas) + } + if result["status"] != "scaling" { + t.Errorf("expected status=scaling, got %v", result["status"]) + } +} + +func TestDoModuleInstance_InvokeMethod_Scale_ReplicasAsFloat(t *testing.T) { + // JSON numbers unmarshal as float64; the dispatch must handle both int and float64. + stub := &stubResourceDriver{scaleOutput: &interfaces.ResourceOutput{Status: "scaling"}} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.k8s_cluster": stub}} + mi := &doModuleInstance{provider: provider} + + _, err := mi.InvokeMethod("ResourceDriver.Scale", map[string]any{ + "resource_type": "infra.k8s_cluster", + "ref_name": "my-cluster", + "ref_type": "infra.k8s_cluster", + "replicas": float64(5), + }) + if err != nil { + t.Fatalf("Scale with float64 replicas: %v", err) + } + if stub.scaleReplicas != 5 { + t.Errorf("expected replicas=5, got %d", stub.scaleReplicas) + } +} + +func TestDoModuleInstance_InvokeMethod_SensitiveKeys_DispatchesToDriver(t *testing.T) { + stub := &stubResourceDriver{sensitiveKeys: []string{"password", "api_key"}} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.database": stub}} + mi := &doModuleInstance{provider: provider} + + result, err := mi.InvokeMethod("ResourceDriver.SensitiveKeys", map[string]any{ + "resource_type": "infra.database", + }) + if err != nil { + t.Fatalf("SensitiveKeys: %v", err) + } + if !stub.sensitiveKeysCalled { + t.Error("SensitiveKeys was not called on the driver") + } + keys, _ := result["keys"].([]string) + if len(keys) != 2 || keys[0] != "password" || keys[1] != "api_key" { + t.Errorf("expected [password api_key], got %v", result["keys"]) + } +} + +func TestDoModuleInstance_InvokeMethod_ResourceOutputSensitive(t *testing.T) { + // Verify resourceOutputToMap includes the sensitive field. + stub := &stubResourceDriver{createOutput: &interfaces.ResourceOutput{ + ProviderID: "do-999", + Status: "active", + Sensitive: map[string]bool{"password": true}, + }} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.database": stub}} + mi := &doModuleInstance{provider: provider} + + result, err := mi.InvokeMethod("ResourceDriver.Create", map[string]any{ + "resource_type": "infra.database", + "spec_name": "my-db", + "spec_type": "infra.database", + "spec_config": map[string]any{}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + sensitive, _ := result["sensitive"].(map[string]bool) + if !sensitive["password"] { + t.Errorf("expected sensitive[password]=true, got %v", result["sensitive"]) + } +} + // ── stub driver ─────────────────────────────────────────────────────────────── type stubResourceDriver struct { - updateCalled bool - healthyResult bool - healthMessage string + // call tracking + createCalled bool + readCalled bool + updateCalled bool + deleteCalled bool + diffCalled bool + scaleCalled bool + scaleReplicas int + sensitiveKeysCalled bool + healthyResult bool + healthMessage string + + // return values + createOutput *interfaces.ResourceOutput + readOutput *interfaces.ResourceOutput + diffOutput *interfaces.DiffResult + scaleOutput *interfaces.ResourceOutput + sensitiveKeys []string } func (s *stubResourceDriver) Create(_ context.Context, _ interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { + s.createCalled = true + if s.createOutput != nil { + return s.createOutput, nil + } return &interfaces.ResourceOutput{}, nil } func (s *stubResourceDriver) Read(_ context.Context, _ interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { + s.readCalled = true + if s.readOutput != nil { + return s.readOutput, nil + } return &interfaces.ResourceOutput{}, nil } func (s *stubResourceDriver) Update(_ context.Context, _ interfaces.ResourceRef, _ interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { s.updateCalled = true return &interfaces.ResourceOutput{Status: "running"}, nil } -func (s *stubResourceDriver) Delete(_ context.Context, _ interfaces.ResourceRef) error { return nil } +func (s *stubResourceDriver) Delete(_ context.Context, _ interfaces.ResourceRef) error { + s.deleteCalled = true + return nil +} func (s *stubResourceDriver) Diff(_ context.Context, _ interfaces.ResourceSpec, _ *interfaces.ResourceOutput) (*interfaces.DiffResult, error) { + s.diffCalled = true + if s.diffOutput != nil { + return s.diffOutput, nil + } return &interfaces.DiffResult{}, nil } func (s *stubResourceDriver) HealthCheck(_ context.Context, _ interfaces.ResourceRef) (*interfaces.HealthResult, error) { return &interfaces.HealthResult{Healthy: s.healthyResult, Message: s.healthMessage}, nil } -func (s *stubResourceDriver) Scale(_ context.Context, _ interfaces.ResourceRef, _ int) (*interfaces.ResourceOutput, error) { +func (s *stubResourceDriver) Scale(_ context.Context, _ interfaces.ResourceRef, replicas int) (*interfaces.ResourceOutput, error) { + s.scaleCalled = true + s.scaleReplicas = replicas + if s.scaleOutput != nil { + return s.scaleOutput, nil + } return &interfaces.ResourceOutput{}, nil } -func (s *stubResourceDriver) SensitiveKeys() []string { return nil } +func (s *stubResourceDriver) SensitiveKeys() []string { + s.sensitiveKeysCalled = true + return s.sensitiveKeys +} From 89fa1652b859cc21e5fba6e593cbd23474d3dce7 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Tue, 21 Apr 2026 09:08:09 -0400 Subject: [PATCH 2/2] fix(plugin): handle map[string]any for current_sensitive in currentFromArgs Through the gRPC boundary, protobuf Struct deserializes nested objects as map[string]any, not map[string]bool. The single-type assertion silently failed, so Diff calls that passed current sensitive state would always receive nil Sensitive in the ResourceOutput. Fix with a two-pass switch matching the host-side decodeResourceOutput pattern: accept map[string]bool (in-process) and map[string]any (gRPC). Add TestDoModuleInstance_InvokeMethod_Diff_CurrentSensitive_GRPCForm to cover the gRPC-deserialized form. Also capture lastDiffCurrent on stubResourceDriver so the test can inspect what was passed to Diff. Co-Authored-By: Claude Sonnet 4.6 --- internal/module_instance.go | 16 +++++++-- internal/module_instance_test.go | 62 ++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/internal/module_instance.go b/internal/module_instance.go index 17213b1..1f2ade7 100644 --- a/internal/module_instance.go +++ b/internal/module_instance.go @@ -294,8 +294,20 @@ func currentFromArgs(args map[string]any) *interfaces.ResourceOutput { if outputs, ok := args["current_outputs"].(map[string]any); ok { out.Outputs = outputs } - if sensitive, ok := args["current_sensitive"].(map[string]bool); ok { - out.Sensitive = sensitive + switch v := args["current_sensitive"].(type) { + case map[string]bool: + out.Sensitive = v + case map[string]any: + // gRPC/protobuf Struct deserializes nested objects as map[string]any. + sens := make(map[string]bool, len(v)) + for k, val := range v { + if b, ok := val.(bool); ok { + sens[k] = b + } + } + if len(sens) > 0 { + out.Sensitive = sens + } } return out } diff --git a/internal/module_instance_test.go b/internal/module_instance_test.go index 3ea1b31..69c77e4 100644 --- a/internal/module_instance_test.go +++ b/internal/module_instance_test.go @@ -334,6 +334,44 @@ func TestDoModuleInstance_InvokeMethod_Diff_DispatchesToDriver(t *testing.T) { } } +func TestDoModuleInstance_InvokeMethod_Diff_CurrentSensitive_GRPCForm(t *testing.T) { + // current_sensitive arrives as map[string]any through the gRPC boundary + // (protobuf Struct deserializes nested objects as map[string]any, not map[string]bool). + // Verify currentFromArgs handles both forms correctly. + stub := &stubResourceDriver{diffOutput: &interfaces.DiffResult{NeedsUpdate: false}} + provider := &DOProvider{drivers: map[string]interfaces.ResourceDriver{"infra.database": stub}} + mi := &doModuleInstance{provider: provider} + + _, err := mi.InvokeMethod("ResourceDriver.Diff", map[string]any{ + "resource_type": "infra.database", + "spec_name": "my-db", + "spec_type": "infra.database", + "spec_config": map[string]any{}, + "current_provider_id": "do-abc", + "current_name": "my-db", + "current_type": "infra.database", + "current_status": "running", + // gRPC-deserialized form: values are any (bool), not a typed map[string]bool + "current_sensitive": map[string]any{"password": true, "api_key": true}, + }) + if err != nil { + t.Fatalf("Diff with gRPC-form current_sensitive: %v", err) + } + if !stub.diffCalled { + t.Error("Diff was not called on the driver") + } + // Verify the sensitive map was decoded into the ResourceOutput passed to Diff. + if stub.lastDiffCurrent == nil { + t.Fatal("expected non-nil current passed to Diff") + } + if !stub.lastDiffCurrent.Sensitive["password"] { + t.Errorf("expected sensitive[password]=true, got %v", stub.lastDiffCurrent.Sensitive) + } + if !stub.lastDiffCurrent.Sensitive["api_key"] { + t.Errorf("expected sensitive[api_key]=true, got %v", stub.lastDiffCurrent.Sensitive) + } +} + func TestDoModuleInstance_InvokeMethod_Scale_DispatchesToDriver(t *testing.T) { stub := &stubResourceDriver{scaleOutput: &interfaces.ResourceOutput{ ProviderID: "do-777", Status: "scaling", @@ -431,16 +469,19 @@ func TestDoModuleInstance_InvokeMethod_ResourceOutputSensitive(t *testing.T) { type stubResourceDriver struct { // call tracking - createCalled bool - readCalled bool - updateCalled bool - deleteCalled bool - diffCalled bool - scaleCalled bool - scaleReplicas int + createCalled bool + readCalled bool + updateCalled bool + deleteCalled bool + diffCalled bool + scaleCalled bool + scaleReplicas int sensitiveKeysCalled bool - healthyResult bool - healthMessage string + healthyResult bool + healthMessage string + + // args captured on last call + lastDiffCurrent *interfaces.ResourceOutput // return values createOutput *interfaces.ResourceOutput @@ -472,8 +513,9 @@ func (s *stubResourceDriver) Delete(_ context.Context, _ interfaces.ResourceRef) s.deleteCalled = true return nil } -func (s *stubResourceDriver) Diff(_ context.Context, _ interfaces.ResourceSpec, _ *interfaces.ResourceOutput) (*interfaces.DiffResult, error) { +func (s *stubResourceDriver) Diff(_ context.Context, _ interfaces.ResourceSpec, current *interfaces.ResourceOutput) (*interfaces.DiffResult, error) { s.diffCalled = true + s.lastDiffCurrent = current if s.diffOutput != nil { return s.diffOutput, nil }