From 13d82bcb5731dc4c7fe432084e5676c171887c95 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Fri, 25 Aug 2023 09:10:47 -0400 Subject: [PATCH] all: Initial support for protocol version 5.4 and 6.4 Reference: https://github.com/hashicorp/terraform-plugin-mux/issues/185 --- go.mod | 14 +- go.sum | 28 +- internal/tf5testserver/tf5testserver.go | 13 + internal/tf6testserver/tf6testserver.go | 13 + internal/tfprotov5tov6/tfprotov5tov6.go | 65 ++- internal/tfprotov6tov5/tfprotov6tov5.go | 54 +++ tf5muxserver/mux_server.go | 21 + .../mux_server_ApplyResourceChange.go | 8 +- tf5muxserver/mux_server_GetMetadata.go | 100 ++++ tf5muxserver/mux_server_GetMetadata_test.go | 446 ++++++++++++++++++ tf5muxserver/mux_server_GetProviderSchema.go | 12 +- .../mux_server_GetProviderSchema_test.go | 21 +- .../mux_server_ImportResourceState.go | 8 +- tf5muxserver/mux_server_PlanResourceChange.go | 7 +- tf5muxserver/mux_server_ReadDataSource.go | 8 +- tf5muxserver/mux_server_ReadResource.go | 8 +- .../mux_server_UpgradeResourceState.go | 8 +- .../mux_server_ValidateDataSourceConfig.go | 8 +- .../mux_server_ValidateResourceTypeConfig.go | 8 +- tf5muxserver/server_capabilities.go | 19 + tf5to6server/tf5to6server.go | 11 + tf5to6server/tf5to6server_test.go | 31 ++ tf6muxserver/mux_server.go | 21 + .../mux_server_ApplyResourceChange.go | 8 +- tf6muxserver/mux_server_GetMetadata.go | 100 ++++ tf6muxserver/mux_server_GetMetadata_test.go | 446 ++++++++++++++++++ tf6muxserver/mux_server_GetProviderSchema.go | 12 +- .../mux_server_GetProviderSchema_test.go | 18 +- .../mux_server_ImportResourceState.go | 8 +- tf6muxserver/mux_server_PlanResourceChange.go | 7 +- tf6muxserver/mux_server_ReadDataSource.go | 8 +- tf6muxserver/mux_server_ReadResource.go | 8 +- .../mux_server_UpgradeResourceState.go | 8 +- .../mux_server_ValidateDataResourceConfig.go | 8 +- .../mux_server_ValidateResourceConfig.go | 8 +- tf6muxserver/server_capabilities.go | 19 + tf6to5server/tf6to5server.go | 11 + tf6to5server/tf6to5server_test.go | 31 ++ 38 files changed, 1513 insertions(+), 119 deletions(-) create mode 100644 tf5muxserver/mux_server_GetMetadata.go create mode 100644 tf5muxserver/mux_server_GetMetadata_test.go create mode 100644 tf6muxserver/mux_server_GetMetadata.go create mode 100644 tf6muxserver/mux_server_GetMetadata_test.go diff --git a/go.mod b/go.mod index 1d60b54..0cd8915 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/google/go-cmp v0.5.9 - github.com/hashicorp/terraform-plugin-go v0.18.0 + github.com/hashicorp/terraform-plugin-go v0.18.1-0.20230824194237-31d190886564 github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -14,7 +14,7 @@ require ( github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-plugin v1.4.10 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/terraform-registry-address v0.2.1 // indirect + github.com/hashicorp/terraform-registry-address v0.2.2 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect github.com/mattn/go-colorable v0.1.12 // indirect @@ -23,10 +23,10 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.1 // indirect + golang.org/x/net v0.13.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect + google.golang.org/grpc v1.57.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index 6baeccc..6441fb0 100644 --- a/go.sum +++ b/go.sum @@ -15,12 +15,12 @@ github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQ github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= 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.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE= -github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y= +github.com/hashicorp/terraform-plugin-go v0.18.1-0.20230824194237-31d190886564 h1:INRLdX7mCAVNzcTweLg85StUa0LmaAsl8eBBboOE+r8= +github.com/hashicorp/terraform-plugin-go v0.18.1-0.20230824194237-31d190886564/go.mod h1:OLYUIu74227VwdQiU14UBqqFXac3reHV2YrWTCMAWPk= 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.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM= -github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y= +github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno= +github.com/hashicorp/terraform-registry-address v0.2.2/go.mod h1:LtwNbCihUoUZ3RYriyS2wF/lGPB6gF9ICLRtuDk7hSo= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= @@ -46,22 +46,22 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= -google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= diff --git a/internal/tf5testserver/tf5testserver.go b/internal/tf5testserver/tf5testserver.go index d015a53..09f5afd 100644 --- a/internal/tf5testserver/tf5testserver.go +++ b/internal/tf5testserver/tf5testserver.go @@ -17,6 +17,9 @@ type TestServer struct { ConfigureProviderCalled bool ConfigureProviderResponse *tfprotov5.ConfigureProviderResponse + GetMetadataCalled bool + GetMetadataResponse *tfprotov5.GetMetadataResponse + GetProviderSchemaCalled bool GetProviderSchemaResponse *tfprotov5.GetProviderSchemaResponse @@ -64,6 +67,16 @@ func (s *TestServer) ConfigureProvider(_ context.Context, _ *tfprotov5.Configure return &tfprotov5.ConfigureProviderResponse{}, nil } +func (s *TestServer) GetMetadata(_ context.Context, _ *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) { + s.GetMetadataCalled = true + + if s.GetMetadataResponse != nil { + return s.GetMetadataResponse, nil + } + + return &tfprotov5.GetMetadataResponse{}, nil +} + func (s *TestServer) GetProviderSchema(_ context.Context, _ *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { s.GetProviderSchemaCalled = true diff --git a/internal/tf6testserver/tf6testserver.go b/internal/tf6testserver/tf6testserver.go index 4ff5688..229341b 100644 --- a/internal/tf6testserver/tf6testserver.go +++ b/internal/tf6testserver/tf6testserver.go @@ -17,6 +17,9 @@ type TestServer struct { ConfigureProviderCalled bool ConfigureProviderResponse *tfprotov6.ConfigureProviderResponse + GetMetadataCalled bool + GetMetadataResponse *tfprotov6.GetMetadataResponse + GetProviderSchemaCalled bool GetProviderSchemaResponse *tfprotov6.GetProviderSchemaResponse @@ -64,6 +67,16 @@ func (s *TestServer) ConfigureProvider(_ context.Context, _ *tfprotov6.Configure return &tfprotov6.ConfigureProviderResponse{}, nil } +func (s *TestServer) GetMetadata(_ context.Context, _ *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { + s.GetMetadataCalled = true + + if s.GetMetadataResponse != nil { + return s.GetMetadataResponse, nil + } + + return &tfprotov6.GetMetadataResponse{}, nil +} + func (s *TestServer) GetProviderSchema(_ context.Context, _ *tfprotov6.GetProviderSchemaRequest) (*tfprotov6.GetProviderSchemaResponse, error) { s.GetProviderSchemaCalled = true diff --git a/internal/tfprotov5tov6/tfprotov5tov6.go b/internal/tfprotov5tov6/tfprotov5tov6.go index 9ebb1bc..8924ea8 100644 --- a/internal/tfprotov5tov6/tfprotov5tov6.go +++ b/internal/tfprotov5tov6/tfprotov5tov6.go @@ -57,6 +57,12 @@ func ConfigureProviderResponse(in *tfprotov5.ConfigureProviderResponse) *tfproto } } +func DataSourceMetadata(in tfprotov5.DataSourceMetadata) tfprotov6.DataSourceMetadata { + return tfprotov6.DataSourceMetadata{ + TypeName: in.TypeName, + } +} + func Diagnostics(in []*tfprotov5.Diagnostic) []*tfprotov6.Diagnostic { if in == nil { return nil @@ -92,6 +98,37 @@ func DynamicValue(in *tfprotov5.DynamicValue) *tfprotov6.DynamicValue { } } +func GetMetadataRequest(in *tfprotov5.GetMetadataRequest) *tfprotov6.GetMetadataRequest { + if in == nil { + return nil + } + + return &tfprotov6.GetMetadataRequest{} +} + +func GetMetadataResponse(in *tfprotov5.GetMetadataResponse) *tfprotov6.GetMetadataResponse { + if in == nil { + return nil + } + + resp := &tfprotov6.GetMetadataResponse{ + DataSources: make([]tfprotov6.DataSourceMetadata, 0, len(in.DataSources)), + Diagnostics: Diagnostics(in.Diagnostics), + Resources: make([]tfprotov6.ResourceMetadata, 0, len(in.Resources)), + ServerCapabilities: ServerCapabilities(in.ServerCapabilities), + } + + for _, datasource := range in.DataSources { + resp.DataSources = append(resp.DataSources, DataSourceMetadata(datasource)) + } + + for _, resource := range in.Resources { + resp.Resources = append(resp.Resources, ResourceMetadata(resource)) + } + + return resp +} + func GetProviderSchemaRequest(in *tfprotov5.GetProviderSchemaRequest) *tfprotov6.GetProviderSchemaRequest { if in == nil { return nil @@ -118,11 +155,12 @@ func GetProviderSchemaResponse(in *tfprotov5.GetProviderSchemaResponse) *tfproto } return &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: dataSourceSchemas, - Diagnostics: Diagnostics(in.Diagnostics), - Provider: Schema(in.Provider), - ProviderMeta: Schema(in.ProviderMeta), - ResourceSchemas: resourceSchemas, + DataSourceSchemas: dataSourceSchemas, + Diagnostics: Diagnostics(in.Diagnostics), + Provider: Schema(in.Provider), + ProviderMeta: Schema(in.ProviderMeta), + ResourceSchemas: resourceSchemas, + ServerCapabilities: ServerCapabilities(in.ServerCapabilities), } } @@ -258,6 +296,12 @@ func ReadResourceResponse(in *tfprotov5.ReadResourceResponse) *tfprotov6.ReadRes } } +func ResourceMetadata(in tfprotov5.ResourceMetadata) tfprotov6.ResourceMetadata { + return tfprotov6.ResourceMetadata{ + TypeName: in.TypeName, + } +} + func Schema(in *tfprotov5.Schema) *tfprotov6.Schema { if in == nil { return nil @@ -336,6 +380,17 @@ func SchemaNestedBlock(in *tfprotov5.SchemaNestedBlock) *tfprotov6.SchemaNestedB } } +func ServerCapabilities(in *tfprotov5.ServerCapabilities) *tfprotov6.ServerCapabilities { + if in == nil { + return nil + } + + return &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: in.GetProviderSchemaOptional, + PlanDestroy: in.PlanDestroy, + } +} + func StopProviderRequest(in *tfprotov5.StopProviderRequest) *tfprotov6.StopProviderRequest { if in == nil { return nil diff --git a/internal/tfprotov6tov5/tfprotov6tov5.go b/internal/tfprotov6tov5/tfprotov6tov5.go index a0fd200..39356fb 100644 --- a/internal/tfprotov6tov5/tfprotov6tov5.go +++ b/internal/tfprotov6tov5/tfprotov6tov5.go @@ -62,6 +62,12 @@ func ConfigureProviderResponse(in *tfprotov6.ConfigureProviderResponse) *tfproto } } +func DataSourceMetadata(in tfprotov6.DataSourceMetadata) tfprotov5.DataSourceMetadata { + return tfprotov5.DataSourceMetadata{ + TypeName: in.TypeName, + } +} + func Diagnostics(in []*tfprotov6.Diagnostic) []*tfprotov5.Diagnostic { if in == nil { return nil @@ -97,6 +103,37 @@ func DynamicValue(in *tfprotov6.DynamicValue) *tfprotov5.DynamicValue { } } +func GetMetadataRequest(in *tfprotov6.GetMetadataRequest) *tfprotov5.GetMetadataRequest { + if in == nil { + return nil + } + + return &tfprotov5.GetMetadataRequest{} +} + +func GetMetadataResponse(in *tfprotov6.GetMetadataResponse) *tfprotov5.GetMetadataResponse { + if in == nil { + return nil + } + + resp := &tfprotov5.GetMetadataResponse{ + DataSources: make([]tfprotov5.DataSourceMetadata, 0, len(in.DataSources)), + Diagnostics: Diagnostics(in.Diagnostics), + Resources: make([]tfprotov5.ResourceMetadata, 0, len(in.Resources)), + ServerCapabilities: ServerCapabilities(in.ServerCapabilities), + } + + for _, datasource := range in.DataSources { + resp.DataSources = append(resp.DataSources, DataSourceMetadata(datasource)) + } + + for _, resource := range in.Resources { + resp.Resources = append(resp.Resources, ResourceMetadata(resource)) + } + + return resp +} + func GetProviderSchemaRequest(in *tfprotov6.GetProviderSchemaRequest) *tfprotov5.GetProviderSchemaRequest { if in == nil { return nil @@ -307,6 +344,12 @@ func ReadResourceResponse(in *tfprotov6.ReadResourceResponse) *tfprotov5.ReadRes } } +func ResourceMetadata(in tfprotov6.ResourceMetadata) tfprotov5.ResourceMetadata { + return tfprotov5.ResourceMetadata{ + TypeName: in.TypeName, + } +} + func Schema(in *tfprotov6.Schema) (*tfprotov5.Schema, error) { if in == nil { return nil, nil @@ -413,6 +456,17 @@ func SchemaNestedBlock(in *tfprotov6.SchemaNestedBlock) (*tfprotov5.SchemaNested }, nil } +func ServerCapabilities(in *tfprotov6.ServerCapabilities) *tfprotov5.ServerCapabilities { + if in == nil { + return nil + } + + return &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: in.GetProviderSchemaOptional, + PlanDestroy: in.PlanDestroy, + } +} + func StopProviderRequest(in *tfprotov6.StopProviderRequest) *tfprotov5.StopProviderRequest { if in == nil { return nil diff --git a/tf5muxserver/mux_server.go b/tf5muxserver/mux_server.go index 7bb87e8..d4e6eb1 100644 --- a/tf5muxserver/mux_server.go +++ b/tf5muxserver/mux_server.go @@ -5,6 +5,7 @@ package tf5muxserver import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) @@ -37,6 +38,26 @@ func (s muxServer) ProviderServer() tfprotov5.ProviderServer { return &s } +func (s *muxServer) getDataSourceServer(_ context.Context, typeName string) (tfprotov5.ProviderServer, error) { + server, ok := s.dataSources[typeName] + + if !ok { + return nil, fmt.Errorf("data source type %q is not implemented by any servers", typeName) + } + + return server, nil +} + +func (s *muxServer) getResourceServer(_ context.Context, typeName string) (tfprotov5.ProviderServer, error) { + server, ok := s.resources[typeName] + + if !ok { + return nil, fmt.Errorf("resource type %q is not implemented by any servers", typeName) + } + + return server, nil +} + // NewMuxServer returns a muxed server that will route gRPC requests between // tfprotov5.ProviderServers specified. The GetProviderSchema method of each // is called to verify that the overall muxed server is compatible by ensuring: diff --git a/tf5muxserver/mux_server_ApplyResourceChange.go b/tf5muxserver/mux_server_ApplyResourceChange.go index 0941ec1..126932b 100644 --- a/tf5muxserver/mux_server_ApplyResourceChange.go +++ b/tf5muxserver/mux_server_ApplyResourceChange.go @@ -5,7 +5,6 @@ package tf5muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ApplyResourceChange(ctx context.Context, req *tfprotov5.Apply rpc := "ApplyResourceChange" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_GetMetadata.go b/tf5muxserver/mux_server_GetMetadata.go new file mode 100644 index 0000000..f2b886d --- /dev/null +++ b/tf5muxserver/mux_server_GetMetadata.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// GetMetadata merges the metadata returned by the +// tfprotov5.ProviderServers associated with muxServer into a single response. +// Resources and data sources 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" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + resp := &tfprotov5.GetMetadataResponse{ + DataSources: make([]tfprotov5.DataSourceMetadata, 0), + Resources: make([]tfprotov5.ResourceMetadata, 0), + ServerCapabilities: serverCapabilities, + } + + for _, server := range s.servers { + ctx := logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + serverResp, err := server.GetMetadata(ctx, &tfprotov5.GetMetadataRequest{}) + + if err != nil { + return resp, fmt.Errorf("error calling GetMetadata for %T: %w", server, err) + } + + resp.Diagnostics = append(resp.Diagnostics, serverResp.Diagnostics...) + + for _, datasource := range serverResp.DataSources { + var duplicate bool + + for _, existingDatasource := range resp.DataSources { + if datasource.TypeName == existingDatasource.TypeName { + duplicate = true + + break + } + } + + if duplicate { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same data source type across providers. " + + "Data source types must be implemented by only one provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate data source type: " + datasource.TypeName, + }) + } else { + s.dataSources[datasource.TypeName] = server + resp.DataSources = append(resp.DataSources, datasource) + } + + s.resourceCapabilities[datasource.TypeName] = serverResp.ServerCapabilities + } + + for _, resource := range serverResp.Resources { + var duplicate bool + + for _, existingResource := range resp.Resources { + if resource.TypeName == existingResource.TypeName { + duplicate = true + + break + } + } + + if duplicate { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same resource type across providers. " + + "Resource types must be implemented by only one provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate resource type: " + resource.TypeName, + }) + } else { + s.resources[resource.TypeName] = server + resp.Resources = append(resp.Resources, resource) + } + + s.resourceCapabilities[resource.TypeName] = serverResp.ServerCapabilities + } + } + + return resp, nil +} diff --git a/tf5muxserver/mux_server_GetMetadata_test.go b/tf5muxserver/mux_server_GetMetadata_test.go new file mode 100644 index 0000000..40ed9e8 --- /dev/null +++ b/tf5muxserver/mux_server_GetMetadata_test.go @@ -0,0 +1,446 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerGetMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + servers []func() tfprotov5.ProviderServer + expectedDataSources []tfprotov5.DataSourceMetadata + expectedDiagnostics []*tfprotov5.Diagnostic + expectedResources []tfprotov5.ResourceMetadata + expectedServerCapabilities *tfprotov5.ServerCapabilities + }{ + "combined": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + }, + DataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_quux", + }, + }, + DataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_bar", + }, + { + TypeName: "test_quux", + }, + }, + }, + }).ProviderServer, + }, + expectedResources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + { + TypeName: "test_quux", + }, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + { + TypeName: "test_quux", + }, + }, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "duplicate-data-source-type": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + }, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same data source type across providers. " + + "Data source types must be implemented by only one provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate data source type: test_foo", + }, + }, + expectedResources: []tfprotov5.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "duplicate-resource-type": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same resource type across providers. " + + "Resource types must be implemented by only one provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate resource type: test_foo", + }, + }, + expectedResources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "server-capabilities": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_with_server_capabilities", + }, + }, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_without_server_capabilities", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedResources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_with_server_capabilities", + }, + { + TypeName: "test_without_server_capabilities", + }, + }, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "error-once": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + expectedResources: []tfprotov5.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "error-multiple": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + expectedResources: []tfprotov5.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "warning-once": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + expectedResources: []tfprotov5.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "warning-multiple": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + expectedResources: []tfprotov5.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "warning-then-error": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov5.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + expectedResources: []tfprotov5.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + muxServer, err := tf5muxserver.NewMuxServer(context.Background(), testCase.servers...) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + resp, err := muxServer.ProviderServer().GetMetadata(context.Background(), &tfprotov5.GetMetadataRequest{}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if diff := cmp.Diff(resp.DataSources, testCase.expectedDataSources); diff != "" { + t.Errorf("data sources didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.Diagnostics, testCase.expectedDiagnostics); diff != "" { + t.Errorf("diagnostics didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.Resources, testCase.expectedResources); diff != "" { + t.Errorf("resources didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.ServerCapabilities, testCase.expectedServerCapabilities); diff != "" { + t.Errorf("server capabilities didn't match expectations: %s", diff) + } + }) + } +} diff --git a/tf5muxserver/mux_server_GetProviderSchema.go b/tf5muxserver/mux_server_GetProviderSchema.go index 26e8e39..984d220 100644 --- a/tf5muxserver/mux_server_GetProviderSchema.go +++ b/tf5muxserver/mux_server_GetProviderSchema.go @@ -22,15 +22,9 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetPro ctx = logging.RpcContext(ctx, rpc) resp := &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: make(map[string]*tfprotov5.Schema), - ResourceSchemas: make(map[string]*tfprotov5.Schema), - - // Always announce all ServerCapabilities. Individual capabilities are - // handled in their respective RPCs to protect downstream servers if - // they are not compatible with a capability. - ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, - }, + DataSourceSchemas: make(map[string]*tfprotov5.Schema), + ResourceSchemas: make(map[string]*tfprotov5.Schema), + ServerCapabilities: serverCapabilities, } for _, server := range s.servers { diff --git a/tf5muxserver/mux_server_GetProviderSchema_test.go b/tf5muxserver/mux_server_GetProviderSchema_test.go index dddb0d6..4b70379 100644 --- a/tf5muxserver/mux_server_GetProviderSchema_test.go +++ b/tf5muxserver/mux_server_GetProviderSchema_test.go @@ -426,7 +426,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "duplicate-data-source-type": { @@ -461,7 +462,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "duplicate-resource-type": { @@ -496,7 +498,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "test_foo": {}, }, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "provider-mismatch": { @@ -579,7 +582,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "provider-meta-mismatch": { @@ -662,7 +666,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, expectedResourceSchemas: map[string]*tfprotov5.Schema{}, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "server-capabilities": { @@ -673,7 +678,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "test_with_server_capabilities": {}, }, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }).ProviderServer, @@ -691,7 +697,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "test_without_server_capabilities": {}, }, expectedServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "error-once": { diff --git a/tf5muxserver/mux_server_ImportResourceState.go b/tf5muxserver/mux_server_ImportResourceState.go index 1bc867e..548ed4d 100644 --- a/tf5muxserver/mux_server_ImportResourceState.go +++ b/tf5muxserver/mux_server_ImportResourceState.go @@ -5,7 +5,6 @@ package tf5muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ImportResourceState(ctx context.Context, req *tfprotov5.Impor rpc := "ImportResourceState" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_PlanResourceChange.go b/tf5muxserver/mux_server_PlanResourceChange.go index 85245a4..0a186d3 100644 --- a/tf5muxserver/mux_server_PlanResourceChange.go +++ b/tf5muxserver/mux_server_PlanResourceChange.go @@ -18,10 +18,11 @@ func (s muxServer) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanRe rpc := "PlanResourceChange" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ReadDataSource.go b/tf5muxserver/mux_server_ReadDataSource.go index 72456ac..d83f36d 100644 --- a/tf5muxserver/mux_server_ReadDataSource.go +++ b/tf5muxserver/mux_server_ReadDataSource.go @@ -5,7 +5,6 @@ package tf5muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSo rpc := "ReadDataSource" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.dataSources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getDataSourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ReadResource.go b/tf5muxserver/mux_server_ReadResource.go index 716531b..98980f5 100644 --- a/tf5muxserver/mux_server_ReadResource.go +++ b/tf5muxserver/mux_server_ReadResource.go @@ -5,7 +5,6 @@ package tf5muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -17,10 +16,11 @@ func (s muxServer) ReadResource(ctx context.Context, req *tfprotov5.ReadResource rpc := "ReadResource" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_UpgradeResourceState.go b/tf5muxserver/mux_server_UpgradeResourceState.go index 52c6722..0abc5c2 100644 --- a/tf5muxserver/mux_server_UpgradeResourceState.go +++ b/tf5muxserver/mux_server_UpgradeResourceState.go @@ -5,7 +5,6 @@ package tf5muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) UpgradeResourceState(ctx context.Context, req *tfprotov5.Upgr rpc := "UpgradeResourceState" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ValidateDataSourceConfig.go b/tf5muxserver/mux_server_ValidateDataSourceConfig.go index 31ded35..30fae38 100644 --- a/tf5muxserver/mux_server_ValidateDataSourceConfig.go +++ b/tf5muxserver/mux_server_ValidateDataSourceConfig.go @@ -5,7 +5,6 @@ package tf5muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ValidateDataSourceConfig(ctx context.Context, req *tfprotov5. rpc := "ValidateDataSourceConfig" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.dataSources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getDataSourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ValidateResourceTypeConfig.go b/tf5muxserver/mux_server_ValidateResourceTypeConfig.go index b8348a1..2490ded 100644 --- a/tf5muxserver/mux_server_ValidateResourceTypeConfig.go +++ b/tf5muxserver/mux_server_ValidateResourceTypeConfig.go @@ -5,7 +5,6 @@ package tf5muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov rpc := "ValidateResourceTypeConfig" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/server_capabilities.go b/tf5muxserver/server_capabilities.go index 042679f..03f9e87 100644 --- a/tf5muxserver/server_capabilities.go +++ b/tf5muxserver/server_capabilities.go @@ -5,6 +5,25 @@ package tf5muxserver import "github.com/hashicorp/terraform-plugin-go/tfprotov5" +// serverCapabilities always announces all ServerCapabilities. Individual +// capabilities are handled in their respective RPCs to protect downstream +// servers if they are not compatible with a capability. +var serverCapabilities = &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, +} + +// serverSupportsGetProviderSchemaOptional returns true if the given +// ServerCapabilities is not nil and enables the GetProviderSchemaOptional +// capability. +func serverSupportsGetProviderSchemaOptional(capabilities *tfprotov5.ServerCapabilities) bool { + if capabilities == nil { + return false + } + + return capabilities.GetProviderSchemaOptional +} + // serverSupportsPlanDestroy returns true if the given ServerCapabilities is not // nil and enables the PlanDestroy capability. func serverSupportsPlanDestroy(capabilities *tfprotov5.ServerCapabilities) bool { diff --git a/tf5to6server/tf5to6server.go b/tf5to6server/tf5to6server.go index 1b16f1f..dadd7b5 100644 --- a/tf5to6server/tf5to6server.go +++ b/tf5to6server/tf5to6server.go @@ -54,6 +54,17 @@ func (s v5tov6Server) ConfigureProvider(ctx context.Context, req *tfprotov6.Conf return tfprotov5tov6.ConfigureProviderResponse(v5Resp), nil } +func (s v5tov6Server) GetMetadata(ctx context.Context, req *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { + v5Req := tfprotov6tov5.GetMetadataRequest(req) + v5Resp, err := s.v5Server.GetMetadata(ctx, v5Req) + + if err != nil { + return nil, err + } + + return tfprotov5tov6.GetMetadataResponse(v5Resp), nil +} + func (s v5tov6Server) GetProviderSchema(ctx context.Context, req *tfprotov6.GetProviderSchemaRequest) (*tfprotov6.GetProviderSchemaResponse, error) { v5Req := tfprotov6tov5.GetProviderSchemaRequest(req) v5Resp, err := s.v5Server.GetProviderSchema(ctx, v5Req) diff --git a/tf5to6server/tf5to6server_test.go b/tf5to6server/tf5to6server_test.go index 0e1f370..7f7d6b9 100644 --- a/tf5to6server/tf5to6server_test.go +++ b/tf5to6server/tf5to6server_test.go @@ -186,6 +186,37 @@ func TestV6ToV5ServerConfigureProvider(t *testing.T) { } } +func TestV6ToV5ServerGetMetadata(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v5server := &tf5testserver.TestServer{ + GetMetadataResponse: &tfprotov5.GetMetadataResponse{ + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_resource", + }, + }, + }, + } + + v6server, err := tf5to6server.UpgradeServer(context.Background(), v5server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + _, err = v6server.GetMetadata(ctx, &tfprotov6.GetMetadataRequest{}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v5server.GetMetadataCalled { + t.Errorf("expected GetMetadata to be called") + } +} + func TestV6ToV5ServerGetProviderSchema(t *testing.T) { t.Parallel() diff --git a/tf6muxserver/mux_server.go b/tf6muxserver/mux_server.go index 264318b..9aa0725 100644 --- a/tf6muxserver/mux_server.go +++ b/tf6muxserver/mux_server.go @@ -5,6 +5,7 @@ package tf6muxserver import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) @@ -37,6 +38,26 @@ func (s muxServer) ProviderServer() tfprotov6.ProviderServer { return &s } +func (s *muxServer) getDataSourceServer(_ context.Context, typeName string) (tfprotov6.ProviderServer, error) { + server, ok := s.dataSources[typeName] + + if !ok { + return nil, fmt.Errorf("data source type %q is not implemented by any servers", typeName) + } + + return server, nil +} + +func (s *muxServer) getResourceServer(_ context.Context, typeName string) (tfprotov6.ProviderServer, error) { + server, ok := s.resources[typeName] + + if !ok { + return nil, fmt.Errorf("resource type %q is not implemented by any servers", typeName) + } + + return server, nil +} + // NewMuxServer returns a muxed server that will route gRPC requests between // tfprotov6.ProviderServers specified. When the GetProviderSchema RPC of each // is called, there is verification that the overall muxed server is compatible diff --git a/tf6muxserver/mux_server_ApplyResourceChange.go b/tf6muxserver/mux_server_ApplyResourceChange.go index baefe35..86b73ea 100644 --- a/tf6muxserver/mux_server_ApplyResourceChange.go +++ b/tf6muxserver/mux_server_ApplyResourceChange.go @@ -5,7 +5,6 @@ package tf6muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ApplyResourceChange(ctx context.Context, req *tfprotov6.Apply rpc := "ApplyResourceChange" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_GetMetadata.go b/tf6muxserver/mux_server_GetMetadata.go new file mode 100644 index 0000000..b5c1b9b --- /dev/null +++ b/tf6muxserver/mux_server_GetMetadata.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// GetMetadata merges the metadata returned by the +// tfprotov6.ProviderServers associated with muxServer into a single response. +// Resources and data sources must be returned from only one server or an error +// diagnostic is returned. +func (s *muxServer) GetMetadata(ctx context.Context, req *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { + rpc := "GetMetadata" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + + resp := &tfprotov6.GetMetadataResponse{ + DataSources: make([]tfprotov6.DataSourceMetadata, 0), + Resources: make([]tfprotov6.ResourceMetadata, 0), + ServerCapabilities: serverCapabilities, + } + + for _, server := range s.servers { + ctx := logging.Tfprotov6ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + serverResp, err := server.GetMetadata(ctx, &tfprotov6.GetMetadataRequest{}) + + if err != nil { + return resp, fmt.Errorf("error calling GetMetadata for %T: %w", server, err) + } + + resp.Diagnostics = append(resp.Diagnostics, serverResp.Diagnostics...) + + for _, datasource := range serverResp.DataSources { + var duplicate bool + + for _, existingDatasource := range resp.DataSources { + if datasource.TypeName == existingDatasource.TypeName { + duplicate = true + + break + } + } + + if duplicate { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same data source type across providers. " + + "Data source types must be implemented by only one provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate data source type: " + datasource.TypeName, + }) + } else { + s.dataSources[datasource.TypeName] = server + resp.DataSources = append(resp.DataSources, datasource) + } + + s.resourceCapabilities[datasource.TypeName] = serverResp.ServerCapabilities + } + + for _, resource := range serverResp.Resources { + var duplicate bool + + for _, existingResource := range resp.Resources { + if resource.TypeName == existingResource.TypeName { + duplicate = true + + break + } + } + + if duplicate { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same resource type across providers. " + + "Resource types must be implemented by only one provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate resource type: " + resource.TypeName, + }) + } else { + s.resources[resource.TypeName] = server + resp.Resources = append(resp.Resources, resource) + } + + s.resourceCapabilities[resource.TypeName] = serverResp.ServerCapabilities + } + } + + return resp, nil +} diff --git a/tf6muxserver/mux_server_GetMetadata_test.go b/tf6muxserver/mux_server_GetMetadata_test.go new file mode 100644 index 0000000..8497ef4 --- /dev/null +++ b/tf6muxserver/mux_server_GetMetadata_test.go @@ -0,0 +1,446 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-mux/internal/tf6testserver" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" +) + +func TestMuxServerGetMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + servers []func() tfprotov6.ProviderServer + expectedDataSources []tfprotov6.DataSourceMetadata + expectedDiagnostics []*tfprotov6.Diagnostic + expectedResources []tfprotov6.ResourceMetadata + expectedServerCapabilities *tfprotov6.ServerCapabilities + }{ + "combined": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + }, + DataSources: []tfprotov6.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_quux", + }, + }, + DataSources: []tfprotov6.DataSourceMetadata{ + { + TypeName: "test_bar", + }, + { + TypeName: "test_quux", + }, + }, + }, + }).ProviderServer, + }, + expectedResources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + { + TypeName: "test_quux", + }, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + { + TypeName: "test_bar", + }, + { + TypeName: "test_quux", + }, + }, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "duplicate-data-source-type": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{ + { + TypeName: "test_foo", + }, + }, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same data source type across providers. " + + "Data source types must be implemented by only one provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate data source type: test_foo", + }, + }, + expectedResources: []tfprotov6.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "duplicate-resource-type": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Invalid Provider Server Combination", + Detail: "The combined provider has multiple implementations of the same resource type across providers. " + + "Resource types must be implemented by only one provider. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Duplicate resource type: test_foo", + }, + }, + expectedResources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_foo", + }, + }, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "server-capabilities": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_with_server_capabilities", + }, + }, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_without_server_capabilities", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedResources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_with_server_capabilities", + }, + { + TypeName: "test_without_server_capabilities", + }, + }, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "error-once": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{}).ProviderServer, + (&tf6testserver.TestServer{}).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + expectedResources: []tfprotov6.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "error-multiple": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{}).ProviderServer, + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + expectedResources: []tfprotov6.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "warning-once": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{}).ProviderServer, + (&tf6testserver.TestServer{}).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + expectedResources: []tfprotov6.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "warning-multiple": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{}).ProviderServer, + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + expectedResources: []tfprotov6.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + "warning-then-error": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf6testserver.TestServer{}).ProviderServer, + (&tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + }, + expectedDataSources: []tfprotov6.DataSourceMetadata{}, + expectedDiagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + expectedResources: []tfprotov6.ResourceMetadata{}, + expectedServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + muxServer, err := tf6muxserver.NewMuxServer(context.Background(), testCase.servers...) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + resp, err := muxServer.ProviderServer().GetMetadata(context.Background(), &tfprotov6.GetMetadataRequest{}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if diff := cmp.Diff(resp.DataSources, testCase.expectedDataSources); diff != "" { + t.Errorf("data sources didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.Diagnostics, testCase.expectedDiagnostics); diff != "" { + t.Errorf("diagnostics didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.Resources, testCase.expectedResources); diff != "" { + t.Errorf("resources didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.ServerCapabilities, testCase.expectedServerCapabilities); diff != "" { + t.Errorf("server capabilities didn't match expectations: %s", diff) + } + }) + } +} diff --git a/tf6muxserver/mux_server_GetProviderSchema.go b/tf6muxserver/mux_server_GetProviderSchema.go index f1fbd70..ea892e1 100644 --- a/tf6muxserver/mux_server_GetProviderSchema.go +++ b/tf6muxserver/mux_server_GetProviderSchema.go @@ -22,15 +22,9 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetPro ctx = logging.RpcContext(ctx, rpc) resp := &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: make(map[string]*tfprotov6.Schema), - ResourceSchemas: make(map[string]*tfprotov6.Schema), - - // Always announce all ServerCapabilities. Individual capabilities are - // handled in their respective RPCs to protect downstream servers if - // they are not compatible with a capability. - ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, - }, + DataSourceSchemas: make(map[string]*tfprotov6.Schema), + ResourceSchemas: make(map[string]*tfprotov6.Schema), + ServerCapabilities: serverCapabilities, } for _, server := range s.servers { diff --git a/tf6muxserver/mux_server_GetProviderSchema_test.go b/tf6muxserver/mux_server_GetProviderSchema_test.go index 5a8eb92..139beca 100644 --- a/tf6muxserver/mux_server_GetProviderSchema_test.go +++ b/tf6muxserver/mux_server_GetProviderSchema_test.go @@ -426,7 +426,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, }, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "duplicate-data-source-type": { @@ -461,7 +462,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "duplicate-resource-type": { @@ -496,7 +498,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "test_foo": {}, }, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "provider-mismatch": { @@ -579,7 +582,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "provider-meta-mismatch": { @@ -662,7 +666,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { }, expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "server-capabilities": { @@ -691,7 +696,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { "test_without_server_capabilities": {}, }, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "error-once": { diff --git a/tf6muxserver/mux_server_ImportResourceState.go b/tf6muxserver/mux_server_ImportResourceState.go index fb3a085..e5c6bd1 100644 --- a/tf6muxserver/mux_server_ImportResourceState.go +++ b/tf6muxserver/mux_server_ImportResourceState.go @@ -5,7 +5,6 @@ package tf6muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ImportResourceState(ctx context.Context, req *tfprotov6.Impor rpc := "ImportResourceState" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_PlanResourceChange.go b/tf6muxserver/mux_server_PlanResourceChange.go index 9d5cfca..aa82c24 100644 --- a/tf6muxserver/mux_server_PlanResourceChange.go +++ b/tf6muxserver/mux_server_PlanResourceChange.go @@ -18,10 +18,11 @@ func (s muxServer) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanRe rpc := "PlanResourceChange" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ReadDataSource.go b/tf6muxserver/mux_server_ReadDataSource.go index bc11ea1..a726f1a 100644 --- a/tf6muxserver/mux_server_ReadDataSource.go +++ b/tf6muxserver/mux_server_ReadDataSource.go @@ -5,7 +5,6 @@ package tf6muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSo rpc := "ReadDataSource" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.dataSources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getDataSourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ReadResource.go b/tf6muxserver/mux_server_ReadResource.go index 47ad8f0..4de1af4 100644 --- a/tf6muxserver/mux_server_ReadResource.go +++ b/tf6muxserver/mux_server_ReadResource.go @@ -5,7 +5,6 @@ package tf6muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -17,10 +16,11 @@ func (s muxServer) ReadResource(ctx context.Context, req *tfprotov6.ReadResource rpc := "ReadResource" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_UpgradeResourceState.go b/tf6muxserver/mux_server_UpgradeResourceState.go index 3004938..4f24ddf 100644 --- a/tf6muxserver/mux_server_UpgradeResourceState.go +++ b/tf6muxserver/mux_server_UpgradeResourceState.go @@ -5,7 +5,6 @@ package tf6muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) UpgradeResourceState(ctx context.Context, req *tfprotov6.Upgr rpc := "UpgradeResourceState" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ValidateDataResourceConfig.go b/tf6muxserver/mux_server_ValidateDataResourceConfig.go index 06584e5..2774262 100644 --- a/tf6muxserver/mux_server_ValidateDataResourceConfig.go +++ b/tf6muxserver/mux_server_ValidateDataResourceConfig.go @@ -5,7 +5,6 @@ package tf6muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov rpc := "ValidateDataResourceConfig" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.dataSources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getDataSourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ValidateResourceConfig.go b/tf6muxserver/mux_server_ValidateResourceConfig.go index 7dd70cb..abdd594 100644 --- a/tf6muxserver/mux_server_ValidateResourceConfig.go +++ b/tf6muxserver/mux_server_ValidateResourceConfig.go @@ -5,7 +5,6 @@ package tf6muxserver import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" @@ -18,10 +17,11 @@ func (s muxServer) ValidateResourceConfig(ctx context.Context, req *tfprotov6.Va rpc := "ValidateResourceConfig" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - server, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + server, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/server_capabilities.go b/tf6muxserver/server_capabilities.go index cac9470..1a062bc 100644 --- a/tf6muxserver/server_capabilities.go +++ b/tf6muxserver/server_capabilities.go @@ -5,6 +5,25 @@ package tf6muxserver import "github.com/hashicorp/terraform-plugin-go/tfprotov6" +// serverCapabilities always announces all ServerCapabilities. Individual +// capabilities are handled in their respective RPCs to protect downstream +// servers if they are not compatible with a capability. +var serverCapabilities = &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, +} + +// serverSupportsGetProviderSchemaOptional returns true if the given +// ServerCapabilities is not nil and enables the GetProviderSchemaOptional +// capability. +func serverSupportsGetProviderSchemaOptional(capabilities *tfprotov6.ServerCapabilities) bool { + if capabilities == nil { + return false + } + + return capabilities.GetProviderSchemaOptional +} + // serverSupportsPlanDestroy returns true if the given ServerCapabilities is not // nil and enables the PlanDestroy capability. func serverSupportsPlanDestroy(capabilities *tfprotov6.ServerCapabilities) bool { diff --git a/tf6to5server/tf6to5server.go b/tf6to5server/tf6to5server.go index 7b26f99..6d13b48 100644 --- a/tf6to5server/tf6to5server.go +++ b/tf6to5server/tf6to5server.go @@ -66,6 +66,17 @@ func (s v6tov5Server) ConfigureProvider(ctx context.Context, req *tfprotov5.Conf return tfprotov6tov5.ConfigureProviderResponse(v6Resp), nil } +func (s v6tov5Server) GetMetadata(ctx context.Context, req *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) { + v6Req := tfprotov5tov6.GetMetadataRequest(req) + v6Resp, err := s.v6Server.GetMetadata(ctx, v6Req) + + if err != nil { + return nil, err + } + + return tfprotov6tov5.GetMetadataResponse(v6Resp), nil +} + func (s v6tov5Server) GetProviderSchema(ctx context.Context, req *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { v6Req := tfprotov5tov6.GetProviderSchemaRequest(req) v6Resp, err := s.v6Server.GetProviderSchema(ctx, v6Req) diff --git a/tf6to5server/tf6to5server_test.go b/tf6to5server/tf6to5server_test.go index 93b911f..006cfec 100644 --- a/tf6to5server/tf6to5server_test.go +++ b/tf6to5server/tf6to5server_test.go @@ -284,6 +284,37 @@ func TestV6ToV5ServerConfigureProvider(t *testing.T) { } } +func TestV5ToV6ServerGetMetadata(t *testing.T) { + t.Parallel() + + ctx := context.Background() + v6server := &tf6testserver.TestServer{ + GetMetadataResponse: &tfprotov6.GetMetadataResponse{ + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_resource", + }, + }, + }, + } + + v5server, err := tf6to5server.DowngradeServer(context.Background(), v6server.ProviderServer) + + if err != nil { + t.Fatalf("unexpected error downgrading server: %s", err) + } + + _, err = v5server.GetMetadata(ctx, &tfprotov5.GetMetadataRequest{}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !v6server.GetMetadataCalled { + t.Errorf("expected GetMetadata to be called") + } +} + func TestV6ToV5ServerGetProviderSchema(t *testing.T) { t.Parallel()