-
Notifications
You must be signed in to change notification settings - Fork 0
feat(deploy): add remoteIaCProvider adapter for gRPC plugin IaC dispatch #429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -68,9 +68,7 @@ func newDeployProvider(provider string, wfCfg *config.WorkflowConfig) (DeployPro | |||||
| // the provider and an io.Closer that shuts down any background subprocess. | ||||||
| // Tests override this var to inject fakes without touching the filesystem; | ||||||
| // they may return nil for the closer. | ||||||
| var resolveIaCProvider = func(ctx context.Context, providerName string, cfg map[string]any) (interfaces.IaCProvider, io.Closer, error) { | ||||||
| return discoverAndLoadIaCProvider(ctx, providerName, cfg) | ||||||
| } | ||||||
| var resolveIaCProvider = discoverAndLoadIaCProvider | ||||||
|
|
||||||
| // iacPluginManifest is the minimal shape needed to read capabilities.iacProvider.name | ||||||
| // from a plugin.json without relying on the full PluginCapabilities struct. | ||||||
|
|
@@ -160,19 +158,161 @@ func discoverAndLoadIaCProvider(ctx context.Context, providerName string, cfg ma | |||||
| return nil, nil, fmt.Errorf("plugin %q iac.provider factory returned nil", pluginName) | ||||||
| } | ||||||
|
|
||||||
| iacProvider, ok := mod.(interfaces.IaCProvider) | ||||||
| // RemoteModule does not directly implement interfaces.IaCProvider; instead it | ||||||
| // exposes InvokeService for cross-process method dispatch. Wrap it in a | ||||||
| // remoteIaCProvider that routes each IaCProvider call through InvokeService. | ||||||
| invoker, ok := mod.(remoteServiceInvoker) | ||||||
| if !ok { | ||||||
| mgr.Shutdown() | ||||||
| return nil, nil, fmt.Errorf("plugin %q iac.provider module (%T) does not implement interfaces.IaCProvider — upgrade with: wfctl plugin update %s", pluginName, mod, pluginName) | ||||||
| return nil, nil, fmt.Errorf("plugin %q iac.provider module (%T) does not support service invocation — upgrade with: wfctl plugin update %s", pluginName, mod, pluginName) | ||||||
| } | ||||||
|
|
||||||
| iacProvider := &remoteIaCProvider{invoker: invoker} | ||||||
| // Notify the plugin that Initialize has been called (the plugin may treat | ||||||
| // this as a no-op if it already ran Initialize inside CreateModule). | ||||||
| if initErr := iacProvider.Initialize(ctx, cfg); initErr != nil { | ||||||
| mgr.Shutdown() | ||||||
| return nil, nil, fmt.Errorf("initialize provider %q: %w", providerName, initErr) | ||||||
| } | ||||||
| return iacProvider, closer, nil | ||||||
| } | ||||||
|
|
||||||
| // remoteServiceInvoker is satisfied by *external.RemoteModule, which provides | ||||||
| // InvokeService for cross-process method dispatch. | ||||||
| type remoteServiceInvoker interface { | ||||||
| InvokeService(method string, args map[string]any) (map[string]any, error) | ||||||
| } | ||||||
|
|
||||||
| // remoteIaCProvider implements interfaces.IaCProvider by routing every method | ||||||
| // through InvokeService to the plugin subprocess. Only the methods needed by | ||||||
| // wfctl ci run deploy are fully implemented; the rest return a clear error. | ||||||
| type remoteIaCProvider struct { | ||||||
| invoker remoteServiceInvoker | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Name() string { | ||||||
| res, err := r.invoker.InvokeService("IaCProvider.Name", nil) | ||||||
| if err != nil { | ||||||
| return "" | ||||||
| } | ||||||
| name, _ := res["name"].(string) | ||||||
| return name | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Version() string { | ||||||
| res, err := r.invoker.InvokeService("IaCProvider.Version", nil) | ||||||
| if err != nil { | ||||||
| return "" | ||||||
| } | ||||||
| v, _ := res["version"].(string) | ||||||
| return v | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Initialize(_ context.Context, cfg map[string]any) error { | ||||||
| _, err := r.invoker.InvokeService("IaCProvider.Initialize", cfg) | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Capabilities() []interfaces.IaCCapabilityDeclaration { return nil } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Plan(_ context.Context, _ []interfaces.ResourceSpec, _ []interfaces.ResourceState) (*interfaces.IaCPlan, error) { | ||||||
| return nil, fmt.Errorf("IaCProvider.Plan not supported via remote deploy — use wfctl infra apply") | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Apply(_ context.Context, _ *interfaces.IaCPlan) (*interfaces.ApplyResult, error) { | ||||||
| return nil, fmt.Errorf("IaCProvider.Apply not supported via remote deploy — use wfctl infra apply") | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Destroy(_ context.Context, _ []interfaces.ResourceRef) (*interfaces.DestroyResult, error) { | ||||||
| return nil, fmt.Errorf("IaCProvider.Destroy not supported via remote deploy — use wfctl infra apply") | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Status(_ context.Context, _ []interfaces.ResourceRef) ([]interfaces.ResourceStatus, error) { | ||||||
| return nil, fmt.Errorf("IaCProvider.Status not supported via remote deploy") | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) DetectDrift(_ context.Context, _ []interfaces.ResourceRef) ([]interfaces.DriftResult, error) { | ||||||
| return nil, fmt.Errorf("IaCProvider.DetectDrift not supported via remote deploy") | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Import(_ context.Context, _ string, _ string) (*interfaces.ResourceState, error) { | ||||||
| return nil, fmt.Errorf("IaCProvider.Import not supported via remote deploy") | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) ResolveSizing(_ string, _ interfaces.Size, _ *interfaces.ResourceHints) (*interfaces.ProviderSizing, error) { | ||||||
| return nil, fmt.Errorf("IaCProvider.ResolveSizing not supported via remote deploy") | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) ResourceDriver(resourceType string) (interfaces.ResourceDriver, error) { | ||||||
| return &remoteResourceDriver{invoker: r.invoker, resourceType: resourceType}, nil | ||||||
| } | ||||||
|
|
||||||
| func (r *remoteIaCProvider) Close() error { return nil } | ||||||
|
|
||||||
| // remoteResourceDriver routes ResourceDriver calls to the plugin via InvokeService. | ||||||
| type remoteResourceDriver struct { | ||||||
| invoker remoteServiceInvoker | ||||||
| resourceType string | ||||||
| } | ||||||
|
|
||||||
| func (d *remoteResourceDriver) Update(_ context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { | ||||||
| res, err := d.invoker.InvokeService("ResourceDriver.Update", map[string]any{ | ||||||
| "resource_type": d.resourceType, | ||||||
| "ref_name": ref.Name, | ||||||
| "ref_type": ref.Type, | ||||||
| "ref_provider_id": ref.ProviderID, | ||||||
| "spec_name": spec.Name, | ||||||
| "spec_type": spec.Type, | ||||||
| "spec_config": spec.Config, | ||||||
| }) | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| return &interfaces.ResourceOutput{ | ||||||
| ProviderID: stringFromMap(res, "provider_id"), | ||||||
| Name: stringFromMap(res, "name"), | ||||||
| Type: stringFromMap(res, "type"), | ||||||
| Status: stringFromMap(res, "status"), | ||||||
| }, nil | ||||||
| } | ||||||
|
|
||||||
| func (d *remoteResourceDriver) HealthCheck(_ context.Context, ref interfaces.ResourceRef) (*interfaces.HealthResult, error) { | ||||||
| res, err := d.invoker.InvokeService("ResourceDriver.HealthCheck", map[string]any{ | ||||||
| "resource_type": d.resourceType, | ||||||
| "ref_name": ref.Name, | ||||||
| "ref_type": ref.Type, | ||||||
| "ref_provider_id": ref.ProviderID, | ||||||
| }) | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| healthy, _ := res["healthy"].(bool) | ||||||
| message, _ := res["message"].(string) | ||||||
| return &interfaces.HealthResult{Healthy: healthy, Message: message}, nil | ||||||
| } | ||||||
|
|
||||||
| func (d *remoteResourceDriver) Create(_ context.Context, _ interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { | ||||||
| return nil, fmt.Errorf("ResourceDriver.Create not yet supported via remote deploy — use wfctl infra apply") | ||||||
| } | ||||||
| func (d *remoteResourceDriver) Read(_ context.Context, _ interfaces.ResourceRef) (*interfaces.ResourceOutput, error) { | ||||||
| return nil, fmt.Errorf("ResourceDriver.Read not yet supported via remote deploy — use wfctl infra apply") | ||||||
| } | ||||||
| func (d *remoteResourceDriver) Delete(_ context.Context, _ interfaces.ResourceRef) error { | ||||||
| return fmt.Errorf("ResourceDriver.Delete not yet supported via remote deploy — use wfctl infra apply") | ||||||
|
||||||
| return fmt.Errorf("ResourceDriver.Delete not yet supported via remote deploy — use wfctl infra apply") | |
| return fmt.Errorf("ResourceDriver.Delete not yet supported via remote deploy — use wfctl infra destroy") |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,173 @@ | ||||||||||||||||||||||||||||
| package main | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||
| "testing" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| "github.com/GoCodeAlone/workflow/interfaces" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // fakeRemoteInvoker implements remoteServiceInvoker using an in-memory dispatch | ||||||||||||||||||||||||||||
| // table, so tests exercise remoteIaCProvider without a live plugin subprocess. | ||||||||||||||||||||||||||||
| type fakeRemoteInvoker struct { | ||||||||||||||||||||||||||||
| methods map[string]map[string]any // method → result | ||||||||||||||||||||||||||||
| errors map[string]string // method → error string | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func (f *fakeRemoteInvoker) InvokeService(method string, _ map[string]any) (map[string]any, error) { | ||||||||||||||||||||||||||||
| if errStr, ok := f.errors[method]; ok { | ||||||||||||||||||||||||||||
| return nil, errString(errStr) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if res, ok := f.methods[method]; ok { | ||||||||||||||||||||||||||||
| return res, nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return map[string]any{}, nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+26
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| type errString string | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func (e errString) Error() string { return string(e) } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func newFakeInvoker() *fakeRemoteInvoker { | ||||||||||||||||||||||||||||
| return &fakeRemoteInvoker{ | ||||||||||||||||||||||||||||
| methods: map[string]map[string]any{ | ||||||||||||||||||||||||||||
| "IaCProvider.Name": {"name": "test-provider"}, | ||||||||||||||||||||||||||||
| "IaCProvider.Version": {"version": "1.0.0"}, | ||||||||||||||||||||||||||||
| "IaCProvider.Initialize": {}, | ||||||||||||||||||||||||||||
| "ResourceDriver.Update": { | ||||||||||||||||||||||||||||
| "provider_id": "app-123", | ||||||||||||||||||||||||||||
| "status": "running", | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| "ResourceDriver.HealthCheck": { | ||||||||||||||||||||||||||||
| "healthy": true, | ||||||||||||||||||||||||||||
| "message": "", | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| errors: map[string]string{}, | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // ── remoteIaCProvider ───────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRemoteIaCProvider_Name(t *testing.T) { | ||||||||||||||||||||||||||||
| p := &remoteIaCProvider{invoker: newFakeInvoker()} | ||||||||||||||||||||||||||||
| if got := p.Name(); got != "test-provider" { | ||||||||||||||||||||||||||||
| t.Errorf("Name() = %q, want %q", got, "test-provider") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRemoteIaCProvider_Initialize_RoutesViaInvoker(t *testing.T) { | ||||||||||||||||||||||||||||
| inv := newFakeInvoker() | ||||||||||||||||||||||||||||
| p := &remoteIaCProvider{invoker: inv} | ||||||||||||||||||||||||||||
| if err := p.Initialize(context.Background(), map[string]any{"token": "x"}); err != nil { | ||||||||||||||||||||||||||||
| t.Fatalf("Initialize: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRemoteIaCProvider_Initialize_PropagatesError(t *testing.T) { | ||||||||||||||||||||||||||||
| inv := newFakeInvoker() | ||||||||||||||||||||||||||||
| inv.errors["IaCProvider.Initialize"] = "invalid token" | ||||||||||||||||||||||||||||
| p := &remoteIaCProvider{invoker: inv} | ||||||||||||||||||||||||||||
| err := p.Initialize(context.Background(), nil) | ||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||
| t.Fatal("expected error") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if !strings.Contains(err.Error(), "invalid token") { | ||||||||||||||||||||||||||||
| t.Errorf("expected 'invalid token' in error, got: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRemoteIaCProvider_ResourceDriver_ReturnsRemoteDriver(t *testing.T) { | ||||||||||||||||||||||||||||
| p := &remoteIaCProvider{invoker: newFakeInvoker()} | ||||||||||||||||||||||||||||
| drv, err := p.ResourceDriver("infra.container_service") | ||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||
| t.Fatalf("ResourceDriver: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if _, ok := drv.(*remoteResourceDriver); !ok { | ||||||||||||||||||||||||||||
| t.Fatalf("expected *remoteResourceDriver, got %T", drv) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // ── remoteResourceDriver ────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRemoteResourceDriver_Update_RoutesViaInvoker(t *testing.T) { | ||||||||||||||||||||||||||||
| drv := &remoteResourceDriver{ | ||||||||||||||||||||||||||||
| invoker: newFakeInvoker(), | ||||||||||||||||||||||||||||
| resourceType: "infra.container_service", | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| ref := interfaces.ResourceRef{Name: "bmw-app", Type: "infra.container_service"} | ||||||||||||||||||||||||||||
| spec := interfaces.ResourceSpec{ | ||||||||||||||||||||||||||||
| Name: "bmw-app", | ||||||||||||||||||||||||||||
| Type: "infra.container_service", | ||||||||||||||||||||||||||||
| Config: map[string]any{"image": "registry.example.com/bmw:v2"}, | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| out, err := drv.Update(context.Background(), ref, spec) | ||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||
| t.Fatalf("Update: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if out.ProviderID != "app-123" { | ||||||||||||||||||||||||||||
| t.Errorf("ProviderID = %q, want %q", out.ProviderID, "app-123") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRemoteResourceDriver_HealthCheck_Healthy(t *testing.T) { | ||||||||||||||||||||||||||||
| drv := &remoteResourceDriver{ | ||||||||||||||||||||||||||||
| invoker: newFakeInvoker(), | ||||||||||||||||||||||||||||
| resourceType: "infra.container_service", | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| ref := interfaces.ResourceRef{Name: "bmw-app", Type: "infra.container_service"} | ||||||||||||||||||||||||||||
| result, err := drv.HealthCheck(context.Background(), ref) | ||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||
| t.Fatalf("HealthCheck: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if !result.Healthy { | ||||||||||||||||||||||||||||
| t.Error("expected Healthy=true") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRemoteResourceDriver_HealthCheck_Unhealthy(t *testing.T) { | ||||||||||||||||||||||||||||
| inv := newFakeInvoker() | ||||||||||||||||||||||||||||
| inv.methods["ResourceDriver.HealthCheck"] = map[string]any{ | ||||||||||||||||||||||||||||
| "healthy": false, | ||||||||||||||||||||||||||||
| "message": "app is degraded", | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| drv := &remoteResourceDriver{ | ||||||||||||||||||||||||||||
| invoker: inv, | ||||||||||||||||||||||||||||
| resourceType: "infra.container_service", | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| ref := interfaces.ResourceRef{Name: "bmw-app", Type: "infra.container_service"} | ||||||||||||||||||||||||||||
| result, err := drv.HealthCheck(context.Background(), ref) | ||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||
| t.Fatalf("HealthCheck: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if result.Healthy { | ||||||||||||||||||||||||||||
| t.Error("expected Healthy=false") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if result.Message != "app is degraded" { | ||||||||||||||||||||||||||||
| t.Errorf("Message = %q, want %q", result.Message, "app is degraded") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| func TestRemoteResourceDriver_Update_PropagatesError(t *testing.T) { | ||||||||||||||||||||||||||||
| inv := newFakeInvoker() | ||||||||||||||||||||||||||||
| inv.errors["ResourceDriver.Update"] = "deployment failed" | ||||||||||||||||||||||||||||
| drv := &remoteResourceDriver{ | ||||||||||||||||||||||||||||
| invoker: inv, | ||||||||||||||||||||||||||||
| resourceType: "infra.container_service", | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| _, err := drv.Update(context.Background(), interfaces.ResourceRef{}, interfaces.ResourceSpec{}) | ||||||||||||||||||||||||||||
| if err == nil || !strings.Contains(err.Error(), "deployment failed") { | ||||||||||||||||||||||||||||
| t.Errorf("expected 'deployment failed' error, got: %v", err) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // TestDiscoverAndLoadIaCProvider_WrapsModuleAsRemoteIaCProvider verifies that | ||||||||||||||||||||||||||||
| // when a plugin's iac.provider module does NOT directly implement IaCProvider | ||||||||||||||||||||||||||||
| // (the normal case for gRPC plugins), discoverAndLoadIaCProvider wraps it in | ||||||||||||||||||||||||||||
| // remoteIaCProvider instead of failing the type assertion. | ||||||||||||||||||||||||||||
| func TestDiscoverAndLoadIaCProvider_WrapsModuleAsRemoteIaCProvider(t *testing.T) { | ||||||||||||||||||||||||||||
| // This is covered end-to-end by the plugin tests; here we just confirm that | ||||||||||||||||||||||||||||
| // remoteIaCProvider satisfies interfaces.IaCProvider at compile time. | ||||||||||||||||||||||||||||
| var _ interfaces.IaCProvider = (*remoteIaCProvider)(nil) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+165
to
+173
|
||||||||||||||||||||||||||||
| // TestDiscoverAndLoadIaCProvider_WrapsModuleAsRemoteIaCProvider verifies that | |
| // when a plugin's iac.provider module does NOT directly implement IaCProvider | |
| // (the normal case for gRPC plugins), discoverAndLoadIaCProvider wraps it in | |
| // remoteIaCProvider instead of failing the type assertion. | |
| func TestDiscoverAndLoadIaCProvider_WrapsModuleAsRemoteIaCProvider(t *testing.T) { | |
| // This is covered end-to-end by the plugin tests; here we just confirm that | |
| // remoteIaCProvider satisfies interfaces.IaCProvider at compile time. | |
| var _ interfaces.IaCProvider = (*remoteIaCProvider)(nil) | |
| } | |
| // Ensure remoteIaCProvider continues to satisfy interfaces.IaCProvider. | |
| // Wrapping behavior for discoverAndLoadIaCProvider is covered by higher-level | |
| // plugin tests; this is only a compile-time interface assertion. | |
| var _ interfaces.IaCProvider = (*remoteIaCProvider)(nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IaCProvider.Destroyreturns an error that tells users to runwfctl infra apply, but there is a dedicatedwfctl infra destroycommand. This message is likely to misdirect users trying to tear down infra; update it to point at the destroy flow (or otherwise clarify the correct command).