From 64fd9727b4b1e1b71bc8244f28489403c966da00 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 5 Jun 2025 09:24:02 -0400 Subject: [PATCH 1/2] Started adding ValidateListResourceConfig --- go.mod | 4 +- go.sum | 8 +-- internal/tf5testserver/tf5testserver.go | 11 +++ internal/tf6testserver/tf6testserver.go | 11 +++ internal/tfprotov5tov6/tfprotov5tov6.go | 33 +++++++++ internal/tfprotov6tov5/tfprotov6tov5.go | 39 ++++++++++ tf5muxserver/diagnostics.go | 21 ++++++ tf5muxserver/mux_server.go | 63 +++++++++++++++- tf5muxserver/mux_server_GetMetadata.go | 24 ++++++- tf5muxserver/mux_server_GetProviderSchema.go | 14 +++- .../mux_server_ValidateListResourceConfig.go | 35 +++++++++ ..._server_ValidateListResourceConfig_test.go | 72 +++++++++++++++++++ tf5to6server/tf5to6server.go | 29 ++++++++ tf5to6server/tf5to6server_test.go | 31 ++++++++ tf6muxserver/diagnostics.go | 21 ++++++ tf6muxserver/mux_server.go | 62 +++++++++++++++- .../mux_server_ValidateListResourceConfig.go | 35 +++++++++ ..._server_ValidateListResourceConfig_test.go | 72 +++++++++++++++++++ tf6to5server/tf6to5server.go | 29 ++++++++ tf6to5server/tf6to5server_test.go | 31 ++++++++ 20 files changed, 635 insertions(+), 10 deletions(-) create mode 100644 tf5muxserver/mux_server_ValidateListResourceConfig.go create mode 100644 tf5muxserver/mux_server_ValidateListResourceConfig_test.go create mode 100644 tf6muxserver/mux_server_ValidateListResourceConfig.go create mode 100644 tf6muxserver/mux_server_ValidateListResourceConfig_test.go diff --git a/go.mod b/go.mod index d826260..d42a175 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ toolchain go1.23.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/terraform-plugin-go v0.28.0 + github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250604150620-c6d59498a4e1 github.com/hashicorp/terraform-plugin-log v0.9.0 - google.golang.org/grpc v1.72.1 + google.golang.org/grpc v1.72.2 ) require ( diff --git a/go.sum b/go.sum index 1020ed5..8f73cf7 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0U github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.28.0 h1:zJmu2UDwhVN0J+J20RE5huiF3XXlTYVIleaevHZgKPA= -github.com/hashicorp/terraform-plugin-go v0.28.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= +github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250604150620-c6d59498a4e1 h1:TbYGMFWFJDeGgLf5r/vZ8U7sV/f8dg3PR/tDDV4lw/I= +github.com/hashicorp/terraform-plugin-go v0.28.1-0.20250604150620-c6d59498a4e1/go.mod h1:2t+AK4nKnRw39xPaCHHEMz79e8BIZoEWsSsVUkC8jVU= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M= @@ -80,8 +80,8 @@ golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/tf5testserver/tf5testserver.go b/internal/tf5testserver/tf5testserver.go index ee70718..419514a 100644 --- a/internal/tf5testserver/tf5testserver.go +++ b/internal/tf5testserver/tf5testserver.go @@ -64,6 +64,8 @@ type TestServer struct { ValidateDataSourceConfigCalled map[string]bool ValidateResourceTypeConfigCalled map[string]bool + + ValidateListResourceConfigCalled map[string]bool } func (s *TestServer) ProviderServer() tfprotov5.ProviderServer { @@ -269,6 +271,15 @@ func (s *TestServer) ValidateResourceTypeConfig(_ context.Context, req *tfprotov return nil, nil } +func (s *TestServer) ValidateListResourceConfig(_ context.Context, req *tfprotov5.ValidateListResourceConfigRequest) (*tfprotov5.ValidateListResourceConfigResponse, error) { + if s.ValidateListResourceConfigCalled == nil { + s.ValidateListResourceConfigCalled = make(map[string]bool) + } + + s.ValidateListResourceConfigCalled[req.TypeName] = true + return nil, nil +} + func (s *TestServer) PrepareProviderConfig(_ context.Context, req *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) { s.PrepareProviderConfigCalled = true return s.PrepareProviderConfigResponse, nil diff --git a/internal/tf6testserver/tf6testserver.go b/internal/tf6testserver/tf6testserver.go index c908588..823582d 100644 --- a/internal/tf6testserver/tf6testserver.go +++ b/internal/tf6testserver/tf6testserver.go @@ -64,6 +64,8 @@ type TestServer struct { ValidateProviderConfigResponse *tfprotov6.ValidateProviderConfigResponse ValidateResourceConfigCalled map[string]bool + + ValidateListResourceConfigCalled map[string]bool } func (s *TestServer) ProviderServer() tfprotov6.ProviderServer { @@ -273,3 +275,12 @@ func (s *TestServer) ValidateProviderConfig(_ context.Context, req *tfprotov6.Va s.ValidateProviderConfigCalled = true return s.ValidateProviderConfigResponse, nil } + +func (s *TestServer) ValidateListResourceConfig(_ context.Context, req *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { + if s.ValidateListResourceConfigCalled == nil { + s.ValidateListResourceConfigCalled = make(map[string]bool) + } + + s.ValidateListResourceConfigCalled[req.TypeName] = true + return nil, nil +} diff --git a/internal/tfprotov5tov6/tfprotov5tov6.go b/internal/tfprotov5tov6/tfprotov5tov6.go index 46b058e..3374cba 100644 --- a/internal/tfprotov5tov6/tfprotov5tov6.go +++ b/internal/tfprotov5tov6/tfprotov5tov6.go @@ -298,6 +298,7 @@ func GetMetadataResponse(in *tfprotov5.GetMetadataResponse) *tfprotov6.GetMetada DataSources: make([]tfprotov6.DataSourceMetadata, 0, len(in.DataSources)), Diagnostics: Diagnostics(in.Diagnostics), EphemeralResources: make([]tfprotov6.EphemeralResourceMetadata, 0, len(in.Resources)), + ListResources: make([]tfprotov6.ListResourceMetadata, 0, len(in.Resources)), Functions: make([]tfprotov6.FunctionMetadata, 0, len(in.Functions)), Resources: make([]tfprotov6.ResourceMetadata, 0, len(in.Resources)), ServerCapabilities: ServerCapabilities(in.ServerCapabilities), @@ -311,6 +312,10 @@ func GetMetadataResponse(in *tfprotov5.GetMetadataResponse) *tfprotov6.GetMetada resp.EphemeralResources = append(resp.EphemeralResources, EphemeralResourceMetadata(ephemeralResource)) } + for _, listResource := range in.ListResources { + resp.ListResources = append(resp.ListResources, ListResourceMetadata(listResource)) + } + for _, function := range in.Functions { resp.Functions = append(resp.Functions, FunctionMetadata(function)) } @@ -347,6 +352,12 @@ func GetProviderSchemaResponse(in *tfprotov5.GetProviderSchemaResponse) *tfproto ephemeralResourceSchemas[k] = Schema(v) } + listResourceSchemas := make(map[string]*tfprotov6.Schema, len(in.ListResourceSchemas)) + + for k, v := range in.ListResourceSchemas { + listResourceSchemas[k] = Schema(v) + } + functions := make(map[string]*tfprotov6.Function, len(in.Functions)) for name, function := range in.Functions { @@ -363,6 +374,7 @@ func GetProviderSchemaResponse(in *tfprotov5.GetProviderSchemaResponse) *tfproto DataSourceSchemas: dataSourceSchemas, Diagnostics: Diagnostics(in.Diagnostics), EphemeralResourceSchemas: ephemeralResourceSchemas, + ListResourceSchemas: listResourceSchemas, Functions: functions, Provider: Schema(in.Provider), ProviderMeta: Schema(in.ProviderMeta), @@ -977,3 +989,24 @@ func ValidateResourceConfigResponse(in *tfprotov5.ValidateResourceTypeConfigResp Diagnostics: Diagnostics(in.Diagnostics), } } + +func ValidateListResourceConfigRequest(in *tfprotov5.ValidateListResourceConfigRequest) *tfprotov6.ValidateListResourceConfigRequest { + if in == nil { + return nil + } + + return &tfprotov6.ValidateListResourceConfigRequest{ + Config: DynamicValue(in.Config), + TypeName: in.TypeName, + } +} + +func ValidateListResourceConfigResponse(in *tfprotov5.ValidateListResourceConfigResponse) *tfprotov6.ValidateListResourceConfigResponse { + if in == nil { + return nil + } + + return &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: Diagnostics(in.Diagnostics), + } +} diff --git a/internal/tfprotov6tov5/tfprotov6tov5.go b/internal/tfprotov6tov5/tfprotov6tov5.go index 9991a44..e82743f 100644 --- a/internal/tfprotov6tov5/tfprotov6tov5.go +++ b/internal/tfprotov6tov5/tfprotov6tov5.go @@ -303,6 +303,7 @@ func GetMetadataResponse(in *tfprotov6.GetMetadataResponse) *tfprotov5.GetMetada DataSources: make([]tfprotov5.DataSourceMetadata, 0, len(in.DataSources)), Diagnostics: Diagnostics(in.Diagnostics), EphemeralResources: make([]tfprotov5.EphemeralResourceMetadata, 0, len(in.Resources)), + ListResources: make([]tfprotov5.ListResourceMetadata, 0, len(in.Resources)), Functions: make([]tfprotov5.FunctionMetadata, 0, len(in.Functions)), Resources: make([]tfprotov5.ResourceMetadata, 0, len(in.Resources)), ServerCapabilities: ServerCapabilities(in.ServerCapabilities), @@ -316,6 +317,10 @@ func GetMetadataResponse(in *tfprotov6.GetMetadataResponse) *tfprotov5.GetMetada resp.EphemeralResources = append(resp.EphemeralResources, EphemeralResourceMetadata(ephemeralResource)) } + for _, listResource := range in.ListResources { + resp.ListResources = append(resp.ListResources, ListResourceMetadata(listResource)) + } + for _, function := range in.Functions { resp.Functions = append(resp.Functions, FunctionMetadata(function)) } @@ -364,6 +369,18 @@ func GetProviderSchemaResponse(in *tfprotov6.GetProviderSchemaResponse) (*tfprot ephemeralResourceSchemas[k] = v5Schema } + listResourceSchemas := make(map[string]*tfprotov5.Schema, len(in.ListResourceSchemas)) + + for k, v := range in.ListResourceSchemas { + v5Schema, err := Schema(v) + + if err != nil { + return nil, fmt.Errorf("unable to convert list resource %q schema: %w", k, err) + } + + listResourceSchemas[k] = v5Schema + } + functions := make(map[string]*tfprotov5.Function, len(in.Functions)) for name, function := range in.Functions { @@ -398,6 +415,7 @@ func GetProviderSchemaResponse(in *tfprotov6.GetProviderSchemaResponse) (*tfprot DataSourceSchemas: dataSourceSchemas, Diagnostics: Diagnostics(in.Diagnostics), EphemeralResourceSchemas: ephemeralResourceSchemas, + ListResourceSchemas: listResourceSchemas, Functions: functions, Provider: provider, ProviderMeta: providerMeta, @@ -1038,3 +1056,24 @@ func ValidateResourceTypeConfigResponse(in *tfprotov6.ValidateResourceConfigResp Diagnostics: Diagnostics(in.Diagnostics), } } + +func ValidateListResourceConfigRequest(in *tfprotov6.ValidateListResourceConfigRequest) *tfprotov5.ValidateListResourceConfigRequest { + if in == nil { + return nil + } + + return &tfprotov5.ValidateListResourceConfigRequest{ + Config: DynamicValue(in.Config), + TypeName: in.TypeName, + } +} + +func ValidateListResourceConfigResponse(in *tfprotov6.ValidateListResourceConfigResponse) *tfprotov5.ValidateListResourceConfigResponse { + if in == nil { + return nil + } + + return &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: Diagnostics(in.Diagnostics), + } +} diff --git a/tf5muxserver/diagnostics.go b/tf5muxserver/diagnostics.go index 014a738..aa11362 100644 --- a/tf5muxserver/diagnostics.go +++ b/tf5muxserver/diagnostics.go @@ -47,6 +47,27 @@ func ephemeralResourceMissingError(typeName string) *tfprotov5.Diagnostic { } } +func listResourceDuplicateError(typeName string) *tfprotov5.Diagnostic { + return &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same list resource type across underlying providers. " + + "List resource types must be implemented by only one underlying provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate list resource type: " + typeName, + } +} + +func listResourceMissingError(typeName string) *tfprotov5.Diagnostic { + return &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "List Resource Not Implemented", + Detail: "The combined provider does not implement the requested list resource type. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Missing list resource type: " + typeName, + } +} + func diagnosticsHasError(diagnostics []*tfprotov5.Diagnostic) bool { for _, diagnostic := range diagnostics { if diagnostic == nil { diff --git a/tf5muxserver/mux_server.go b/tf5muxserver/mux_server.go index 1253bd9..ab0393b 100644 --- a/tf5muxserver/mux_server.go +++ b/tf5muxserver/mux_server.go @@ -26,6 +26,9 @@ type muxServer struct { // Routing for ephemeral resource types ephemeralResources map[string]tfprotov5.ProviderServer + // Routing for list resource types + listResources map[string]tfprotov5.ProviderServer + // Routing for functions functions map[string]tfprotov5.ProviderServer @@ -129,6 +132,41 @@ func (s *muxServer) getEphemeralResourceServer(ctx context.Context, typeName str return server, s.serverDiscoveryDiagnostics, nil } +func (s *muxServer) getListResourceServer(ctx context.Context, typeName string) (tfprotov5.ProviderServer, []*tfprotov5.Diagnostic, error) { + s.serverDiscoveryMutex.RLock() + server, ok := s.listResources[typeName] + discoveryComplete := s.serverDiscoveryComplete + s.serverDiscoveryMutex.RUnlock() + + if discoveryComplete { + if ok { + return server, s.serverDiscoveryDiagnostics, nil + } + + return nil, []*tfprotov5.Diagnostic{ + listResourceMissingError(typeName), + }, nil + } + + err := s.serverDiscovery(ctx) + + if err != nil || diagnosticsHasError(s.serverDiscoveryDiagnostics) { + return nil, s.serverDiscoveryDiagnostics, err + } + + s.serverDiscoveryMutex.RLock() + server, ok = s.listResources[typeName] + s.serverDiscoveryMutex.RUnlock() + + if !ok { + return nil, []*tfprotov5.Diagnostic{ + listResourceMissingError(typeName), + }, nil + } + + return server, s.serverDiscoveryDiagnostics, nil +} + func (s *muxServer) getFunctionServer(ctx context.Context, name string) (tfprotov5.ProviderServer, []*tfprotov5.Diagnostic, error) { s.serverDiscoveryMutex.RLock() server, ok := s.functions[name] @@ -202,7 +240,8 @@ func (s *muxServer) getResourceServer(ctx context.Context, typeName string) (tfp // serverDiscovery will populate the mux server "routing" for functions and // resource types by calling all underlying server GetMetadata RPC and falling // back to GetProviderSchema RPC. It is intended to only be called through -// getDataSourceServer, getEphemeralResourceServer, getFunctionServer, and getResourceServer. +// getDataSourceServer, getEphemeralResourceServer, getListResourceServer, +// getFunctionServer, and getResourceServer. // // The error return represents gRPC errors, which except for the GetMetadata // call returning the gRPC unimplemented error, is always returned. @@ -250,6 +289,16 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { s.ephemeralResources[serverEphemeralResource.TypeName] = server } + for _, serverListResource := range metadataResp.ListResources { + if _, ok := s.listResources[serverListResource.TypeName]; ok { + s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, listResourceDuplicateError(serverListResource.TypeName)) + + continue + } + + s.listResources[serverListResource.TypeName] = server + } + for _, serverFunction := range metadataResp.Functions { if _, ok := s.functions[serverFunction.Name]; ok { s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, functionDuplicateError(serverFunction.Name)) @@ -312,6 +361,16 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { s.ephemeralResources[typeName] = server } + for typeName := range providerSchemaResp.ListResourceSchemas { + if _, ok := s.listResources[typeName]; ok { + s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, listResourceDuplicateError(typeName)) + + continue + } + + s.listResources[typeName] = server + } + for name := range providerSchemaResp.Functions { if _, ok := s.functions[name]; ok { s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, functionDuplicateError(name)) @@ -349,11 +408,13 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { // - Only one provider implements each data source // - Only one provider implements each function // - Only one provider implements each ephemeral resource +// - Only one provider implements each list resource // - Only one provider implements each resource identity func NewMuxServer(_ context.Context, servers ...func() tfprotov5.ProviderServer) (*muxServer, error) { result := muxServer{ dataSources: make(map[string]tfprotov5.ProviderServer), ephemeralResources: make(map[string]tfprotov5.ProviderServer), + listResources: make(map[string]tfprotov5.ProviderServer), functions: make(map[string]tfprotov5.ProviderServer), resources: make(map[string]tfprotov5.ProviderServer), resourceCapabilities: make(map[string]*tfprotov5.ServerCapabilities), diff --git a/tf5muxserver/mux_server_GetMetadata.go b/tf5muxserver/mux_server_GetMetadata.go index 4e57e66..ef1b054 100644 --- a/tf5muxserver/mux_server_GetMetadata.go +++ b/tf5muxserver/mux_server_GetMetadata.go @@ -14,7 +14,7 @@ import ( // GetMetadata merges the metadata returned by the // tfprotov5.ProviderServers associated with muxServer into a single response. -// Resources, data sources, ephemeral resources, and functions must be returned +// Resources, data sources, ephemeral resources, list resources, and functions must be returned // from only one server or an error diagnostic is returned. func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) { rpc := "GetMetadata" @@ -27,6 +27,7 @@ func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov5.GetMetadataR resp := &tfprotov5.GetMetadataResponse{ DataSources: make([]tfprotov5.DataSourceMetadata, 0), EphemeralResources: make([]tfprotov5.EphemeralResourceMetadata, 0), + ListResources: make([]tfprotov5.ListResourceMetadata, 0), Functions: make([]tfprotov5.FunctionMetadata, 0), Resources: make([]tfprotov5.ResourceMetadata, 0), ServerCapabilities: serverCapabilities, @@ -66,6 +67,17 @@ func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov5.GetMetadataR resp.EphemeralResources = append(resp.EphemeralResources, ephemeralResource) } + for _, listResource := range serverResp.ListResources { + if listResourceMetadataContainsTypeName(resp.ListResources, listResource.TypeName) { + resp.Diagnostics = append(resp.Diagnostics, listResourceDuplicateError(listResource.TypeName)) + + continue + } + + s.listResources[listResource.TypeName] = server + resp.ListResources = append(resp.ListResources, listResource) + } + for _, function := range serverResp.Functions { if functionMetadataContainsName(resp.Functions, function.Name) { resp.Diagnostics = append(resp.Diagnostics, functionDuplicateError(function.Name)) @@ -113,6 +125,16 @@ func ephemeralResourceMetadataContainsTypeName(metadatas []tfprotov5.EphemeralRe return false } +func listResourceMetadataContainsTypeName(metadatas []tfprotov5.ListResourceMetadata, typeName string) bool { + for _, metadata := range metadatas { + if typeName == metadata.TypeName { + return true + } + } + + return false +} + func functionMetadataContainsName(metadatas []tfprotov5.FunctionMetadata, name string) bool { for _, metadata := range metadatas { if name == metadata.Name { diff --git a/tf5muxserver/mux_server_GetProviderSchema.go b/tf5muxserver/mux_server_GetProviderSchema.go index 70b2440..df97f80 100644 --- a/tf5muxserver/mux_server_GetProviderSchema.go +++ b/tf5muxserver/mux_server_GetProviderSchema.go @@ -14,7 +14,7 @@ import ( // GetProviderSchema merges the schemas returned by the // tfprotov5.ProviderServers associated with muxServer into a single schema. -// Resources, data sources, ephemeral resources, and functions must be returned +// Resources, data sources, ephemeral resources, list resources, and functions must be returned // from only one server. Provider and ProviderMeta schemas must be identical between all servers. func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { rpc := "GetProviderSchema" @@ -27,6 +27,7 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetPro resp := &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: make(map[string]*tfprotov5.Schema), EphemeralResourceSchemas: make(map[string]*tfprotov5.Schema), + ListResourceSchemas: make(map[string]*tfprotov5.Schema), Functions: make(map[string]*tfprotov5.Function), ResourceSchemas: make(map[string]*tfprotov5.Schema), ServerCapabilities: serverCapabilities, @@ -118,6 +119,17 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetPro s.ephemeralResources[ephemeralResourceType] = server resp.EphemeralResourceSchemas[ephemeralResourceType] = schema } + + for listResourceType, schema := range serverResp.ListResourceSchemas { + if _, ok := resp.ListResourceSchemas[listResourceType]; ok { + resp.Diagnostics = append(resp.Diagnostics, listResourceDuplicateError(listResourceType)) + + continue + } + + s.listResources[listResourceType] = server + resp.ListResourceSchemas[listResourceType] = schema + } } s.serverDiscoveryComplete = true diff --git a/tf5muxserver/mux_server_ValidateListResourceConfig.go b/tf5muxserver/mux_server_ValidateListResourceConfig.go new file mode 100644 index 0000000..3c7dcc9 --- /dev/null +++ b/tf5muxserver/mux_server_ValidateListResourceConfig.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) ValidateListResourceConfig(ctx context.Context, req *tfprotov5.ValidateListResourceConfigRequest) (*tfprotov5.ValidateListResourceConfigResponse, error) { + rpc := "ValidateListResourceTypeConfig" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getListResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: diags, + }, nil + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.ValidateListResourceConfig(ctx, req) +} diff --git a/tf5muxserver/mux_server_ValidateListResourceConfig_test.go b/tf5muxserver/mux_server_ValidateListResourceConfig_test.go new file mode 100644 index 0000000..a964c30 --- /dev/null +++ b/tf5muxserver/mux_server_ValidateListResourceConfig_test.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerValidateListResourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + ListResourceSchemas: map[string]*tfprotov5.Schema{ + "test_list_resource_server1": {}, + }, + }, + } + testServer2 := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + ListResourceSchemas: map[string]*tfprotov5.Schema{ + "test_list_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov5.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ + TypeName: "test_list_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.ValidateListResourceConfigCalled["test_list_resource_server1"] { + t.Errorf("expected test_list_resource_server1 ValidateListResourceConfig to be called on server1") + } + + if testServer2.ValidateListResourceConfigCalled["test_list_resource_server1"] { + t.Errorf("unexpected test_list_resource_server1 ValidateListResourceConfig called on server2") + } + + _, err = muxServer.ProviderServer().ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ + TypeName: "test_list_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.ValidateListResourceConfigCalled["test_list_resource_server2"] { + t.Errorf("unexpected test_list_resource_server2 ValidateListResourceConfig called on server1") + } + + if !testServer2.ValidateListResourceConfigCalled["test_list_resource_server2"] { + t.Errorf("expected test_list_resource_server2 ValidateListResourceConfig to be called on server2") + } +} diff --git a/tf5to6server/tf5to6server.go b/tf5to6server/tf5to6server.go index c84f63b..d40aa06 100644 --- a/tf5to6server/tf5to6server.go +++ b/tf5to6server/tf5to6server.go @@ -280,3 +280,32 @@ func (s v5tov6Server) ValidateResourceConfig(ctx context.Context, req *tfprotov6 return tfprotov5tov6.ValidateResourceConfigResponse(v5Resp), nil } + +func (s v5tov6Server) ValidateListResourceConfig(ctx context.Context, req *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { + // TODO: Remove and call s.v5Server.ValidateListResourceConfig below directly once interface becomes required + //nolint:staticcheck // Intentionally verifying interface implementation + listResourceServer, ok := s.v5Server.(tfprotov5.ProviderServerWithListResource) + if !ok { + v6Resp := &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "ValidateListResourceConfig Not Implemented", + Detail: "A ValidateListResourceConfig call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements ValidateListResourceConfig or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v6Resp, nil + } + + v5Req := tfprotov6tov5.ValidateListResourceConfigRequest(req) + + v5Resp, err := s.v5Server.ValidateListResourceConfig(ctx, v5Req) + if err != nil { + return nil, err + } + + return tfprotov5tov6.ValidateListResourceConfigResponse(v5Resp), nil +} diff --git a/tf5to6server/tf5to6server_test.go b/tf5to6server/tf5to6server_test.go index 0b989e2..845a37c 100644 --- a/tf5to6server/tf5to6server_test.go +++ b/tf5to6server/tf5to6server_test.go @@ -796,3 +796,34 @@ func TestV6ToV5ServerValidateResourceConfig(t *testing.T) { t.Errorf("expected test_resource ValidateResourceConfig to be called") } } + +func TestV6ToV5ServerValidateListResourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v5server := &tf5testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ + ListResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource": {}, + }, + }, + } + + v6server, err := tf5to6server.UpgradeServer(context.Background(), v5server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + _, err = v6server.ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ + TypeName: "test_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v5server.ValidateListResourceConfigCalled["test_resource"] { + t.Errorf("expected test_resource ValidateListResourceConfig to be called") + } +} diff --git a/tf6muxserver/diagnostics.go b/tf6muxserver/diagnostics.go index 36c706a..33d3f71 100644 --- a/tf6muxserver/diagnostics.go +++ b/tf6muxserver/diagnostics.go @@ -49,6 +49,27 @@ func ephemeralResourceMissingError(typeName string) *tfprotov6.Diagnostic { } } +func listResourceDuplicateError(typeName string) *tfprotov6.Diagnostic { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same list resource type across underlying providers. " + + "List resource types must be implemented by only one underlying provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate list resource type: " + typeName, + } +} + +func listResourceMissingError(typeName string) *tfprotov6.Diagnostic { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "List Resource Not Implemented", + Detail: "The combined provider does not implement the requested list resource type. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Missing list resource type: " + typeName, + } +} + func diagnosticsHasError(diagnostics []*tfprotov6.Diagnostic) bool { for _, diagnostic := range diagnostics { if diagnostic == nil { diff --git a/tf6muxserver/mux_server.go b/tf6muxserver/mux_server.go index 9ca8db5..320af12 100644 --- a/tf6muxserver/mux_server.go +++ b/tf6muxserver/mux_server.go @@ -26,6 +26,9 @@ type muxServer struct { // Routing for ephemeral resource types ephemeralResources map[string]tfprotov6.ProviderServer + // Routing for list resource types + listResources map[string]tfprotov6.ProviderServer + // Routing for functions functions map[string]tfprotov6.ProviderServer @@ -129,6 +132,41 @@ func (s *muxServer) getEphemeralResourceServer(ctx context.Context, typeName str return server, s.serverDiscoveryDiagnostics, nil } +func (s *muxServer) getListResourceServer(ctx context.Context, typeName string) (tfprotov6.ProviderServer, []*tfprotov6.Diagnostic, error) { + s.serverDiscoveryMutex.RLock() + server, ok := s.listResources[typeName] + discoveryComplete := s.serverDiscoveryComplete + s.serverDiscoveryMutex.RUnlock() + + if discoveryComplete { + if ok { + return server, s.serverDiscoveryDiagnostics, nil + } + + return nil, []*tfprotov6.Diagnostic{ + listResourceMissingError(typeName), + }, nil + } + + err := s.serverDiscovery(ctx) + + if err != nil || diagnosticsHasError(s.serverDiscoveryDiagnostics) { + return nil, s.serverDiscoveryDiagnostics, err + } + + s.serverDiscoveryMutex.RLock() + server, ok = s.listResources[typeName] + s.serverDiscoveryMutex.RUnlock() + + if !ok { + return nil, []*tfprotov6.Diagnostic{ + listResourceMissingError(typeName), + }, nil + } + + return server, s.serverDiscoveryDiagnostics, nil +} + func (s *muxServer) getFunctionServer(ctx context.Context, name string) (tfprotov6.ProviderServer, []*tfprotov6.Diagnostic, error) { s.serverDiscoveryMutex.RLock() server, ok := s.functions[name] @@ -202,7 +240,7 @@ func (s *muxServer) getResourceServer(ctx context.Context, typeName string) (tfp // serverDiscovery will populate the mux server "routing" for functions and // resource types by calling all underlying server GetMetadata RPC and falling // back to GetProviderSchema RPC. It is intended to only be called through -// getDataSourceServer, getEphemeralResourceServer, getFunctionServer, and getResourceServer. +// getDataSourceServer, getEphemeralResourceServer, getListResourceServer, getFunctionServer, and getResourceServer. // // The error return represents gRPC errors, which except for the GetMetadata // call returning the gRPC unimplemented error, is always returned. @@ -250,6 +288,16 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { s.ephemeralResources[serverEphemeralResource.TypeName] = server } + for _, serverListResource := range metadataResp.ListResources { + if _, ok := s.listResources[serverListResource.TypeName]; ok { + s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, listResourceDuplicateError(serverListResource.TypeName)) + + continue + } + + s.listResources[serverListResource.TypeName] = server + } + for _, serverFunction := range metadataResp.Functions { if _, ok := s.functions[serverFunction.Name]; ok { s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, functionDuplicateError(serverFunction.Name)) @@ -312,6 +360,16 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { s.ephemeralResources[typeName] = server } + for typeName := range providerSchemaResp.ListResourceSchemas { + if _, ok := s.listResources[typeName]; ok { + s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, listResourceDuplicateError(typeName)) + + continue + } + + s.listResources[typeName] = server + } + for name := range providerSchemaResp.Functions { if _, ok := s.functions[name]; ok { s.serverDiscoveryDiagnostics = append(s.serverDiscoveryDiagnostics, functionDuplicateError(name)) @@ -350,11 +408,13 @@ func (s *muxServer) serverDiscovery(ctx context.Context) error { // - Only one provider implements each data source // - Only one provider implements each function // - Only one provider implements each ephemeral resource +// - Only one provider implements each list resource // - Only one provider implements each resource identity func NewMuxServer(_ context.Context, servers ...func() tfprotov6.ProviderServer) (*muxServer, error) { result := muxServer{ dataSources: make(map[string]tfprotov6.ProviderServer), ephemeralResources: make(map[string]tfprotov6.ProviderServer), + listResources: make(map[string]tfprotov6.ProviderServer), functions: make(map[string]tfprotov6.ProviderServer), resources: make(map[string]tfprotov6.ProviderServer), resourceCapabilities: make(map[string]*tfprotov6.ServerCapabilities), diff --git a/tf6muxserver/mux_server_ValidateListResourceConfig.go b/tf6muxserver/mux_server_ValidateListResourceConfig.go new file mode 100644 index 0000000..6a24eb1 --- /dev/null +++ b/tf6muxserver/mux_server_ValidateListResourceConfig.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +func (s *muxServer) ValidateListResourceConfig(ctx context.Context, req *tfprotov6.ValidateListResourceConfigRequest) (*tfprotov6.ValidateListResourceConfigResponse, error) { + rpc := "ValidateListResourceTypeConfig" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + server, diags, err := s.getListResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: diags, + }, nil + } + + ctx = logging.Tfprotov6ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.ValidateListResourceConfig(ctx, req) +} diff --git a/tf6muxserver/mux_server_ValidateListResourceConfig_test.go b/tf6muxserver/mux_server_ValidateListResourceConfig_test.go new file mode 100644 index 0000000..9f4c6e3 --- /dev/null +++ b/tf6muxserver/mux_server_ValidateListResourceConfig_test.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf6testserver" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" +) + +func TestMuxServerValidateListResourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + testServer1 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + ListResourceSchemas: map[string]*tfprotov6.Schema{ + "test_list_resource_server1": {}, + }, + }, + } + testServer2 := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + ListResourceSchemas: map[string]*tfprotov6.Schema{ + "test_list_resource_server2": {}, + }, + }, + } + servers := []func() tfprotov6.ProviderServer{testServer1.ProviderServer, testServer2.ProviderServer} + muxServer, err := tf6muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ + TypeName: "test_list_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !testServer1.ValidateListResourceConfigCalled["test_list_resource_server1"] { + t.Errorf("expected test_list_resource_server1 ValidateListResourceConfig to be called on server1") + } + + if testServer2.ValidateListResourceConfigCalled["test_list_resource_server1"] { + t.Errorf("unexpected test_list_resource_server1 ValidateListResourceConfig called on server2") + } + + _, err = muxServer.ProviderServer().ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ + TypeName: "test_list_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if testServer1.ValidateListResourceConfigCalled["test_list_resource_server2"] { + t.Errorf("unexpected test_list_resource_server2 ValidateListResourceConfig called on server1") + } + + if !testServer2.ValidateListResourceConfigCalled["test_list_resource_server2"] { + t.Errorf("expected test_list_resource_server2 ValidateListResourceConfig to be called on server2") + } +} diff --git a/tf6to5server/tf6to5server.go b/tf6to5server/tf6to5server.go index 39e01f8..4745286 100644 --- a/tf6to5server/tf6to5server.go +++ b/tf6to5server/tf6to5server.go @@ -291,3 +291,32 @@ func (s v6tov5Server) ValidateResourceTypeConfig(ctx context.Context, req *tfpro return tfprotov6tov5.ValidateResourceTypeConfigResponse(v6Resp), nil } + +func (s v6tov5Server) ValidateListResourceConfig(ctx context.Context, req *tfprotov5.ValidateListResourceConfigRequest) (*tfprotov5.ValidateListResourceConfigResponse, error) { + // TODO: Remove and call s.v6Server.ValidateListResourceConfig below directly once interface becomes required + //nolint:staticcheck // Intentionally verifying interface implementation + listResourceServer, ok := s.v6Server.(tfprotov6.ProviderServerWithListResource) + if !ok { + v5Resp := &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "ValidateListResourceConfig Not Implemented", + Detail: "A ValidateListResourceConfig call was received by the provider, however the provider does not implement the RPC. " + + "Either upgrade the provider to a version that implements ValidateListResourceConfig or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return v5Resp, nil + } + + v6Req := tfprotov5tov6.ValidateListResourceConfigRequest(req) + + v6Resp, err := s.v6Server.ValidateListResourceConfig(ctx, v6Req) + if err != nil { + return nil, err + } + + return tfprotov6tov5.ValidateListResourceConfigResponse(v6Resp), nil +} diff --git a/tf6to5server/tf6to5server_test.go b/tf6to5server/tf6to5server_test.go index 08df9c5..ab40d4b 100644 --- a/tf6to5server/tf6to5server_test.go +++ b/tf6to5server/tf6to5server_test.go @@ -894,3 +894,34 @@ func TestV6ToV5ServerValidateResourceTypeConfig(t *testing.T) { t.Errorf("expected test_resource ValidateResourceConfig to be called") } } + +func TestV6ToV5ServerValidateListResourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v6server := &tf6testserver.TestServer{ + GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ + ResourceSchemas: map[string]*tfprotov6.Schema{ + "test_ephemeral_resource": {}, + }, + }, + } + + v5server, err := tf6to5server.DowngradeServer(context.Background(), v6server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + _, err = v5server.ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ + TypeName: "test_ephemeral_resource", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v6server.ValidateListResourceConfigCalled["test_ephemeral_resource"] { + t.Errorf("expected test_ephemeral_resource ValidateListResourceConfig to be called") + } +} From e3bd470c8253fdaf9d1fde54ead496a38ae319dc Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 5 Jun 2025 11:50:38 -0400 Subject: [PATCH 2/2] Added temporary interface to pass tests --- internal/tfprotov5tov6/tfprotov5tov6.go | 6 ++ internal/tfprotov5tov6/tfprotov5tov6_test.go | 92 +++++++++++++++++++ internal/tfprotov6tov5/tfprotov6tov5.go | 6 ++ internal/tfprotov6tov5/tfprotov6tov5_test.go | 92 +++++++++++++++++++ .../mux_server_ValidateListResourceConfig.go | 20 +++- ..._server_ValidateListResourceConfig_test.go | 10 +- tf5to6server/tf5to6server.go | 2 +- tf5to6server/tf5to6server_test.go | 16 +++- .../mux_server_ValidateListResourceConfig.go | 21 ++++- ..._server_ValidateListResourceConfig_test.go | 10 +- tf6to5server/tf6to5server.go | 2 +- tf6to5server/tf6to5server_test.go | 16 +++- 12 files changed, 274 insertions(+), 19 deletions(-) diff --git a/internal/tfprotov5tov6/tfprotov5tov6.go b/internal/tfprotov5tov6/tfprotov5tov6.go index 3374cba..040d69d 100644 --- a/internal/tfprotov5tov6/tfprotov5tov6.go +++ b/internal/tfprotov5tov6/tfprotov5tov6.go @@ -190,6 +190,12 @@ func EphemeralResourceMetadata(in tfprotov5.EphemeralResourceMetadata) tfprotov6 } } +func ListResourceMetadata(in tfprotov5.ListResourceMetadata) tfprotov6.ListResourceMetadata { + return tfprotov6.ListResourceMetadata{ + TypeName: in.TypeName, + } +} + func Function(in *tfprotov5.Function) *tfprotov6.Function { if in == nil { return nil diff --git a/internal/tfprotov5tov6/tfprotov5tov6_test.go b/internal/tfprotov5tov6/tfprotov5tov6_test.go index 58eb4bf..f36d205 100644 --- a/internal/tfprotov5tov6/tfprotov5tov6_test.go +++ b/internal/tfprotov5tov6/tfprotov5tov6_test.go @@ -55,6 +55,14 @@ var ( TypeName: "test_ephemeral_resource", } + testTfprotov5ListResourceMetadata tfprotov5.ListResourceMetadata = tfprotov5.ListResourceMetadata{ + TypeName: "test_list_resource", + } + + testTfprotov6ListResourceMetadata tfprotov6.ListResourceMetadata = tfprotov6.ListResourceMetadata{ + TypeName: "test_list_resource", + } + testTfprotov5Function *tfprotov5.Function = &tfprotov5.Function{ Parameters: []*tfprotov5.FunctionParameter{}, Return: &tfprotov5.FunctionReturn{ @@ -921,6 +929,9 @@ func TestGetMetadataResponse(t *testing.T) { EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ testTfprotov5EphemeralResourceMetadata, }, + ListResources: []tfprotov5.ListResourceMetadata{ + testTfprotov5ListResourceMetadata, + }, Functions: []tfprotov5.FunctionMetadata{ testTfprotov5FunctionMetadata, }, @@ -936,6 +947,9 @@ func TestGetMetadataResponse(t *testing.T) { EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ testTfprotov6EphemeralResourceMetadata, }, + ListResources: []tfprotov6.ListResourceMetadata{ + testTfprotov6ListResourceMetadata, + }, Functions: []tfprotov6.FunctionMetadata{ testTfprotov6FunctionMetadata, }, @@ -1011,6 +1025,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": testTfprotov5Schema, }, + ListResourceSchemas: map[string]*tfprotov5.Schema{ + "test_list_resource": testTfprotov5Schema, + }, Functions: map[string]*tfprotov5.Function{ "test_function": testTfprotov5Function, }, @@ -1028,6 +1045,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": testTfprotov6Schema, }, + ListResourceSchemas: map[string]*tfprotov6.Schema{ + "test_list_resource": testTfprotov6Schema, + }, Functions: map[string]*tfprotov6.Function{ "test_function": testTfprotov6Function, }, @@ -2898,6 +2918,78 @@ func TestValidateResourceConfigResponse(t *testing.T) { } } +func TestValidateListResourceConfigRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.ValidateListResourceConfigRequest + expected *tfprotov6.ValidateListResourceConfigRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.ValidateListResourceConfigRequest{ + Config: &testTfprotov5DynamicValue, + TypeName: "test_list_resource", + }, + expected: &tfprotov6.ValidateListResourceConfigRequest{ + Config: &testTfprotov6DynamicValue, + TypeName: "test_list_resource", + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.ValidateListResourceConfigRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestValidateListResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov5.ValidateListResourceConfigResponse + expected *tfprotov6.ValidateListResourceConfigResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: testTfprotov5Diagnostics, + }, + expected: &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: testTfprotov6Diagnostics, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov5tov6.ValidateListResourceConfigResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func pointer[T any](value T) *T { return &value } diff --git a/internal/tfprotov6tov5/tfprotov6tov5.go b/internal/tfprotov6tov5/tfprotov6tov5.go index e82743f..96b6ff5 100644 --- a/internal/tfprotov6tov5/tfprotov6tov5.go +++ b/internal/tfprotov6tov5/tfprotov6tov5.go @@ -195,6 +195,12 @@ func EphemeralResourceMetadata(in tfprotov6.EphemeralResourceMetadata) tfprotov5 } } +func ListResourceMetadata(in tfprotov6.ListResourceMetadata) tfprotov5.ListResourceMetadata { + return tfprotov5.ListResourceMetadata{ + TypeName: in.TypeName, + } +} + func Function(in *tfprotov6.Function) *tfprotov5.Function { if in == nil { return nil diff --git a/internal/tfprotov6tov5/tfprotov6tov5_test.go b/internal/tfprotov6tov5/tfprotov6tov5_test.go index e9e3704..31f57b9 100644 --- a/internal/tfprotov6tov5/tfprotov6tov5_test.go +++ b/internal/tfprotov6tov5/tfprotov6tov5_test.go @@ -57,6 +57,14 @@ var ( TypeName: "test_ephemeral_resource", } + testTfprotov5ListResourceMetadata tfprotov5.ListResourceMetadata = tfprotov5.ListResourceMetadata{ + TypeName: "test_list_resource", + } + + testTfprotov6ListResourceMetadata tfprotov6.ListResourceMetadata = tfprotov6.ListResourceMetadata{ + TypeName: "test_list_resource", + } + testTfprotov5Function *tfprotov5.Function = &tfprotov5.Function{ Parameters: []*tfprotov5.FunctionParameter{}, Return: &tfprotov5.FunctionReturn{ @@ -923,6 +931,9 @@ func TestGetMetadataResponse(t *testing.T) { EphemeralResources: []tfprotov6.EphemeralResourceMetadata{ testTfprotov6EphemeralResourceMetadata, }, + ListResources: []tfprotov6.ListResourceMetadata{ + testTfprotov6ListResourceMetadata, + }, Functions: []tfprotov6.FunctionMetadata{ testTfprotov6FunctionMetadata, }, @@ -938,6 +949,9 @@ func TestGetMetadataResponse(t *testing.T) { EphemeralResources: []tfprotov5.EphemeralResourceMetadata{ testTfprotov5EphemeralResourceMetadata, }, + ListResources: []tfprotov5.ListResourceMetadata{ + testTfprotov5ListResourceMetadata, + }, Functions: []tfprotov5.FunctionMetadata{ testTfprotov5FunctionMetadata, }, @@ -1014,6 +1028,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { EphemeralResourceSchemas: map[string]*tfprotov6.Schema{ "test_ephemeral_resource": testTfprotov6Schema, }, + ListResourceSchemas: map[string]*tfprotov6.Schema{ + "test_list_resource": testTfprotov6Schema, + }, Functions: map[string]*tfprotov6.Function{ "test_function": testTfprotov6Function, }, @@ -1031,6 +1048,9 @@ func TestGetProviderSchemaResponse(t *testing.T) { EphemeralResourceSchemas: map[string]*tfprotov5.Schema{ "test_ephemeral_resource": testTfprotov5Schema, }, + ListResourceSchemas: map[string]*tfprotov5.Schema{ + "test_list_resource": testTfprotov5Schema, + }, Functions: map[string]*tfprotov5.Function{ "test_function": testTfprotov5Function, }, @@ -3173,6 +3193,78 @@ func TestValidateResourceTypeConfigResponse(t *testing.T) { } } +func TestValidateListResourceConfigRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.ValidateListResourceConfigRequest + expected *tfprotov5.ValidateListResourceConfigRequest + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.ValidateListResourceConfigRequest{ + Config: &testTfprotov6DynamicValue, + TypeName: "test_list_resource", + }, + expected: &tfprotov5.ValidateListResourceConfigRequest{ + Config: &testTfprotov5DynamicValue, + TypeName: "test_list_resource", + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.ValidateListResourceConfigRequest(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestValidateListResourceConfigResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + in *tfprotov6.ValidateListResourceConfigResponse + expected *tfprotov5.ValidateListResourceConfigResponse + }{ + "nil": { + in: nil, + expected: nil, + }, + "all-valid-fields": { + in: &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: testTfprotov6Diagnostics, + }, + expected: &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: testTfprotov5Diagnostics, + }, + }, + } + + for name, testCase := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tfprotov6tov5.ValidateListResourceConfigResponse(testCase.in) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + func pointer[T any](value T) *T { return &value } diff --git a/tf5muxserver/mux_server_ValidateListResourceConfig.go b/tf5muxserver/mux_server_ValidateListResourceConfig.go index 3c7dcc9..e392f16 100644 --- a/tf5muxserver/mux_server_ValidateListResourceConfig.go +++ b/tf5muxserver/mux_server_ValidateListResourceConfig.go @@ -28,8 +28,26 @@ func (s *muxServer) ValidateListResourceConfig(ctx context.Context, req *tfproto }, nil } + // TODO: Remove and call server.ValidateListResourceConfig below directly once interface becomes required. + //nolint:staticcheck // Intentionally verifying interface implementation + listResourceServer, ok := server.(tfprotov5.ProviderServerWithListResource) + if !ok { + resp := &tfprotov5.ValidateListResourceConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "ValidateListResourceConfig Not Implemented", + Detail: "A ValidateListResourceConfig call was received by the provider, however the provider does not implement ValidateListResourceConfig. " + + "Either upgrade the provider to a version that implements ValidateListResourceConfig or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) logging.MuxTrace(ctx, "calling downstream server") - return server.ValidateListResourceConfig(ctx, req) + return listResourceServer.ValidateListResourceConfig(ctx, req) } diff --git a/tf5muxserver/mux_server_ValidateListResourceConfig_test.go b/tf5muxserver/mux_server_ValidateListResourceConfig_test.go index a964c30..7959511 100644 --- a/tf5muxserver/mux_server_ValidateListResourceConfig_test.go +++ b/tf5muxserver/mux_server_ValidateListResourceConfig_test.go @@ -38,7 +38,13 @@ func TestMuxServerValidateListResourceConfig(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - _, err = muxServer.ProviderServer().ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ + //nolint:staticcheck // Intentionally verifying interface implementation + listResourceServer, ok := muxServer.ProviderServer().(tfprotov5.ProviderServerWithListResource) + if !ok { + t.Fatal("muxServer should implement tfprotov5.ProviderServerWithListResource") + } + + _, err = listResourceServer.ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ TypeName: "test_list_resource_server1", }) @@ -54,7 +60,7 @@ func TestMuxServerValidateListResourceConfig(t *testing.T) { t.Errorf("unexpected test_list_resource_server1 ValidateListResourceConfig called on server2") } - _, err = muxServer.ProviderServer().ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ + _, err = listResourceServer.ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ TypeName: "test_list_resource_server2", }) diff --git a/tf5to6server/tf5to6server.go b/tf5to6server/tf5to6server.go index d40aa06..e19af1f 100644 --- a/tf5to6server/tf5to6server.go +++ b/tf5to6server/tf5to6server.go @@ -302,7 +302,7 @@ func (s v5tov6Server) ValidateListResourceConfig(ctx context.Context, req *tfpro v5Req := tfprotov6tov5.ValidateListResourceConfigRequest(req) - v5Resp, err := s.v5Server.ValidateListResourceConfig(ctx, v5Req) + v5Resp, err := listResourceServer.ValidateListResourceConfig(ctx, v5Req) if err != nil { return nil, err } diff --git a/tf5to6server/tf5to6server_test.go b/tf5to6server/tf5to6server_test.go index 845a37c..f65e73a 100644 --- a/tf5to6server/tf5to6server_test.go +++ b/tf5to6server/tf5to6server_test.go @@ -804,7 +804,7 @@ func TestV6ToV5ServerValidateListResourceConfig(t *testing.T) { v5server := &tf5testserver.TestServer{ GetProviderSchemaResponse: &tfprotov5.GetProviderSchemaResponse{ ListResourceSchemas: map[string]*tfprotov5.Schema{ - "test_resource": {}, + "test_list_resource": {}, }, }, } @@ -815,15 +815,21 @@ func TestV6ToV5ServerValidateListResourceConfig(t *testing.T) { t.Fatalf("unexpected error downgrading server: %s", err) } - _, err = v6server.ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ - TypeName: "test_resource", + //nolint:staticcheck // Intentionally verifying interface implementation + listResourceServer, ok := v6server.(tfprotov6.ProviderServerWithListResource) + if !ok { + t.Fatal("v6server should implement tfprotov6.ProviderServerWithResourceIdentity") + } + + _, err = listResourceServer.ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ + TypeName: "test_list_resource", }) if err != nil { t.Fatalf("unexpected error: %s", err) } - if !v5server.ValidateListResourceConfigCalled["test_resource"] { - t.Errorf("expected test_resource ValidateListResourceConfig to be called") + if !v5server.ValidateListResourceConfigCalled["test_list_resource"] { + t.Errorf("expected test_list_resource ValidateListResourceConfig to be called") } } diff --git a/tf6muxserver/mux_server_ValidateListResourceConfig.go b/tf6muxserver/mux_server_ValidateListResourceConfig.go index 6a24eb1..488a5d7 100644 --- a/tf6muxserver/mux_server_ValidateListResourceConfig.go +++ b/tf6muxserver/mux_server_ValidateListResourceConfig.go @@ -5,7 +5,6 @@ package tf6muxserver import ( "context" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -28,8 +27,26 @@ func (s *muxServer) ValidateListResourceConfig(ctx context.Context, req *tfproto }, nil } + // TODO: Remove and call server.ValidateListResourceConfig below directly once interface becomes required. + //nolint:staticcheck // Intentionally verifying interface implementation + listResourceServer, ok := server.(tfprotov6.ProviderServerWithListResource) + if !ok { + resp := &tfprotov6.ValidateListResourceConfigResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "ValidateListResourceConfig Not Implemented", + Detail: "A ValidateListResourceConfig call was received by the provider, however the provider does not implement ValidateListResourceConfig. " + + "Either upgrade the provider to a version that implements ValidateListResourceConfig or this is a bug in Terraform that should be reported to the Terraform maintainers.", + }, + }, + } + + return resp, nil + } + ctx = logging.Tfprotov6ProviderServerContext(ctx, server) logging.MuxTrace(ctx, "calling downstream server") - return server.ValidateListResourceConfig(ctx, req) + return listResourceServer.ValidateListResourceConfig(ctx, req) } diff --git a/tf6muxserver/mux_server_ValidateListResourceConfig_test.go b/tf6muxserver/mux_server_ValidateListResourceConfig_test.go index 9f4c6e3..4837af2 100644 --- a/tf6muxserver/mux_server_ValidateListResourceConfig_test.go +++ b/tf6muxserver/mux_server_ValidateListResourceConfig_test.go @@ -38,7 +38,13 @@ func TestMuxServerValidateListResourceConfig(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - _, err = muxServer.ProviderServer().ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ + //nolint:staticcheck // Intentionally verifying interface implementation + listResourceServer, ok := muxServer.ProviderServer().(tfprotov6.ProviderServerWithListResource) + if !ok { + t.Fatal("muxServer should implement tfprotov6.ProviderServerWithListResource") + } + + _, err = listResourceServer.ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ TypeName: "test_list_resource_server1", }) @@ -54,7 +60,7 @@ func TestMuxServerValidateListResourceConfig(t *testing.T) { t.Errorf("unexpected test_list_resource_server1 ValidateListResourceConfig called on server2") } - _, err = muxServer.ProviderServer().ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ + _, err = listResourceServer.ValidateListResourceConfig(ctx, &tfprotov6.ValidateListResourceConfigRequest{ TypeName: "test_list_resource_server2", }) diff --git a/tf6to5server/tf6to5server.go b/tf6to5server/tf6to5server.go index 4745286..89dc802 100644 --- a/tf6to5server/tf6to5server.go +++ b/tf6to5server/tf6to5server.go @@ -313,7 +313,7 @@ func (s v6tov5Server) ValidateListResourceConfig(ctx context.Context, req *tfpro v6Req := tfprotov5tov6.ValidateListResourceConfigRequest(req) - v6Resp, err := s.v6Server.ValidateListResourceConfig(ctx, v6Req) + v6Resp, err := listResourceServer.ValidateListResourceConfig(ctx, v6Req) if err != nil { return nil, err } diff --git a/tf6to5server/tf6to5server_test.go b/tf6to5server/tf6to5server_test.go index ab40d4b..85c020c 100644 --- a/tf6to5server/tf6to5server_test.go +++ b/tf6to5server/tf6to5server_test.go @@ -902,7 +902,7 @@ func TestV6ToV5ServerValidateListResourceConfig(t *testing.T) { v6server := &tf6testserver.TestServer{ GetProviderSchemaResponse: &tfprotov6.GetProviderSchemaResponse{ ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_ephemeral_resource": {}, + "test_list_resource": {}, }, }, } @@ -913,15 +913,21 @@ func TestV6ToV5ServerValidateListResourceConfig(t *testing.T) { t.Fatalf("unexpected error downgrading server: %s", err) } - _, err = v5server.ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ - TypeName: "test_ephemeral_resource", + //nolint:staticcheck // Intentionally verifying interface implementation + listResourceServer, ok := v5server.(tfprotov5.ProviderServerWithListResource) + if !ok { + t.Fatal("v6server should implement tfprotov5.ProviderServerWithResourceIdentity") + } + + _, err = listResourceServer.ValidateListResourceConfig(ctx, &tfprotov5.ValidateListResourceConfigRequest{ + TypeName: "test_list_resource", }) if err != nil { t.Fatalf("unexpected error: %s", err) } - if !v6server.ValidateListResourceConfigCalled["test_ephemeral_resource"] { - t.Errorf("expected test_ephemeral_resource ValidateListResourceConfig to be called") + if !v6server.ValidateListResourceConfigCalled["test_list_resource"] { + t.Errorf("expected test_list_resource ValidateListResourceConfig to be called") } }