diff --git a/cmd/workflow-plugin-aws/main.go b/cmd/workflow-plugin-aws/main.go index d24e597..5279221 100644 --- a/cmd/workflow-plugin-aws/main.go +++ b/cmd/workflow-plugin-aws/main.go @@ -1,7 +1,13 @@ // Command workflow-plugin-aws is a workflow engine external plugin that -// provides AWS infrastructure provisioning via the IaCProvider interface. -// It runs as a subprocess and communicates with the host workflow engine via -// the go-plugin protocol. +// provides AWS infrastructure provisioning via the typed IaC gRPC contract. +// It runs as a subprocess and communicates with the host (wfctl) via the +// go-plugin protocol. +// +// As of the strict-contracts force-cutover (workflow v0.51.0+, issue #8), +// the plugin is served via sdk.ServeIaCPlugin which auto-registers every +// typed pb.IaCProvider*Server interface the underlying *AWSProvider satisfies. +// The legacy sdk.Serve / PluginService InvokeService string-dispatch surface +// has been removed entirely — there is no fallback path. package main import ( @@ -10,5 +16,5 @@ import ( ) func main() { - sdk.Serve(internal.NewAWSPlugin()) + sdk.ServeIaCPlugin(internal.NewIaCServer(), sdk.IaCServeOptions{}) } diff --git a/docs/plans/2026-05-13-plugin-aws-typed-iac-conformance.md b/docs/plans/2026-05-13-plugin-aws-typed-iac-conformance.md new file mode 100644 index 0000000..9591679 --- /dev/null +++ b/docs/plans/2026-05-13-plugin-aws-typed-iac-conformance.md @@ -0,0 +1,1783 @@ +# AWS Plugin Typed-IaC Conformance Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Migrate workflow-plugin-aws from the legacy `sdk.Serve`/`PluginProvider` string-dispatch surface to the typed-IaC gRPC pattern (`sdk.ServeIaCPlugin`) used by workflow-plugin-digitalocean v1.0.1, bumping to v1.0.0. + +**Architecture:** A new `awsIaCServer` struct (in `internal/iacserver.go`) embeds all `pb.Unimplemented*Server` types and implements every method that `*AWSProvider` supports. The existing `AWSProvider` is unchanged. The legacy `plugin.go`, `module.go`, and `plugin_test.go` are deleted atomically in a single commit. `cmd/workflow-plugin-aws/main.go` calls `sdk.ServeIaCPlugin` instead of `sdk.Serve`. The workflow dependency is bumped to v0.51.7. + +**Tech Stack:** Go 1.26, workflow v0.51.7, `plugin/external/proto` pb package, `plugin/external/sdk` for `ServeIaCPlugin` and `RegisterAllIaCProviderServices`. + +**Base branch:** feat/issue-8-typed-iac-conformance + +--- + +## Scope Manifest + +**PR Count:** 1 +**Tasks:** 7 +**Estimated Lines of Change:** ~700 (400 new iacserver.go + resourcedriver_server.go, 200 deleted legacy, 100 updated tests/metadata) + +**Out of scope:** +- Implementing `Enumerator`/`EnumeratorAll` (no AWS tag-query implementation yet) +- Implementing `ProviderCredentialRevoker` (no AWS credential rotation yet) +- Implementing `ProviderMigrationRepairer` (no migration repair yet) +- Implementing `ProviderValidator` (no cross-resource plan validator yet) +- Implementing `DriftConfigDetector.DetectDriftConfig` (separate service; Unimplemented embed only) +- Any changes to `drivers/` package +- Any changes to `provider/` package logic + +**PR Grouping:** + +| PR # | Title | Tasks | Branch | +|------|-------|-------|--------| +| 1 | feat: typed-IaC conformance migration to v1.0.0 (issue #8) | Task 1, Task 2, Task 3, Task 4, Task 5, Task 6, Task 7 | feat/issue-8-typed-iac-conformance | + +**Status:** Locked 2026-05-13T00:00:00Z + +--- + +### Task 1: Bump workflow dependency and verify API surface + +**Files:** +- Modify: `go.mod` +- Modify: `go.sum` (regenerated by go mod tidy) + +**Step 1: Edit go.mod to bump workflow** + +In `go.mod`, change: +``` +github.com/GoCodeAlone/workflow v0.19.2 +``` +to: +``` +github.com/GoCodeAlone/workflow v0.51.7 +``` + +**Step 2: Run go mod tidy** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go mod tidy +``` +Expected: exits 0; `go.sum` updated; no error output. + +**Step 3: Verify build still compiles before writing new code** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go build ./... +``` +Expected: exits 0 (the existing code still compiles against the new workflow version; API surface of `interfaces.*` and `plugin/external/proto/*` is stable per DO v1.0.1 precedent at v0.51.2). + +If any API break surfaces here, STOP and diagnose before proceeding. + +**Step 4: Run existing tests to confirm no regression** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./... -count=1 -timeout 60s +``` +Expected: all pass (unit tests do not require live AWS credentials). + +**Step 5: Commit** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && git add go.mod go.sum && git commit -m "chore: bump workflow v0.19.2 → v0.51.7 for typed-IaC conformance" +``` +Expected: commit succeeds; `go build ./...` still exits 0 on this commit. + +Rollback: `git revert HEAD && go mod tidy` restores v0.19.2 and the prior go.sum. + +--- + +### Task 2: Write `internal/iacserver.go` — typed IaC server (TDD) + +**Files:** +- Create: `internal/iacserver_test.go` +- Create: `internal/iacserver.go` + +**Step 1: Write the failing test file first** + +Create `internal/iacserver_test.go`: + +```go +// Package internal_test exercises the awsIaCServer typed gRPC methods. +// Tests use a real *provider.AWSProvider with no initialized AWS session; +// only methods that do NOT require a live AWS credential are covered here. +// Initialize, Plan, Apply, Destroy, Import, Status test coverage lives in +// provider/provider_test.go (existing suite). +package internal + +import ( + "context" + "testing" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +func TestNewIaCServer_NotNil(t *testing.T) { + s := NewIaCServer() + if s == nil { + t.Fatal("NewIaCServer returned nil") + } +} + +func TestIaCServer_Name(t *testing.T) { + s := NewIaCServer() + resp, err := s.Name(context.Background(), &pb.NameRequest{}) + if err != nil { + t.Fatalf("Name: %v", err) + } + if resp.GetName() != "aws" { + t.Errorf("Name = %q, want %q", resp.GetName(), "aws") + } +} + +func TestIaCServer_Version(t *testing.T) { + s := NewIaCServer() + resp, err := s.Version(context.Background(), &pb.VersionRequest{}) + if err != nil { + t.Fatalf("Version: %v", err) + } + if resp.GetVersion() == "" { + t.Error("Version returned empty string") + } +} + +func TestIaCServer_Capabilities(t *testing.T) { + s := NewIaCServer() + resp, err := s.Capabilities(context.Background(), &pb.CapabilitiesRequest{}) + if err != nil { + t.Fatalf("Capabilities: %v", err) + } + found := false + for _, c := range resp.GetCapabilities() { + if c.GetResourceType() == "infra.container_service" { + found = true + break + } + } + if !found { + t.Errorf("Capabilities missing infra.container_service, got: %v", resp.GetCapabilities()) + } +} + +func TestIaCServer_Initialize_EmptyConfig(t *testing.T) { + s := NewIaCServer() + // Empty config_json: Initialize should return an error (no region defaults to us-east-1, + // but nil map should succeed since AWSProvider.Initialize handles nil gracefully). + _, err := s.Initialize(context.Background(), &pb.InitializeRequest{ConfigJson: []byte(`{}`)}) + // No credential required for unit test — Initialize sets up the SDK config. + // In CI without AWS credentials, LoadDefaultConfig may still succeed with the ambient chain. + // We only assert it does not panic. + _ = err // error acceptable; not nil is fine without credentials +} + +func TestIaCServer_CompileTimeGuards(t *testing.T) { + // This test exists to document the compile-time guards. + // If any of the interface assertions below fail to compile, this file will not build. + var _ pb.IaCProviderRequiredServer = (*awsIaCServer)(nil) + var _ pb.IaCProviderDriftDetectorServer = (*awsIaCServer)(nil) + var _ pb.ResourceDriverServer = (*awsIaCServer)(nil) +} + +func TestIaCServer_DetectDrift_Uninitialized(t *testing.T) { + s := NewIaCServer() + refs := []*pb.ResourceRef{{Name: "test", Type: "infra.container_service"}} + _, err := s.DetectDrift(context.Background(), &pb.DetectDriftRequest{Refs: refs}) + // Uninitialized provider returns "not initialized" error. + if err == nil { + t.Error("expected error from uninitialized provider") + } +} + +func TestIaCServer_DetectDriftWithSpecs_DelegatesToDetectDrift(t *testing.T) { + s := NewIaCServer() + refs := []*pb.ResourceRef{{Name: "test", Type: "infra.container_service"}} + _, err := s.DetectDriftWithSpecs(context.Background(), &pb.DetectDriftWithSpecsRequest{Refs: refs}) + // Uninitialized provider returns "not initialized" error — same as DetectDrift. + if err == nil { + t.Error("expected error from uninitialized provider") + } +} +``` + +**Step 2: Run the failing tests** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./internal/ -run TestIaCServer -v 2>&1 | head -30 +``` +Expected: FAIL with compilation errors (functions/types not yet defined: `NewIaCServer`, `awsIaCServer`). + +**Step 3: Create `internal/iacserver.go`** + +Create `internal/iacserver.go` with the `awsIaCServer` struct, `NewIaCServer` constructor, +compile-time guards, and all required + drift-detector RPC methods. Use the exact +marshalling helpers pattern from workflow-plugin-digitalocean v1.0.1. + +The file has three logical sections: +1. The `awsIaCServer` struct + constructor + compile-time guards +2. Required service methods (IaCProviderRequired) +3. Drift detector methods (IaCProviderDriftDetector) +4. Marshalling helpers (pb↔Go) — copy directly from DO iacserver.go with `aws` prefix on error messages + +```go +// Package internal — typed pb.IaCProvider*Server implementation. +// +// awsIaCServer is the SERVER side of the typed IaC contract. It satisfies +// pb.IaCProviderRequiredServer plus the optional pb.IaCProviderDriftDetectorServer +// interface by delegating each typed RPC to the matching method on the +// underlying *provider.AWSProvider. +// +// The remaining optional services (Enumerator, CredentialRevoker, MigrationRepairer, +// Validator, DriftConfigDetector) are present as Unimplemented*Server embeds only +// (forward-compat; not auto-registered by sdk.RegisterAllIaCProviderServices). +// +// Hard invariants (strict-contracts force-cutover): +// - NO structpb.Struct, NO Any.UnmarshalTo on the wire — provider-specific +// config / outputs cross as JSON bytes (config_json, outputs_json). +// - REQUIRED service methods MUST be implemented; the SDK type-assert in +// sdk.RegisterAllIaCProviderServices fails at plugin startup otherwise. +package internal + +import ( + "context" + "encoding/json" + "fmt" + "math" + "time" + + "github.com/GoCodeAlone/workflow-plugin-aws/provider" + "github.com/GoCodeAlone/workflow/interfaces" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// awsIaCServer wraps *provider.AWSProvider and exposes the typed +// pb.IaCProvider*Server + ResourceDriverServer surface. The Unimplemented*Server +// embeds satisfy the gRPC forward-compat contract and let the SDK type-assert +// succeed. awsIaCServer overrides all Required methods and the DriftDetector methods. +type awsIaCServer struct { + pb.UnimplementedIaCProviderRequiredServer + pb.UnimplementedIaCProviderEnumeratorServer + pb.UnimplementedIaCProviderDriftDetectorServer + pb.UnimplementedIaCProviderCredentialRevokerServer + pb.UnimplementedIaCProviderMigrationRepairerServer + pb.UnimplementedIaCProviderValidatorServer + pb.UnimplementedIaCProviderDriftConfigDetectorServer + pb.UnimplementedResourceDriverServer + + provider *provider.AWSProvider +} + +// newAWSIaCServer constructs a typed-IaC server backed by the given +// *provider.AWSProvider. The provider is NOT initialized here; Initialize is +// the first typed RPC the host sends after the gRPC dial completes. +func newAWSIaCServer(p *provider.AWSProvider) *awsIaCServer { + return &awsIaCServer{provider: p} +} + +// NewIaCServer is the package entrypoint used by cmd/workflow-plugin-aws/main.go. +// It constructs a fresh *provider.AWSProvider and wraps it in the typed +// pb.IaCProvider* server surface. The returned value is suitable to pass to +// sdk.ServeIaCPlugin; the SDK auto-registers every typed gRPC service the +// server satisfies via Go type-assertion at plugin startup. +func NewIaCServer() *awsIaCServer { + return newAWSIaCServer(provider.NewAWSProvider().(*provider.AWSProvider)) +} + +// Compile-time guards: every typed server interface this AWS plugin advertises +// MUST be satisfied. A signature drift on any of these will fail the build at +// this file rather than at first RPC dispatch. +var ( + _ pb.IaCProviderRequiredServer = (*awsIaCServer)(nil) + // IaCProviderDriftDetectorServer requires BOTH DetectDrift AND DetectDriftWithSpecs. + // Both are implemented below: DetectDrift is the real check; DetectDriftWithSpecs + // delegates to DetectDrift (existence-only behavior; ignores the specs map). + _ pb.IaCProviderDriftDetectorServer = (*awsIaCServer)(nil) + _ pb.ResourceDriverServer = (*awsIaCServer)(nil) +) + +// ── Required service methods ──────────────────────────────────────────────── + +func (s *awsIaCServer) Initialize(ctx context.Context, req *pb.InitializeRequest) (*pb.InitializeResponse, error) { + cfg, err := unmarshalJSONMap(req.GetConfigJson()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: parse Initialize config_json: %w", err) + } + if err := s.provider.Initialize(ctx, cfg); err != nil { + return nil, err + } + return &pb.InitializeResponse{}, nil +} + +func (s *awsIaCServer) Name(_ context.Context, _ *pb.NameRequest) (*pb.NameResponse, error) { + return &pb.NameResponse{Name: s.provider.Name()}, nil +} + +func (s *awsIaCServer) Version(_ context.Context, _ *pb.VersionRequest) (*pb.VersionResponse, error) { + return &pb.VersionResponse{Version: s.provider.Version()}, nil +} + +func (s *awsIaCServer) Capabilities(_ context.Context, _ *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) { + caps := s.provider.Capabilities() + out := make([]*pb.IaCCapabilityDeclaration, 0, len(caps)) + for _, c := range caps { + tier := c.Tier + if tier < math.MinInt32 { + tier = math.MinInt32 + } else if tier > math.MaxInt32 { + tier = math.MaxInt32 + } + out = append(out, &pb.IaCCapabilityDeclaration{ + ResourceType: c.ResourceType, + Tier: int32(tier), //nolint:gosec // G115: clamped above + Operations: append([]string(nil), c.Operations...), + }) + } + return &pb.CapabilitiesResponse{Capabilities: out}, nil +} + +func (s *awsIaCServer) Plan(ctx context.Context, req *pb.PlanRequest) (*pb.PlanResponse, error) { + desired, err := specsFromPB(req.GetDesired()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: decode Plan desired: %w", err) + } + current, err := statesFromPB(req.GetCurrent()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: decode Plan current: %w", err) + } + plan, err := s.provider.Plan(ctx, desired, current) + if err != nil { + return nil, err + } + pbPlan, err := planToPB(plan) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode Plan response: %w", err) + } + return &pb.PlanResponse{Plan: pbPlan}, nil +} + +func (s *awsIaCServer) Apply(ctx context.Context, req *pb.ApplyRequest) (*pb.ApplyResponse, error) { + plan, err := planFromPB(req.GetPlan()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: decode Apply plan: %w", err) + } + result, err := s.provider.Apply(ctx, plan) + if err != nil { + return nil, err + } + pbResult, err := applyResultToPB(result) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode Apply response: %w", err) + } + return &pb.ApplyResponse{Result: pbResult}, nil +} + +func (s *awsIaCServer) Destroy(ctx context.Context, req *pb.DestroyRequest) (*pb.DestroyResponse, error) { + refs := refsFromPB(req.GetRefs()) + result, err := s.provider.Destroy(ctx, refs) + if err != nil { + return nil, err + } + return &pb.DestroyResponse{Result: destroyResultToPB(result)}, nil +} + +func (s *awsIaCServer) Status(ctx context.Context, req *pb.StatusRequest) (*pb.StatusResponse, error) { + refs := refsFromPB(req.GetRefs()) + statuses, err := s.provider.Status(ctx, refs) + if err != nil { + return nil, err + } + pbStatuses, err := statusesToPB(statuses) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode Status response: %w", err) + } + return &pb.StatusResponse{Statuses: pbStatuses}, nil +} + +func (s *awsIaCServer) Import(ctx context.Context, req *pb.ImportRequest) (*pb.ImportResponse, error) { + state, err := s.provider.Import(ctx, req.GetProviderId(), req.GetResourceType()) + if err != nil { + return nil, err + } + pbState, err := stateToPB(state) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode Import response: %w", err) + } + return &pb.ImportResponse{State: pbState}, nil +} + +func (s *awsIaCServer) ResolveSizing(_ context.Context, req *pb.ResolveSizingRequest) (*pb.ResolveSizingResponse, error) { + sizing, err := s.provider.ResolveSizing( + req.GetResourceType(), + interfaces.Size(req.GetSize()), + hintsFromPB(req.GetHints()), + ) + if err != nil { + return nil, err + } + pbSizing, err := sizingToPB(sizing) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode ResolveSizing response: %w", err) + } + return &pb.ResolveSizingResponse{Sizing: pbSizing}, nil +} + +func (s *awsIaCServer) BootstrapStateBackend(ctx context.Context, req *pb.BootstrapStateBackendRequest) (*pb.BootstrapStateBackendResponse, error) { + cfg, err := unmarshalJSONMap(req.GetConfigJson()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: parse BootstrapStateBackend config_json: %w", err) + } + result, err := s.provider.BootstrapStateBackend(ctx, cfg) + if err != nil { + return nil, err + } + return &pb.BootstrapStateBackendResponse{Result: bootstrapResultToPB(result)}, nil +} + +// ── Optional: DriftDetector ──────────────────────────────────────────────── + +func (s *awsIaCServer) DetectDrift(ctx context.Context, req *pb.DetectDriftRequest) (*pb.DetectDriftResponse, error) { + refs := refsFromPB(req.GetRefs()) + drifts, err := s.provider.DetectDrift(ctx, refs) + if err != nil { + return nil, err + } + pbDrifts, err := driftsToPB(drifts) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode DetectDrift response: %w", err) + } + return &pb.DetectDriftResponse{Drifts: pbDrifts}, nil +} + +// DetectDriftWithSpecs satisfies pb.IaCProviderDriftDetectorServer. +// AWSProvider only implements existence-check drift detection; this method +// delegates to DetectDrift and ignores the specs map (consistent with +// existence-only behavior). Both methods are required for IaCProviderDriftDetector +// to register cleanly via sdk.RegisterAllIaCProviderServices. +func (s *awsIaCServer) DetectDriftWithSpecs(ctx context.Context, req *pb.DetectDriftWithSpecsRequest) (*pb.DetectDriftWithSpecsResponse, error) { + refs := refsFromPB(req.GetRefs()) + drifts, err := s.provider.DetectDrift(ctx, refs) + if err != nil { + return nil, err + } + pbDrifts, err := driftsToPB(drifts) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode DetectDriftWithSpecs response: %w", err) + } + return &pb.DetectDriftWithSpecsResponse{Drifts: pbDrifts}, nil +} + +// ── Marshalling helpers (pb ↔ Go) ─────────────────────────────────────────── +// +// These mirror the inverse-direction helpers in cmd/wfctl/iac_typed_adapter.go +// (workflow). Pattern copied from workflow-plugin-digitalocean v1.0.1 iacserver.go. + +func unmarshalJSONMap(b []byte) (map[string]any, error) { + if len(b) == 0 { + return nil, nil + } + var out map[string]any + if err := json.Unmarshal(b, &out); err != nil { + return nil, err + } + return out, nil +} + +func marshalJSONMap(m map[string]any) ([]byte, error) { + if m == nil { + return nil, nil + } + return json.Marshal(m) +} + +func marshalJSONAny(v any) ([]byte, error) { + if v == nil { + return nil, nil + } + return json.Marshal(v) +} + +func unmarshalJSONAny(b []byte) (any, error) { + if len(b) == 0 { + return nil, nil + } + var out any + if err := json.Unmarshal(b, &out); err != nil { + return nil, err + } + return out, nil +} + +func refToPB(r interfaces.ResourceRef) *pb.ResourceRef { + return &pb.ResourceRef{Name: r.Name, Type: r.Type, ProviderId: r.ProviderID} +} + +func refFromPB(r *pb.ResourceRef) interfaces.ResourceRef { + if r == nil { + return interfaces.ResourceRef{} + } + return interfaces.ResourceRef{Name: r.GetName(), Type: r.GetType(), ProviderID: r.GetProviderId()} +} + +func refsToPB(refs []interfaces.ResourceRef) []*pb.ResourceRef { + out := make([]*pb.ResourceRef, 0, len(refs)) + for _, r := range refs { + out = append(out, refToPB(r)) + } + return out +} + +func refsFromPB(refs []*pb.ResourceRef) []interfaces.ResourceRef { + out := make([]interfaces.ResourceRef, 0, len(refs)) + for _, r := range refs { + out = append(out, refFromPB(r)) + } + return out +} + +func hintsToPB(h *interfaces.ResourceHints) *pb.ResourceHints { + if h == nil { + return nil + } + return &pb.ResourceHints{Cpu: h.CPU, Memory: h.Memory, Storage: h.Storage} +} + +func hintsFromPB(h *pb.ResourceHints) *interfaces.ResourceHints { + if h == nil { + return nil + } + return &interfaces.ResourceHints{CPU: h.GetCpu(), Memory: h.GetMemory(), Storage: h.GetStorage()} +} + +func specToPB(s interfaces.ResourceSpec) (*pb.ResourceSpec, error) { + cfgJSON, err := marshalJSONMap(s.Config) + if err != nil { + return nil, err + } + return &pb.ResourceSpec{ + Name: s.Name, + Type: s.Type, + ConfigJson: cfgJSON, + Size: string(s.Size), + Hints: hintsToPB(s.Hints), + DependsOn: append([]string(nil), s.DependsOn...), + }, nil +} + +func specFromPB(s *pb.ResourceSpec) (interfaces.ResourceSpec, error) { + if s == nil { + return interfaces.ResourceSpec{}, nil + } + cfg, err := unmarshalJSONMap(s.GetConfigJson()) + if err != nil { + return interfaces.ResourceSpec{}, err + } + return interfaces.ResourceSpec{ + Name: s.GetName(), + Type: s.GetType(), + Config: cfg, + Size: interfaces.Size(s.GetSize()), + Hints: hintsFromPB(s.GetHints()), + DependsOn: append([]string(nil), s.GetDependsOn()...), + }, nil +} + +func specsFromPB(specs []*pb.ResourceSpec) ([]interfaces.ResourceSpec, error) { + out := make([]interfaces.ResourceSpec, 0, len(specs)) + for _, s := range specs { + gs, err := specFromPB(s) + if err != nil { + return nil, err + } + out = append(out, gs) + } + return out, nil +} + +func stateToPB(st *interfaces.ResourceState) (*pb.ResourceState, error) { + if st == nil { + return nil, nil + } + appliedJSON, err := marshalJSONMap(st.AppliedConfig) + if err != nil { + return nil, err + } + outputsJSON, err := marshalJSONMap(st.Outputs) + if err != nil { + return nil, err + } + return &pb.ResourceState{ + Id: st.ID, + Name: st.Name, + Type: st.Type, + Provider: st.Provider, + ProviderRef: st.ProviderRef, + ProviderId: st.ProviderID, + ConfigHash: st.ConfigHash, + AppliedConfigJson: appliedJSON, + AppliedConfigSource: st.AppliedConfigSource, + OutputsJson: outputsJSON, + Dependencies: append([]string(nil), st.Dependencies...), + CreatedAt: timeToPB(st.CreatedAt), + UpdatedAt: timeToPB(st.UpdatedAt), + LastDriftCheck: timeToPB(st.LastDriftCheck), + }, nil +} + +func stateFromPB(s *pb.ResourceState) (*interfaces.ResourceState, error) { + if s == nil { + return nil, nil + } + applied, err := unmarshalJSONMap(s.GetAppliedConfigJson()) + if err != nil { + return nil, err + } + outputs, err := unmarshalJSONMap(s.GetOutputsJson()) + if err != nil { + return nil, err + } + return &interfaces.ResourceState{ + ID: s.GetId(), + Name: s.GetName(), + Type: s.GetType(), + Provider: s.GetProvider(), + ProviderRef: s.GetProviderRef(), + ProviderID: s.GetProviderId(), + ConfigHash: s.GetConfigHash(), + AppliedConfig: applied, + AppliedConfigSource: s.GetAppliedConfigSource(), + Outputs: outputs, + Dependencies: append([]string(nil), s.GetDependencies()...), + CreatedAt: timeFromPB(s.GetCreatedAt()), + UpdatedAt: timeFromPB(s.GetUpdatedAt()), + LastDriftCheck: timeFromPB(s.GetLastDriftCheck()), + }, nil +} + +func statesFromPB(states []*pb.ResourceState) ([]interfaces.ResourceState, error) { + out := make([]interfaces.ResourceState, 0, len(states)) + for _, s := range states { + gs, err := stateFromPB(s) + if err != nil { + return nil, err + } + if gs != nil { + out = append(out, *gs) + } + } + return out, nil +} + +func outputToPB(o *interfaces.ResourceOutput) (*pb.ResourceOutput, error) { + if o == nil { + return nil, nil + } + outputsJSON, err := marshalJSONMap(o.Outputs) + if err != nil { + return nil, err + } + sensitive := make(map[string]bool, len(o.Sensitive)) + for k, v := range o.Sensitive { + sensitive[k] = v + } + return &pb.ResourceOutput{ + Name: o.Name, + Type: o.Type, + ProviderId: o.ProviderID, + OutputsJson: outputsJSON, + Sensitive: sensitive, + Status: o.Status, + }, nil +} + +func statusesToPB(ss []interfaces.ResourceStatus) ([]*pb.ResourceStatus, error) { + out := make([]*pb.ResourceStatus, 0, len(ss)) + for i := range ss { + o, err := marshalJSONMap(ss[i].Outputs) + if err != nil { + return nil, err + } + out = append(out, &pb.ResourceStatus{ + Name: ss[i].Name, + Type: ss[i].Type, + ProviderId: ss[i].ProviderID, + Status: ss[i].Status, + OutputsJson: o, + }) + } + return out, nil +} + +func driftClassToPB(c interfaces.DriftClass) pb.DriftClass { + switch c { + case interfaces.DriftClassInSync: + return pb.DriftClass_DRIFT_CLASS_IN_SYNC + case interfaces.DriftClassGhost: + return pb.DriftClass_DRIFT_CLASS_GHOST + case interfaces.DriftClassConfig: + return pb.DriftClass_DRIFT_CLASS_CONFIG + default: + return pb.DriftClass_DRIFT_CLASS_UNKNOWN + } +} + +func driftsToPB(drifts []interfaces.DriftResult) ([]*pb.DriftResult, error) { + out := make([]*pb.DriftResult, 0, len(drifts)) + for _, d := range drifts { + expectedJSON, err := marshalJSONMap(d.Expected) + if err != nil { + return nil, err + } + actualJSON, err := marshalJSONMap(d.Actual) + if err != nil { + return nil, err + } + out = append(out, &pb.DriftResult{ + Name: d.Name, + Type: d.Type, + Drifted: d.Drifted, + Class: driftClassToPB(d.Class), + ExpectedJson: expectedJSON, + ActualJson: actualJSON, + Fields: append([]string(nil), d.Fields...), + }) + } + return out, nil +} + +func planActionToPB(a interfaces.PlanAction) (*pb.PlanAction, error) { + pbSpec, err := specToPB(a.Resource) + if err != nil { + return nil, err + } + var pbCurrent *pb.ResourceState + if a.Current != nil { + pbCurrent, err = stateToPB(a.Current) + if err != nil { + return nil, err + } + } + pbChanges, err := changesToPB(a.Changes) + if err != nil { + return nil, err + } + return &pb.PlanAction{ + Action: a.Action, + Resource: pbSpec, + Current: pbCurrent, + Changes: pbChanges, + ResolvedConfigHash: a.ResolvedConfigHash, + }, nil +} + +func planActionFromPB(a *pb.PlanAction) (interfaces.PlanAction, error) { + if a == nil { + return interfaces.PlanAction{}, nil + } + spec, err := specFromPB(a.GetResource()) + if err != nil { + return interfaces.PlanAction{}, err + } + var current *interfaces.ResourceState + if a.GetCurrent() != nil { + current, err = stateFromPB(a.GetCurrent()) + if err != nil { + return interfaces.PlanAction{}, err + } + } + changes, err := changesFromPB(a.GetChanges()) + if err != nil { + return interfaces.PlanAction{}, err + } + return interfaces.PlanAction{ + Action: a.GetAction(), + Resource: spec, + Current: current, + Changes: changes, + ResolvedConfigHash: a.GetResolvedConfigHash(), + }, nil +} + +func changesToPB(changes []interfaces.FieldChange) ([]*pb.FieldChange, error) { + out := make([]*pb.FieldChange, 0, len(changes)) + for _, c := range changes { + oldJSON, err := marshalJSONAny(c.Old) + if err != nil { + return nil, err + } + newJSON, err := marshalJSONAny(c.New) + if err != nil { + return nil, err + } + out = append(out, &pb.FieldChange{ + Path: c.Path, + OldJson: oldJSON, + NewJson: newJSON, + ForceNew: c.ForceNew, + }) + } + return out, nil +} + +func changesFromPB(changes []*pb.FieldChange) ([]interfaces.FieldChange, error) { + out := make([]interfaces.FieldChange, 0, len(changes)) + for _, c := range changes { + oldVal, err := unmarshalJSONAny(c.GetOldJson()) + if err != nil { + return nil, err + } + newVal, err := unmarshalJSONAny(c.GetNewJson()) + if err != nil { + return nil, err + } + out = append(out, interfaces.FieldChange{ + Path: c.GetPath(), + Old: oldVal, + New: newVal, + ForceNew: c.GetForceNew(), + }) + } + return out, nil +} + +func planToPB(p *interfaces.IaCPlan) (*pb.IaCPlan, error) { + if p == nil { + return nil, nil + } + pbActions := make([]*pb.PlanAction, 0, len(p.Actions)) + for i := range p.Actions { + pa, err := planActionToPB(p.Actions[i]) + if err != nil { + return nil, err + } + pbActions = append(pbActions, pa) + } + if p.SchemaVersion < math.MinInt32 || p.SchemaVersion > math.MaxInt32 { + return nil, fmt.Errorf("aws iacserver: plan SchemaVersion %d out of int32 range", p.SchemaVersion) + } + return &pb.IaCPlan{ + Id: p.ID, + Actions: pbActions, + CreatedAt: timeToPB(p.CreatedAt), + DesiredHash: p.DesiredHash, + SchemaVersion: int32(p.SchemaVersion), //nolint:gosec // G115: range-checked above + InputSnapshot: copyStringMap(p.InputSnapshot), + }, nil +} + +func planFromPB(p *pb.IaCPlan) (*interfaces.IaCPlan, error) { + if p == nil { + return nil, nil + } + actions := make([]interfaces.PlanAction, 0, len(p.GetActions())) + for _, a := range p.GetActions() { + pa, err := planActionFromPB(a) + if err != nil { + return nil, err + } + actions = append(actions, pa) + } + return &interfaces.IaCPlan{ + ID: p.GetId(), + Actions: actions, + CreatedAt: timeFromPB(p.GetCreatedAt()), + DesiredHash: p.GetDesiredHash(), + SchemaVersion: int(p.GetSchemaVersion()), + InputSnapshot: copyStringMap(p.GetInputSnapshot()), + }, nil +} + +func applyResultToPB(r *interfaces.ApplyResult) (*pb.ApplyResult, error) { + if r == nil { + return nil, nil + } + resources := make([]*pb.ResourceOutput, 0, len(r.Resources)) + for i := range r.Resources { + ro, err := outputToPB(&r.Resources[i]) + if err != nil { + return nil, err + } + if ro != nil { + resources = append(resources, ro) + } + } + errs := make([]*pb.ActionError, 0, len(r.Errors)) + for _, e := range r.Errors { + errs = append(errs, &pb.ActionError{Resource: e.Resource, Action: e.Action, Error: e.Error}) + } + driftReport := make([]*pb.DriftEntry, 0, len(r.InputDriftReport)) + for _, d := range r.InputDriftReport { + driftReport = append(driftReport, &pb.DriftEntry{ + Name: d.Name, + PlanFingerprint: d.PlanFingerprint, + ApplyFingerprint: d.ApplyFingerprint, + }) + } + return &pb.ApplyResult{ + PlanId: r.PlanID, + Resources: resources, + Errors: errs, + InitialInputSnapshot: copyStringMap(r.InitialInputSnapshot), + InputDriftReport: driftReport, + ReplaceIdMap: copyStringMap(r.ReplaceIDMap), + }, nil +} + +func destroyResultToPB(r *interfaces.DestroyResult) *pb.DestroyResult { + if r == nil { + return nil + } + errs := make([]*pb.ActionError, 0, len(r.Errors)) + for _, e := range r.Errors { + errs = append(errs, &pb.ActionError{Resource: e.Resource, Action: e.Action, Error: e.Error}) + } + return &pb.DestroyResult{Destroyed: append([]string(nil), r.Destroyed...), Errors: errs} +} + +func bootstrapResultToPB(r *interfaces.BootstrapResult) *pb.BootstrapResult { + if r == nil { + return nil + } + return &pb.BootstrapResult{ + Bucket: r.Bucket, + Region: r.Region, + Endpoint: r.Endpoint, + EnvVars: copyStringMap(r.EnvVars), + } +} + +func sizingToPB(s *interfaces.ProviderSizing) (*pb.ProviderSizing, error) { + if s == nil { + return nil, nil + } + specsJSON, err := marshalJSONMap(s.Specs) + if err != nil { + return nil, err + } + return &pb.ProviderSizing{InstanceType: s.InstanceType, SpecsJson: specsJSON}, nil +} + +func timeToPB(t time.Time) *timestamppb.Timestamp { + if t.IsZero() { + return nil + } + return timestamppb.New(t) +} + +func timeFromPB(t *timestamppb.Timestamp) time.Time { + if t == nil { + return time.Time{} + } + return t.AsTime() +} + +func copyStringMap(m map[string]string) map[string]string { + if m == nil { + return nil + } + out := make(map[string]string, len(m)) + for k, v := range m { + out[k] = v + } + return out +} +``` + +**Step 4: Run the tests — must pass** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./internal/ -run TestIaCServer -v -count=1 +``` +Expected: all `TestIaCServer_*` tests PASS. + +**Step 5: Run full test suite** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./... -count=1 -timeout 60s +``` +Expected: all tests PASS (existing `plugin_test.go` still tests the legacy path which still exists at this point). + +**Step 6: Commit** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && git add internal/iacserver.go internal/iacserver_test.go && git commit -m "feat: add awsIaCServer typed gRPC server wrapping AWSProvider" +``` +Expected: commit succeeds. + +--- + +### Task 3: Write `internal/resourcedriver_server.go` — ResourceDriver CRUD dispatch (TDD) + +**Files:** +- Create: `internal/resourcedriver_server_test.go` +- Create: `internal/resourcedriver_server.go` + +**Step 1: Write the failing test** + +Create `internal/resourcedriver_server_test.go`: + +```go +package internal + +import ( + "context" + "testing" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +func TestResourceDriverServer_CompileTimeGuard(t *testing.T) { + var _ pb.ResourceDriverServer = (*awsIaCServer)(nil) +} + +func TestResourceDriverServer_ResolveDriver_Empty(t *testing.T) { + s := NewIaCServer() + _, err := s.resolveResourceDriver("") + if err == nil { + t.Fatal("expected error for empty resource_type") + } +} + +func TestResourceDriverServer_ResolveDriver_Unknown(t *testing.T) { + s := NewIaCServer() + // Provider not initialized — resolveResourceDriver returns error. + _, err := s.resolveResourceDriver("infra.unknown_type") + if err == nil { + t.Fatal("expected error for unknown resource type on uninitialized provider") + } +} + +func TestResourceDriverServer_Create_UnknownType(t *testing.T) { + s := NewIaCServer() + req := &pb.ResourceCreateRequest{ + ResourceType: "infra.unknown", + Spec: &pb.ResourceSpec{Name: "x", Type: "infra.unknown"}, + } + _, err := s.Create(context.Background(), req) + if err == nil { + t.Fatal("expected error for unknown resource type") + } +} + +func TestResourceDriverServer_SensitiveKeys_UnknownType(t *testing.T) { + s := NewIaCServer() + _, err := s.SensitiveKeys(context.Background(), &pb.SensitiveKeysRequest{ResourceType: "infra.unknown"}) + if err == nil { + t.Fatal("expected error for unknown resource type") + } +} +``` + +**Step 2: Run the failing test** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./internal/ -run TestResourceDriverServer -v 2>&1 | head -20 +``` +Expected: FAIL with "resolveResourceDriver undefined" (function not yet declared). + +**Step 3: Create `internal/resourcedriver_server.go`** + +Create `internal/resourcedriver_server.go` with `resolveResourceDriver` helper and all 9 `pb.ResourceDriverServer` method overrides on `*awsIaCServer`. Pattern mirrors workflow-plugin-digitalocean v1.0.1 exactly: + +```go +// Package internal — typed pb.ResourceDriverServer implementation. +// +// Extends *awsIaCServer (declared in iacserver.go) with the 9 RPC methods +// required by pb.ResourceDriverServer. Routing dispatches per-resource-type +// CRUD by looking up the driver via *provider.AWSProvider.ResourceDriver(type). +// +// Once *awsIaCServer satisfies pb.ResourceDriverServer at the Go type level, +// sdk.RegisterAllIaCProviderServices auto-registers it — no manual call needed. +package internal + +import ( + "context" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/GoCodeAlone/workflow/interfaces" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +// resolveResourceDriver looks up the per-type driver registered on the +// underlying *provider.AWSProvider. Returns a typed gRPC error with +// codes.NotFound when the resource_type is not registered. +func (s *awsIaCServer) resolveResourceDriver(resourceType string) (interfaces.ResourceDriver, error) { + if resourceType == "" { + return nil, status.Error(codes.InvalidArgument, "aws ResourceDriver: resource_type is required") + } + d, err := s.provider.ResourceDriver(resourceType) + if err != nil { + return nil, status.Errorf(codes.NotFound, "aws ResourceDriver: %v", err) + } + return d, nil +} + +func (s *awsIaCServer) Create(ctx context.Context, req *pb.ResourceCreateRequest) (*pb.ResourceCreateResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + spec, err := specFromPB(req.GetSpec()) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Create: decode spec: %w", req.GetResourceType(), err) + } + out, err := driver.Create(ctx, spec) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Create: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceCreateResponse{Output: pbOut}, nil +} + +func (s *awsIaCServer) Read(ctx context.Context, req *pb.ResourceReadRequest) (*pb.ResourceReadResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + out, err := driver.Read(ctx, refFromPB(req.GetRef())) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Read: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceReadResponse{Output: pbOut}, nil +} + +func (s *awsIaCServer) Update(ctx context.Context, req *pb.ResourceUpdateRequest) (*pb.ResourceUpdateResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + spec, err := specFromPB(req.GetSpec()) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Update: decode spec: %w", req.GetResourceType(), err) + } + out, err := driver.Update(ctx, refFromPB(req.GetRef()), spec) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Update: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceUpdateResponse{Output: pbOut}, nil +} + +func (s *awsIaCServer) Delete(ctx context.Context, req *pb.ResourceDeleteRequest) (*pb.ResourceDeleteResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + if err := driver.Delete(ctx, refFromPB(req.GetRef())); err != nil { + return nil, err + } + return &pb.ResourceDeleteResponse{}, nil +} + +func (s *awsIaCServer) Diff(ctx context.Context, req *pb.ResourceDiffRequest) (*pb.ResourceDiffResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + desired, err := specFromPB(req.GetDesired()) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Diff: decode desired: %w", req.GetResourceType(), err) + } + current, err := outputFromPB(req.GetCurrent()) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Diff: decode current: %w", req.GetResourceType(), err) + } + result, err := driver.Diff(ctx, desired, current) + if err != nil { + return nil, err + } + pbResult, err := diffResultToPB(result) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Diff: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceDiffResponse{Result: pbResult}, nil +} + +func (s *awsIaCServer) Scale(ctx context.Context, req *pb.ResourceScaleRequest) (*pb.ResourceScaleResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + out, err := driver.Scale(ctx, refFromPB(req.GetRef()), int(req.GetReplicas())) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Scale: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceScaleResponse{Output: pbOut}, nil +} + +func (s *awsIaCServer) HealthCheck(ctx context.Context, req *pb.ResourceHealthCheckRequest) (*pb.ResourceHealthCheckResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + result, err := driver.HealthCheck(ctx, refFromPB(req.GetRef())) + if err != nil { + return nil, err + } + return &pb.ResourceHealthCheckResponse{Result: healthResultToPB(result)}, nil +} + +func (s *awsIaCServer) SensitiveKeys(_ context.Context, req *pb.SensitiveKeysRequest) (*pb.SensitiveKeysResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + keys := driver.SensitiveKeys() + return &pb.SensitiveKeysResponse{Keys: append([]string(nil), keys...)}, nil +} + +func (s *awsIaCServer) Troubleshoot(ctx context.Context, req *pb.TroubleshootRequest) (*pb.TroubleshootResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + tr, ok := driver.(interfaces.Troubleshooter) + if !ok { + return nil, status.Errorf(codes.Unimplemented, + "aws ResourceDriver(%s).Troubleshoot: driver does not implement interfaces.Troubleshooter", + req.GetResourceType()) + } + diags, err := tr.Troubleshoot(ctx, refFromPB(req.GetRef()), req.GetFailureMsg()) + if err != nil { + return nil, err + } + out := make([]*pb.Diagnostic, 0, len(diags)) + for _, d := range diags { + out = append(out, &pb.Diagnostic{ + Id: d.ID, + Phase: d.Phase, + Cause: d.Cause, + At: timeToPB(d.At), + Detail: d.Detail, + }) + } + return &pb.TroubleshootResponse{Diagnostics: out}, nil +} + +// ── Marshalling helpers specific to ResourceDriver ────────────────────────── + +func diffResultToPB(r *interfaces.DiffResult) (*pb.DiffResult, error) { + if r == nil { + return nil, nil + } + pbChanges, err := changesToPB(r.Changes) + if err != nil { + return nil, err + } + return &pb.DiffResult{ + NeedsUpdate: r.NeedsUpdate, + NeedsReplace: r.NeedsReplace, + Changes: pbChanges, + }, nil +} + +func healthResultToPB(r *interfaces.HealthResult) *pb.HealthResult { + if r == nil { + return nil + } + return &pb.HealthResult{Healthy: r.Healthy, Message: r.Message} +} + +func outputFromPB(o *pb.ResourceOutput) (*interfaces.ResourceOutput, error) { + if o == nil { + return nil, nil + } + outputs, err := unmarshalJSONMap(o.GetOutputsJson()) + if err != nil { + return nil, err + } + sensitive := make(map[string]bool, len(o.GetSensitive())) + for k, v := range o.GetSensitive() { + sensitive[k] = v + } + return &interfaces.ResourceOutput{ + Name: o.GetName(), + Type: o.GetType(), + ProviderID: o.GetProviderId(), + Outputs: outputs, + Sensitive: sensitive, + Status: o.GetStatus(), + }, nil +} +``` + +**Step 4: Run the tests — must pass** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./internal/ -run TestResourceDriverServer -v -count=1 +``` +Expected: all `TestResourceDriverServer_*` tests PASS. + +**Step 5: Run full test suite** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./... -count=1 -timeout 60s +``` +Expected: all tests PASS. + +**Step 6: Commit** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && git add internal/resourcedriver_server.go internal/resourcedriver_server_test.go && git commit -m "feat: add ResourceDriver typed gRPC dispatch on awsIaCServer" +``` +Expected: commit succeeds; `go build ./...` exits 0. + +--- + +### Task 4: Update entrypoint + delete legacy files (atomic) + +**Files:** +- Modify: `cmd/workflow-plugin-aws/main.go` +- Delete: `internal/plugin.go` +- Delete: `internal/module.go` +- Delete: `internal/plugin_test.go` + +> **IMPORTANT:** All four file changes MUST be in a single commit. `plugin_test.go` references +> `iacProviderModule` (from `module.go`) and `NewAWSPlugin` (from `plugin.go`). Deleting either +> without the other causes a compile break. Stage all changes atomically. + +**Step 1: Update `cmd/workflow-plugin-aws/main.go`** + +Replace the file content entirely: + +```go +// Command workflow-plugin-aws is a workflow engine external plugin that +// provides AWS infrastructure provisioning via the typed IaC gRPC contract. +// It runs as a subprocess and communicates with the host (wfctl) via the +// go-plugin protocol. +// +// As of the strict-contracts force-cutover (workflow v0.51.0+, issue #8), +// the plugin is served via sdk.ServeIaCPlugin which auto-registers every +// typed pb.IaCProvider*Server interface the underlying *AWSProvider satisfies. +// The legacy sdk.Serve / PluginService InvokeService string-dispatch surface +// has been removed entirely — there is no fallback path. +package main + +import ( + "github.com/GoCodeAlone/workflow-plugin-aws/internal" + sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +func main() { + sdk.ServeIaCPlugin(internal.NewIaCServer(), sdk.IaCServeOptions{}) +} +``` + +**Step 2: Delete legacy files** + +```bash +rm /Users/jon/workspace/workflow-plugin-aws/internal/plugin.go +rm /Users/jon/workspace/workflow-plugin-aws/internal/module.go +rm /Users/jon/workspace/workflow-plugin-aws/internal/plugin_test.go +``` + +**Step 3: Verify build after deletions** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go build ./... +``` +Expected: exits 0 — no remaining references to `NewAWSPlugin`, `iacProviderModule`, `awsPlugin`. + +**Step 4: Run tests** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./... -count=1 -timeout 60s +``` +Expected: all remaining tests PASS. (`plugin_test.go` is gone; `iacserver_test.go` and existing `provider/` and `drivers/` tests remain.) + +**Step 5: Verify plugin binary builds and starts** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go build -o /tmp/workflow-plugin-aws ./cmd/workflow-plugin-aws && echo "binary built OK" +``` +Expected: outputs `binary built OK`; binary exists at `/tmp/workflow-plugin-aws`. + +**Step 6: Commit atomically** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && git add cmd/workflow-plugin-aws/main.go && git rm internal/plugin.go internal/module.go internal/plugin_test.go && git commit -m "feat: force-cutover to sdk.ServeIaCPlugin; remove legacy sdk.Serve surface" +``` +Expected: commit message shows 1 modified + 3 deleted; `go build ./...` exits 0. + +Rollback: `git revert HEAD` + `go mod tidy` if needed. + +--- + +### Task 5: Update metadata — plugin.json and ProviderVersion + +**Files:** +- Modify: `plugin.json` +- Modify: `provider/provider.go` + +**Step 1: Update `plugin.json`** + +Change `"version": "0.1.0"` → `"version": "1.0.0"` and `"minEngineVersion": "0.19.0"` → `"minEngineVersion": "0.51.0"`. +Also update download URLs from `v0.1.0` → `v1.0.0` in all download entries. + +The final `plugin.json` (key fields shown): +```json +{ + "name": "workflow-plugin-aws", + "version": "1.0.0", + "minEngineVersion": "0.51.0", + ... + "downloads": [ + { + "os": "linux", + "arch": "amd64", + "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v1.0.0/workflow-plugin-aws_1.0.0_linux_amd64.tar.gz" + }, + ... + ] +} +``` + +**Step 2: Update `ProviderVersion` in `provider/provider.go`** + +Change: +```go +ProviderVersion = "0.1.0" +``` +to: +```go +ProviderVersion = "1.0.0" +``` + +**Step 3: Verify Version RPC returns new version** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./internal/ -run TestIaCServer_Version -v +``` +Expected: test passes and the version string is non-empty (the ProviderVersion constant flows through to the RPC). + +**Step 4: Run full test suite** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./... -count=1 -timeout 60s +``` +Expected: all tests PASS. + +**Step 5: Commit** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && git add plugin.json provider/provider.go && git commit -m "chore: bump version to 1.0.0, minEngineVersion to 0.51.0" +``` +Expected: commit succeeds. + +--- + +### Task 6: Rewrite `internal/host_conformance_test.go` for typed-IaC load path + +**Files:** +- Modify: `internal/host_conformance_test.go` + +The existing conformance test validates the legacy module-kind contract path. It must be rewritten to match the DO v1.0.1 pattern: service-kind contract assertion + live `Name()` + `Capabilities()` RPC calls. + +**Step 1: Replace `internal/host_conformance_test.go`** + +```go +package internal + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/GoCodeAlone/workflow/plugin/external" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +// TestWorkflowHostConformance_LoadsTypedIaCPlugin validates the host/plugin +// boundary for the typed-IaC gRPC pattern (sdk.ServeIaCPlugin). Skipped by +// default; set WORKFLOW_IAC_HOST_CONFORMANCE=1 to run. +// +// This test mirrors workflow-plugin-digitalocean v1.0.1 +// internal/host_conformance_test.go exactly. +func TestWorkflowHostConformance_LoadsTypedIaCPlugin(t *testing.T) { + if os.Getenv("WORKFLOW_IAC_HOST_CONFORMANCE") != "1" { + t.Skip("set WORKFLOW_IAC_HOST_CONFORMANCE=1 to run host compatibility smoke") + } + + repoRoot := hostConformanceRepoRoot(t) + pluginName := hostConformancePluginName(t, filepath.Join(repoRoot, "plugin.json")) + + pluginsDir := filepath.Join(t.TempDir(), "data", "plugins") + pluginDir := filepath.Join(pluginsDir, pluginName) + if err := os.MkdirAll(pluginDir, 0o755); err != nil { + t.Fatalf("mkdir plugin dir: %v", err) + } + hostConformanceCopyFile(t, filepath.Join(repoRoot, "plugin.json"), filepath.Join(pluginDir, "plugin.json")) + hostConformanceCopyFile(t, filepath.Join(repoRoot, "plugin.contracts.json"), filepath.Join(pluginDir, "plugin.contracts.json")) + + build := exec.Command("go", "build", "-o", filepath.Join(pluginDir, pluginName), "./cmd/workflow-plugin-aws") + build.Dir = repoRoot + if out, err := build.CombinedOutput(); err != nil { + t.Fatalf("build plugin binary: %v\n%s", err, out) + } + + mgr := external.NewExternalPluginManager(pluginsDir, nil) + t.Cleanup(mgr.Shutdown) + + adapter, err := mgr.LoadPlugin(pluginName) + if err != nil { + t.Fatalf("load plugin through Workflow external host: %v", err) + } + + registry := adapter.ContractRegistry() + if registry == nil { + t.Fatal("contract registry is nil") + } + // Typed-IaC plugins expose SERVICE-kind contracts (not module-kind). + if !registryHasService(registry, pb.IaCProviderRequired_ServiceDesc.ServiceName) { + t.Fatalf("contract registry missing required service %q: %v", + pb.IaCProviderRequired_ServiceDesc.ServiceName, registry.GetContracts()) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + + required := pb.NewIaCProviderRequiredClient(adapter.Conn()) + name, err := required.Name(ctx, &pb.NameRequest{}) + if err != nil { + t.Fatalf("call typed IaCProviderRequired.Name: %v", err) + } + if name.GetName() != "aws" { + t.Fatalf("provider name = %q, want %q", name.GetName(), "aws") + } + + capabilities, err := required.Capabilities(ctx, &pb.CapabilitiesRequest{}) + if err != nil { + t.Fatalf("call typed IaCProviderRequired.Capabilities: %v", err) + } + if !capabilitiesHasResource(capabilities, "infra.container_service") { + t.Fatalf("provider capabilities missing infra.container_service: %v", + capabilities.GetCapabilities()) + } +} + +func hostConformanceRepoRoot(t *testing.T) string { + t.Helper() + return filepath.Clean(filepath.Join(filepath.Dir(testCallerFile(t)), "..")) +} + +func testCallerFile(t *testing.T) string { + t.Helper() + _, file, _, ok := callerFile() + if !ok { + t.Fatal("runtime.Caller failed") + } + return file +} + +func hostConformancePluginName(t *testing.T, path string) string { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read plugin manifest: %v", err) + } + var manifest struct { + Name string `json:"name"` + } + if err := json.Unmarshal(data, &manifest); err != nil { + t.Fatalf("parse plugin manifest: %v", err) + } + if manifest.Name == "" { + t.Fatal("plugin manifest missing name") + } + return manifest.Name +} + +func hostConformanceCopyFile(t *testing.T, src, dst string) { + t.Helper() + data, err := os.ReadFile(src) + if err != nil { + t.Fatalf("read %s: %v", src, err) + } + if err := os.WriteFile(dst, data, 0o644); err != nil { + t.Fatalf("write %s: %v", dst, err) + } +} + +func registryHasService(registry *pb.ContractRegistry, serviceName string) bool { + for _, contract := range registry.GetContracts() { + if contract.GetKind() == pb.ContractKind_CONTRACT_KIND_SERVICE && + contract.GetServiceName() == serviceName { + return true + } + } + return false +} + +func capabilitiesHasResource(capabilities *pb.CapabilitiesResponse, resourceType string) bool { + for _, capability := range capabilities.GetCapabilities() { + if capability.GetResourceType() == resourceType { + return true + } + } + return false +} +``` + +NOTE: The existing `host_conformance_test.go` uses `runtime.Caller` directly. The rewrite above uses helper functions. If `runtime.Caller` is imported in `iacserver_test.go` or elsewhere, ensure no duplicate declarations. Check for `callerFile()` helper — if not defined elsewhere in the package, add it: + +```go +import "runtime" + +func callerFile() (uintptr, string, int, bool) { + return runtime.Caller(1) +} +``` + +Alternatively, inline `runtime.Caller` directly in `hostConformanceRepoRoot` without the helper: + +```go +func hostConformanceRepoRoot(t *testing.T) string { + t.Helper() + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller failed") + } + return filepath.Clean(filepath.Join(filepath.Dir(file), "..")) +} +``` + +Use the inline `runtime.Caller` form (simpler, no helper function needed). + +**Step 2: Verify the test compiles and skips by default** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./internal/ -run TestWorkflowHostConformance -v -count=1 +``` +Expected: `--- SKIP: TestWorkflowHostConformance_LoadsTypedIaCPlugin (set WORKFLOW_IAC_HOST_CONFORMANCE=1 to run host compatibility smoke)`. + +**Step 3: Run full test suite** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test ./... -count=1 -timeout 60s +``` +Expected: all tests PASS. + +**Step 4: Run host conformance gate locally** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && WORKFLOW_IAC_HOST_CONFORMANCE=1 go test ./internal/ -run TestWorkflowHostConformance -v -count=1 -timeout 30s +``` +Expected: `--- PASS: TestWorkflowHostConformance_LoadsTypedIaCPlugin` — plugin loads, service-kind contract present, `Name()` returns `"aws"`, `Capabilities()` returns `infra.container_service`. + +This is the local equivalent of the CI host-conformance gate. It MUST pass before committing. + +**Step 5: Commit** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && git add internal/host_conformance_test.go && git commit -m "test: rewrite host_conformance_test for typed-IaC load path (service-kind + live RPC)" +``` +Expected: commit succeeds. + +--- + +### Task 7: Final verification — full build, lint, and PR + +**Files:** +- None new; verification only. + +**Step 1: Full clean build** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go build ./... +``` +Expected: exits 0, no warnings. + +**Step 2: Full test suite with race detector** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && go test -race ./... -count=1 -timeout 120s +``` +Expected: all tests PASS, no data races reported. + +**Step 3: Run lint if golangci-lint is available** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && golangci-lint run ./... 2>&1 | head -30 +``` +Expected: exits 0 or only known suppressions (nolint comments for G115 int32 casts — already present in DO precedent). + +**Step 4: Verify plugin binary builds for all architectures (spot check)** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && GOOS=linux GOARCH=amd64 go build -o /tmp/workflow-plugin-aws-linux-amd64 ./cmd/workflow-plugin-aws && echo "linux/amd64 OK" +cd /Users/jon/workspace/workflow-plugin-aws && GOOS=darwin GOARCH=arm64 go build -o /tmp/workflow-plugin-aws-darwin-arm64 ./cmd/workflow-plugin-aws && echo "darwin/arm64 OK" +``` +Expected: both print OK. + +**Step 5: Run host conformance gate one final time** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && WORKFLOW_IAC_HOST_CONFORMANCE=1 go test ./internal/ -run TestWorkflowHostConformance -v -count=1 -timeout 30s +``` +Expected: `--- PASS: TestWorkflowHostConformance_LoadsTypedIaCPlugin`. + +**Step 6: Push branch and create PR** + +```bash +cd /Users/jon/workspace/workflow-plugin-aws && git push -u origin feat/issue-8-typed-iac-conformance +``` + +```bash +gh pr create \ + --repo GoCodeAlone/workflow-plugin-aws \ + --base main \ + --head feat/issue-8-typed-iac-conformance \ + --title "feat: typed-IaC conformance migration to v1.0.0 (issue #8)" \ + --body "$(cat <<'EOF' +## Summary +- Adds `internal/iacserver.go` with `awsIaCServer` struct satisfying `pb.IaCProviderRequiredServer`, `pb.IaCProviderDriftDetectorServer`, and `pb.ResourceDriverServer` +- Adds `internal/resourcedriver_server.go` with per-type CRUD dispatch delegating to `*AWSProvider.ResourceDriver` +- Removes legacy `sdk.Serve` / `PluginProvider` string-dispatch surface (`plugin.go`, `module.go`, `plugin_test.go`) +- Bumps workflow dependency v0.19.2 → v0.51.7 and plugin version to v1.0.0 +- Rewrites `host_conformance_test.go` to validate typed-IaC load path (service-kind contract + live RPC) + +## Fixes +Closes #8 + +## Test plan +- [ ] `go test ./... -race` passes +- [ ] `WORKFLOW_IAC_HOST_CONFORMANCE=1 go test ./internal/ -run TestWorkflowHostConformance` passes +- [ ] `go build ./cmd/workflow-plugin-aws` exits 0 +- [ ] CI passes + +## Breaking change +Old workflow engine tags (pre-v0.51.0) are permanently incompatible. Consumers must pin `workflow-plugin-aws@v1.0.0` and use a workflow engine at v0.51.0+. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` +Expected: PR URL printed. + +**Step 7: Add @copilot reviewer** + +```bash +gh pr edit --add-reviewer "copilot" --repo GoCodeAlone/workflow-plugin-aws +``` +Expected: Copilot added as reviewer. diff --git a/docs/plans/2026-05-13-plugin-aws-typed-iac-conformance.md.scope-lock b/docs/plans/2026-05-13-plugin-aws-typed-iac-conformance.md.scope-lock new file mode 100644 index 0000000..88651e3 --- /dev/null +++ b/docs/plans/2026-05-13-plugin-aws-typed-iac-conformance.md.scope-lock @@ -0,0 +1 @@ +6c5bc7d08946a863b1287bc62bbb1086981bf90c4f755b505b158f9b50b9fbd0 diff --git a/docs/plans/2026-05-13-plugin-aws-typed-iac-design.md b/docs/plans/2026-05-13-plugin-aws-typed-iac-design.md new file mode 100644 index 0000000..f41e97e --- /dev/null +++ b/docs/plans/2026-05-13-plugin-aws-typed-iac-design.md @@ -0,0 +1,215 @@ +# Design: AWS Plugin Typed-IaC Conformance Migration (Issue #8) + +**Date:** 2026-05-13 +**Author:** Claude Code (autonomous pipeline) +**Status:** Draft → Approved + +## Context + +workflow-plugin-aws v0.2.0 still uses the legacy `sdk.Serve(internal.NewAWSPlugin())` +string-dispatch surface (`plugin.go` / `module.go` / `internal/typed.go`). The workflow +engine's strict-contracts force-cutover (v0.50.0+) removed this path from the host side. +Issue #8 asks that the AWS plugin adopt the same typed-IaC gRPC pattern that +workflow-plugin-digitalocean v1.0.1 shipped under. + +## Precedent + +workflow-plugin-digitalocean v1.0.1 (the "force-cutover" reference): +- `cmd/plugin/main.go` calls `sdk.ServeIaCPlugin(internal.NewIaCServer(), sdk.IaCServeOptions{})` +- `internal/iacserver.go` — `doIaCServer` embeds all `pb.Unimplemented*Server` types, + implements every required RPC, and delegates to the underlying `*DOProvider` +- `internal/resourcedriver_server.go` — `pb.ResourceDriverServer` per-type CRUD dispatch +- `internal/provider.go` — `DOProvider` unchanged; server wraps it +- Pinned to `workflow v0.51.2` + +## Approach (single chosen option) + +**Single-PR force-cutover mirroring DO v1.0.1.** + +No compat shim. The legacy `internal/plugin.go`, `internal/module.go`, and +`internal/typed.go` (from the `strict-contracts` branch) are deleted. The new entrypoint +calls `sdk.ServeIaCPlugin`. The plugin surfaces every typed gRPC service that +`*AWSProvider` satisfies at the Go interface level. + +Alternatives considered: +- **Keep `sdk.Serve` + add `InvokeTypedMethod` bridge** — rejected: this is the old + `strict-contracts` branch approach, incompatible with engine v0.50.0+. +- **Two-PR (add typed server first, remove legacy second)** — rejected per memory + `feedback_force_strict_contracts_no_compat`: one combined cutover PR is correct. + +## Scope + +### Phase 1 — Typed server layer (new files) + +| File | Action | +|------|--------| +| `internal/iacserver.go` | NEW: `awsIaCServer` struct with required + optional pb service methods | +| `internal/resourcedriver_server.go` | NEW: ResourceDriver CRUD dispatch | + +`awsIaCServer` embeds (forward-compat: every embed is present so future proto additions +do not break the build; only services with actual method implementations are +meaningfully registered by `sdk.RegisterAllIaCProviderServices`): +``` +pb.UnimplementedIaCProviderRequiredServer +pb.UnimplementedIaCProviderEnumeratorServer // forward-compat only; no AWS impl yet +pb.UnimplementedIaCProviderDriftDetectorServer +pb.UnimplementedIaCProviderCredentialRevokerServer // forward-compat only; no AWS impl yet +pb.UnimplementedIaCProviderMigrationRepairerServer // forward-compat only; no AWS impl yet +pb.UnimplementedIaCProviderValidatorServer // forward-compat only; no AWS impl yet +pb.UnimplementedIaCProviderDriftConfigDetectorServer +pb.UnimplementedResourceDriverServer +``` + +**What gets implemented** (methods `*AWSProvider` actually supports): +- All `IaCProviderRequiredServer` methods: `Initialize`, `Name`, `Version`, + `Capabilities`, `Plan`, `Apply`, `Destroy`, `Status`, `Import`, `ResolveSizing`, + `BootstrapStateBackend` +- `IaCProviderDriftDetectorServer.DetectDrift` AND `DetectDriftWithSpecs` — + `DetectDrift` is the real impl (existence-check); `DetectDriftWithSpecs` is a + thin delegator to `DetectDrift` (ignores the specs map, consistent with existence-only + behavior). Both methods required for `IaCProviderDriftDetectorServer` to register cleanly. +- `ResourceDriverServer` 9 CRUD methods (AWSProvider.ResourceDriver works) + +**What is left as Unimplemented** (forward-compat embed only, not auto-registered): +- `EnumerateAll`, `EnumerateByTag` — no AWS tag-query implementation +- `RevokeProviderCredential` — no AWS credential rotation +- `RepairDirtyMigration` — no migration repair +- `ValidatePlan` — no cross-resource plan validator +- `DetectDriftConfig` — DriftConfigDetector is a separate service; leave Unimplemented + +Marshalling helpers (pb↔Go): copy pattern exactly from DO `iacserver.go`. +No JSON↔structpb conversion — config/outputs cross as `config_json`/`outputs_json` +(JSON bytes), matching the hard invariant. + +### Phase 2 — Entrypoint cutover + +**Note:** Phase 2 deletions and Phase 4 test deletions MUST happen atomically in +the same commit to avoid transient compile failures (`plugin_test.go` references +`iacProviderModule` from `module.go`; deleting `module.go` without deleting +`plugin_test.go` in the same commit breaks the build). + +| File | Action | +|------|--------| +| `cmd/workflow-plugin-aws/main.go` | Change `sdk.Serve(NewAWSPlugin())` → `sdk.ServeIaCPlugin(internal.NewIaCServer(), sdk.IaCServeOptions{})` | +| `internal/plugin.go` | DELETE (atomically with plugin_test.go; see Phase 4) | +| `internal/module.go` | DELETE (atomically with plugin_test.go; see Phase 4) | + +### Phase 3 — Version/metadata updates + +**Note:** After bumping `go.mod`, run `go mod tidy && go build ./...` before +proceeding to write `iacserver.go`. The v0.19.2 → v0.51.7 jump (32 minor versions) +may introduce transitive dependency changes. Surface any API breaks in +`interfaces.*` or `plugin/external/proto/*` as a blocker before writing new code. + +| File | Action | +|------|--------| +| `go.mod` | Bump `workflow v0.19.2` → `v0.51.7`; run `go mod tidy` | +| `plugin.json` | Bump `version` to `1.0.0`, `minEngineVersion` to `0.51.0` | +| `provider/provider.go` | Bump `ProviderVersion` constant to `1.0.0` | + +### Phase 4 — Tests + +**Deletion ordering:** `internal/plugin_test.go` and `internal/plugin.go` + +`internal/module.go` MUST be deleted in the same commit (see Phase 2 note). + +| File | Action | +|------|--------| +| `internal/iacserver_test.go` | NEW: unit tests for all server methods (mock provider) | +| `internal/host_conformance_test.go` | Rewrite: match DO v1.0.1 pattern (see below) | +| `internal/plugin_test.go` | DELETE atomically with plugin.go + module.go | + +**`host_conformance_test.go` rewrite spec** (must match DO v1.0.1 exactly): +1. Build plugin binary via `go build -o /workflow-plugin-aws ./cmd/workflow-plugin-aws` +2. Load via `external.NewExternalPluginManager` + `LoadPlugin` +3. Assert `adapter.ContractRegistry()` contains a **service-kind** contract + with `pb.IaCProviderRequired_ServiceDesc.ServiceName` (not module-kind) +4. Make a live `pb.NewIaCProviderRequiredClient(adapter.Conn()).Name()` RPC call +5. Make a live `required.Capabilities()` RPC call; assert `infra.container_service` present +6. These assertions mirror the DO `TestWorkflowHostConformance_LoadsTypedIaCPlugin` exactly + +### Phase 5 — CI + +CI already runs `go test ./...` and the host-conformance gate. No new gates needed. +The `WORKFLOW_IAC_HOST_CONFORMANCE=1` gate in `host_conformance_test.go` will be +updated to validate the typed-IaC load path (service-kind contract + live RPC). + +## Compile-time guards + +```go +var ( + _ pb.IaCProviderRequiredServer = (*awsIaCServer)(nil) + // IaCProviderDriftDetectorServer requires BOTH DetectDrift AND DetectDriftWithSpecs. + // Both are implemented: DetectDrift is the real check; DetectDriftWithSpecs delegates + // to DetectDrift and ignores the specs map (existence-only behavior). + _ pb.IaCProviderDriftDetectorServer = (*awsIaCServer)(nil) + _ pb.ResourceDriverServer = (*awsIaCServer)(nil) +) +``` + +Optional services auto-registered by `sdk.RegisterAllIaCProviderServices` when satisfied +at the Go type level — no manual registration required. + +**Service auto-registration behavior:** +- `IaCProviderDriftDetector` service IS registered (both methods implemented) +- `IaCProviderEnumerator` service is NOT registered (only embed, no real methods) +- `IaCProviderCredentialRevoker` service is NOT registered (only embed) +- `IaCProviderMigrationRepairer` service is NOT registered (only embed) +- `IaCProviderValidator` service is NOT registered (only embed) +- `IaCProviderDriftConfigDetector` service is NOT registered (only embed) + +## Wire invariants (from strict-contracts hard invariants) + +- NO `structpb.Struct` on the wire +- NO `Any.UnmarshalTo` for config/outputs — use `config_json` / `outputs_json` (JSON bytes) +- Outputs that are `map[string]any` are marshalled to JSON, never via `structpb.NewStruct` +- Typed slices (`[]string`, `[]X`) are safe because `pb.ResourceOutput.outputs_json` is `bytes` + — no structpb round-trip + +## Rollback + +This is a gRPC protocol change at the plugin boundary. Rollback = revert the commit +and retag v0.2.0. Consumers must pin the old tag explicitly. No database migrations, +no state mutations, no side effects outside the plugin binary. + +After reverting, run `go mod tidy` to restore go.sum to the v0.19.2-pinned state +(the workflow version bump changes transitive dependencies in go.sum; revert does +not auto-restore it). + +Old workflow engine tags (pre-v0.50.0) are permanently incompatible after this PR merges — +per the force-cutover mandate (`feedback_force_strict_contracts_no_compat`). + +## Assumptions + +1. `sdk.ServeIaCPlugin` and `sdk.RegisterAllIaCProviderServices` are present and stable + in workflow v0.51.7 (confirmed: DO v1.0.1 used v0.51.2 with this API). +2. `pb.Unimplemented*Server` embeds satisfy the forward-compat contract for optional + services not yet implemented by `*AWSProvider`. Services where ONLY the embed is + present are NOT auto-registered by the SDK (type-assertion fails) — so callers + get "service not registered" rather than `codes.Unimplemented`. +3. The `host_conformance_test.go` can be updated to validate the typed-IaC load path + (service-kind contract + live `Name()` + `Capabilities()` RPC) without requiring + a live AWS credential — the plugin binary starts and responds to these RPCs without + an initialized AWS session (same as DO pattern). +4. `workflow v0.51.7` is the latest stable tag (confirmed via `git tag`). +5. `*AWSProvider.DetectDrift` only implements existence-check (no spec comparison). + `DetectDriftWithSpecs` is implemented as a thin delegator to `DetectDrift`. + `DriftConfigDetector` (`DetectDriftConfig`) remains Unimplemented (different service). +6. The `strict-contracts` local branch's `internal/typed.go` uses the old + `InvokeTypedMethod` string-dispatch (predates force-cutover) and must NOT be merged. +7. `plugin.contracts.json` is loaded by the engine manager from disk independent of + the gRPC service registration (confirmed: DO v1.0.1 retains `plugin.contracts.json` + and the engine loads it separately). The file remains valid after cutover. +8. The v0.19.2 → v0.51.7 go.mod bump will not break `interfaces.*` or + `plugin/external/proto/*` APIs used by `AWSProvider` — confirmed by verifying + DO v1.0.1 (which has the same interface surface) compiled cleanly at v0.51.2. + +## Open questions resolved + +- Q: Should we implement `ValidatePlan`? A: No — AWSProvider has no cross-resource + constraint validator yet. Unimplemented embed is forward-compatible. +- Q: Multi-PR vs single-PR? A: Single-PR force-cutover per precedent and memory + `feedback_force_strict_contracts_no_compat`. +- Q: Keep `internal/contracts/aws.proto` / `AWSProviderConfig`? A: Yes — the proto + config message is still valid for `plugin.contracts.json` strict module config. + The typed module factory in `CreateTypedModule` becomes irrelevant after deletion + of `plugin.go`, but the proto descriptor remains useful for documentation. diff --git a/go.mod b/go.mod index 707734a..126d2a0 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/GoCodeAlone/workflow-plugin-aws go 1.26.0 require ( - github.com/GoCodeAlone/workflow v0.19.2 + github.com/GoCodeAlone/workflow v0.51.7 github.com/aws/aws-sdk-go-v2 v1.41.7 - github.com/aws/aws-sdk-go-v2/config v1.32.12 - github.com/aws/aws-sdk-go-v2/credentials v1.19.12 + github.com/aws/aws-sdk-go-v2/config v1.32.16 + github.com/aws/aws-sdk-go-v2/credentials v1.19.15 github.com/aws/aws-sdk-go-v2/service/acm v1.32.1 github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.33.8 github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.16 @@ -53,24 +53,23 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RoaringBitmap/roaring v1.9.4 // indirect github.com/Workiva/go-datastructures v1.1.7 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect github.com/aws/aws-sdk-go-v2/service/codebuild v1.68.12 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 // indirect github.com/aws/smithy-go v1.25.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect @@ -83,12 +82,11 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/deckarep/golang-set/v2 v2.8.0 // indirect + github.com/deckarep/golang-set/v2 v2.9.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/digitalocean/godo v1.178.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.2+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/eapache/go-resiliency v1.7.0 // indirect @@ -100,7 +98,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flowchartsman/retry v1.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -109,13 +107,12 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golobby/cast v1.3.3 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/go-querystring v1.2.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.19.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect @@ -150,7 +147,7 @@ require ( github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect github.com/miekg/dns v1.1.72 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -160,7 +157,7 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nats-io/nats.go v1.50.0 // indirect + github.com/nats-io/nats.go v1.51.0 // indirect github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect @@ -186,7 +183,7 @@ require ( github.com/tidwall/btree v1.8.1 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/redcon v1.6.2 // indirect - github.com/tochemey/goakt/v4 v4.1.1 // indirect + github.com/tochemey/goakt/v4 v4.2.2 // indirect github.com/tochemey/olric v0.3.9 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -197,6 +194,7 @@ require ( github.com/zalando/go-keyring v0.2.8 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.etcd.io/bbolt v1.4.3 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect @@ -214,19 +212,20 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/crypto v0.50.0 // indirect - golang.org/x/mod v0.34.0 // indirect + golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.43.0 // indirect + golang.org/x/tools v0.44.0 // indirect google.golang.org/api v0.272.0 // indirect google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect google.golang.org/grpc v1.80.0 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index 90f2856..be0d221 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0 h1:xb1mI4NZkzvNKQ2F6nk github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0/go.mod h1:hhGouwAVsonmJ4Lain4jINZ9nZCoc9l9eF3BHbmR8eE= github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0 h1:cvdLHbM/vzvygQTcAWSJsy+dAPzzwWyjzKMmTBFcFIo= github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0/go.mod h1:/9ipMG4qM2CHQ14BfXKdVlYRJelef6M8MFI5TbZv67M= -github.com/GoCodeAlone/workflow v0.19.2 h1:WPcF3fio/uvREvjIm+pE4AX9gCxFJo/QzblV46NTh70= -github.com/GoCodeAlone/workflow v0.19.2/go.mod h1:ypkCqXTwnIPqNjS8h38KZfwzdVsgwgkS1d6Dq0lXyQQ= +github.com/GoCodeAlone/workflow v0.51.7 h1:+81UNlLQPfnB6hwncWM6DPHHmonLoiqBL0YGQ6OW9g4= +github.com/GoCodeAlone/workflow v0.51.7/go.mod h1:5dh9esKq48kH4zKWjccXmyOirWL+T+YzfLclzhdRIV4= github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= @@ -84,8 +84,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI= github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antithesishq/antithesis-sdk-go v0.7.0 h1:uWDG8BqLD1lI2ps38WDz2vXflrTX2+vLX0SvZtztJtE= github.com/antithesishq/antithesis-sdk-go v0.7.0/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= @@ -94,20 +94,18 @@ github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6t github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= -github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= -github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= -github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/config v1.32.16 h1:Q0iQ7quUgJP0F/SCRTieScnaMdXr9h/2+wze1u3cNeM= +github.com/aws/aws-sdk-go-v2/config v1.32.16/go.mod h1:duCCnJEFqpt2RC6no1iK6q+8HpwOAkiUua0pY507dQc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4= +github.com/aws/aws-sdk-go-v2/credentials v1.19.15/go.mod h1:gJiYyMOjNg8OEdRWOf3CrFQxM2a98qmrtjx1zuiQfB8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8OuZZBtMJLZbk4P5HDjJO0jQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 h1:FPXsW9+gMuIeKmz7j6ENWcWtBGTe1kH8r9thNt5Uxx4= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23/go.mod h1:7J8iGMdRKk6lw2C+cMIphgAnT8uTwBwNOsGkyOCm80U= github.com/aws/aws-sdk-go-v2/service/acm v1.32.1 h1:KAK08un+8LhHlG6OEUmDTqFpQth2tYA+6EX0NNocgl4= github.com/aws/aws-sdk-go-v2/service/acm v1.32.1/go.mod h1:3sKYAgRbuBa2QMYGh/WEclwnmfx+QoPhhX25PdSQSQM= github.com/aws/aws-sdk-go-v2/service/apigatewayv2 v1.33.8 h1:I0AMtyv5tqQ/VNDDalbbujALCWl64TP3F61bBw4U8Qs= @@ -132,12 +130,12 @@ github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6 h1:fQR1aeZKa github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6/go.mod h1:oJRLDix51wqBDlP9dv+blFkvvf7HESolQz5cdhdmV4A= github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 h1:n9YLiWtX3+6pTLZWvRJmtq5JIB9NA/KFelyCg5fOlTU= github.com/aws/aws-sdk-go-v2/service/iam v1.53.7/go.mod h1:sP46Vo6MeJcM4s0ZXcG2PFmfiSyixhIuC/74W52yKuk= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 h1:HtOTYcbVcGABLOVuPYaIihj6IlkqubBwFj10K5fxRek= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8/go.mod h1:VsK9abqQeGlzPgUr+isNWzPlK2vKe9INMLWnY65f5Xs= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 h1:PUmZeJU6Y1Lbvt9WFuJ0ugUK2xn6hIWUBBbKuOWF30s= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22/go.mod h1:nO6egFBoAaoXze24a2C0NjQCvdpk8OueRoYimvEB9jo= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw= github.com/aws/aws-sdk-go-v2/service/kinesis v1.43.4 h1:3m9iJtMtLq75jKRAfw0kapoHUlbzi0CRVigysBN/FHA= @@ -148,14 +146,14 @@ github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 h1:Z+/OLsb85Kpq7TVLCspskqeP github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5/go.mod h1:TmxGowuBYwjmHFOsEDxaZdsQE62JJzOmtiWafTi/czg= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2 h1:MRNiP6nqa20aEl8fQ6PJpEq11b2d40b16sm4WD7QgMU= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.2/go.mod h1:FrNA56srbsr3WShiaelyWYEo70x80mXnVZ17ZZfbeqg= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 h1:a1Fq/KXn75wSzoJaPQTgZO0wHGqE9mjFnylnqEPTchA= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.10/go.mod h1:p6+MXNxW7IA6dMgHfTAzljuwSKD0NCm/4lbS4t6+7vI= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 h1:x6bKbmDhsgSZwv6q19wY/u3rLk/3FGjJWyqKcIRufpE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.16/go.mod h1:CudnEVKRtLn0+3uMV0yEXZ+YZOKnAtUJ5DmDhilVnIw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 h1:oK/njaL8GtyEihkWMD4k3VgHCT64RQKkZwh0DG5j8ak= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20/go.mod h1:JHs8/y1f3zY7U5WcuzoJ/yAYGYtNIVPKLIbp61euvmg= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcuxk8l+PG4+A0exds= +github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI= github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -211,20 +209,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= -github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.9.0 h1:prva4eP9UysWagLyKrtn074ughi0NnkIf0A4M5yOCKI= +github.com/deckarep/golang-set/v2 v2.9.0/go.mod h1:EWknQXbs0mcFpat2QOoXV0Ee57cD+w6ZEN76BR2JVrM= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/digitalocean/godo v1.178.0 h1:+B4xGOaoFwwwpM7TKhoyGHdmFg5eF9zDB1YfOLvNJ2E= -github.com/digitalocean/godo v1.178.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -258,8 +254,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= +github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= @@ -277,34 +273,34 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= -github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= +github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= -github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= -github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= -github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= -github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= -github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= -github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= -github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= -github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= -github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= -github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= -github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= -github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= -github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= -github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= -github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= -github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= -github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= -github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= -github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= -github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= -github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= -github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= -github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/swag v0.26.0 h1:GVDXCmfvhfu1BxiHo8/FA+BbKmhecHnG3varjON5/RI= +github.com/go-openapi/swag v0.26.0/go.mod h1:82g3193sZJRbocs7bNCqGfIgq8pkuwVwCfhKIRlEQF0= +github.com/go-openapi/swag/cmdutils v0.26.0 h1:iowihOcvq7y4egO8cOq0dmfohz6wfeQ63U1EnuhO2TU= +github.com/go-openapi/swag/cmdutils v0.26.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= +github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= +github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU= +github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= +github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= +github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= +github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= +github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= +github.com/go-openapi/swag/mangling v0.26.0 h1:Du2YC4YLA/Y5m/YKQd7AnY5qq0wRKSFZTTt8ktFaXcQ= +github.com/go-openapi/swag/mangling v0.26.0/go.mod h1:jifS7W9vbg+pw63bT+GI53otluMQL3CeemuyCHKwVx0= +github.com/go-openapi/swag/netutils v0.26.0 h1:CmZp+ZT7HrmFwrC3GdGsXBq2+42T1bjKBapcqVpIs3c= +github.com/go-openapi/swag/netutils v0.26.0/go.mod h1:5iK+Ok3ZohWWex1C50BFTPexi03UaPwjW4Oj8kgrpwo= +github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= +github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= +github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= +github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= +github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= +github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= @@ -345,11 +341,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= -github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -369,10 +362,10 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/hashicorp/consul/api v1.33.7 h1:apLZVzX7O7BLgHyh4pvczcsBzPmYSVXGKZQbOaA1ae0= -github.com/hashicorp/consul/api v1.33.7/go.mod h1:SjR3cjwCUSLLDfVw5dFg76rnnKjOySxr8W8lC5s01C8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hashicorp/consul/api v1.34.1 h1:/qKFfBJ5GEmY3sIlK1HulRJciLoT1pCGBkq2gwILXVg= +github.com/hashicorp/consul/api v1.34.1/go.mod h1:K+7fQ7o5QEyoFRyXtX3/iGSxDbg9jYWGJ5rDyrXzHU8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -459,8 +452,8 @@ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/ github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kapetan-io/tackle v0.13.0 h1:kcQTbgZN+4T89ktqlpW2TBATjiBmfjIyuZUukvRrYZU= -github.com/kapetan-io/tackle v0.13.0/go.mod h1:5ZGq3U/Qgpq0ccxyx2+Zovg2ceM9yl6DOVL2R90of4g= +github.com/kapetan-io/tackle v0.14.0 h1:Qu3zq6+95DLX7n71Up9X8SGFycCuvMyzo2D0m5Hkblk= +github.com/kapetan-io/tackle v0.14.0/go.mod h1:pDr4mjpo2RQO/q/je1dGuGwnBVwZcsRp60wgDV2hA3c= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -478,8 +471,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 h1:Qj3hTcdWH8uMZDI41HNuTuJN525C7NBrbtH5kSO6fPk= -github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -488,8 +481,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= @@ -505,6 +498,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -535,10 +532,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= -github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8= -github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo= -github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds= -github.com/nats-io/nats.go v1.50.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nats-server/v2 v2.12.7 h1:prQ9cPiWHcnwfT81Wi5lU9LL8TLY+7pxDru6fQYLCQQ= +github.com/nats-io/nats-server/v2 v2.12.7/go.mod h1:dOnmkprKMluTmTF7/QHZioxlau3sKHUM/LBPy9AiBPw= +github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= +github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= @@ -614,8 +611,8 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEV github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= -github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -642,12 +639,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= -github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= -github.com/testcontainers/testcontainers-go/modules/consul v0.41.0 h1:ssCWgKf4dst0Ys2J69kohXuXjINJXph0QgFf6mlwCbU= -github.com/testcontainers/testcontainers-go/modules/consul v0.41.0/go.mod h1:PwMdtDOg4IIImUWFLX2ZDMulqR70JCrfkZdIVM/lMN8= -github.com/testcontainers/testcontainers-go/modules/etcd v0.41.0 h1:HDEpWRH7JTCSUeJkcwkbRCUClZ8qyT6Z4RgfjR3JMr4= -github.com/testcontainers/testcontainers-go/modules/etcd v0.41.0/go.mod h1:PpbzL8aLFNc8VFd6yAqleklm60cvN+s8BiQh8VsNDfg= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/consul v0.42.0 h1:oQqQAPaiv5WvLB6lCapjohWRbMi1pYmPSTSDQrVv3nc= +github.com/testcontainers/testcontainers-go/modules/consul v0.42.0/go.mod h1:5/t9MNZTBLJ08QzPdVe0XXjLg7W31+udMM3+hoRYXa4= +github.com/testcontainers/testcontainers-go/modules/etcd v0.42.0 h1:Hy4Zt7/JfoNW35Vz99lH/yeRMgRy7ebxnwNJPHhpkZg= +github.com/testcontainers/testcontainers-go/modules/etcd v0.42.0/go.mod h1:+2oLnkMw0McOfhjlXEljY7LoXruENqsTaSIeHFy/VWU= github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= @@ -661,12 +658,10 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= -github.com/tochemey/goakt/v4 v4.1.1 h1:MO3HmcsDxTANSEZ9Js+dpwb/YD4qELBzV+0gbO9WrFQ= -github.com/tochemey/goakt/v4 v4.1.1/go.mod h1:fdUODkdd7FRkM4jumOd9jVBoCjB1L4YnAAF6WTYHMo0= +github.com/tochemey/goakt/v4 v4.2.2 h1:1SauIOrd4MdKsPFo8YUfy8V8+zZVYWodNhV7lP3DD5w= +github.com/tochemey/goakt/v4 v4.2.2/go.mod h1:ScoWT3Qb0SF4AjKvwBxiGzLGXrMikpfOunyx1XFefGo= github.com/tochemey/olric v0.3.9 h1:MU3VVQ3TZwdRzyxai0myxNMZj0lMK/RCjhaYh2Xe6aQ= github.com/tochemey/olric v0.3.9/go.mod h1:r5OVAIw1zaZJ5WKvKj1c4XnLwFaYpH8EJpm4dAD8Bp0= -github.com/travisjeffery/go-dynaport v1.0.0 h1:m/qqf5AHgB96CMMSworIPyo1i7NZueRsnwdzdCJ8Ajw= -github.com/travisjeffery/go-dynaport v1.0.0/go.mod h1:0LHuDS4QAx+mAc4ri3WkQdavgVoBIZ7cE9ob17KIAJk= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -700,12 +695,14 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.etcd.io/etcd/api/v3 v3.6.9 h1:UA7iKfEW1AzgihcBSGXci2kDGQiokSq41F9HMCI/RTI= -go.etcd.io/etcd/api/v3 v3.6.9/go.mod h1:csEk/qTfxKL36NqJdU15Tgtl65A8dyEY2BYo7PRsIwk= -go.etcd.io/etcd/client/pkg/v3 v3.6.9 h1:T8nuk8Lz64C+Hzb0coBFLMSlVSQZBpAtFk46swdM1DA= -go.etcd.io/etcd/client/pkg/v3 v3.6.9/go.mod h1:WEy3PpwbbEBVRdh1NVJYsuUe/8eyI21PNJRazeD8z/Y= -go.etcd.io/etcd/client/v3 v3.6.9 h1:3X555hQXmhRr27O37wls53g68CpUiPOiHXrZfz2Al+o= -go.etcd.io/etcd/client/v3 v3.6.9/go.mod h1:KO7H1HLYh1qaljuVZJQwBFk1lRce6pJzt+C81GEnrlM= +go.etcd.io/etcd/api/v3 v3.6.10 h1:jlwjtELjA8yi2VWpOFH+0w0lGr3K6mVDyn0RDB9aaAY= +go.etcd.io/etcd/api/v3 v3.6.10/go.mod h1:pdV4VeFmvhdNjB4LWRkC8ReLyRBAxUOze3GarMhE2sk= +go.etcd.io/etcd/client/pkg/v3 v3.6.10 h1:tBT7podcPhuVbCVkAEzx8bC5I+aqxfLwBN8/As1arrA= +go.etcd.io/etcd/client/pkg/v3 v3.6.10/go.mod h1:WEy3PpwbbEBVRdh1NVJYsuUe/8eyI21PNJRazeD8z/Y= +go.etcd.io/etcd/client/v3 v3.6.10 h1:J598zJ+C/ZPvImypmq5waj84+bovePrlZERHklf34y0= +go.etcd.io/etcd/client/v3 v3.6.10/go.mod h1:iHhUDUcEwaKs1YFq3MgmI9U4zhTVasp/vgdVbFf1RS8= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= @@ -754,13 +751,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= -golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -816,7 +813,6 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -839,8 +835,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -852,12 +848,14 @@ google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwR google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= -google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529 h1:zUWMZsvo/IJcD1t6MNCPO/azZTwz0TvwCBqr5aifoVY= +google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529/go.mod h1:a5OGAgyRr4lqco7AG9hQM9Fwh0N2ZV4grR0eXFEsXQg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 h1:/WILD1UcXj/ujCxgoL/DvRgt2CP3txG8+FwkUbb9110= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1/go.mod h1:YNKnb2OAApgYn2oYY47Rn7alMr1zWjb2U8Q0aoGWiNc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -886,16 +884,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= -k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= -k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= -k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= -k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= +k8s.io/api v0.35.4 h1:P7nFYKl5vo9AGUp1Z+Pmd3p2tA7bX2wbFWCvDeRv988= +k8s.io/api v0.35.4/go.mod h1:yl4lqySWOgYJJf9RERXKUwE9g2y+CkuwG+xmcOK8wXU= +k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds= +k8s.io/apimachinery v0.35.4/go.mod h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc= +k8s.io/client-go v0.35.4 h1:DN6fyaGuzK64UvnKO5fOA6ymSjvfGAnCAHAR0C66kD8= +k8s.io/client-go v0.35.4/go.mod h1:2Pg9WpsS4NeOpoYTfHHfMxBG8zFMSAUi4O/qoiJC3nY= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9 h1:Sztf7ESG9tAXRW/ACJZjrj5jhdOUqS2KFRQT+CTvu78= -k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/kube-openapi v0.0.0-20260414162039-ec9c827d403f h1:4Qiq0YAoQATdgmHALJWz9rJ4fj20pB3xebpB4CFNhYM= +k8s.io/kube-openapi v0.0.0-20260414162039-ec9c827d403f/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= @@ -930,7 +928,7 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo= +sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/host_conformance_test.go b/internal/host_conformance_test.go index 381bebb..8ebd26b 100644 --- a/internal/host_conformance_test.go +++ b/internal/host_conformance_test.go @@ -1,26 +1,30 @@ package internal import ( + "context" "encoding/json" "os" "os/exec" "path/filepath" "runtime" "testing" + "time" "github.com/GoCodeAlone/workflow/plugin/external" pb "github.com/GoCodeAlone/workflow/plugin/external/proto" ) -func TestWorkflowHostConformance_LoadsLegacyIaCModulePlugin(t *testing.T) { +// TestWorkflowHostConformance_LoadsTypedIaCPlugin validates the host/plugin +// boundary for the typed-IaC gRPC pattern (sdk.ServeIaCPlugin). Skipped by +// default; set WORKFLOW_IAC_HOST_CONFORMANCE=1 to run. +// +// This test mirrors workflow-plugin-digitalocean v1.0.1 +// internal/host_conformance_test.go exactly. +func TestWorkflowHostConformance_LoadsTypedIaCPlugin(t *testing.T) { if os.Getenv("WORKFLOW_IAC_HOST_CONFORMANCE") != "1" { t.Skip("set WORKFLOW_IAC_HOST_CONFORMANCE=1 to run host compatibility smoke") } - // AWS is still a legacy sdk.Serve module plugin, not a strict-cutover - // sdk.ServeIaCPlugin provider. This gate validates the host/plugin boundary - // it actually ships today: external plugin load, iac.provider discovery, and - // strict module contract registry exposure across engine versions. repoRoot := hostConformanceRepoRoot(t) pluginName := hostConformancePluginName(t, filepath.Join(repoRoot, "plugin.json")) @@ -45,19 +49,36 @@ func TestWorkflowHostConformance_LoadsLegacyIaCModulePlugin(t *testing.T) { if err != nil { t.Fatalf("load plugin through Workflow external host: %v", err) } - if adapter.Name() != pluginName { - t.Fatalf("host adapter name = %q, want %q", adapter.Name(), pluginName) - } - if !hasString(adapter.EngineManifest().ModuleTypes, moduleTypeIaCProvider) { - t.Fatalf("host adapter module types = %v, want %q", adapter.EngineManifest().ModuleTypes, moduleTypeIaCProvider) - } registry := adapter.ContractRegistry() if registry == nil { t.Fatal("contract registry is nil") } - if !registryHasModule(registry, moduleTypeIaCProvider) { - t.Fatalf("contract registry missing module %q: %v", moduleTypeIaCProvider, registry.GetContracts()) + // Typed-IaC plugins expose SERVICE-kind contracts (not module-kind). + if !registryHasService(registry, pb.IaCProviderRequired_ServiceDesc.ServiceName) { + t.Fatalf("contract registry missing required service %q: %v", + pb.IaCProviderRequired_ServiceDesc.ServiceName, registry.GetContracts()) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + + required := pb.NewIaCProviderRequiredClient(adapter.Conn()) + name, err := required.Name(ctx, &pb.NameRequest{}) + if err != nil { + t.Fatalf("call typed IaCProviderRequired.Name: %v", err) + } + if name.GetName() != "aws" { + t.Fatalf("provider name = %q, want %q", name.GetName(), "aws") + } + + capabilities, err := required.Capabilities(ctx, &pb.CapabilitiesRequest{}) + if err != nil { + t.Fatalf("call typed IaCProviderRequired.Capabilities: %v", err) + } + if !capabilitiesHasResource(capabilities, "infra.container_service") { + t.Fatalf("provider capabilities missing infra.container_service: %v", + capabilities.GetCapabilities()) } } @@ -99,18 +120,19 @@ func hostConformanceCopyFile(t *testing.T, src, dst string) { } } -func registryHasModule(registry *pb.ContractRegistry, moduleType string) bool { +func registryHasService(registry *pb.ContractRegistry, serviceName string) bool { for _, contract := range registry.GetContracts() { - if contract.GetKind() == pb.ContractKind_CONTRACT_KIND_MODULE && contract.GetModuleType() == moduleType { + if contract.GetKind() == pb.ContractKind_CONTRACT_KIND_SERVICE && + contract.GetServiceName() == serviceName { return true } } return false } -func hasString(values []string, want string) bool { - for _, value := range values { - if value == want { +func capabilitiesHasResource(capabilities *pb.CapabilitiesResponse, resourceType string) bool { + for _, capability := range capabilities.GetCapabilities() { + if capability.GetResourceType() == resourceType { return true } } diff --git a/internal/iacserver.go b/internal/iacserver.go new file mode 100644 index 0000000..ab3f3b4 --- /dev/null +++ b/internal/iacserver.go @@ -0,0 +1,761 @@ +// Package internal — typed pb.IaCProvider*Server implementation. +// +// awsIaCServer is the SERVER side of the typed IaC contract. It satisfies +// pb.IaCProviderRequiredServer plus the optional pb.IaCProviderDriftDetectorServer +// interface by delegating each typed RPC to the matching method on the +// underlying *provider.AWSProvider. +// +// The remaining optional services (Enumerator, CredentialRevoker, MigrationRepairer, +// Validator, DriftConfigDetector) are present as Unimplemented*Server embeds only +// (forward-compat; not auto-registered by sdk.RegisterAllIaCProviderServices). +// +// Hard invariants (strict-contracts force-cutover): +// - NO structpb.Struct, NO Any.UnmarshalTo on the wire — provider-specific +// config / outputs cross as JSON bytes (config_json, outputs_json). +// - REQUIRED service methods MUST be implemented; the SDK type-assert in +// sdk.RegisterAllIaCProviderServices fails at plugin startup otherwise. +package internal + +import ( + "context" + "encoding/json" + "fmt" + "math" + "time" + + "github.com/GoCodeAlone/workflow-plugin-aws/provider" + "github.com/GoCodeAlone/workflow/interfaces" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// awsIaCServer wraps *provider.AWSProvider and exposes the typed +// pb.IaCProvider*Server + ResourceDriverServer surface. The Unimplemented*Server +// embeds satisfy the gRPC forward-compat contract and let the SDK type-assert +// succeed. awsIaCServer overrides all Required methods and the DriftDetector methods. +type awsIaCServer struct { + pb.UnimplementedIaCProviderRequiredServer + pb.UnimplementedIaCProviderEnumeratorServer + pb.UnimplementedIaCProviderDriftDetectorServer + pb.UnimplementedIaCProviderCredentialRevokerServer + pb.UnimplementedIaCProviderMigrationRepairerServer + pb.UnimplementedIaCProviderValidatorServer + pb.UnimplementedIaCProviderDriftConfigDetectorServer + pb.UnimplementedResourceDriverServer + + provider *provider.AWSProvider +} + +// newAWSIaCServer constructs a typed-IaC server backed by the given +// *provider.AWSProvider. The provider is NOT initialized here; Initialize is +// the first typed RPC the host sends after the gRPC dial completes. +func newAWSIaCServer(p *provider.AWSProvider) *awsIaCServer { + return &awsIaCServer{provider: p} +} + +// NewIaCServer is the package entrypoint used by cmd/workflow-plugin-aws/main.go. +// It constructs a fresh *provider.AWSProvider and wraps it in the typed +// pb.IaCProvider* server surface. The returned value is suitable to pass to +// sdk.ServeIaCPlugin; the SDK auto-registers every typed gRPC service the +// server satisfies via Go type-assertion at plugin startup. +func NewIaCServer() *awsIaCServer { + return newAWSIaCServer(provider.NewAWSProviderConcrete()) +} + +// Compile-time guards: every typed server interface this AWS plugin advertises +// MUST be satisfied. A signature drift on any of these will fail the build at +// this file rather than at first RPC dispatch. +var ( + _ pb.IaCProviderRequiredServer = (*awsIaCServer)(nil) + // IaCProviderDriftDetectorServer requires BOTH DetectDrift AND DetectDriftWithSpecs. + // Both are implemented below: DetectDrift is the real check; DetectDriftWithSpecs + // delegates to DetectDrift (existence-only behavior; ignores the specs map). + _ pb.IaCProviderDriftDetectorServer = (*awsIaCServer)(nil) + _ pb.ResourceDriverServer = (*awsIaCServer)(nil) +) + +// ── Required service methods ──────────────────────────────────────────────── + +func (s *awsIaCServer) Initialize(ctx context.Context, req *pb.InitializeRequest) (*pb.InitializeResponse, error) { + cfg, err := unmarshalJSONMap(req.GetConfigJson()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: parse Initialize config_json: %w", err) + } + if err := s.provider.Initialize(ctx, cfg); err != nil { + return nil, err + } + return &pb.InitializeResponse{}, nil +} + +func (s *awsIaCServer) Name(_ context.Context, _ *pb.NameRequest) (*pb.NameResponse, error) { + return &pb.NameResponse{Name: s.provider.Name()}, nil +} + +func (s *awsIaCServer) Version(_ context.Context, _ *pb.VersionRequest) (*pb.VersionResponse, error) { + return &pb.VersionResponse{Version: s.provider.Version()}, nil +} + +func (s *awsIaCServer) Capabilities(_ context.Context, _ *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) { + caps := s.provider.Capabilities() + out := make([]*pb.IaCCapabilityDeclaration, 0, len(caps)) + for _, c := range caps { + tier := c.Tier + if tier < math.MinInt32 { + tier = math.MinInt32 + } else if tier > math.MaxInt32 { + tier = math.MaxInt32 + } + out = append(out, &pb.IaCCapabilityDeclaration{ + ResourceType: c.ResourceType, + Tier: int32(tier), //nolint:gosec // G115: clamped above + Operations: append([]string(nil), c.Operations...), + }) + } + return &pb.CapabilitiesResponse{Capabilities: out}, nil +} + +func (s *awsIaCServer) Plan(ctx context.Context, req *pb.PlanRequest) (*pb.PlanResponse, error) { + desired, err := specsFromPB(req.GetDesired()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: decode Plan desired: %w", err) + } + current, err := statesFromPB(req.GetCurrent()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: decode Plan current: %w", err) + } + plan, err := s.provider.Plan(ctx, desired, current) + if err != nil { + return nil, err + } + pbPlan, err := planToPB(plan) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode Plan response: %w", err) + } + return &pb.PlanResponse{Plan: pbPlan}, nil +} + +func (s *awsIaCServer) Apply(ctx context.Context, req *pb.ApplyRequest) (*pb.ApplyResponse, error) { + plan, err := planFromPB(req.GetPlan()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: decode Apply plan: %w", err) + } + result, err := s.provider.Apply(ctx, plan) + if err != nil { + return nil, err + } + pbResult, err := applyResultToPB(result) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode Apply response: %w", err) + } + return &pb.ApplyResponse{Result: pbResult}, nil +} + +func (s *awsIaCServer) Destroy(ctx context.Context, req *pb.DestroyRequest) (*pb.DestroyResponse, error) { + refs := refsFromPB(req.GetRefs()) + result, err := s.provider.Destroy(ctx, refs) + if err != nil { + return nil, err + } + return &pb.DestroyResponse{Result: destroyResultToPB(result)}, nil +} + +func (s *awsIaCServer) Status(ctx context.Context, req *pb.StatusRequest) (*pb.StatusResponse, error) { + refs := refsFromPB(req.GetRefs()) + statuses, err := s.provider.Status(ctx, refs) + if err != nil { + return nil, err + } + pbStatuses, err := statusesToPB(statuses) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode Status response: %w", err) + } + return &pb.StatusResponse{Statuses: pbStatuses}, nil +} + +func (s *awsIaCServer) Import(ctx context.Context, req *pb.ImportRequest) (*pb.ImportResponse, error) { + state, err := s.provider.Import(ctx, req.GetProviderId(), req.GetResourceType()) + if err != nil { + return nil, err + } + pbState, err := stateToPB(state) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode Import response: %w", err) + } + return &pb.ImportResponse{State: pbState}, nil +} + +func (s *awsIaCServer) ResolveSizing(_ context.Context, req *pb.ResolveSizingRequest) (*pb.ResolveSizingResponse, error) { + sizing, err := s.provider.ResolveSizing( + req.GetResourceType(), + interfaces.Size(req.GetSize()), + hintsFromPB(req.GetHints()), + ) + if err != nil { + return nil, err + } + pbSizing, err := sizingToPB(sizing) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode ResolveSizing response: %w", err) + } + return &pb.ResolveSizingResponse{Sizing: pbSizing}, nil +} + +func (s *awsIaCServer) BootstrapStateBackend(ctx context.Context, req *pb.BootstrapStateBackendRequest) (*pb.BootstrapStateBackendResponse, error) { + cfg, err := unmarshalJSONMap(req.GetConfigJson()) + if err != nil { + return nil, fmt.Errorf("aws iacserver: parse BootstrapStateBackend config_json: %w", err) + } + result, err := s.provider.BootstrapStateBackend(ctx, cfg) + if err != nil { + return nil, err + } + return &pb.BootstrapStateBackendResponse{Result: bootstrapResultToPB(result)}, nil +} + +// ── Optional: DriftDetector ──────────────────────────────────────────────── + +func (s *awsIaCServer) DetectDrift(ctx context.Context, req *pb.DetectDriftRequest) (*pb.DetectDriftResponse, error) { + refs := refsFromPB(req.GetRefs()) + drifts, err := s.provider.DetectDrift(ctx, refs) + if err != nil { + return nil, err + } + pbDrifts, err := driftsToPB(drifts) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode DetectDrift response: %w", err) + } + return &pb.DetectDriftResponse{Drifts: pbDrifts}, nil +} + +// DetectDriftWithSpecs satisfies pb.IaCProviderDriftDetectorServer. +// AWSProvider only implements existence-check drift detection; this method +// delegates to DetectDrift and ignores the specs map (consistent with +// existence-only behavior). Both methods are required for IaCProviderDriftDetector +// to register cleanly via sdk.RegisterAllIaCProviderServices. +func (s *awsIaCServer) DetectDriftWithSpecs(ctx context.Context, req *pb.DetectDriftWithSpecsRequest) (*pb.DetectDriftWithSpecsResponse, error) { + refs := refsFromPB(req.GetRefs()) + drifts, err := s.provider.DetectDrift(ctx, refs) + if err != nil { + return nil, err + } + pbDrifts, err := driftsToPB(drifts) + if err != nil { + return nil, fmt.Errorf("aws iacserver: encode DetectDriftWithSpecs response: %w", err) + } + return &pb.DetectDriftWithSpecsResponse{Drifts: pbDrifts}, nil +} + +// ── Marshalling helpers (pb ↔ Go) ─────────────────────────────────────────── +// +// These mirror the inverse-direction helpers in cmd/wfctl/iac_typed_adapter.go +// (workflow). Pattern copied from workflow-plugin-digitalocean v1.0.1 iacserver.go. + +func unmarshalJSONMap(b []byte) (map[string]any, error) { + if len(b) == 0 { + return nil, nil + } + var out map[string]any + if err := json.Unmarshal(b, &out); err != nil { + return nil, err + } + return out, nil +} + +func marshalJSONMap(m map[string]any) ([]byte, error) { + if m == nil { + return nil, nil + } + return json.Marshal(m) +} + +func marshalJSONAny(v any) ([]byte, error) { + if v == nil { + return nil, nil + } + return json.Marshal(v) +} + +func unmarshalJSONAny(b []byte) (any, error) { + if len(b) == 0 { + return nil, nil + } + var out any + if err := json.Unmarshal(b, &out); err != nil { + return nil, err + } + return out, nil +} + +func refToPB(r interfaces.ResourceRef) *pb.ResourceRef { + return &pb.ResourceRef{Name: r.Name, Type: r.Type, ProviderId: r.ProviderID} +} + +func refFromPB(r *pb.ResourceRef) interfaces.ResourceRef { + if r == nil { + return interfaces.ResourceRef{} + } + return interfaces.ResourceRef{Name: r.GetName(), Type: r.GetType(), ProviderID: r.GetProviderId()} +} + +func refsToPB(refs []interfaces.ResourceRef) []*pb.ResourceRef { + out := make([]*pb.ResourceRef, 0, len(refs)) + for _, r := range refs { + out = append(out, refToPB(r)) + } + return out +} + +func refsFromPB(refs []*pb.ResourceRef) []interfaces.ResourceRef { + out := make([]interfaces.ResourceRef, 0, len(refs)) + for _, r := range refs { + out = append(out, refFromPB(r)) + } + return out +} + +func hintsToPB(h *interfaces.ResourceHints) *pb.ResourceHints { + if h == nil { + return nil + } + return &pb.ResourceHints{Cpu: h.CPU, Memory: h.Memory, Storage: h.Storage} +} + +func hintsFromPB(h *pb.ResourceHints) *interfaces.ResourceHints { + if h == nil { + return nil + } + return &interfaces.ResourceHints{CPU: h.GetCpu(), Memory: h.GetMemory(), Storage: h.GetStorage()} +} + +func specToPB(s interfaces.ResourceSpec) (*pb.ResourceSpec, error) { + cfgJSON, err := marshalJSONMap(s.Config) + if err != nil { + return nil, err + } + return &pb.ResourceSpec{ + Name: s.Name, + Type: s.Type, + ConfigJson: cfgJSON, + Size: string(s.Size), + Hints: hintsToPB(s.Hints), + DependsOn: append([]string(nil), s.DependsOn...), + }, nil +} + +func specFromPB(s *pb.ResourceSpec) (interfaces.ResourceSpec, error) { + if s == nil { + return interfaces.ResourceSpec{}, nil + } + cfg, err := unmarshalJSONMap(s.GetConfigJson()) + if err != nil { + return interfaces.ResourceSpec{}, err + } + return interfaces.ResourceSpec{ + Name: s.GetName(), + Type: s.GetType(), + Config: cfg, + Size: interfaces.Size(s.GetSize()), + Hints: hintsFromPB(s.GetHints()), + DependsOn: append([]string(nil), s.GetDependsOn()...), + }, nil +} + +func specsFromPB(specs []*pb.ResourceSpec) ([]interfaces.ResourceSpec, error) { + out := make([]interfaces.ResourceSpec, 0, len(specs)) + for _, s := range specs { + gs, err := specFromPB(s) + if err != nil { + return nil, err + } + out = append(out, gs) + } + return out, nil +} + +func stateToPB(st *interfaces.ResourceState) (*pb.ResourceState, error) { + if st == nil { + return nil, nil + } + appliedJSON, err := marshalJSONMap(st.AppliedConfig) + if err != nil { + return nil, err + } + outputsJSON, err := marshalJSONMap(st.Outputs) + if err != nil { + return nil, err + } + return &pb.ResourceState{ + Id: st.ID, + Name: st.Name, + Type: st.Type, + Provider: st.Provider, + ProviderRef: st.ProviderRef, + ProviderId: st.ProviderID, + ConfigHash: st.ConfigHash, + AppliedConfigJson: appliedJSON, + AppliedConfigSource: st.AppliedConfigSource, + OutputsJson: outputsJSON, + Dependencies: append([]string(nil), st.Dependencies...), + CreatedAt: timeToPB(st.CreatedAt), + UpdatedAt: timeToPB(st.UpdatedAt), + LastDriftCheck: timeToPB(st.LastDriftCheck), + }, nil +} + +func stateFromPB(s *pb.ResourceState) (*interfaces.ResourceState, error) { + if s == nil { + return nil, nil + } + applied, err := unmarshalJSONMap(s.GetAppliedConfigJson()) + if err != nil { + return nil, err + } + outputs, err := unmarshalJSONMap(s.GetOutputsJson()) + if err != nil { + return nil, err + } + return &interfaces.ResourceState{ + ID: s.GetId(), + Name: s.GetName(), + Type: s.GetType(), + Provider: s.GetProvider(), + ProviderRef: s.GetProviderRef(), + ProviderID: s.GetProviderId(), + ConfigHash: s.GetConfigHash(), + AppliedConfig: applied, + AppliedConfigSource: s.GetAppliedConfigSource(), + Outputs: outputs, + Dependencies: append([]string(nil), s.GetDependencies()...), + CreatedAt: timeFromPB(s.GetCreatedAt()), + UpdatedAt: timeFromPB(s.GetUpdatedAt()), + LastDriftCheck: timeFromPB(s.GetLastDriftCheck()), + }, nil +} + +func statesFromPB(states []*pb.ResourceState) ([]interfaces.ResourceState, error) { + out := make([]interfaces.ResourceState, 0, len(states)) + for _, s := range states { + gs, err := stateFromPB(s) + if err != nil { + return nil, err + } + if gs != nil { + out = append(out, *gs) + } + } + return out, nil +} + +func outputToPB(o *interfaces.ResourceOutput) (*pb.ResourceOutput, error) { + if o == nil { + return nil, nil + } + outputsJSON, err := marshalJSONMap(o.Outputs) + if err != nil { + return nil, err + } + sensitive := make(map[string]bool, len(o.Sensitive)) + for k, v := range o.Sensitive { + sensitive[k] = v + } + return &pb.ResourceOutput{ + Name: o.Name, + Type: o.Type, + ProviderId: o.ProviderID, + OutputsJson: outputsJSON, + Sensitive: sensitive, + Status: o.Status, + }, nil +} + +func statusesToPB(ss []interfaces.ResourceStatus) ([]*pb.ResourceStatus, error) { + out := make([]*pb.ResourceStatus, 0, len(ss)) + for i := range ss { + o, err := marshalJSONMap(ss[i].Outputs) + if err != nil { + return nil, err + } + out = append(out, &pb.ResourceStatus{ + Name: ss[i].Name, + Type: ss[i].Type, + ProviderId: ss[i].ProviderID, + Status: ss[i].Status, + OutputsJson: o, + }) + } + return out, nil +} + +func driftClassToPB(c interfaces.DriftClass) pb.DriftClass { + switch c { + case interfaces.DriftClassInSync: + return pb.DriftClass_DRIFT_CLASS_IN_SYNC + case interfaces.DriftClassGhost: + return pb.DriftClass_DRIFT_CLASS_GHOST + case interfaces.DriftClassConfig: + return pb.DriftClass_DRIFT_CLASS_CONFIG + default: + return pb.DriftClass_DRIFT_CLASS_UNKNOWN + } +} + +func driftsToPB(drifts []interfaces.DriftResult) ([]*pb.DriftResult, error) { + out := make([]*pb.DriftResult, 0, len(drifts)) + for _, d := range drifts { + expectedJSON, err := marshalJSONMap(d.Expected) + if err != nil { + return nil, err + } + actualJSON, err := marshalJSONMap(d.Actual) + if err != nil { + return nil, err + } + out = append(out, &pb.DriftResult{ + Name: d.Name, + Type: d.Type, + Drifted: d.Drifted, + Class: driftClassToPB(d.Class), + ExpectedJson: expectedJSON, + ActualJson: actualJSON, + Fields: append([]string(nil), d.Fields...), + }) + } + return out, nil +} + +func planActionToPB(a interfaces.PlanAction) (*pb.PlanAction, error) { + pbSpec, err := specToPB(a.Resource) + if err != nil { + return nil, err + } + var pbCurrent *pb.ResourceState + if a.Current != nil { + pbCurrent, err = stateToPB(a.Current) + if err != nil { + return nil, err + } + } + pbChanges, err := changesToPB(a.Changes) + if err != nil { + return nil, err + } + return &pb.PlanAction{ + Action: a.Action, + Resource: pbSpec, + Current: pbCurrent, + Changes: pbChanges, + ResolvedConfigHash: a.ResolvedConfigHash, + }, nil +} + +func planActionFromPB(a *pb.PlanAction) (interfaces.PlanAction, error) { + if a == nil { + return interfaces.PlanAction{}, nil + } + spec, err := specFromPB(a.GetResource()) + if err != nil { + return interfaces.PlanAction{}, err + } + var current *interfaces.ResourceState + if a.GetCurrent() != nil { + current, err = stateFromPB(a.GetCurrent()) + if err != nil { + return interfaces.PlanAction{}, err + } + } + changes, err := changesFromPB(a.GetChanges()) + if err != nil { + return interfaces.PlanAction{}, err + } + return interfaces.PlanAction{ + Action: a.GetAction(), + Resource: spec, + Current: current, + Changes: changes, + ResolvedConfigHash: a.GetResolvedConfigHash(), + }, nil +} + +func changesToPB(changes []interfaces.FieldChange) ([]*pb.FieldChange, error) { + out := make([]*pb.FieldChange, 0, len(changes)) + for _, c := range changes { + oldJSON, err := marshalJSONAny(c.Old) + if err != nil { + return nil, err + } + newJSON, err := marshalJSONAny(c.New) + if err != nil { + return nil, err + } + out = append(out, &pb.FieldChange{ + Path: c.Path, + OldJson: oldJSON, + NewJson: newJSON, + ForceNew: c.ForceNew, + }) + } + return out, nil +} + +func changesFromPB(changes []*pb.FieldChange) ([]interfaces.FieldChange, error) { + out := make([]interfaces.FieldChange, 0, len(changes)) + for _, c := range changes { + oldVal, err := unmarshalJSONAny(c.GetOldJson()) + if err != nil { + return nil, err + } + newVal, err := unmarshalJSONAny(c.GetNewJson()) + if err != nil { + return nil, err + } + out = append(out, interfaces.FieldChange{ + Path: c.GetPath(), + Old: oldVal, + New: newVal, + ForceNew: c.GetForceNew(), + }) + } + return out, nil +} + +func planToPB(p *interfaces.IaCPlan) (*pb.IaCPlan, error) { + if p == nil { + return nil, nil + } + pbActions := make([]*pb.PlanAction, 0, len(p.Actions)) + for i := range p.Actions { + pa, err := planActionToPB(p.Actions[i]) + if err != nil { + return nil, err + } + pbActions = append(pbActions, pa) + } + if p.SchemaVersion < math.MinInt32 || p.SchemaVersion > math.MaxInt32 { + return nil, fmt.Errorf("aws iacserver: plan SchemaVersion %d out of int32 range", p.SchemaVersion) + } + return &pb.IaCPlan{ + Id: p.ID, + Actions: pbActions, + CreatedAt: timeToPB(p.CreatedAt), + DesiredHash: p.DesiredHash, + SchemaVersion: int32(p.SchemaVersion), //nolint:gosec // G115: range-checked above + InputSnapshot: copyStringMap(p.InputSnapshot), + }, nil +} + +func planFromPB(p *pb.IaCPlan) (*interfaces.IaCPlan, error) { + if p == nil { + return nil, nil + } + actions := make([]interfaces.PlanAction, 0, len(p.GetActions())) + for _, a := range p.GetActions() { + pa, err := planActionFromPB(a) + if err != nil { + return nil, err + } + actions = append(actions, pa) + } + return &interfaces.IaCPlan{ + ID: p.GetId(), + Actions: actions, + CreatedAt: timeFromPB(p.GetCreatedAt()), + DesiredHash: p.GetDesiredHash(), + SchemaVersion: int(p.GetSchemaVersion()), + InputSnapshot: copyStringMap(p.GetInputSnapshot()), + }, nil +} + +func applyResultToPB(r *interfaces.ApplyResult) (*pb.ApplyResult, error) { + if r == nil { + return nil, nil + } + resources := make([]*pb.ResourceOutput, 0, len(r.Resources)) + for i := range r.Resources { + ro, err := outputToPB(&r.Resources[i]) + if err != nil { + return nil, err + } + if ro != nil { + resources = append(resources, ro) + } + } + errs := make([]*pb.ActionError, 0, len(r.Errors)) + for _, e := range r.Errors { + errs = append(errs, &pb.ActionError{Resource: e.Resource, Action: e.Action, Error: e.Error}) + } + driftReport := make([]*pb.DriftEntry, 0, len(r.InputDriftReport)) + for _, d := range r.InputDriftReport { + driftReport = append(driftReport, &pb.DriftEntry{ + Name: d.Name, + PlanFingerprint: d.PlanFingerprint, + ApplyFingerprint: d.ApplyFingerprint, + }) + } + return &pb.ApplyResult{ + PlanId: r.PlanID, + Resources: resources, + Errors: errs, + InitialInputSnapshot: copyStringMap(r.InitialInputSnapshot), + InputDriftReport: driftReport, + ReplaceIdMap: copyStringMap(r.ReplaceIDMap), + }, nil +} + +func destroyResultToPB(r *interfaces.DestroyResult) *pb.DestroyResult { + if r == nil { + return nil + } + errs := make([]*pb.ActionError, 0, len(r.Errors)) + for _, e := range r.Errors { + errs = append(errs, &pb.ActionError{Resource: e.Resource, Action: e.Action, Error: e.Error}) + } + return &pb.DestroyResult{Destroyed: append([]string(nil), r.Destroyed...), Errors: errs} +} + +func bootstrapResultToPB(r *interfaces.BootstrapResult) *pb.BootstrapResult { + if r == nil { + return nil + } + return &pb.BootstrapResult{ + Bucket: r.Bucket, + Region: r.Region, + Endpoint: r.Endpoint, + EnvVars: copyStringMap(r.EnvVars), + } +} + +func sizingToPB(s *interfaces.ProviderSizing) (*pb.ProviderSizing, error) { + if s == nil { + return nil, nil + } + specsJSON, err := marshalJSONMap(s.Specs) + if err != nil { + return nil, err + } + return &pb.ProviderSizing{InstanceType: s.InstanceType, SpecsJson: specsJSON}, nil +} + +func timeToPB(t time.Time) *timestamppb.Timestamp { + if t.IsZero() { + return nil + } + return timestamppb.New(t) +} + +func timeFromPB(t *timestamppb.Timestamp) time.Time { + if t == nil { + return time.Time{} + } + return t.AsTime() +} + +func copyStringMap(m map[string]string) map[string]string { + if m == nil { + return nil + } + out := make(map[string]string, len(m)) + for k, v := range m { + out[k] = v + } + return out +} diff --git a/internal/iacserver_test.go b/internal/iacserver_test.go new file mode 100644 index 0000000..078c550 --- /dev/null +++ b/internal/iacserver_test.go @@ -0,0 +1,99 @@ +// Package internal_test exercises the awsIaCServer typed gRPC methods. +// Tests use a real *provider.AWSProvider with no initialized AWS session; +// only methods that do NOT require a live AWS credential are covered here. +// Initialize, Plan, Apply, Destroy, Import, Status test coverage lives in +// provider/provider_test.go (existing suite). +package internal + +import ( + "context" + "testing" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +func TestNewIaCServer_NotNil(t *testing.T) { + s := NewIaCServer() + if s == nil { + t.Fatal("NewIaCServer returned nil") + } +} + +func TestIaCServer_Name(t *testing.T) { + s := NewIaCServer() + resp, err := s.Name(context.Background(), &pb.NameRequest{}) + if err != nil { + t.Fatalf("Name: %v", err) + } + if resp.GetName() != "aws" { + t.Errorf("Name = %q, want %q", resp.GetName(), "aws") + } +} + +func TestIaCServer_Version(t *testing.T) { + s := NewIaCServer() + resp, err := s.Version(context.Background(), &pb.VersionRequest{}) + if err != nil { + t.Fatalf("Version: %v", err) + } + if resp.GetVersion() == "" { + t.Error("Version returned empty string") + } +} + +func TestIaCServer_Capabilities(t *testing.T) { + s := NewIaCServer() + resp, err := s.Capabilities(context.Background(), &pb.CapabilitiesRequest{}) + if err != nil { + t.Fatalf("Capabilities: %v", err) + } + found := false + for _, c := range resp.GetCapabilities() { + if c.GetResourceType() == "infra.container_service" { + found = true + break + } + } + if !found { + t.Errorf("Capabilities missing infra.container_service, got: %v", resp.GetCapabilities()) + } +} + +func TestIaCServer_Initialize_EmptyConfig(t *testing.T) { + s := NewIaCServer() + // Empty config_json: Initialize should return an error (no region defaults to us-east-1, + // but nil map should succeed since AWSProvider.Initialize handles nil gracefully). + _, err := s.Initialize(context.Background(), &pb.InitializeRequest{ConfigJson: []byte(`{}`)}) + // No credential required for unit test — Initialize sets up the SDK config. + // In CI without AWS credentials, LoadDefaultConfig may still succeed with the ambient chain. + // We only assert it does not panic. + _ = err // error acceptable; not nil is fine without credentials +} + +func TestIaCServer_CompileTimeGuards(t *testing.T) { + // This test exists to document the compile-time guards. + // If any of the interface assertions below fail to compile, this file will not build. + var _ pb.IaCProviderRequiredServer = (*awsIaCServer)(nil) + var _ pb.IaCProviderDriftDetectorServer = (*awsIaCServer)(nil) + var _ pb.ResourceDriverServer = (*awsIaCServer)(nil) +} + +func TestIaCServer_DetectDrift_Uninitialized(t *testing.T) { + s := NewIaCServer() + refs := []*pb.ResourceRef{{Name: "test", Type: "infra.container_service"}} + _, err := s.DetectDrift(context.Background(), &pb.DetectDriftRequest{Refs: refs}) + // Uninitialized provider returns "not initialized" error. + if err == nil { + t.Error("expected error from uninitialized provider") + } +} + +func TestIaCServer_DetectDriftWithSpecs_DelegatesToDetectDrift(t *testing.T) { + s := NewIaCServer() + refs := []*pb.ResourceRef{{Name: "test", Type: "infra.container_service"}} + _, err := s.DetectDriftWithSpecs(context.Background(), &pb.DetectDriftWithSpecsRequest{Refs: refs}) + // Uninitialized provider returns "not initialized" error — same as DetectDrift. + if err == nil { + t.Error("expected error from uninitialized provider") + } +} diff --git a/internal/module.go b/internal/module.go deleted file mode 100644 index 66d23ce..0000000 --- a/internal/module.go +++ /dev/null @@ -1,30 +0,0 @@ -package internal - -import ( - "context" - - "github.com/GoCodeAlone/workflow-plugin-aws/provider" - "github.com/GoCodeAlone/workflow/interfaces" -) - -// iacProviderModule wraps the AWS IaCProvider as a plugin ModuleInstance. -type iacProviderModule struct { - name string - config map[string]any - provider interfaces.IaCProvider -} - -func newIaCProviderModule(name string, config map[string]any) *iacProviderModule { - return &iacProviderModule{ - name: name, - config: config, - } -} - -func (m *iacProviderModule) Init() error { - m.provider = provider.NewAWSProvider() - return m.provider.Initialize(context.Background(), m.config) -} - -func (m *iacProviderModule) Start(_ context.Context) error { return nil } -func (m *iacProviderModule) Stop(_ context.Context) error { return nil } diff --git a/internal/plugin.go b/internal/plugin.go deleted file mode 100644 index f287b9c..0000000 --- a/internal/plugin.go +++ /dev/null @@ -1,112 +0,0 @@ -// Package internal implements the AWS workflow plugin. -package internal - -import ( - "fmt" - - "github.com/GoCodeAlone/workflow-plugin-aws/internal/contracts" - "github.com/GoCodeAlone/workflow-plugin-aws/provider" - pb "github.com/GoCodeAlone/workflow/plugin/external/proto" - sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" - "google.golang.org/protobuf/reflect/protodesc" - "google.golang.org/protobuf/types/descriptorpb" - "google.golang.org/protobuf/types/known/anypb" -) - -// moduleTypeIaCProvider is the canonical module-type name for the AWS IaC -// provider. It is used in ModuleTypes, TypedModuleTypes, CreateModule, -// CreateTypedModule, ContractRegistry, plugin.json, and plugin.contracts.json. -// Keeping it in one place prevents the names from drifting. -const moduleTypeIaCProvider = "iac.provider" - -// Version is set at build time via -ldflags -// "-X github.com/GoCodeAlone/workflow-plugin-aws/internal.Version=X.Y.Z". -// Default is a bare semver so plugin loaders that validate semver accept -// unreleased dev builds; goreleaser overrides with the real release tag. -var Version = "0.0.0" - -type awsPlugin struct{} - -// NewAWSPlugin returns the AWS SDK plugin provider. -func NewAWSPlugin() sdk.PluginProvider { - return &awsPlugin{} -} - -func (p *awsPlugin) Manifest() sdk.PluginManifest { - return sdk.PluginManifest{ - Name: "workflow-plugin-aws", - Version: provider.ProviderVersion, - Author: "GoCodeAlone", - Description: "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, and ACM resources", - } -} - -// ModuleTypes returns the module type names this plugin provides. -func (p *awsPlugin) ModuleTypes() []string { - return []string{moduleTypeIaCProvider} -} - -// CreateModule creates a module instance of the given type using a legacy -// map-based config. Prefer CreateTypedModule for strict typed config. -func (p *awsPlugin) CreateModule(typeName, name string, config map[string]any) (sdk.ModuleInstance, error) { - switch typeName { - case moduleTypeIaCProvider: - return newIaCProviderModule(name, config), nil - default: - return nil, fmt.Errorf("unknown module type: %s", typeName) - } -} - -// TypedModuleTypes returns the module type names for which strict typed config -// is supported. -func (p *awsPlugin) TypedModuleTypes() []string { - return []string{moduleTypeIaCProvider} -} - -// CreateTypedModule creates a typed module instance after unpacking and -// validating the AWSProviderConfig protobuf Any payload. -func (p *awsPlugin) CreateTypedModule(typeName, name string, config *anypb.Any) (sdk.ModuleInstance, error) { - factory := sdk.NewTypedModuleFactory( - moduleTypeIaCProvider, - &contracts.AWSProviderConfig{}, - func(name string, cfg *contracts.AWSProviderConfig) (sdk.ModuleInstance, error) { - // Reject a one-sided static-credential pair: supplying only one of - // access_key_id / secret_access_key would silently fall back to the - // ambient AWS credential chain and potentially deploy to the wrong - // account. - hasKey := cfg.GetAccessKeyId() != "" - hasSecret := cfg.GetSecretAccessKey() != "" - if hasKey != hasSecret { - return nil, fmt.Errorf("aws: access_key_id and secret_access_key must both be set or both be empty") - } - legacyConfig := map[string]any{ - "region": cfg.GetRegion(), - "access_key_id": cfg.GetAccessKeyId(), - "secret_access_key": cfg.GetSecretAccessKey(), - "ecs_cluster": cfg.GetEcsCluster(), - } - return newIaCProviderModule(name, legacyConfig), nil - }, - ) - return factory.CreateTypedModule(typeName, name, config) -} - -// ContractRegistry returns strict protobuf contract descriptors for every -// module type this plugin advertises. -func (p *awsPlugin) ContractRegistry() *pb.ContractRegistry { - return &pb.ContractRegistry{ - Contracts: []*pb.ContractDescriptor{ - { - Kind: pb.ContractKind_CONTRACT_KIND_MODULE, - ModuleType: moduleTypeIaCProvider, - ConfigMessage: "workflow.plugins.aws.v1.AWSProviderConfig", - Mode: pb.ContractMode_CONTRACT_MODE_STRICT_PROTO, - }, - }, - FileDescriptorSet: &descriptorpb.FileDescriptorSet{ - File: []*descriptorpb.FileDescriptorProto{ - protodesc.ToFileDescriptorProto(contracts.File_internal_contracts_aws_proto), - }, - }, - } -} diff --git a/internal/plugin_test.go b/internal/plugin_test.go deleted file mode 100644 index 89a1a7d..0000000 --- a/internal/plugin_test.go +++ /dev/null @@ -1,299 +0,0 @@ -package internal - -import ( - "encoding/json" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/GoCodeAlone/workflow-plugin-aws/internal/contracts" - pb "github.com/GoCodeAlone/workflow/plugin/external/proto" - sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk" - "google.golang.org/protobuf/reflect/protodesc" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/known/anypb" - "google.golang.org/protobuf/types/known/wrapperspb" -) - -func TestAWSPluginImplementsStrictContractProviders(t *testing.T) { - provider := NewAWSPlugin() - if _, ok := provider.(sdk.TypedModuleProvider); !ok { - t.Fatal("expected TypedModuleProvider") - } - if _, ok := provider.(sdk.ContractProvider); !ok { - t.Fatal("expected ContractProvider") - } -} - -func TestContractRegistryDeclaresStrictModuleContracts(t *testing.T) { - provider := NewAWSPlugin().(sdk.ContractProvider) - registry := provider.ContractRegistry() - if registry == nil { - t.Fatal("expected contract registry") - } - if registry.FileDescriptorSet == nil || len(registry.FileDescriptorSet.File) == 0 { - t.Fatal("expected file descriptor set") - } - files, err := protodesc.NewFiles(registry.FileDescriptorSet) - if err != nil { - t.Fatalf("descriptor set: %v", err) - } - - manifestContracts := loadManifestContracts(t) - contractsByKey := map[string]*pb.ContractDescriptor{} - for _, contract := range registry.Contracts { - if contract.Kind != pb.ContractKind_CONTRACT_KIND_MODULE { - t.Fatalf("unexpected contract kind %s", contract.Kind) - } - key := "module:" + contract.ModuleType - contractsByKey[key] = contract - if contract.Mode != pb.ContractMode_CONTRACT_MODE_STRICT_PROTO { - t.Fatalf("%s mode = %s, want strict proto", key, contract.Mode) - } - if contract.ConfigMessage == "" { - t.Fatalf("%s missing config message", key) - } - if _, err := files.FindDescriptorByName(protoreflect.FullName(contract.ConfigMessage)); err != nil { - t.Fatalf("%s references unknown config message %s: %v", key, contract.ConfigMessage, err) - } - if want, ok := manifestContracts[key]; !ok { - t.Fatalf("%s missing from plugin.contracts.json", key) - } else if want.ConfigMessage != contract.ConfigMessage { - t.Fatalf("%s manifest contract = %#v, runtime = %#v", key, want, contract) - } - } - - for _, moduleType := range pluginTypedModuleTypes() { - key := "module:" + moduleType - if _, ok := contractsByKey[key]; !ok { - t.Fatalf("missing contract %s", key) - } - } - if len(manifestContracts) != len(contractsByKey) { - t.Fatalf("plugin.contracts.json contract count = %d, runtime = %d", len(manifestContracts), len(contractsByKey)) - } -} - -func TestTypedModuleProviderValidatesTypedConfig(t *testing.T) { - provider := NewAWSPlugin().(sdk.TypedModuleProvider) - config, err := anypb.New(&contracts.AWSProviderConfig{ - Region: "us-east-1", - EcsCluster: "my-cluster", - }) - if err != nil { - t.Fatalf("pack config: %v", err) - } - module, err := provider.CreateTypedModule("iac.provider", "aws", config) - if err != nil { - t.Fatalf("CreateTypedModule: %v", err) - } - if module == nil { - t.Fatal("expected non-nil module") - } -} - -func TestTypedModuleProviderRejectsWrongType(t *testing.T) { - provider := NewAWSPlugin().(sdk.TypedModuleProvider) - config, err := anypb.New(&contracts.AWSProviderConfig{Region: "us-east-1"}) - if err != nil { - t.Fatalf("pack config: %v", err) - } - // Reject unknown module type name. - if _, err := provider.CreateTypedModule("iac.unknown", "x", config); err == nil { - t.Fatal("CreateTypedModule accepted unknown module type") - } - - // Reject correct module type but wrong proto message payload. - wrongConfig, err := anypb.New(wrapperspb.String("bad-payload")) - if err != nil { - t.Fatalf("pack wrong config: %v", err) - } - if _, err := provider.CreateTypedModule("iac.provider", "x", wrongConfig); err == nil { - t.Fatal("CreateTypedModule accepted wrong proto message type for iac.provider") - } -} - -func TestTypedModuleProviderConfigMapsToLegacyModule(t *testing.T) { - provider := NewAWSPlugin().(sdk.TypedModuleProvider) - config, err := anypb.New(&contracts.AWSProviderConfig{ - Region: "eu-west-1", - AccessKeyId: "AKID", - SecretAccessKey: "SECRET", - EcsCluster: "prod", - }) - if err != nil { - t.Fatalf("pack config: %v", err) - } - module, err := provider.CreateTypedModule("iac.provider", "aws", config) - if err != nil { - t.Fatalf("CreateTypedModule: %v", err) - } - wrapped, ok := module.(*sdk.TypedModuleInstance[*contracts.AWSProviderConfig]) - if !ok { - t.Fatalf("module type = %T, want *sdk.TypedModuleInstance[*contracts.AWSProviderConfig]", module) - } - legacy, ok := wrapped.ModuleInstance.(*iacProviderModule) - if !ok { - t.Fatalf("wrapped module type = %T, want *iacProviderModule", wrapped.ModuleInstance) - } - if got := legacy.config["region"]; got != "eu-west-1" { - t.Fatalf("region = %q, want eu-west-1", got) - } - if got := legacy.config["access_key_id"]; got != "AKID" { - t.Fatalf("access_key_id = %q, want AKID", got) - } - if got := legacy.config["ecs_cluster"]; got != "prod" { - t.Fatalf("ecs_cluster = %q, want prod", got) - } - if got := legacy.config["secret_access_key"]; got != "SECRET" { - t.Fatalf("secret_access_key = %q, want SECRET", got) - } -} - -// pluginTypedModuleTypes calls TypedModuleTypes() on a fresh plugin instance. -// It is called lazily within tests rather than at package init to avoid side -// effects during test binary loading. -func pluginTypedModuleTypes() []string { - return NewAWSPlugin().(sdk.TypedModuleProvider).TypedModuleTypes() -} - -// TestPluginManifestModuleTypesInSync verifies that plugin.json's -// capabilities.moduleTypes list exactly matches the runtime TypedModuleTypes(), -// so a future rename cannot leave the discovery metadata stale. -func TestPluginManifestModuleTypesInSync(t *testing.T) { - _, file, _, ok := runtime.Caller(0) - if !ok { - t.Fatal("runtime.Caller failed") - } - data, err := os.ReadFile(filepath.Join(filepath.Dir(file), "..", "plugin.json")) - if err != nil { - t.Fatalf("read plugin.json: %v", err) - } - var manifest struct { - Capabilities struct { - ModuleTypes []string `json:"moduleTypes"` - } `json:"capabilities"` - } - if err := json.Unmarshal(data, &manifest); err != nil { - t.Fatalf("parse plugin.json: %v", err) - } - runtimeTypes := pluginTypedModuleTypes() - manifestSet := make(map[string]bool, len(manifest.Capabilities.ModuleTypes)) - for _, mt := range manifest.Capabilities.ModuleTypes { - manifestSet[mt] = true - } - runtimeSet := make(map[string]bool, len(runtimeTypes)) - for _, mt := range runtimeTypes { - runtimeSet[mt] = true - } - for _, mt := range runtimeTypes { - if !manifestSet[mt] { - t.Errorf("TypedModuleTypes() has %q but plugin.json capabilities.moduleTypes does not", mt) - } - } - for _, mt := range manifest.Capabilities.ModuleTypes { - if !runtimeSet[mt] { - t.Errorf("plugin.json capabilities.moduleTypes has %q but TypedModuleTypes() does not", mt) - } - } -} - -// TestTypedModuleProviderRejectsPartialCredentials verifies that supplying -// only one of access_key_id / secret_access_key is rejected with an error, -// preventing a silent fallback to the ambient AWS credential chain. -func TestTypedModuleProviderRejectsPartialCredentials(t *testing.T) { - p := NewAWSPlugin().(sdk.TypedModuleProvider) - - // Only access_key_id set — should fail. - config, err := anypb.New(&contracts.AWSProviderConfig{ - Region: "us-east-1", - AccessKeyId: "AKID", - }) - if err != nil { - t.Fatalf("pack config: %v", err) - } - if _, err := p.CreateTypedModule("iac.provider", "x", config); err == nil { - t.Error("CreateTypedModule accepted config with access_key_id but no secret_access_key") - } - - // Only secret_access_key set — should fail. - config2, err := anypb.New(&contracts.AWSProviderConfig{ - Region: "us-east-1", - SecretAccessKey: "SECRET", - }) - if err != nil { - t.Fatalf("pack config2: %v", err) - } - if _, err := p.CreateTypedModule("iac.provider", "x", config2); err == nil { - t.Error("CreateTypedModule accepted config with secret_access_key but no access_key_id") - } - - // Both set — should succeed. - config3, err := anypb.New(&contracts.AWSProviderConfig{ - Region: "us-east-1", - AccessKeyId: "AKID", - SecretAccessKey: "SECRET", - }) - if err != nil { - t.Fatalf("pack config3: %v", err) - } - if _, err := p.CreateTypedModule("iac.provider", "x", config3); err != nil { - t.Errorf("CreateTypedModule rejected valid full credential pair: %v", err) - } - - // Neither set — should succeed (uses ambient credentials). - config4, err := anypb.New(&contracts.AWSProviderConfig{Region: "us-east-1"}) - if err != nil { - t.Fatalf("pack config4: %v", err) - } - if _, err := p.CreateTypedModule("iac.provider", "x", config4); err != nil { - t.Errorf("CreateTypedModule rejected config with no static credentials: %v", err) - } -} - -type manifestContract struct { - Mode string `json:"mode"` - ConfigMessage string `json:"config"` -} - -func loadManifestContracts(t *testing.T) map[string]manifestContract { - t.Helper() - _, file, _, ok := runtime.Caller(0) - if !ok { - t.Fatal("runtime.Caller failed") - } - data, err := os.ReadFile(filepath.Join(filepath.Dir(file), "..", "plugin.contracts.json")) - if err != nil { - t.Fatalf("read plugin.contracts.json: %v", err) - } - var manifest struct { - Version string `json:"version"` - Contracts []struct { - Kind string `json:"kind"` - Type string `json:"type"` - manifestContract - } `json:"contracts"` - } - if err := json.Unmarshal(data, &manifest); err != nil { - t.Fatalf("parse plugin.contracts.json: %v", err) - } - if manifest.Version != "v1" { - t.Fatalf("plugin.contracts.json version = %q, want v1", manifest.Version) - } - result := make(map[string]manifestContract, len(manifest.Contracts)) - for _, contract := range manifest.Contracts { - if contract.Kind != "module" { - t.Fatalf("unexpected contract kind %q in plugin.contracts.json", contract.Kind) - } - if contract.Mode != "strict" { - t.Fatalf("%s mode = %q, want strict", contract.Type, contract.Mode) - } - key := "module:" + contract.Type - if _, exists := result[key]; exists { - t.Fatalf("duplicate contract %q in plugin.contracts.json", key) - } - result[key] = contract.manifestContract - } - return result -} diff --git a/internal/resourcedriver_server.go b/internal/resourcedriver_server.go new file mode 100644 index 0000000..22d4170 --- /dev/null +++ b/internal/resourcedriver_server.go @@ -0,0 +1,236 @@ +// Package internal — typed pb.ResourceDriverServer implementation. +// +// Extends *awsIaCServer (declared in iacserver.go) with the 9 RPC methods +// required by pb.ResourceDriverServer. Routing dispatches per-resource-type +// CRUD by looking up the driver via *provider.AWSProvider.ResourceDriver(type). +// +// Once *awsIaCServer satisfies pb.ResourceDriverServer at the Go type level, +// sdk.RegisterAllIaCProviderServices auto-registers it — no manual call needed. +package internal + +import ( + "context" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/GoCodeAlone/workflow/interfaces" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +// resolveResourceDriver looks up the per-type driver registered on the +// underlying *provider.AWSProvider. Returns a typed gRPC error with +// codes.NotFound when the resource_type is not registered. +func (s *awsIaCServer) resolveResourceDriver(resourceType string) (interfaces.ResourceDriver, error) { + if resourceType == "" { + return nil, status.Error(codes.InvalidArgument, "aws ResourceDriver: resource_type is required") + } + d, err := s.provider.ResourceDriver(resourceType) + if err != nil { + return nil, status.Errorf(codes.NotFound, "aws ResourceDriver: %v", err) + } + return d, nil +} + +func (s *awsIaCServer) Create(ctx context.Context, req *pb.ResourceCreateRequest) (*pb.ResourceCreateResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + spec, err := specFromPB(req.GetSpec()) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Create: decode spec: %w", req.GetResourceType(), err) + } + out, err := driver.Create(ctx, spec) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Create: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceCreateResponse{Output: pbOut}, nil +} + +func (s *awsIaCServer) Read(ctx context.Context, req *pb.ResourceReadRequest) (*pb.ResourceReadResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + out, err := driver.Read(ctx, refFromPB(req.GetRef())) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Read: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceReadResponse{Output: pbOut}, nil +} + +func (s *awsIaCServer) Update(ctx context.Context, req *pb.ResourceUpdateRequest) (*pb.ResourceUpdateResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + spec, err := specFromPB(req.GetSpec()) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Update: decode spec: %w", req.GetResourceType(), err) + } + out, err := driver.Update(ctx, refFromPB(req.GetRef()), spec) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Update: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceUpdateResponse{Output: pbOut}, nil +} + +func (s *awsIaCServer) Delete(ctx context.Context, req *pb.ResourceDeleteRequest) (*pb.ResourceDeleteResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + if err := driver.Delete(ctx, refFromPB(req.GetRef())); err != nil { + return nil, err + } + return &pb.ResourceDeleteResponse{}, nil +} + +func (s *awsIaCServer) Diff(ctx context.Context, req *pb.ResourceDiffRequest) (*pb.ResourceDiffResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + desired, err := specFromPB(req.GetDesired()) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Diff: decode desired: %w", req.GetResourceType(), err) + } + current, err := outputFromPB(req.GetCurrent()) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Diff: decode current: %w", req.GetResourceType(), err) + } + result, err := driver.Diff(ctx, desired, current) + if err != nil { + return nil, err + } + pbResult, err := diffResultToPB(result) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Diff: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceDiffResponse{Result: pbResult}, nil +} + +func (s *awsIaCServer) Scale(ctx context.Context, req *pb.ResourceScaleRequest) (*pb.ResourceScaleResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + out, err := driver.Scale(ctx, refFromPB(req.GetRef()), int(req.GetReplicas())) + if err != nil { + return nil, err + } + pbOut, err := outputToPB(out) + if err != nil { + return nil, fmt.Errorf("aws ResourceDriver(%s).Scale: encode response: %w", req.GetResourceType(), err) + } + return &pb.ResourceScaleResponse{Output: pbOut}, nil +} + +func (s *awsIaCServer) HealthCheck(ctx context.Context, req *pb.ResourceHealthCheckRequest) (*pb.ResourceHealthCheckResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + result, err := driver.HealthCheck(ctx, refFromPB(req.GetRef())) + if err != nil { + return nil, err + } + return &pb.ResourceHealthCheckResponse{Result: healthResultToPB(result)}, nil +} + +func (s *awsIaCServer) SensitiveKeys(_ context.Context, req *pb.SensitiveKeysRequest) (*pb.SensitiveKeysResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + keys := driver.SensitiveKeys() + return &pb.SensitiveKeysResponse{Keys: append([]string(nil), keys...)}, nil +} + +func (s *awsIaCServer) Troubleshoot(ctx context.Context, req *pb.TroubleshootRequest) (*pb.TroubleshootResponse, error) { + driver, err := s.resolveResourceDriver(req.GetResourceType()) + if err != nil { + return nil, err + } + tr, ok := driver.(interfaces.Troubleshooter) + if !ok { + return nil, status.Errorf(codes.Unimplemented, + "aws ResourceDriver(%s).Troubleshoot: driver does not implement interfaces.Troubleshooter", + req.GetResourceType()) + } + diags, err := tr.Troubleshoot(ctx, refFromPB(req.GetRef()), req.GetFailureMsg()) + if err != nil { + return nil, err + } + out := make([]*pb.Diagnostic, 0, len(diags)) + for _, d := range diags { + out = append(out, &pb.Diagnostic{ + Id: d.ID, + Phase: d.Phase, + Cause: d.Cause, + At: timeToPB(d.At), + Detail: d.Detail, + }) + } + return &pb.TroubleshootResponse{Diagnostics: out}, nil +} + +// ── Marshalling helpers specific to ResourceDriver ────────────────────────── + +func diffResultToPB(r *interfaces.DiffResult) (*pb.DiffResult, error) { + if r == nil { + return nil, nil + } + pbChanges, err := changesToPB(r.Changes) + if err != nil { + return nil, err + } + return &pb.DiffResult{ + NeedsUpdate: r.NeedsUpdate, + NeedsReplace: r.NeedsReplace, + Changes: pbChanges, + }, nil +} + +func healthResultToPB(r *interfaces.HealthResult) *pb.HealthResult { + if r == nil { + return nil + } + return &pb.HealthResult{Healthy: r.Healthy, Message: r.Message} +} + +func outputFromPB(o *pb.ResourceOutput) (*interfaces.ResourceOutput, error) { + if o == nil { + return nil, nil + } + outputs, err := unmarshalJSONMap(o.GetOutputsJson()) + if err != nil { + return nil, err + } + sensitive := make(map[string]bool, len(o.GetSensitive())) + for k, v := range o.GetSensitive() { + sensitive[k] = v + } + return &interfaces.ResourceOutput{ + Name: o.GetName(), + Type: o.GetType(), + ProviderID: o.GetProviderId(), + Outputs: outputs, + Sensitive: sensitive, + Status: o.GetStatus(), + }, nil +} diff --git a/internal/resourcedriver_server_test.go b/internal/resourcedriver_server_test.go new file mode 100644 index 0000000..5d1c0f9 --- /dev/null +++ b/internal/resourcedriver_server_test.go @@ -0,0 +1,49 @@ +package internal + +import ( + "context" + "testing" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +func TestResourceDriverServer_CompileTimeGuard(t *testing.T) { + var _ pb.ResourceDriverServer = (*awsIaCServer)(nil) +} + +func TestResourceDriverServer_ResolveDriver_Empty(t *testing.T) { + s := NewIaCServer() + _, err := s.resolveResourceDriver("") + if err == nil { + t.Fatal("expected error for empty resource_type") + } +} + +func TestResourceDriverServer_ResolveDriver_Unknown(t *testing.T) { + s := NewIaCServer() + // Provider not initialized — resolveResourceDriver returns error. + _, err := s.resolveResourceDriver("infra.unknown_type") + if err == nil { + t.Fatal("expected error for unknown resource type on uninitialized provider") + } +} + +func TestResourceDriverServer_Create_UnknownType(t *testing.T) { + s := NewIaCServer() + req := &pb.ResourceCreateRequest{ + ResourceType: "infra.unknown", + Spec: &pb.ResourceSpec{Name: "x", Type: "infra.unknown"}, + } + _, err := s.Create(context.Background(), req) + if err == nil { + t.Fatal("expected error for unknown resource type") + } +} + +func TestResourceDriverServer_SensitiveKeys_UnknownType(t *testing.T) { + s := NewIaCServer() + _, err := s.SensitiveKeys(context.Background(), &pb.SensitiveKeysRequest{ResourceType: "infra.unknown"}) + if err == nil { + t.Fatal("expected error for unknown resource type") + } +} diff --git a/plugin.json b/plugin.json index a05a377..bdbcea7 100644 --- a/plugin.json +++ b/plugin.json @@ -1,12 +1,12 @@ { "name": "workflow-plugin-aws", - "version": "0.1.0", + "version": "1.0.0", "author": "GoCodeAlone", "description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, ACM, and AutoScaling Group resources", "license": "MIT", "type": "external", "tier": "community", - "minEngineVersion": "0.19.0", + "minEngineVersion": "0.51.0", "keywords": ["aws", "iac", "infrastructure", "ecs", "eks", "rds", "vpc", "s3", "autoscaling"], "homepage": "https://github.com/GoCodeAlone/workflow-plugin-aws", "repository": "https://github.com/GoCodeAlone/workflow-plugin-aws", @@ -22,32 +22,32 @@ { "os": "linux", "arch": "amd64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.1.0/workflow-plugin-aws_0.1.0_linux_amd64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v1.0.0/workflow-plugin-aws_1.0.0_linux_amd64.tar.gz" }, { "os": "linux", "arch": "arm64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.1.0/workflow-plugin-aws_0.1.0_linux_arm64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v1.0.0/workflow-plugin-aws_1.0.0_linux_arm64.tar.gz" }, { "os": "darwin", "arch": "amd64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.1.0/workflow-plugin-aws_0.1.0_darwin_amd64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v1.0.0/workflow-plugin-aws_1.0.0_darwin_amd64.tar.gz" }, { "os": "darwin", "arch": "arm64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.1.0/workflow-plugin-aws_0.1.0_darwin_arm64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v1.0.0/workflow-plugin-aws_1.0.0_darwin_arm64.tar.gz" }, { "os": "windows", "arch": "amd64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.1.0/workflow-plugin-aws_0.1.0_windows_amd64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v1.0.0/workflow-plugin-aws_1.0.0_windows_amd64.tar.gz" }, { "os": "windows", "arch": "arm64", - "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.1.0/workflow-plugin-aws_0.1.0_windows_arm64.tar.gz" + "url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v1.0.0/workflow-plugin-aws_1.0.0_windows_arm64.tar.gz" } ] } diff --git a/provider/provider.go b/provider/provider.go index ee196c6..48076ee 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -18,7 +18,7 @@ import ( const ( ProviderName = "aws" - ProviderVersion = "0.1.0" + ProviderVersion = "1.0.0" ) // AWSProvider implements interfaces.IaCProvider for Amazon Web Services. @@ -32,6 +32,13 @@ type AWSProvider struct { // NewAWSProvider creates a new AWS provider. func NewAWSProvider() interfaces.IaCProvider { + return NewAWSProviderConcrete() +} + +// NewAWSProviderConcrete creates a new *AWSProvider (concrete type). +// Used by internal.NewIaCServer to avoid a type assertion on the +// interfaces.IaCProvider return of NewAWSProvider. +func NewAWSProviderConcrete() *AWSProvider { return &AWSProvider{ driverMap: make(map[string]interfaces.ResourceDriver), } diff --git a/provider/provider_test.go b/provider/provider_test.go index d8675fb..7e63556 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -17,8 +17,8 @@ func TestNewAWSProvider(t *testing.T) { if p.Name() != "aws" { t.Errorf("expected name aws, got %s", p.Name()) } - if p.Version() != "0.1.0" { - t.Errorf("expected version 0.1.0, got %s", p.Version()) + if p.Version() != "1.0.0" { + t.Errorf("expected version 1.0.0, got %s", p.Version()) } }