Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions plugin/external/sdk/iacserver.go
Original file line number Diff line number Diff line change
@@ -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
}
126 changes: 126 additions & 0 deletions plugin/external/sdk/iacserver_test.go
Original file line number Diff line number Diff line change
@@ -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{}
Loading