From 31cfd5ad8ea253f41f96ab263520d0af650b9699 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 10 May 2026 02:13:29 -0400 Subject: [PATCH] feat(sdk): RegisterAllIaCProviderServices auto-registration helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 4 of the strict-contracts force-cutover plan (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5). Adds plugin/external/sdk/iacserver.go: a single helper that uses Go type-assertion to register every typed IaC gRPC service the provider satisfies, in one call. REQUIRED service: pb.IaCProviderRequiredServer — surfaced as a clear startup-time error if the provider type doesn't satisfy it (rather than failing at the first RPC dispatch with a generic "unimplemented" status). OPTIONAL services (auto-detected): IaCProviderEnumerator, IaCProviderDriftDetector, IaCProviderCredentialRevoker, IaCProviderMigrationRepairer, IaCProviderValidator, IaCProviderDriftConfigDetector. Plus ResourceDriver. Per cycle 3 I-1 of the design: plugin authors write ONE call; they cannot omit registration for a capability they implemented. This removes the registration-omission bug class (the same shape as the legacy InvokeService case-string-typo bug) by removing the manual step entirely. Tests cover four cases: - required-satisfied → required service registered + advertised by grpcSrv.GetServiceInfo(). - enumerator-only → only the optional Enumerator service registered; other optionals stay absent (auto-detection precision). - empty-stub → returns an error naming the unsatisfied required interface, with a docs pointer. - all-capabilities-stub → all 8 typed services (Required + 6 optional + ResourceDriver) registered. Stacked on feat/iac-proto-task3 (Task 3 PR #598 provides the generated server interfaces this helper consumes). Verification: GOWORK=off go test -race ./plugin/external/sdk/... PASS; GOWORK=off go build ./plugin/... ./cmd/... ./module/... clean; GOWORK=off go vet ./plugin/external/... clean. Rollback: revert this commit; SDK consumers can still register services manually via the per-service Register* helpers protoc generated. --- plugin/external/sdk/iacserver.go | 88 ++++++++++++++++++ plugin/external/sdk/iacserver_test.go | 126 ++++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 plugin/external/sdk/iacserver.go create mode 100644 plugin/external/sdk/iacserver_test.go diff --git a/plugin/external/sdk/iacserver.go b/plugin/external/sdk/iacserver.go new file mode 100644 index 00000000..0dfb2588 --- /dev/null +++ b/plugin/external/sdk/iacserver.go @@ -0,0 +1,88 @@ +package sdk + +import ( + "fmt" + + "google.golang.org/grpc" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +// RegisterAllIaCProviderServices uses Go type-assertion to register every +// typed IaC gRPC service that the provider satisfies, in a single call. +// +// REQUIRED service: +// +// pb.IaCProviderRequiredServer — every IaC plugin MUST implement every +// method on this interface. The type-assert here surfaces missing +// methods at plugin-startup time as a clear error rather than at the +// first RPC dispatch with a generic "unimplemented" status. +// +// OPTIONAL services (auto-detected): +// +// pb.IaCProviderEnumeratorServer +// pb.IaCProviderDriftDetectorServer +// pb.IaCProviderCredentialRevokerServer +// pb.IaCProviderMigrationRepairerServer +// pb.IaCProviderValidatorServer +// pb.IaCProviderDriftConfigDetectorServer +// +// ResourceDriver: +// +// pb.ResourceDriverServer — separate gRPC service, also auto-registered +// when the provider satisfies it. +// +// Per cycle 3 I-1 of the strict-contracts force-cutover design: plugin +// authors write ONE call; they cannot omit registration for a capability +// they implemented. That eliminates the registration-omission bug class +// (the same shape as the legacy InvokeService case-string-typo bug) by +// removing the manual step entirely. +// +// Capability discovery on the host side uses the existing ContractRegistry +// RPC + FileDescriptorSet mechanism (kept via §Salvage in the design); +// the SDK auto-publishes the registered services there in Task 5. +// +// Plugin authors who DO NOT want a capability advertised must NOT +// implement those methods at the Go level — there is no half-implemented +// stub-and-forget-to-register failure mode. +func RegisterAllIaCProviderServices(s *grpc.Server, provider any) error { + if s == nil { + return fmt.Errorf("RegisterAllIaCProviderServices: grpc server is nil") + } + if provider == nil { + return fmt.Errorf("RegisterAllIaCProviderServices: provider is nil") + } + required, ok := provider.(pb.IaCProviderRequiredServer) + if !ok { + return fmt.Errorf( + "RegisterAllIaCProviderServices: provider %T does not satisfy "+ + "pb.IaCProviderRequiredServer (missing methods); see "+ + "docs/plans/2026-05-10-strict-contracts-force-cutover-design.md", + provider, + ) + } + pb.RegisterIaCProviderRequiredServer(s, required) + + if v, ok := provider.(pb.IaCProviderEnumeratorServer); ok { + pb.RegisterIaCProviderEnumeratorServer(s, v) + } + if v, ok := provider.(pb.IaCProviderDriftDetectorServer); ok { + pb.RegisterIaCProviderDriftDetectorServer(s, v) + } + if v, ok := provider.(pb.IaCProviderCredentialRevokerServer); ok { + pb.RegisterIaCProviderCredentialRevokerServer(s, v) + } + if v, ok := provider.(pb.IaCProviderMigrationRepairerServer); ok { + pb.RegisterIaCProviderMigrationRepairerServer(s, v) + } + if v, ok := provider.(pb.IaCProviderValidatorServer); ok { + pb.RegisterIaCProviderValidatorServer(s, v) + } + if v, ok := provider.(pb.IaCProviderDriftConfigDetectorServer); ok { + pb.RegisterIaCProviderDriftConfigDetectorServer(s, v) + } + if v, ok := provider.(pb.ResourceDriverServer); ok { + pb.RegisterResourceDriverServer(s, v) + } + return nil +} diff --git a/plugin/external/sdk/iacserver_test.go b/plugin/external/sdk/iacserver_test.go new file mode 100644 index 00000000..a40035cb --- /dev/null +++ b/plugin/external/sdk/iacserver_test.go @@ -0,0 +1,126 @@ +package sdk_test + +import ( + "strings" + "testing" + + "google.golang.org/grpc" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" + "github.com/GoCodeAlone/workflow/plugin/external/sdk" +) + +// TestRegisterAllIaCProviderServices_RequiredSatisfied_RegistersRequired +// asserts that a provider satisfying IaCProviderRequiredServer succeeds and +// the gRPC server actually advertises the typed service. +func TestRegisterAllIaCProviderServices_RequiredSatisfied_RegistersRequired(t *testing.T) { + grpcSrv := grpc.NewServer() + provider := &fullProviderStub{} + if err := sdk.RegisterAllIaCProviderServices(grpcSrv, provider); err != nil { + t.Fatalf("unexpected error: %v", err) + } + info := grpcSrv.GetServiceInfo() + if _, ok := info["workflow.plugin.external.iac.IaCProviderRequired"]; !ok { + t.Fatalf("required service not registered; have services: %v", serviceNames(info)) + } +} + +// TestRegisterAllIaCProviderServices_OptionalSatisfied_RegistersOptional +// asserts auto-detection: a provider that satisfies the Enumerator interface +// (and only that optional) gets the Enumerator service registered, but other +// optional services stay absent. +func TestRegisterAllIaCProviderServices_OptionalSatisfied_RegistersOptional(t *testing.T) { + grpcSrv := grpc.NewServer() + provider := &enumeratorOnlyStub{} + if err := sdk.RegisterAllIaCProviderServices(grpcSrv, provider); err != nil { + t.Fatalf("unexpected error: %v", err) + } + info := grpcSrv.GetServiceInfo() + if _, ok := info["workflow.plugin.external.iac.IaCProviderEnumerator"]; !ok { + t.Fatalf("Enumerator optional service NOT registered despite provider satisfying interface; have: %v", serviceNames(info)) + } + if _, ok := info["workflow.plugin.external.iac.IaCProviderDriftDetector"]; ok { + t.Fatalf("DriftDetector incorrectly registered (provider doesn't satisfy)") + } +} + +// TestRegisterAllIaCProviderServices_RequiredMissing_ReturnsError +// asserts that an empty provider produces an actionable error naming the +// unsatisfied required interface — the bug-class prevention pivot. +func TestRegisterAllIaCProviderServices_RequiredMissing_ReturnsError(t *testing.T) { + grpcSrv := grpc.NewServer() + provider := &emptyStub{} // doesn't satisfy IaCProviderRequiredServer + err := sdk.RegisterAllIaCProviderServices(grpcSrv, provider) + if err == nil { + t.Fatalf("expected error for unsatisfied required interface; got nil") + } + if !strings.Contains(err.Error(), "IaCProviderRequiredServer") { + t.Fatalf("error message must name the unsatisfied interface; got %q", err.Error()) + } +} + +// TestRegisterAllIaCProviderServices_AllOptionals_AllRegistered +// asserts that a provider satisfying every optional + required interface +// triggers registration of all 7 typed services (Required + 6 optional) +// plus the ResourceDriver. +func TestRegisterAllIaCProviderServices_AllOptionals_AllRegistered(t *testing.T) { + grpcSrv := grpc.NewServer() + provider := &allCapabilitiesStub{} + if err := sdk.RegisterAllIaCProviderServices(grpcSrv, provider); err != nil { + t.Fatalf("unexpected error: %v", err) + } + info := grpcSrv.GetServiceInfo() + wantServices := []string{ + "workflow.plugin.external.iac.IaCProviderRequired", + "workflow.plugin.external.iac.IaCProviderEnumerator", + "workflow.plugin.external.iac.IaCProviderDriftDetector", + "workflow.plugin.external.iac.IaCProviderCredentialRevoker", + "workflow.plugin.external.iac.IaCProviderMigrationRepairer", + "workflow.plugin.external.iac.IaCProviderValidator", + "workflow.plugin.external.iac.IaCProviderDriftConfigDetector", + "workflow.plugin.external.iac.ResourceDriver", + } + for _, name := range wantServices { + if _, ok := info[name]; !ok { + t.Errorf("expected service %q registered; have: %v", name, serviceNames(info)) + } + } +} + +func serviceNames(info map[string]grpc.ServiceInfo) []string { + out := make([]string, 0, len(info)) + for k := range info { + out = append(out, k) + } + return out +} + +// fullProviderStub satisfies IaCProviderRequired + Enumerator + DriftDetector +// (representative of an early-stage DO plugin shape). +type fullProviderStub struct { + pb.UnimplementedIaCProviderRequiredServer + pb.UnimplementedIaCProviderEnumeratorServer + pb.UnimplementedIaCProviderDriftDetectorServer +} + +// enumeratorOnlyStub satisfies Required + Enumerator only. +type enumeratorOnlyStub struct { + pb.UnimplementedIaCProviderRequiredServer + pb.UnimplementedIaCProviderEnumeratorServer +} + +// allCapabilitiesStub satisfies every required + optional IaC service plus +// ResourceDriver — used to assert auto-registration covers the full surface. +type allCapabilitiesStub struct { + pb.UnimplementedIaCProviderRequiredServer + pb.UnimplementedIaCProviderEnumeratorServer + pb.UnimplementedIaCProviderDriftDetectorServer + pb.UnimplementedIaCProviderCredentialRevokerServer + pb.UnimplementedIaCProviderMigrationRepairerServer + pb.UnimplementedIaCProviderValidatorServer + pb.UnimplementedIaCProviderDriftConfigDetectorServer + pb.UnimplementedResourceDriverServer +} + +// emptyStub satisfies no IaC interface; the helper must reject it. +type emptyStub struct{}