From 2ced62e60006cfe892ba880da779214d90bf4a13 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 14 May 2026 18:10:54 -0400 Subject: [PATCH 1/3] feat: add IaCStateBackend.Configure RPC for backend config plumbing --- module/iac_state_grpc_client.go | 13 + module/iac_state_grpc_client_test.go | 56 +++ module/iac_state_plugin_registry_test.go | 3 + plugin/external/proto/iac.pb.go | 360 +++++++++++------- plugin/external/proto/iac.proto | 8 + plugin/external/proto/iac_grpc.pb.go | 38 ++ .../external/proto/iac_statebackend_test.go | 2 + 7 files changed, 351 insertions(+), 129 deletions(-) diff --git a/module/iac_state_grpc_client.go b/module/iac_state_grpc_client.go index e7c145d9..522277d5 100644 --- a/module/iac_state_grpc_client.go +++ b/module/iac_state_grpc_client.go @@ -107,6 +107,19 @@ func newGRPCIaCStateStore(c pb.IaCStateBackendClient) *grpcIaCStateStore { return &grpcIaCStateStore{client: c} } +// Configure delivers the iac.state module's YAML config to the plugin-served +// backend so it can construct its SDK-backed store. cfg is JSON-encoded — the +// iac.proto hard invariant — and backendName selects which backend the config +// targets. See decisions/0036. +func (s *grpcIaCStateStore) Configure(ctx context.Context, backendName string, cfg map[string]any) error { + cfgJSON, err := json.Marshal(cfg) + if err != nil { + return err + } + _, err = s.client.Configure(ctx, &pb.ConfigureRequest{BackendName: backendName, ConfigJson: cfgJSON}) + return err +} + // GetState retrieves a state record by resource ID. Returns nil, nil when the // backend reports the record does not exist. func (s *grpcIaCStateStore) GetState(ctx context.Context, resourceID string) (*IaCState, error) { diff --git a/module/iac_state_grpc_client_test.go b/module/iac_state_grpc_client_test.go index ab4a2675..f5b0406c 100644 --- a/module/iac_state_grpc_client_test.go +++ b/module/iac_state_grpc_client_test.go @@ -11,6 +11,62 @@ import ( "google.golang.org/grpc/test/bufconn" ) +// captureStateBackendClient is a pb.IaCStateBackendClient stub that records the +// last Configure request it received. Only Configure is exercised; the other +// methods exist solely to satisfy the interface. +type captureStateBackendClient struct { + gotConfigure *pb.ConfigureRequest +} + +func (*captureStateBackendClient) GetState(context.Context, *pb.GetStateRequest, ...grpc.CallOption) (*pb.GetStateResponse, error) { + return nil, nil +} +func (*captureStateBackendClient) SaveState(context.Context, *pb.SaveStateRequest, ...grpc.CallOption) (*pb.SaveStateResponse, error) { + return nil, nil +} +func (*captureStateBackendClient) ListStates(context.Context, *pb.ListStatesRequest, ...grpc.CallOption) (*pb.ListStatesResponse, error) { + return nil, nil +} +func (*captureStateBackendClient) DeleteState(context.Context, *pb.DeleteStateRequest, ...grpc.CallOption) (*pb.DeleteStateResponse, error) { + return nil, nil +} +func (*captureStateBackendClient) Lock(context.Context, *pb.LockRequest, ...grpc.CallOption) (*pb.LockResponse, error) { + return nil, nil +} +func (*captureStateBackendClient) Unlock(context.Context, *pb.UnlockRequest, ...grpc.CallOption) (*pb.UnlockResponse, error) { + return nil, nil +} +func (*captureStateBackendClient) ListBackendNames(context.Context, *pb.ListBackendNamesRequest, ...grpc.CallOption) (*pb.ListBackendNamesResponse, error) { + return nil, nil +} +func (c *captureStateBackendClient) Configure(_ context.Context, r *pb.ConfigureRequest, _ ...grpc.CallOption) (*pb.ConfigureResponse, error) { + c.gotConfigure = r + return &pb.ConfigureResponse{}, nil +} + +func TestGRPCIaCStateStoreConfigure(t *testing.T) { + fake := &captureStateBackendClient{} + store := newGRPCIaCStateStore(fake) + + cfg := map[string]any{"container": "tfstate", "account": "wf"} + if err := store.Configure(context.Background(), "azure_blob", cfg); err != nil { + t.Fatalf("Configure: %v", err) + } + if fake.gotConfigure == nil { + t.Fatal("Configure did not call the client") + } + if fake.gotConfigure.BackendName != "azure_blob" { + t.Fatalf("BackendName = %q, want azure_blob", fake.gotConfigure.BackendName) + } + got, err := jsonBytesToMap(fake.gotConfigure.ConfigJson) + if err != nil { + t.Fatalf("ConfigJson not valid JSON: %v", err) + } + if got["container"] != "tfstate" || got["account"] != "wf" { + t.Fatalf("ConfigJson round-trip mismatch: %+v", got) + } +} + func TestGRPCIaCStateStoreRoundTrip(t *testing.T) { lis := bufconn.Listen(4 << 20) t.Cleanup(func() { _ = lis.Close() }) diff --git a/module/iac_state_plugin_registry_test.go b/module/iac_state_plugin_registry_test.go index 83719d84..0022df42 100644 --- a/module/iac_state_plugin_registry_test.go +++ b/module/iac_state_plugin_registry_test.go @@ -12,6 +12,9 @@ import ( // tests only need it to satisfy the interface; no method is ever called. type fakeStateBackendClient struct{} +func (*fakeStateBackendClient) Configure(context.Context, *pb.ConfigureRequest, ...grpc.CallOption) (*pb.ConfigureResponse, error) { + return nil, nil +} func (*fakeStateBackendClient) GetState(context.Context, *pb.GetStateRequest, ...grpc.CallOption) (*pb.GetStateResponse, error) { return nil, nil } diff --git a/plugin/external/proto/iac.pb.go b/plugin/external/proto/iac.pb.go index 99236980..c1c5b047 100644 --- a/plugin/external/proto/iac.pb.go +++ b/plugin/external/proto/iac.pb.go @@ -4687,6 +4687,98 @@ func (x *IaCState) GetError() string { return "" } +// Configure delivers the iac.state module's YAML config to the plugin so it can +// construct the SDK-backed store. backend_name selects which backend the config +// is for. config_json is the JSON-encoded module config map[string]any — same +// JSON-bytes invariant as InitializeRequest.config_json. See decisions/0036. +type ConfigureRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + BackendName string `protobuf:"bytes,1,opt,name=backend_name,json=backendName,proto3" json:"backend_name,omitempty"` + ConfigJson []byte `protobuf:"bytes,2,opt,name=config_json,json=configJson,proto3" json:"config_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigureRequest) Reset() { + *x = ConfigureRequest{} + mi := &file_iac_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigureRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigureRequest) ProtoMessage() {} + +func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { + mi := &file_iac_proto_msgTypes[80] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigureRequest.ProtoReflect.Descriptor instead. +func (*ConfigureRequest) Descriptor() ([]byte, []int) { + return file_iac_proto_rawDescGZIP(), []int{80} +} + +func (x *ConfigureRequest) GetBackendName() string { + if x != nil { + return x.BackendName + } + return "" +} + +func (x *ConfigureRequest) GetConfigJson() []byte { + if x != nil { + return x.ConfigJson + } + return nil +} + +type ConfigureResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigureResponse) Reset() { + *x = ConfigureResponse{} + mi := &file_iac_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigureResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigureResponse) ProtoMessage() {} + +func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { + mi := &file_iac_proto_msgTypes[81] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigureResponse.ProtoReflect.Descriptor instead. +func (*ConfigureResponse) Descriptor() ([]byte, []int) { + return file_iac_proto_rawDescGZIP(), []int{81} +} + type GetStateRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ResourceId string `protobuf:"bytes,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` @@ -4696,7 +4788,7 @@ type GetStateRequest struct { func (x *GetStateRequest) Reset() { *x = GetStateRequest{} - mi := &file_iac_proto_msgTypes[80] + mi := &file_iac_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4708,7 +4800,7 @@ func (x *GetStateRequest) String() string { func (*GetStateRequest) ProtoMessage() {} func (x *GetStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[80] + mi := &file_iac_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4721,7 +4813,7 @@ func (x *GetStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetStateRequest.ProtoReflect.Descriptor instead. func (*GetStateRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{80} + return file_iac_proto_rawDescGZIP(), []int{82} } func (x *GetStateRequest) GetResourceId() string { @@ -4741,7 +4833,7 @@ type GetStateResponse struct { func (x *GetStateResponse) Reset() { *x = GetStateResponse{} - mi := &file_iac_proto_msgTypes[81] + mi := &file_iac_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4753,7 +4845,7 @@ func (x *GetStateResponse) String() string { func (*GetStateResponse) ProtoMessage() {} func (x *GetStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[81] + mi := &file_iac_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4766,7 +4858,7 @@ func (x *GetStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetStateResponse.ProtoReflect.Descriptor instead. func (*GetStateResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{81} + return file_iac_proto_rawDescGZIP(), []int{83} } func (x *GetStateResponse) GetState() *IaCState { @@ -4792,7 +4884,7 @@ type SaveStateRequest struct { func (x *SaveStateRequest) Reset() { *x = SaveStateRequest{} - mi := &file_iac_proto_msgTypes[82] + mi := &file_iac_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4804,7 +4896,7 @@ func (x *SaveStateRequest) String() string { func (*SaveStateRequest) ProtoMessage() {} func (x *SaveStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[82] + mi := &file_iac_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4817,7 +4909,7 @@ func (x *SaveStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveStateRequest.ProtoReflect.Descriptor instead. func (*SaveStateRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{82} + return file_iac_proto_rawDescGZIP(), []int{84} } func (x *SaveStateRequest) GetState() *IaCState { @@ -4835,7 +4927,7 @@ type SaveStateResponse struct { func (x *SaveStateResponse) Reset() { *x = SaveStateResponse{} - mi := &file_iac_proto_msgTypes[83] + mi := &file_iac_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4847,7 +4939,7 @@ func (x *SaveStateResponse) String() string { func (*SaveStateResponse) ProtoMessage() {} func (x *SaveStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[83] + mi := &file_iac_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4860,7 +4952,7 @@ func (x *SaveStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveStateResponse.ProtoReflect.Descriptor instead. func (*SaveStateResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{83} + return file_iac_proto_rawDescGZIP(), []int{85} } type ListStatesRequest struct { @@ -4872,7 +4964,7 @@ type ListStatesRequest struct { func (x *ListStatesRequest) Reset() { *x = ListStatesRequest{} - mi := &file_iac_proto_msgTypes[84] + mi := &file_iac_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4884,7 +4976,7 @@ func (x *ListStatesRequest) String() string { func (*ListStatesRequest) ProtoMessage() {} func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[84] + mi := &file_iac_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4897,7 +4989,7 @@ func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStatesRequest.ProtoReflect.Descriptor instead. func (*ListStatesRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{84} + return file_iac_proto_rawDescGZIP(), []int{86} } func (x *ListStatesRequest) GetFilter() map[string]string { @@ -4916,7 +5008,7 @@ type ListStatesResponse struct { func (x *ListStatesResponse) Reset() { *x = ListStatesResponse{} - mi := &file_iac_proto_msgTypes[85] + mi := &file_iac_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4928,7 +5020,7 @@ func (x *ListStatesResponse) String() string { func (*ListStatesResponse) ProtoMessage() {} func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[85] + mi := &file_iac_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4941,7 +5033,7 @@ func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStatesResponse.ProtoReflect.Descriptor instead. func (*ListStatesResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{85} + return file_iac_proto_rawDescGZIP(), []int{87} } func (x *ListStatesResponse) GetStates() []*IaCState { @@ -4960,7 +5052,7 @@ type DeleteStateRequest struct { func (x *DeleteStateRequest) Reset() { *x = DeleteStateRequest{} - mi := &file_iac_proto_msgTypes[86] + mi := &file_iac_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4972,7 +5064,7 @@ func (x *DeleteStateRequest) String() string { func (*DeleteStateRequest) ProtoMessage() {} func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[86] + mi := &file_iac_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4985,7 +5077,7 @@ func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteStateRequest.ProtoReflect.Descriptor instead. func (*DeleteStateRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{86} + return file_iac_proto_rawDescGZIP(), []int{88} } func (x *DeleteStateRequest) GetResourceId() string { @@ -5003,7 +5095,7 @@ type DeleteStateResponse struct { func (x *DeleteStateResponse) Reset() { *x = DeleteStateResponse{} - mi := &file_iac_proto_msgTypes[87] + mi := &file_iac_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5015,7 +5107,7 @@ func (x *DeleteStateResponse) String() string { func (*DeleteStateResponse) ProtoMessage() {} func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[87] + mi := &file_iac_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5028,7 +5120,7 @@ func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteStateResponse.ProtoReflect.Descriptor instead. func (*DeleteStateResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{87} + return file_iac_proto_rawDescGZIP(), []int{89} } type LockRequest struct { @@ -5040,7 +5132,7 @@ type LockRequest struct { func (x *LockRequest) Reset() { *x = LockRequest{} - mi := &file_iac_proto_msgTypes[88] + mi := &file_iac_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5052,7 +5144,7 @@ func (x *LockRequest) String() string { func (*LockRequest) ProtoMessage() {} func (x *LockRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[88] + mi := &file_iac_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5065,7 +5157,7 @@ func (x *LockRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LockRequest.ProtoReflect.Descriptor instead. func (*LockRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{88} + return file_iac_proto_rawDescGZIP(), []int{90} } func (x *LockRequest) GetResourceId() string { @@ -5083,7 +5175,7 @@ type LockResponse struct { func (x *LockResponse) Reset() { *x = LockResponse{} - mi := &file_iac_proto_msgTypes[89] + mi := &file_iac_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5095,7 +5187,7 @@ func (x *LockResponse) String() string { func (*LockResponse) ProtoMessage() {} func (x *LockResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[89] + mi := &file_iac_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5108,7 +5200,7 @@ func (x *LockResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LockResponse.ProtoReflect.Descriptor instead. func (*LockResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{89} + return file_iac_proto_rawDescGZIP(), []int{91} } type UnlockRequest struct { @@ -5120,7 +5212,7 @@ type UnlockRequest struct { func (x *UnlockRequest) Reset() { *x = UnlockRequest{} - mi := &file_iac_proto_msgTypes[90] + mi := &file_iac_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5132,7 +5224,7 @@ func (x *UnlockRequest) String() string { func (*UnlockRequest) ProtoMessage() {} func (x *UnlockRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[90] + mi := &file_iac_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5145,7 +5237,7 @@ func (x *UnlockRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UnlockRequest.ProtoReflect.Descriptor instead. func (*UnlockRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{90} + return file_iac_proto_rawDescGZIP(), []int{92} } func (x *UnlockRequest) GetResourceId() string { @@ -5163,7 +5255,7 @@ type UnlockResponse struct { func (x *UnlockResponse) Reset() { *x = UnlockResponse{} - mi := &file_iac_proto_msgTypes[91] + mi := &file_iac_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5175,7 +5267,7 @@ func (x *UnlockResponse) String() string { func (*UnlockResponse) ProtoMessage() {} func (x *UnlockResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[91] + mi := &file_iac_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5188,7 +5280,7 @@ func (x *UnlockResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UnlockResponse.ProtoReflect.Descriptor instead. func (*UnlockResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{91} + return file_iac_proto_rawDescGZIP(), []int{93} } // ListBackendNames lets the engine ask a loaded plugin which iac.state backend @@ -5202,7 +5294,7 @@ type ListBackendNamesRequest struct { func (x *ListBackendNamesRequest) Reset() { *x = ListBackendNamesRequest{} - mi := &file_iac_proto_msgTypes[92] + mi := &file_iac_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5214,7 +5306,7 @@ func (x *ListBackendNamesRequest) String() string { func (*ListBackendNamesRequest) ProtoMessage() {} func (x *ListBackendNamesRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[92] + mi := &file_iac_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5227,7 +5319,7 @@ func (x *ListBackendNamesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBackendNamesRequest.ProtoReflect.Descriptor instead. func (*ListBackendNamesRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{92} + return file_iac_proto_rawDescGZIP(), []int{94} } type ListBackendNamesResponse struct { @@ -5239,7 +5331,7 @@ type ListBackendNamesResponse struct { func (x *ListBackendNamesResponse) Reset() { *x = ListBackendNamesResponse{} - mi := &file_iac_proto_msgTypes[93] + mi := &file_iac_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5251,7 +5343,7 @@ func (x *ListBackendNamesResponse) String() string { func (*ListBackendNamesResponse) ProtoMessage() {} func (x *ListBackendNamesResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[93] + mi := &file_iac_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5264,7 +5356,7 @@ func (x *ListBackendNamesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBackendNamesResponse.ProtoReflect.Descriptor instead. func (*ListBackendNamesResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{93} + return file_iac_proto_rawDescGZIP(), []int{95} } func (x *ListBackendNamesResponse) GetBackendNames() []string { @@ -5612,7 +5704,12 @@ const file_iac_proto_rawDesc = "" + "created_at\x18\v \x01(\tR\tcreatedAt\x12\x1d\n" + "\n" + "updated_at\x18\f \x01(\tR\tupdatedAt\x12\x14\n" + - "\x05error\x18\r \x01(\tR\x05error\"2\n" + + "\x05error\x18\r \x01(\tR\x05error\"V\n" + + "\x10ConfigureRequest\x12!\n" + + "\fbackend_name\x18\x01 \x01(\tR\vbackendName\x12\x1f\n" + + "\vconfig_json\x18\x02 \x01(\fR\n" + + "configJson\"\x13\n" + + "\x11ConfigureResponse\"2\n" + "\x0fGetStateRequest\x12\x1f\n" + "\vresource_id\x18\x01 \x01(\tR\n" + "resourceId\"h\n" + @@ -5690,8 +5787,9 @@ const file_iac_proto_rawDesc = "" + "\x05Scale\x122.workflow.plugin.external.iac.ResourceScaleRequest\x1a3.workflow.plugin.external.iac.ResourceScaleResponse\x12\x82\x01\n" + "\vHealthCheck\x128.workflow.plugin.external.iac.ResourceHealthCheckRequest\x1a9.workflow.plugin.external.iac.ResourceHealthCheckResponse\x12x\n" + "\rSensitiveKeys\x122.workflow.plugin.external.iac.SensitiveKeysRequest\x1a3.workflow.plugin.external.iac.SensitiveKeysResponse\x12u\n" + - "\fTroubleshoot\x121.workflow.plugin.external.iac.TroubleshootRequest\x1a2.workflow.plugin.external.iac.TroubleshootResponse2\x97\x06\n" + - "\x0fIaCStateBackend\x12i\n" + + "\fTroubleshoot\x121.workflow.plugin.external.iac.TroubleshootRequest\x1a2.workflow.plugin.external.iac.TroubleshootResponse2\x85\a\n" + + "\x0fIaCStateBackend\x12l\n" + + "\tConfigure\x12..workflow.plugin.external.iac.ConfigureRequest\x1a/.workflow.plugin.external.iac.ConfigureResponse\x12i\n" + "\bGetState\x12-.workflow.plugin.external.iac.GetStateRequest\x1a..workflow.plugin.external.iac.GetStateResponse\x12l\n" + "\tSaveState\x12..workflow.plugin.external.iac.SaveStateRequest\x1a/.workflow.plugin.external.iac.SaveStateResponse\x12o\n" + "\n" + @@ -5714,7 +5812,7 @@ func file_iac_proto_rawDescGZIP() []byte { } var file_iac_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_iac_proto_msgTypes = make([]protoimpl.MessageInfo, 103) +var file_iac_proto_msgTypes = make([]protoimpl.MessageInfo, 105) var file_iac_proto_goTypes = []any{ (DriftClass)(0), // 0: workflow.plugin.external.iac.DriftClass (PlanDiagnosticSeverity)(0), // 1: workflow.plugin.external.iac.PlanDiagnosticSeverity @@ -5798,55 +5896,57 @@ var file_iac_proto_goTypes = []any{ (*TroubleshootRequest)(nil), // 79: workflow.plugin.external.iac.TroubleshootRequest (*TroubleshootResponse)(nil), // 80: workflow.plugin.external.iac.TroubleshootResponse (*IaCState)(nil), // 81: workflow.plugin.external.iac.IaCState - (*GetStateRequest)(nil), // 82: workflow.plugin.external.iac.GetStateRequest - (*GetStateResponse)(nil), // 83: workflow.plugin.external.iac.GetStateResponse - (*SaveStateRequest)(nil), // 84: workflow.plugin.external.iac.SaveStateRequest - (*SaveStateResponse)(nil), // 85: workflow.plugin.external.iac.SaveStateResponse - (*ListStatesRequest)(nil), // 86: workflow.plugin.external.iac.ListStatesRequest - (*ListStatesResponse)(nil), // 87: workflow.plugin.external.iac.ListStatesResponse - (*DeleteStateRequest)(nil), // 88: workflow.plugin.external.iac.DeleteStateRequest - (*DeleteStateResponse)(nil), // 89: workflow.plugin.external.iac.DeleteStateResponse - (*LockRequest)(nil), // 90: workflow.plugin.external.iac.LockRequest - (*LockResponse)(nil), // 91: workflow.plugin.external.iac.LockResponse - (*UnlockRequest)(nil), // 92: workflow.plugin.external.iac.UnlockRequest - (*UnlockResponse)(nil), // 93: workflow.plugin.external.iac.UnlockResponse - (*ListBackendNamesRequest)(nil), // 94: workflow.plugin.external.iac.ListBackendNamesRequest - (*ListBackendNamesResponse)(nil), // 95: workflow.plugin.external.iac.ListBackendNamesResponse - nil, // 96: workflow.plugin.external.iac.ResourceOutput.SensitiveEntry - nil, // 97: workflow.plugin.external.iac.IaCPlan.InputSnapshotEntry - nil, // 98: workflow.plugin.external.iac.ApplyResult.InitialInputSnapshotEntry - nil, // 99: workflow.plugin.external.iac.ApplyResult.ReplaceIdMapEntry - nil, // 100: workflow.plugin.external.iac.BootstrapResult.EnvVarsEntry - nil, // 101: workflow.plugin.external.iac.MigrationRepairRequest.EnvEntry - nil, // 102: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry - nil, // 103: workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry - nil, // 104: workflow.plugin.external.iac.ListStatesRequest.FilterEntry - (*timestamppb.Timestamp)(nil), // 105: google.protobuf.Timestamp + (*ConfigureRequest)(nil), // 82: workflow.plugin.external.iac.ConfigureRequest + (*ConfigureResponse)(nil), // 83: workflow.plugin.external.iac.ConfigureResponse + (*GetStateRequest)(nil), // 84: workflow.plugin.external.iac.GetStateRequest + (*GetStateResponse)(nil), // 85: workflow.plugin.external.iac.GetStateResponse + (*SaveStateRequest)(nil), // 86: workflow.plugin.external.iac.SaveStateRequest + (*SaveStateResponse)(nil), // 87: workflow.plugin.external.iac.SaveStateResponse + (*ListStatesRequest)(nil), // 88: workflow.plugin.external.iac.ListStatesRequest + (*ListStatesResponse)(nil), // 89: workflow.plugin.external.iac.ListStatesResponse + (*DeleteStateRequest)(nil), // 90: workflow.plugin.external.iac.DeleteStateRequest + (*DeleteStateResponse)(nil), // 91: workflow.plugin.external.iac.DeleteStateResponse + (*LockRequest)(nil), // 92: workflow.plugin.external.iac.LockRequest + (*LockResponse)(nil), // 93: workflow.plugin.external.iac.LockResponse + (*UnlockRequest)(nil), // 94: workflow.plugin.external.iac.UnlockRequest + (*UnlockResponse)(nil), // 95: workflow.plugin.external.iac.UnlockResponse + (*ListBackendNamesRequest)(nil), // 96: workflow.plugin.external.iac.ListBackendNamesRequest + (*ListBackendNamesResponse)(nil), // 97: workflow.plugin.external.iac.ListBackendNamesResponse + nil, // 98: workflow.plugin.external.iac.ResourceOutput.SensitiveEntry + nil, // 99: workflow.plugin.external.iac.IaCPlan.InputSnapshotEntry + nil, // 100: workflow.plugin.external.iac.ApplyResult.InitialInputSnapshotEntry + nil, // 101: workflow.plugin.external.iac.ApplyResult.ReplaceIdMapEntry + nil, // 102: workflow.plugin.external.iac.BootstrapResult.EnvVarsEntry + nil, // 103: workflow.plugin.external.iac.MigrationRepairRequest.EnvEntry + nil, // 104: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry + nil, // 105: workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry + nil, // 106: workflow.plugin.external.iac.ListStatesRequest.FilterEntry + (*timestamppb.Timestamp)(nil), // 107: google.protobuf.Timestamp } var file_iac_proto_depIdxs = []int32{ 4, // 0: workflow.plugin.external.iac.ResourceSpec.hints:type_name -> workflow.plugin.external.iac.ResourceHints - 105, // 1: workflow.plugin.external.iac.ResourceState.created_at:type_name -> google.protobuf.Timestamp - 105, // 2: workflow.plugin.external.iac.ResourceState.updated_at:type_name -> google.protobuf.Timestamp - 105, // 3: workflow.plugin.external.iac.ResourceState.last_drift_check:type_name -> google.protobuf.Timestamp - 96, // 4: workflow.plugin.external.iac.ResourceOutput.sensitive:type_name -> workflow.plugin.external.iac.ResourceOutput.SensitiveEntry + 107, // 1: workflow.plugin.external.iac.ResourceState.created_at:type_name -> google.protobuf.Timestamp + 107, // 2: workflow.plugin.external.iac.ResourceState.updated_at:type_name -> google.protobuf.Timestamp + 107, // 3: workflow.plugin.external.iac.ResourceState.last_drift_check:type_name -> google.protobuf.Timestamp + 98, // 4: workflow.plugin.external.iac.ResourceOutput.sensitive:type_name -> workflow.plugin.external.iac.ResourceOutput.SensitiveEntry 10, // 5: workflow.plugin.external.iac.DiffResult.changes:type_name -> workflow.plugin.external.iac.FieldChange 0, // 6: workflow.plugin.external.iac.DriftResult.class:type_name -> workflow.plugin.external.iac.DriftClass - 105, // 7: workflow.plugin.external.iac.Diagnostic.at:type_name -> google.protobuf.Timestamp + 107, // 7: workflow.plugin.external.iac.Diagnostic.at:type_name -> google.protobuf.Timestamp 1, // 8: workflow.plugin.external.iac.PlanDiagnostic.severity:type_name -> workflow.plugin.external.iac.PlanDiagnosticSeverity 2, // 9: workflow.plugin.external.iac.PlanAction.resource:type_name -> workflow.plugin.external.iac.ResourceSpec 7, // 10: workflow.plugin.external.iac.PlanAction.current:type_name -> workflow.plugin.external.iac.ResourceState 10, // 11: workflow.plugin.external.iac.PlanAction.changes:type_name -> workflow.plugin.external.iac.FieldChange 17, // 12: workflow.plugin.external.iac.IaCPlan.actions:type_name -> workflow.plugin.external.iac.PlanAction - 105, // 13: workflow.plugin.external.iac.IaCPlan.created_at:type_name -> google.protobuf.Timestamp - 97, // 14: workflow.plugin.external.iac.IaCPlan.input_snapshot:type_name -> workflow.plugin.external.iac.IaCPlan.InputSnapshotEntry + 107, // 13: workflow.plugin.external.iac.IaCPlan.created_at:type_name -> google.protobuf.Timestamp + 99, // 14: workflow.plugin.external.iac.IaCPlan.input_snapshot:type_name -> workflow.plugin.external.iac.IaCPlan.InputSnapshotEntry 8, // 15: workflow.plugin.external.iac.ApplyResult.resources:type_name -> workflow.plugin.external.iac.ResourceOutput 19, // 16: workflow.plugin.external.iac.ApplyResult.errors:type_name -> workflow.plugin.external.iac.ActionError - 98, // 17: workflow.plugin.external.iac.ApplyResult.initial_input_snapshot:type_name -> workflow.plugin.external.iac.ApplyResult.InitialInputSnapshotEntry + 100, // 17: workflow.plugin.external.iac.ApplyResult.initial_input_snapshot:type_name -> workflow.plugin.external.iac.ApplyResult.InitialInputSnapshotEntry 13, // 18: workflow.plugin.external.iac.ApplyResult.input_drift_report:type_name -> workflow.plugin.external.iac.DriftEntry - 99, // 19: workflow.plugin.external.iac.ApplyResult.replace_id_map:type_name -> workflow.plugin.external.iac.ApplyResult.ReplaceIdMapEntry + 101, // 19: workflow.plugin.external.iac.ApplyResult.replace_id_map:type_name -> workflow.plugin.external.iac.ApplyResult.ReplaceIdMapEntry 19, // 20: workflow.plugin.external.iac.DestroyResult.errors:type_name -> workflow.plugin.external.iac.ActionError - 100, // 21: workflow.plugin.external.iac.BootstrapResult.env_vars:type_name -> workflow.plugin.external.iac.BootstrapResult.EnvVarsEntry - 101, // 22: workflow.plugin.external.iac.MigrationRepairRequest.env:type_name -> workflow.plugin.external.iac.MigrationRepairRequest.EnvEntry + 102, // 21: workflow.plugin.external.iac.BootstrapResult.env_vars:type_name -> workflow.plugin.external.iac.BootstrapResult.EnvVarsEntry + 103, // 22: workflow.plugin.external.iac.MigrationRepairRequest.env:type_name -> workflow.plugin.external.iac.MigrationRepairRequest.EnvEntry 15, // 23: workflow.plugin.external.iac.MigrationRepairResult.diagnostics:type_name -> workflow.plugin.external.iac.Diagnostic 6, // 24: workflow.plugin.external.iac.CapabilitiesResponse.capabilities:type_name -> workflow.plugin.external.iac.IaCCapabilityDeclaration 2, // 25: workflow.plugin.external.iac.PlanRequest.desired:type_name -> workflow.plugin.external.iac.ResourceSpec @@ -5867,14 +5967,14 @@ var file_iac_proto_depIdxs = []int32{ 3, // 40: workflow.plugin.external.iac.DetectDriftRequest.refs:type_name -> workflow.plugin.external.iac.ResourceRef 12, // 41: workflow.plugin.external.iac.DetectDriftResponse.drifts:type_name -> workflow.plugin.external.iac.DriftResult 3, // 42: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.refs:type_name -> workflow.plugin.external.iac.ResourceRef - 102, // 43: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.specs:type_name -> workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry + 104, // 43: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.specs:type_name -> workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry 12, // 44: workflow.plugin.external.iac.DetectDriftWithSpecsResponse.drifts:type_name -> workflow.plugin.external.iac.DriftResult 23, // 45: workflow.plugin.external.iac.RepairDirtyMigrationRequest.request:type_name -> workflow.plugin.external.iac.MigrationRepairRequest 24, // 46: workflow.plugin.external.iac.RepairDirtyMigrationResponse.result:type_name -> workflow.plugin.external.iac.MigrationRepairResult 18, // 47: workflow.plugin.external.iac.ValidatePlanRequest.plan:type_name -> workflow.plugin.external.iac.IaCPlan 16, // 48: workflow.plugin.external.iac.ValidatePlanResponse.diagnostics:type_name -> workflow.plugin.external.iac.PlanDiagnostic 3, // 49: workflow.plugin.external.iac.DetectDriftConfigRequest.refs:type_name -> workflow.plugin.external.iac.ResourceRef - 103, // 50: workflow.plugin.external.iac.DetectDriftConfigRequest.specs:type_name -> workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry + 105, // 50: workflow.plugin.external.iac.DetectDriftConfigRequest.specs:type_name -> workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry 12, // 51: workflow.plugin.external.iac.DetectDriftConfigResponse.drifts:type_name -> workflow.plugin.external.iac.DriftResult 2, // 52: workflow.plugin.external.iac.ResourceCreateRequest.spec:type_name -> workflow.plugin.external.iac.ResourceSpec 8, // 53: workflow.plugin.external.iac.ResourceCreateResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput @@ -5895,7 +5995,7 @@ var file_iac_proto_depIdxs = []int32{ 15, // 68: workflow.plugin.external.iac.TroubleshootResponse.diagnostics:type_name -> workflow.plugin.external.iac.Diagnostic 81, // 69: workflow.plugin.external.iac.GetStateResponse.state:type_name -> workflow.plugin.external.iac.IaCState 81, // 70: workflow.plugin.external.iac.SaveStateRequest.state:type_name -> workflow.plugin.external.iac.IaCState - 104, // 71: workflow.plugin.external.iac.ListStatesRequest.filter:type_name -> workflow.plugin.external.iac.ListStatesRequest.FilterEntry + 106, // 71: workflow.plugin.external.iac.ListStatesRequest.filter:type_name -> workflow.plugin.external.iac.ListStatesRequest.FilterEntry 81, // 72: workflow.plugin.external.iac.ListStatesResponse.states:type_name -> workflow.plugin.external.iac.IaCState 2, // 73: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry.value:type_name -> workflow.plugin.external.iac.ResourceSpec 2, // 74: workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry.value:type_name -> workflow.plugin.external.iac.ResourceSpec @@ -5927,50 +6027,52 @@ var file_iac_proto_depIdxs = []int32{ 75, // 100: workflow.plugin.external.iac.ResourceDriver.HealthCheck:input_type -> workflow.plugin.external.iac.ResourceHealthCheckRequest 77, // 101: workflow.plugin.external.iac.ResourceDriver.SensitiveKeys:input_type -> workflow.plugin.external.iac.SensitiveKeysRequest 79, // 102: workflow.plugin.external.iac.ResourceDriver.Troubleshoot:input_type -> workflow.plugin.external.iac.TroubleshootRequest - 82, // 103: workflow.plugin.external.iac.IaCStateBackend.GetState:input_type -> workflow.plugin.external.iac.GetStateRequest - 84, // 104: workflow.plugin.external.iac.IaCStateBackend.SaveState:input_type -> workflow.plugin.external.iac.SaveStateRequest - 86, // 105: workflow.plugin.external.iac.IaCStateBackend.ListStates:input_type -> workflow.plugin.external.iac.ListStatesRequest - 88, // 106: workflow.plugin.external.iac.IaCStateBackend.DeleteState:input_type -> workflow.plugin.external.iac.DeleteStateRequest - 90, // 107: workflow.plugin.external.iac.IaCStateBackend.Lock:input_type -> workflow.plugin.external.iac.LockRequest - 92, // 108: workflow.plugin.external.iac.IaCStateBackend.Unlock:input_type -> workflow.plugin.external.iac.UnlockRequest - 94, // 109: workflow.plugin.external.iac.IaCStateBackend.ListBackendNames:input_type -> workflow.plugin.external.iac.ListBackendNamesRequest - 26, // 110: workflow.plugin.external.iac.IaCProviderRequired.Initialize:output_type -> workflow.plugin.external.iac.InitializeResponse - 28, // 111: workflow.plugin.external.iac.IaCProviderRequired.Name:output_type -> workflow.plugin.external.iac.NameResponse - 30, // 112: workflow.plugin.external.iac.IaCProviderRequired.Version:output_type -> workflow.plugin.external.iac.VersionResponse - 32, // 113: workflow.plugin.external.iac.IaCProviderRequired.Capabilities:output_type -> workflow.plugin.external.iac.CapabilitiesResponse - 34, // 114: workflow.plugin.external.iac.IaCProviderRequired.Plan:output_type -> workflow.plugin.external.iac.PlanResponse - 36, // 115: workflow.plugin.external.iac.IaCProviderRequired.Apply:output_type -> workflow.plugin.external.iac.ApplyResponse - 38, // 116: workflow.plugin.external.iac.IaCProviderRequired.Destroy:output_type -> workflow.plugin.external.iac.DestroyResponse - 40, // 117: workflow.plugin.external.iac.IaCProviderRequired.Status:output_type -> workflow.plugin.external.iac.StatusResponse - 42, // 118: workflow.plugin.external.iac.IaCProviderRequired.Import:output_type -> workflow.plugin.external.iac.ImportResponse - 44, // 119: workflow.plugin.external.iac.IaCProviderRequired.ResolveSizing:output_type -> workflow.plugin.external.iac.ResolveSizingResponse - 46, // 120: workflow.plugin.external.iac.IaCProviderRequired.BootstrapStateBackend:output_type -> workflow.plugin.external.iac.BootstrapStateBackendResponse - 48, // 121: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateAll:output_type -> workflow.plugin.external.iac.EnumerateAllResponse - 50, // 122: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateByTag:output_type -> workflow.plugin.external.iac.EnumerateByTagResponse - 52, // 123: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDrift:output_type -> workflow.plugin.external.iac.DetectDriftResponse - 54, // 124: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDriftWithSpecs:output_type -> workflow.plugin.external.iac.DetectDriftWithSpecsResponse - 56, // 125: workflow.plugin.external.iac.IaCProviderCredentialRevoker.RevokeProviderCredential:output_type -> workflow.plugin.external.iac.RevokeProviderCredentialResponse - 58, // 126: workflow.plugin.external.iac.IaCProviderMigrationRepairer.RepairDirtyMigration:output_type -> workflow.plugin.external.iac.RepairDirtyMigrationResponse - 60, // 127: workflow.plugin.external.iac.IaCProviderValidator.ValidatePlan:output_type -> workflow.plugin.external.iac.ValidatePlanResponse - 62, // 128: workflow.plugin.external.iac.IaCProviderDriftConfigDetector.DetectDriftConfig:output_type -> workflow.plugin.external.iac.DetectDriftConfigResponse - 64, // 129: workflow.plugin.external.iac.ResourceDriver.Create:output_type -> workflow.plugin.external.iac.ResourceCreateResponse - 66, // 130: workflow.plugin.external.iac.ResourceDriver.Read:output_type -> workflow.plugin.external.iac.ResourceReadResponse - 68, // 131: workflow.plugin.external.iac.ResourceDriver.Update:output_type -> workflow.plugin.external.iac.ResourceUpdateResponse - 70, // 132: workflow.plugin.external.iac.ResourceDriver.Delete:output_type -> workflow.plugin.external.iac.ResourceDeleteResponse - 72, // 133: workflow.plugin.external.iac.ResourceDriver.Diff:output_type -> workflow.plugin.external.iac.ResourceDiffResponse - 74, // 134: workflow.plugin.external.iac.ResourceDriver.Scale:output_type -> workflow.plugin.external.iac.ResourceScaleResponse - 76, // 135: workflow.plugin.external.iac.ResourceDriver.HealthCheck:output_type -> workflow.plugin.external.iac.ResourceHealthCheckResponse - 78, // 136: workflow.plugin.external.iac.ResourceDriver.SensitiveKeys:output_type -> workflow.plugin.external.iac.SensitiveKeysResponse - 80, // 137: workflow.plugin.external.iac.ResourceDriver.Troubleshoot:output_type -> workflow.plugin.external.iac.TroubleshootResponse - 83, // 138: workflow.plugin.external.iac.IaCStateBackend.GetState:output_type -> workflow.plugin.external.iac.GetStateResponse - 85, // 139: workflow.plugin.external.iac.IaCStateBackend.SaveState:output_type -> workflow.plugin.external.iac.SaveStateResponse - 87, // 140: workflow.plugin.external.iac.IaCStateBackend.ListStates:output_type -> workflow.plugin.external.iac.ListStatesResponse - 89, // 141: workflow.plugin.external.iac.IaCStateBackend.DeleteState:output_type -> workflow.plugin.external.iac.DeleteStateResponse - 91, // 142: workflow.plugin.external.iac.IaCStateBackend.Lock:output_type -> workflow.plugin.external.iac.LockResponse - 93, // 143: workflow.plugin.external.iac.IaCStateBackend.Unlock:output_type -> workflow.plugin.external.iac.UnlockResponse - 95, // 144: workflow.plugin.external.iac.IaCStateBackend.ListBackendNames:output_type -> workflow.plugin.external.iac.ListBackendNamesResponse - 110, // [110:145] is the sub-list for method output_type - 75, // [75:110] is the sub-list for method input_type + 82, // 103: workflow.plugin.external.iac.IaCStateBackend.Configure:input_type -> workflow.plugin.external.iac.ConfigureRequest + 84, // 104: workflow.plugin.external.iac.IaCStateBackend.GetState:input_type -> workflow.plugin.external.iac.GetStateRequest + 86, // 105: workflow.plugin.external.iac.IaCStateBackend.SaveState:input_type -> workflow.plugin.external.iac.SaveStateRequest + 88, // 106: workflow.plugin.external.iac.IaCStateBackend.ListStates:input_type -> workflow.plugin.external.iac.ListStatesRequest + 90, // 107: workflow.plugin.external.iac.IaCStateBackend.DeleteState:input_type -> workflow.plugin.external.iac.DeleteStateRequest + 92, // 108: workflow.plugin.external.iac.IaCStateBackend.Lock:input_type -> workflow.plugin.external.iac.LockRequest + 94, // 109: workflow.plugin.external.iac.IaCStateBackend.Unlock:input_type -> workflow.plugin.external.iac.UnlockRequest + 96, // 110: workflow.plugin.external.iac.IaCStateBackend.ListBackendNames:input_type -> workflow.plugin.external.iac.ListBackendNamesRequest + 26, // 111: workflow.plugin.external.iac.IaCProviderRequired.Initialize:output_type -> workflow.plugin.external.iac.InitializeResponse + 28, // 112: workflow.plugin.external.iac.IaCProviderRequired.Name:output_type -> workflow.plugin.external.iac.NameResponse + 30, // 113: workflow.plugin.external.iac.IaCProviderRequired.Version:output_type -> workflow.plugin.external.iac.VersionResponse + 32, // 114: workflow.plugin.external.iac.IaCProviderRequired.Capabilities:output_type -> workflow.plugin.external.iac.CapabilitiesResponse + 34, // 115: workflow.plugin.external.iac.IaCProviderRequired.Plan:output_type -> workflow.plugin.external.iac.PlanResponse + 36, // 116: workflow.plugin.external.iac.IaCProviderRequired.Apply:output_type -> workflow.plugin.external.iac.ApplyResponse + 38, // 117: workflow.plugin.external.iac.IaCProviderRequired.Destroy:output_type -> workflow.plugin.external.iac.DestroyResponse + 40, // 118: workflow.plugin.external.iac.IaCProviderRequired.Status:output_type -> workflow.plugin.external.iac.StatusResponse + 42, // 119: workflow.plugin.external.iac.IaCProviderRequired.Import:output_type -> workflow.plugin.external.iac.ImportResponse + 44, // 120: workflow.plugin.external.iac.IaCProviderRequired.ResolveSizing:output_type -> workflow.plugin.external.iac.ResolveSizingResponse + 46, // 121: workflow.plugin.external.iac.IaCProviderRequired.BootstrapStateBackend:output_type -> workflow.plugin.external.iac.BootstrapStateBackendResponse + 48, // 122: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateAll:output_type -> workflow.plugin.external.iac.EnumerateAllResponse + 50, // 123: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateByTag:output_type -> workflow.plugin.external.iac.EnumerateByTagResponse + 52, // 124: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDrift:output_type -> workflow.plugin.external.iac.DetectDriftResponse + 54, // 125: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDriftWithSpecs:output_type -> workflow.plugin.external.iac.DetectDriftWithSpecsResponse + 56, // 126: workflow.plugin.external.iac.IaCProviderCredentialRevoker.RevokeProviderCredential:output_type -> workflow.plugin.external.iac.RevokeProviderCredentialResponse + 58, // 127: workflow.plugin.external.iac.IaCProviderMigrationRepairer.RepairDirtyMigration:output_type -> workflow.plugin.external.iac.RepairDirtyMigrationResponse + 60, // 128: workflow.plugin.external.iac.IaCProviderValidator.ValidatePlan:output_type -> workflow.plugin.external.iac.ValidatePlanResponse + 62, // 129: workflow.plugin.external.iac.IaCProviderDriftConfigDetector.DetectDriftConfig:output_type -> workflow.plugin.external.iac.DetectDriftConfigResponse + 64, // 130: workflow.plugin.external.iac.ResourceDriver.Create:output_type -> workflow.plugin.external.iac.ResourceCreateResponse + 66, // 131: workflow.plugin.external.iac.ResourceDriver.Read:output_type -> workflow.plugin.external.iac.ResourceReadResponse + 68, // 132: workflow.plugin.external.iac.ResourceDriver.Update:output_type -> workflow.plugin.external.iac.ResourceUpdateResponse + 70, // 133: workflow.plugin.external.iac.ResourceDriver.Delete:output_type -> workflow.plugin.external.iac.ResourceDeleteResponse + 72, // 134: workflow.plugin.external.iac.ResourceDriver.Diff:output_type -> workflow.plugin.external.iac.ResourceDiffResponse + 74, // 135: workflow.plugin.external.iac.ResourceDriver.Scale:output_type -> workflow.plugin.external.iac.ResourceScaleResponse + 76, // 136: workflow.plugin.external.iac.ResourceDriver.HealthCheck:output_type -> workflow.plugin.external.iac.ResourceHealthCheckResponse + 78, // 137: workflow.plugin.external.iac.ResourceDriver.SensitiveKeys:output_type -> workflow.plugin.external.iac.SensitiveKeysResponse + 80, // 138: workflow.plugin.external.iac.ResourceDriver.Troubleshoot:output_type -> workflow.plugin.external.iac.TroubleshootResponse + 83, // 139: workflow.plugin.external.iac.IaCStateBackend.Configure:output_type -> workflow.plugin.external.iac.ConfigureResponse + 85, // 140: workflow.plugin.external.iac.IaCStateBackend.GetState:output_type -> workflow.plugin.external.iac.GetStateResponse + 87, // 141: workflow.plugin.external.iac.IaCStateBackend.SaveState:output_type -> workflow.plugin.external.iac.SaveStateResponse + 89, // 142: workflow.plugin.external.iac.IaCStateBackend.ListStates:output_type -> workflow.plugin.external.iac.ListStatesResponse + 91, // 143: workflow.plugin.external.iac.IaCStateBackend.DeleteState:output_type -> workflow.plugin.external.iac.DeleteStateResponse + 93, // 144: workflow.plugin.external.iac.IaCStateBackend.Lock:output_type -> workflow.plugin.external.iac.LockResponse + 95, // 145: workflow.plugin.external.iac.IaCStateBackend.Unlock:output_type -> workflow.plugin.external.iac.UnlockResponse + 97, // 146: workflow.plugin.external.iac.IaCStateBackend.ListBackendNames:output_type -> workflow.plugin.external.iac.ListBackendNamesResponse + 111, // [111:147] is the sub-list for method output_type + 75, // [75:111] is the sub-list for method input_type 75, // [75:75] is the sub-list for extension type_name 75, // [75:75] is the sub-list for extension extendee 0, // [0:75] is the sub-list for field type_name @@ -5987,7 +6089,7 @@ func file_iac_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_iac_proto_rawDesc), len(file_iac_proto_rawDesc)), NumEnums: 2, - NumMessages: 103, + NumMessages: 105, NumExtensions: 0, NumServices: 9, }, diff --git a/plugin/external/proto/iac.proto b/plugin/external/proto/iac.proto index 86467ed5..1c3b32b7 100644 --- a/plugin/external/proto/iac.proto +++ b/plugin/external/proto/iac.proto @@ -605,6 +605,7 @@ message TroubleshootResponse { // plugin backend implements honored expiry with a conformance test. // ───────────────────────────────────────────────────────────────────────────── service IaCStateBackend { + rpc Configure (ConfigureRequest) returns (ConfigureResponse); rpc GetState (GetStateRequest) returns (GetStateResponse); rpc SaveState (SaveStateRequest) returns (SaveStateResponse); rpc ListStates (ListStatesRequest) returns (ListStatesResponse); @@ -633,6 +634,13 @@ message IaCState { string error = 13; } +// Configure delivers the iac.state module's YAML config to the plugin so it can +// construct the SDK-backed store. backend_name selects which backend the config +// is for. config_json is the JSON-encoded module config map[string]any — same +// JSON-bytes invariant as InitializeRequest.config_json. See decisions/0036. +message ConfigureRequest { string backend_name = 1; bytes config_json = 2; } +message ConfigureResponse {} + message GetStateRequest { string resource_id = 1; } message GetStateResponse { IaCState state = 1; bool exists = 2; } message SaveStateRequest { IaCState state = 1; } // idempotent: full-state replace, last-writer-wins diff --git a/plugin/external/proto/iac_grpc.pb.go b/plugin/external/proto/iac_grpc.pb.go index 41aed1ee..acf52960 100644 --- a/plugin/external/proto/iac_grpc.pb.go +++ b/plugin/external/proto/iac_grpc.pb.go @@ -1649,6 +1649,7 @@ var ResourceDriver_ServiceDesc = grpc.ServiceDesc{ } const ( + IaCStateBackend_Configure_FullMethodName = "/workflow.plugin.external.iac.IaCStateBackend/Configure" IaCStateBackend_GetState_FullMethodName = "/workflow.plugin.external.iac.IaCStateBackend/GetState" IaCStateBackend_SaveState_FullMethodName = "/workflow.plugin.external.iac.IaCStateBackend/SaveState" IaCStateBackend_ListStates_FullMethodName = "/workflow.plugin.external.iac.IaCStateBackend/ListStates" @@ -1670,6 +1671,7 @@ const ( // plugin backend implements honored expiry with a conformance test. // ───────────────────────────────────────────────────────────────────────────── type IaCStateBackendClient interface { + Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) GetState(ctx context.Context, in *GetStateRequest, opts ...grpc.CallOption) (*GetStateResponse, error) SaveState(ctx context.Context, in *SaveStateRequest, opts ...grpc.CallOption) (*SaveStateResponse, error) ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error) @@ -1687,6 +1689,16 @@ func NewIaCStateBackendClient(cc grpc.ClientConnInterface) IaCStateBackendClient return &iaCStateBackendClient{cc} } +func (c *iaCStateBackendClient) Configure(ctx context.Context, in *ConfigureRequest, opts ...grpc.CallOption) (*ConfigureResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ConfigureResponse) + err := c.cc.Invoke(ctx, IaCStateBackend_Configure_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *iaCStateBackendClient) GetState(ctx context.Context, in *GetStateRequest, opts ...grpc.CallOption) (*GetStateResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetStateResponse) @@ -1769,6 +1781,7 @@ func (c *iaCStateBackendClient) ListBackendNames(ctx context.Context, in *ListBa // plugin backend implements honored expiry with a conformance test. // ───────────────────────────────────────────────────────────────────────────── type IaCStateBackendServer interface { + Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) GetState(context.Context, *GetStateRequest) (*GetStateResponse, error) SaveState(context.Context, *SaveStateRequest) (*SaveStateResponse, error) ListStates(context.Context, *ListStatesRequest) (*ListStatesResponse, error) @@ -1786,6 +1799,9 @@ type IaCStateBackendServer interface { // pointer dereference when methods are called. type UnimplementedIaCStateBackendServer struct{} +func (UnimplementedIaCStateBackendServer) Configure(context.Context, *ConfigureRequest) (*ConfigureResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Configure not implemented") +} func (UnimplementedIaCStateBackendServer) GetState(context.Context, *GetStateRequest) (*GetStateResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetState not implemented") } @@ -1828,6 +1844,24 @@ func RegisterIaCStateBackendServer(s grpc.ServiceRegistrar, srv IaCStateBackendS s.RegisterService(&IaCStateBackend_ServiceDesc, srv) } +func _IaCStateBackend_Configure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConfigureRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IaCStateBackendServer).Configure(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IaCStateBackend_Configure_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IaCStateBackendServer).Configure(ctx, req.(*ConfigureRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _IaCStateBackend_GetState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetStateRequest) if err := dec(in); err != nil { @@ -1961,6 +1995,10 @@ var IaCStateBackend_ServiceDesc = grpc.ServiceDesc{ ServiceName: "workflow.plugin.external.iac.IaCStateBackend", HandlerType: (*IaCStateBackendServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "Configure", + Handler: _IaCStateBackend_Configure_Handler, + }, { MethodName: "GetState", Handler: _IaCStateBackend_GetState_Handler, diff --git a/plugin/external/proto/iac_statebackend_test.go b/plugin/external/proto/iac_statebackend_test.go index 20560bac..498fede9 100644 --- a/plugin/external/proto/iac_statebackend_test.go +++ b/plugin/external/proto/iac_statebackend_test.go @@ -10,6 +10,8 @@ import ( func TestIaCStateBackendGeneratedTypesExist(t *testing.T) { var _ IaCStateBackendServer // service interface generated var _ IaCStateBackendClient // client interface generated + _ = &ConfigureRequest{BackendName: "x", ConfigJson: []byte("{}")} + _ = ConfigureResponse{} _ = &GetStateRequest{ResourceId: "r"} _ = &GetStateResponse{Exists: true, State: &IaCState{}} _ = &SaveStateRequest{State: &IaCState{}} From d27815603b2a12fc22081b395281b2e9c6d1771a Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 14 May 2026 18:16:46 -0400 Subject: [PATCH 2/3] feat: iac_module passes module config to plugin backends via Configure RPC --- module/iac_module.go | 6 +- module/iac_module_test.go | 119 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 module/iac_module_test.go diff --git a/module/iac_module.go b/module/iac_module.go index 0287c06f..1bba3a06 100644 --- a/module/iac_module.go +++ b/module/iac_module.go @@ -93,7 +93,11 @@ func (m *IaCModule) Init(app modular.Application) error { // The engine populates iacStateBackendRegistryInstance at plugin-load // time; a resolved backend is served over gRPC via grpcIaCStateStore. if client, ok := iacStateBackendRegistryInstance.resolve(m.backend); ok { - m.store = newGRPCIaCStateStore(client) + store := newGRPCIaCStateStore(client) + if err := store.Configure(context.Background(), m.backend, m.config); err != nil { + return fmt.Errorf("iac.state %q: backend %q: configure plugin backend: %w", m.name, m.backend, err) + } + m.store = store break } return fmt.Errorf("iac.state %q: backend %q is not built into workflow core "+ diff --git a/module/iac_module_test.go b/module/iac_module_test.go new file mode 100644 index 00000000..bdce7ff2 --- /dev/null +++ b/module/iac_module_test.go @@ -0,0 +1,119 @@ +package module + +import ( + "context" + "errors" + "reflect" + "strings" + "testing" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" + "google.golang.org/grpc" +) + +// configureStateBackendClient is a pb.IaCStateBackendClient stub for the +// IaCModule.Init() Configure-wiring tests: it records the Configure request it +// received and can be told to fail. +type configureStateBackendClient struct { + gotConfigure *pb.ConfigureRequest + configureErr error +} + +func (c *configureStateBackendClient) Configure(_ context.Context, r *pb.ConfigureRequest, _ ...grpc.CallOption) (*pb.ConfigureResponse, error) { + c.gotConfigure = r + if c.configureErr != nil { + return nil, c.configureErr + } + return &pb.ConfigureResponse{}, nil +} +func (*configureStateBackendClient) GetState(context.Context, *pb.GetStateRequest, ...grpc.CallOption) (*pb.GetStateResponse, error) { + return nil, nil +} +func (*configureStateBackendClient) SaveState(context.Context, *pb.SaveStateRequest, ...grpc.CallOption) (*pb.SaveStateResponse, error) { + return nil, nil +} +func (*configureStateBackendClient) ListStates(context.Context, *pb.ListStatesRequest, ...grpc.CallOption) (*pb.ListStatesResponse, error) { + return nil, nil +} +func (*configureStateBackendClient) DeleteState(context.Context, *pb.DeleteStateRequest, ...grpc.CallOption) (*pb.DeleteStateResponse, error) { + return nil, nil +} +func (*configureStateBackendClient) Lock(context.Context, *pb.LockRequest, ...grpc.CallOption) (*pb.LockResponse, error) { + return nil, nil +} +func (*configureStateBackendClient) Unlock(context.Context, *pb.UnlockRequest, ...grpc.CallOption) (*pb.UnlockResponse, error) { + return nil, nil +} +func (*configureStateBackendClient) ListBackendNames(context.Context, *pb.ListBackendNamesRequest, ...grpc.CallOption) (*pb.ListBackendNamesResponse, error) { + return nil, nil +} + +// TestIaCModuleConfigureWiring asserts IaCModule.Init() calls the plugin +// backend's Configure RPC with the backend name and the JSON-encoded module +// config before the store becomes usable. +func TestIaCModuleConfigureWiring(t *testing.T) { + const backend = "azure_blob_configure_wiring_test" + fake := &configureStateBackendClient{} + if err := iacStateBackendRegistryInstance.register(backend, fake); err != nil { + t.Fatalf("register: %v", err) + } + defer func() { + iacStateBackendRegistryInstance.mu.Lock() + delete(iacStateBackendRegistryInstance.clients, backend) + iacStateBackendRegistryInstance.mu.Unlock() + }() + + cfg := map[string]any{"backend": backend, "container": "tfstate", "account": "wf"} + m := NewIaCModule("iac-plugin", cfg) + if err := m.Init(NewMockApplication()); err != nil { + t.Fatalf("Init: %v", err) + } + + if fake.gotConfigure == nil { + t.Fatal("Init did not call the backend's Configure RPC") + } + if fake.gotConfigure.BackendName != backend { + t.Fatalf("Configure BackendName = %q, want %q", fake.gotConfigure.BackendName, backend) + } + got, err := jsonBytesToMap(fake.gotConfigure.ConfigJson) + if err != nil { + t.Fatalf("Configure ConfigJson not valid JSON: %v", err) + } + if !reflect.DeepEqual(got, cfg) { + t.Fatalf("Configure ConfigJson = %+v, want %+v", got, cfg) + } + if _, ok := m.store.(*grpcIaCStateStore); !ok { + t.Fatalf("m.store is %T, want *grpcIaCStateStore", m.store) + } +} + +// TestIaCModuleConfigureError asserts a Configure RPC failure aborts Init() with +// a wrapped error naming the module and the backend. +func TestIaCModuleConfigureError(t *testing.T) { + const backend = "azure_blob_configure_error_test" + sentinel := errors.New("plugin rejected config") + fake := &configureStateBackendClient{configureErr: sentinel} + if err := iacStateBackendRegistryInstance.register(backend, fake); err != nil { + t.Fatalf("register: %v", err) + } + defer func() { + iacStateBackendRegistryInstance.mu.Lock() + delete(iacStateBackendRegistryInstance.clients, backend) + iacStateBackendRegistryInstance.mu.Unlock() + }() + + m := NewIaCModule("iac-plugin", map[string]any{"backend": backend}) + err := m.Init(NewMockApplication()) + if err == nil { + t.Fatal("Init must fail when the backend's Configure RPC errors") + } + if !errors.Is(err, sentinel) { + t.Fatalf("Init error must wrap the Configure error, got: %v", err) + } + if !strings.Contains(err.Error(), "iac-plugin") || !strings.Contains(err.Error(), backend) { + t.Fatalf("Init error must name the module and backend, got: %v", err) + } + if m.store != nil { + t.Fatalf("m.store must stay nil when Configure fails, got %T", m.store) + } +} From 6e17cb5639b6de045158366a2ea1646aef2eb310 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 14 May 2026 18:38:27 -0400 Subject: [PATCH 3/3] fix: actionable error on Configure Unimplemented When a plugin-served iac.state backend is an older build whose Configure RPC returns gRPC codes.Unimplemented, IaCModule.Init() now returns an actionable error telling the operator to upgrade the backend plugin to a version that supports Configure (decisions/0036 co-deploy requirement). Other gRPC codes keep the existing generic configure-failed error path. codecov note: the codecov/patch failure on #679 is a repo-config gap, not a PR-1 defect. The patch is dominated by generated *.pb.go (iac.pb.go + iac_grpc.pb.go, ~270 lines, 0% coverage) and the repo has no codecov.yml /.codecov.yml ignoring generated files. The real new lines (iac_module.go default arm + grpcIaCStateStore.Configure) are covered by unit tests; only the defensive json.Marshal error branch is uncovered. Co-Authored-By: Claude Opus 4.7 --- module/iac_module.go | 10 ++++++++++ module/iac_module_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/module/iac_module.go b/module/iac_module.go index 1bba3a06..1a359635 100644 --- a/module/iac_module.go +++ b/module/iac_module.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/GoCodeAlone/modular" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // IaCModule registers an IaCStateStore in the service registry. @@ -95,6 +97,14 @@ func (m *IaCModule) Init(app modular.Application) error { if client, ok := iacStateBackendRegistryInstance.resolve(m.backend); ok { store := newGRPCIaCStateStore(client) if err := store.Configure(context.Background(), m.backend, m.config); err != nil { + // codes.Unimplemented means the loaded plugin is an older build + // without the Configure RPC — co-deploy requirement of + // decisions/0036. Give the operator an actionable upgrade hint. + if status.Code(err) == codes.Unimplemented { + return fmt.Errorf("iac.state %q: backend %q: the loaded plugin does not implement the "+ + "Configure RPC — upgrade the backend plugin to a version that supports Configure "+ + "(see decisions/0036): %w", m.name, m.backend, err) + } return fmt.Errorf("iac.state %q: backend %q: configure plugin backend: %w", m.name, m.backend, err) } m.store = store diff --git a/module/iac_module_test.go b/module/iac_module_test.go index bdce7ff2..9a2122f7 100644 --- a/module/iac_module_test.go +++ b/module/iac_module_test.go @@ -9,6 +9,8 @@ import ( pb "github.com/GoCodeAlone/workflow/plugin/external/proto" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // configureStateBackendClient is a pb.IaCStateBackendClient stub for the @@ -117,3 +119,38 @@ func TestIaCModuleConfigureError(t *testing.T) { t.Fatalf("m.store must stay nil when Configure fails, got %T", m.store) } } + +// TestIaCModuleConfigureUnimplemented asserts that when the backend plugin is an +// older build whose Configure RPC returns gRPC codes.Unimplemented, Init() fails +// with an actionable error telling the operator to upgrade the plugin. +func TestIaCModuleConfigureUnimplemented(t *testing.T) { + const backend = "azure_blob_configure_unimplemented_test" + unimpl := status.Error(codes.Unimplemented, "method Configure not implemented") + fake := &configureStateBackendClient{configureErr: unimpl} + if err := iacStateBackendRegistryInstance.register(backend, fake); err != nil { + t.Fatalf("register: %v", err) + } + defer func() { + iacStateBackendRegistryInstance.mu.Lock() + delete(iacStateBackendRegistryInstance.clients, backend) + iacStateBackendRegistryInstance.mu.Unlock() + }() + + m := NewIaCModule("iac-plugin", map[string]any{"backend": backend}) + err := m.Init(NewMockApplication()) + if err == nil { + t.Fatal("Init must fail when the backend's Configure RPC is Unimplemented") + } + if !errors.Is(err, unimpl) { + t.Fatalf("Init error must wrap the Unimplemented error, got: %v", err) + } + if !strings.Contains(err.Error(), "iac-plugin") || !strings.Contains(err.Error(), backend) { + t.Fatalf("Init error must name the module and backend, got: %v", err) + } + if !strings.Contains(err.Error(), "upgrade") { + t.Fatalf("Unimplemented error must tell the operator to upgrade the plugin, got: %v", err) + } + if m.store != nil { + t.Fatalf("m.store must stay nil when Configure fails, got %T", m.store) + } +}