From 1e941b2694e4d302ccd230a18b4a3fa6c3cb3060 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 This implements protocol versions 5.4 and 6.4 in all provider server implementations, including enabling the `GetProviderSchemaOptional` server capability. --- go.mod | 14 +- go.sum | 28 +- internal/tf5dynamicvalue/equals.go | 41 -- internal/tf5dynamicvalue/equals_test.go | 239 ---------- internal/tf5testserver/tf5testserver.go | 19 + internal/tf6dynamicvalue/equals.go | 41 -- internal/tf6dynamicvalue/equals_test.go | 239 ---------- internal/tf6testserver/tf6testserver.go | 19 + internal/tfprotov5tov6/tfprotov5tov6.go | 65 ++- internal/tfprotov6tov5/tfprotov6tov5.go | 54 +++ tf5muxserver/diagnostics.go | 62 +++ tf5muxserver/mux_server.go | 198 +++++++- .../mux_server_ApplyResourceChange.go | 16 +- .../mux_server_ApplyResourceChange_test.go | 7 - tf5muxserver/mux_server_ConfigureProvider.go | 2 +- .../mux_server_ConfigureProvider_test.go | 7 - tf5muxserver/mux_server_GetMetadata.go | 100 ++++ tf5muxserver/mux_server_GetMetadata_test.go | 446 ++++++++++++++++++ tf5muxserver/mux_server_GetProviderSchema.go | 13 +- .../mux_server_GetProviderSchema_test.go | 21 +- .../mux_server_ImportResourceState.go | 16 +- .../mux_server_ImportResourceState_test.go | 7 - tf5muxserver/mux_server_PlanResourceChange.go | 15 +- .../mux_server_PlanResourceChange_test.go | 7 - .../mux_server_PrepareProviderConfig.go | 36 +- .../mux_server_PrepareProviderConfig_test.go | 22 +- tf5muxserver/mux_server_ReadDataSource.go | 16 +- .../mux_server_ReadDataSource_test.go | 7 - tf5muxserver/mux_server_ReadResource.go | 16 +- tf5muxserver/mux_server_ReadResource_test.go | 7 - tf5muxserver/mux_server_StopProvider.go | 2 +- tf5muxserver/mux_server_StopProvider_test.go | 7 - .../mux_server_UpgradeResourceState.go | 16 +- .../mux_server_UpgradeResourceState_test.go | 7 - .../mux_server_ValidateDataSourceConfig.go | 16 +- ...ux_server_ValidateDataSourceConfig_test.go | 7 - .../mux_server_ValidateResourceTypeConfig.go | 16 +- ..._server_ValidateResourceTypeConfig_test.go | 7 - tf5muxserver/server_capabilities.go | 8 + tf5to6server/tf5to6server.go | 11 + tf5to6server/tf5to6server_test.go | 31 ++ tf6muxserver/diagnostics.go | 62 +++ tf6muxserver/mux_server.go | 198 +++++++- .../mux_server_ApplyResourceChange.go | 16 +- .../mux_server_ApplyResourceChange_test.go | 7 - tf6muxserver/mux_server_ConfigureProvider.go | 2 +- .../mux_server_ConfigureProvider_test.go | 7 - tf6muxserver/mux_server_GetMetadata.go | 90 ++++ tf6muxserver/mux_server_GetMetadata_test.go | 446 ++++++++++++++++++ tf6muxserver/mux_server_GetProviderSchema.go | 51 +- .../mux_server_GetProviderSchema_test.go | 26 +- .../mux_server_ImportResourceState.go | 16 +- .../mux_server_ImportResourceState_test.go | 7 - tf6muxserver/mux_server_PlanResourceChange.go | 15 +- .../mux_server_PlanResourceChange_test.go | 7 - tf6muxserver/mux_server_ReadDataSource.go | 16 +- .../mux_server_ReadDataSource_test.go | 7 - tf6muxserver/mux_server_ReadResource.go | 16 +- tf6muxserver/mux_server_ReadResource_test.go | 7 - tf6muxserver/mux_server_StopProvider.go | 2 +- tf6muxserver/mux_server_StopProvider_test.go | 7 - .../mux_server_UpgradeResourceState.go | 16 +- .../mux_server_UpgradeResourceState_test.go | 7 - .../mux_server_ValidateDataResourceConfig.go | 16 +- ..._server_ValidateDataResourceConfig_test.go | 7 - .../mux_server_ValidateProviderConfig.go | 36 +- .../mux_server_ValidateProviderConfig_test.go | 22 +- .../mux_server_ValidateResourceConfig.go | 16 +- .../mux_server_ValidateResourceConfig_test.go | 7 - tf6muxserver/server_capabilities.go | 8 + tf6to5server/tf6to5server.go | 11 + tf6to5server/tf6to5server_test.go | 31 ++ 72 files changed, 2129 insertions(+), 961 deletions(-) delete mode 100644 internal/tf5dynamicvalue/equals.go delete mode 100644 internal/tf5dynamicvalue/equals_test.go delete mode 100644 internal/tf6dynamicvalue/equals.go delete mode 100644 internal/tf6dynamicvalue/equals_test.go create mode 100644 tf5muxserver/diagnostics.go create mode 100644 tf5muxserver/mux_server_GetMetadata.go create mode 100644 tf5muxserver/mux_server_GetMetadata_test.go create mode 100644 tf6muxserver/diagnostics.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/tf5dynamicvalue/equals.go b/internal/tf5dynamicvalue/equals.go deleted file mode 100644 index 30b7c5c..0000000 --- a/internal/tf5dynamicvalue/equals.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package tf5dynamicvalue - -import ( - "fmt" - - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -// Equals performs equality checking of two given *tfprotov5.DynamicValue. -func Equals(schemaType tftypes.Type, i *tfprotov5.DynamicValue, j *tfprotov5.DynamicValue) (bool, error) { - if i == nil { - return j == nil, nil - } - - if j == nil { - return false, nil - } - - // Upstream will panic on DynamicValue.Unmarshal with nil Type - if schemaType == nil { - return false, fmt.Errorf("unable to unmarshal DynamicValue: missing Type") - } - - iValue, err := i.Unmarshal(schemaType) - - if err != nil { - return false, fmt.Errorf("unable to unmarshal DynamicValue: %w", err) - } - - jValue, err := j.Unmarshal(schemaType) - - if err != nil { - return false, fmt.Errorf("unable to unmarshal DynamicValue: %w", err) - } - - return iValue.Equal(jValue), nil -} diff --git a/internal/tf5dynamicvalue/equals_test.go b/internal/tf5dynamicvalue/equals_test.go deleted file mode 100644 index cf78188..0000000 --- a/internal/tf5dynamicvalue/equals_test.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package tf5dynamicvalue_test - -import ( - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-mux/internal/tf5dynamicvalue" -) - -func TestEquals(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - schemaType tftypes.Type - dynamicValue1 *tfprotov5.DynamicValue - dynamicValue2 *tfprotov5.DynamicValue - expected bool - expectedError error - }{ - "all-missing": { - schemaType: nil, - dynamicValue1: nil, - dynamicValue2: nil, - expected: true, - }, - "first-missing": { - schemaType: nil, - dynamicValue1: nil, - dynamicValue2: &tfprotov5.DynamicValue{}, - expected: false, - }, - "second-missing": { - schemaType: nil, - dynamicValue1: &tfprotov5.DynamicValue{}, - dynamicValue2: nil, - expected: false, - }, - "missing-type": { - schemaType: nil, - dynamicValue1: tf5dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - dynamicValue2: tf5dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - expected: false, - expectedError: fmt.Errorf("unable to unmarshal DynamicValue: missing Type"), - }, - "mismatched-type": { - schemaType: tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_bool_attribute": tftypes.Bool, - }, - }, - dynamicValue1: tf5dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - dynamicValue2: tf5dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - expected: false, - expectedError: fmt.Errorf("unable to unmarshal DynamicValue: unknown attribute \"test_string_attribute\""), - }, - "String-different-value": { - schemaType: tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - dynamicValue1: tf5dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value-1"), - }, - ), - ), - dynamicValue2: tf5dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value-2"), - }, - ), - ), - expected: false, - }, - "String-equal-value": { - schemaType: tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - dynamicValue1: tf5dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - dynamicValue2: tf5dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - expected: true, - }, - } - - for name, testCase := range testCases { - name, testCase := name, testCase - - t.Run(name, func(t *testing.T) { - t.Parallel() - - got, err := tf5dynamicvalue.Equals(testCase.schemaType, testCase.dynamicValue1, testCase.dynamicValue2) - - if err != nil { - if testCase.expectedError == nil { - t.Fatalf("wanted no error, got error: %s", err) - } - - if !strings.Contains(err.Error(), testCase.expectedError.Error()) { - t.Fatalf("wanted error %q, got error: %s", testCase.expectedError.Error(), err.Error()) - } - } - - if err == nil && testCase.expectedError != nil { - t.Fatalf("got no error, wanted err: %s", testCase.expectedError) - } - - if got != testCase.expected { - t.Errorf("expected %t, got %t", testCase.expected, got) - } - }) - } -} diff --git a/internal/tf5testserver/tf5testserver.go b/internal/tf5testserver/tf5testserver.go index d015a53..d8b04da 100644 --- a/internal/tf5testserver/tf5testserver.go +++ b/internal/tf5testserver/tf5testserver.go @@ -7,6 +7,8 @@ import ( "context" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var _ tfprotov5.ProviderServer = &TestServer{} @@ -17,6 +19,9 @@ type TestServer struct { ConfigureProviderCalled bool ConfigureProviderResponse *tfprotov5.ConfigureProviderResponse + GetMetadataCalled bool + GetMetadataResponse *tfprotov5.GetMetadataResponse + GetProviderSchemaCalled bool GetProviderSchemaResponse *tfprotov5.GetProviderSchemaResponse @@ -64,6 +69,20 @@ 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 + } + + if s.GetProviderSchemaResponse != nil { + return nil, status.Error(codes.Unimplemented, "only GetProviderSchemaResponse set, simulating GetMetadata as unimplemented") + } + + return &tfprotov5.GetMetadataResponse{}, nil +} + func (s *TestServer) GetProviderSchema(_ context.Context, _ *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { s.GetProviderSchemaCalled = true diff --git a/internal/tf6dynamicvalue/equals.go b/internal/tf6dynamicvalue/equals.go deleted file mode 100644 index 67338f7..0000000 --- a/internal/tf6dynamicvalue/equals.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package tf6dynamicvalue - -import ( - "fmt" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" -) - -// Equals performs equality checking of two given *tfprotov6.DynamicValue. -func Equals(schemaType tftypes.Type, i *tfprotov6.DynamicValue, j *tfprotov6.DynamicValue) (bool, error) { - if i == nil { - return j == nil, nil - } - - if j == nil { - return false, nil - } - - // Upstream will panic on DynamicValue.Unmarshal with nil Type - if schemaType == nil { - return false, fmt.Errorf("unable to unmarshal DynamicValue: missing Type") - } - - iValue, err := i.Unmarshal(schemaType) - - if err != nil { - return false, fmt.Errorf("unable to unmarshal DynamicValue: %w", err) - } - - jValue, err := j.Unmarshal(schemaType) - - if err != nil { - return false, fmt.Errorf("unable to unmarshal DynamicValue: %w", err) - } - - return iValue.Equal(jValue), nil -} diff --git a/internal/tf6dynamicvalue/equals_test.go b/internal/tf6dynamicvalue/equals_test.go deleted file mode 100644 index ba4efef..0000000 --- a/internal/tf6dynamicvalue/equals_test.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package tf6dynamicvalue_test - -import ( - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" - "github.com/hashicorp/terraform-plugin-mux/internal/tf6dynamicvalue" -) - -func TestEquals(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - schemaType tftypes.Type - dynamicValue1 *tfprotov6.DynamicValue - dynamicValue2 *tfprotov6.DynamicValue - expected bool - expectedError error - }{ - "all-missing": { - schemaType: nil, - dynamicValue1: nil, - dynamicValue2: nil, - expected: true, - }, - "first-missing": { - schemaType: nil, - dynamicValue1: nil, - dynamicValue2: &tfprotov6.DynamicValue{}, - expected: false, - }, - "second-missing": { - schemaType: nil, - dynamicValue1: &tfprotov6.DynamicValue{}, - dynamicValue2: nil, - expected: false, - }, - "missing-type": { - schemaType: nil, - dynamicValue1: tf6dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - dynamicValue2: tf6dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - expected: false, - expectedError: fmt.Errorf("unable to unmarshal DynamicValue: missing Type"), - }, - "mismatched-type": { - schemaType: tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_bool_attribute": tftypes.Bool, - }, - }, - dynamicValue1: tf6dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - dynamicValue2: tf6dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - expected: false, - expectedError: fmt.Errorf("unable to unmarshal DynamicValue: unknown attribute \"test_string_attribute\""), - }, - "String-different-value": { - schemaType: tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - dynamicValue1: tf6dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value-1"), - }, - ), - ), - dynamicValue2: tf6dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value-2"), - }, - ), - ), - expected: false, - }, - "String-equal-value": { - schemaType: tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - dynamicValue1: tf6dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - dynamicValue2: tf6dynamicvalue.Must( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - tftypes.NewValue( - tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "test_string_attribute": tftypes.String, - }, - }, - map[string]tftypes.Value{ - "test_string_attribute": tftypes.NewValue(tftypes.String, "test-value"), - }, - ), - ), - expected: true, - }, - } - - for name, testCase := range testCases { - name, testCase := name, testCase - - t.Run(name, func(t *testing.T) { - t.Parallel() - - got, err := tf6dynamicvalue.Equals(testCase.schemaType, testCase.dynamicValue1, testCase.dynamicValue2) - - if err != nil { - if testCase.expectedError == nil { - t.Fatalf("wanted no error, got error: %s", err) - } - - if !strings.Contains(err.Error(), testCase.expectedError.Error()) { - t.Fatalf("wanted error %q, got error: %s", testCase.expectedError.Error(), err.Error()) - } - } - - if err == nil && testCase.expectedError != nil { - t.Fatalf("got no error, wanted err: %s", testCase.expectedError) - } - - if got != testCase.expected { - t.Errorf("expected %t, got %t", testCase.expected, got) - } - }) - } -} diff --git a/internal/tf6testserver/tf6testserver.go b/internal/tf6testserver/tf6testserver.go index 4ff5688..0cced96 100644 --- a/internal/tf6testserver/tf6testserver.go +++ b/internal/tf6testserver/tf6testserver.go @@ -7,6 +7,8 @@ import ( "context" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var _ tfprotov6.ProviderServer = &TestServer{} @@ -17,6 +19,9 @@ type TestServer struct { ConfigureProviderCalled bool ConfigureProviderResponse *tfprotov6.ConfigureProviderResponse + GetMetadataCalled bool + GetMetadataResponse *tfprotov6.GetMetadataResponse + GetProviderSchemaCalled bool GetProviderSchemaResponse *tfprotov6.GetProviderSchemaResponse @@ -64,6 +69,20 @@ 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 + } + + if s.GetProviderSchemaResponse != nil { + return nil, status.Error(codes.Unimplemented, "only GetProviderSchemaResponse set, simulating GetMetadata as unimplemented") + } + + 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/diagnostics.go b/tf5muxserver/diagnostics.go new file mode 100644 index 0000000..a9c1321 --- /dev/null +++ b/tf5muxserver/diagnostics.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf5muxserver + +import "github.com/hashicorp/terraform-plugin-go/tfprotov5" + +func dataSourceDuplicateError(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 data source type across underlying providers. " + + "Data source 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 data source type: " + typeName, + } +} + +func dataSourceMissingError(typeName string) *tfprotov5.Diagnostic { + return &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Data Source Not Implemented", + Detail: "The combined provider does not implement the requested data source type. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Missing data source type: " + typeName, + } +} + +func diagnosticsHasError(diagnostics []*tfprotov5.Diagnostic) bool { + for _, diagnostic := range diagnostics { + if diagnostic == nil { + continue + } + + if diagnostic.Severity == tfprotov5.DiagnosticSeverityError { + return true + } + } + + return false +} + +func resourceDuplicateError(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 resource type across underlying providers. " + + "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 resource type: " + typeName, + } +} + +func resourceMissingError(typeName string) *tfprotov5.Diagnostic { + return &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Resource Not Implemented", + Detail: "The combined provider does not implement the requested resource type. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Missing resource type: " + typeName, + } +} diff --git a/tf5muxserver/mux_server.go b/tf5muxserver/mux_server.go index 7bb87e8..98da696 100644 --- a/tf5muxserver/mux_server.go +++ b/tf5muxserver/mux_server.go @@ -5,8 +5,12 @@ package tf5muxserver import ( "context" + "sync" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var _ tfprotov5.ProviderServer = &muxServer{} @@ -18,23 +22,203 @@ type muxServer struct { // Routing for data source types dataSources map[string]tfprotov5.ProviderServer - // Provider schema is cached during GetProviderSchema for - // ValidateProviderConfig equality checking. - providerSchema *tfprotov5.Schema - // Routing for resource types resources map[string]tfprotov5.ProviderServer - // Resource capabilities are cached during GetProviderSchema + // Resource capabilities are cached during GetMetadata/GetProviderSchema resourceCapabilities map[string]*tfprotov5.ServerCapabilities + // serverDiscoveryComplete is whether the mux server's underlying server + // discovery of resource types has been completed against all servers. + // If false during a resource type specific RPC, the mux server needs to + // pre-emptively call the GetMetadata RPC or GetProviderSchema RPC (as a + // fallback) so it knows which underlying server should receive the RPC. + serverDiscoveryComplete bool + + // serverDiscoveryMutex is a mutex to protect concurrent server discovery + // access from race conditions. + serverDiscoveryMutex sync.RWMutex + // Underlying servers for requests that should be handled by all servers servers []tfprotov5.ProviderServer } // ProviderServer is a function compatible with tf6server.Serve. -func (s muxServer) ProviderServer() tfprotov5.ProviderServer { - return &s +func (s *muxServer) ProviderServer() tfprotov5.ProviderServer { + return s +} + +func (s *muxServer) getDataSourceServer(ctx context.Context, typeName string) (tfprotov5.ProviderServer, []*tfprotov5.Diagnostic, error) { + s.serverDiscoveryMutex.RLock() + server, ok := s.dataSources[typeName] + discoveryComplete := s.serverDiscoveryComplete + s.serverDiscoveryMutex.RUnlock() + + if ok { + return server, nil, nil + } + + if discoveryComplete { + return nil, []*tfprotov5.Diagnostic{ + dataSourceMissingError(typeName), + }, nil + } + + diags, err := s.serverDiscovery(ctx) + + if err != nil || diagnosticsHasError(diags) { + return nil, diags, err + } + + s.serverDiscoveryMutex.RLock() + server, ok = s.dataSources[typeName] + s.serverDiscoveryMutex.RUnlock() + + if !ok { + // Preserve any warning diagnostics + diags = append(diags, dataSourceMissingError(typeName)) + + return nil, diags, nil + } + + return server, diags, nil +} + +func (s *muxServer) getResourceServer(ctx context.Context, typeName string) (tfprotov5.ProviderServer, []*tfprotov5.Diagnostic, error) { + s.serverDiscoveryMutex.RLock() + server, ok := s.resources[typeName] + discoveryComplete := s.serverDiscoveryComplete + s.serverDiscoveryMutex.RUnlock() + + if ok { + return server, nil, nil + } + + if discoveryComplete { + return nil, []*tfprotov5.Diagnostic{ + resourceMissingError(typeName), + }, nil + } + + diags, err := s.serverDiscovery(ctx) + + if err != nil || diagnosticsHasError(diags) { + return nil, diags, err + } + + s.serverDiscoveryMutex.RLock() + server, ok = s.resources[typeName] + s.serverDiscoveryMutex.RUnlock() + + if !ok { + diags = append(diags, resourceMissingError(typeName)) + + return nil, diags, nil + } + + return server, diags, nil +} + +// serverDiscovery will populate the mux server "routing" for resource types by +// calling all underlying server GetMetadata RPC and falling back to +// GetProviderSchema RPC. It is intended to only be called through +// getDataSourceServer and getResourceServer. +// +// The error return represents gRPC errors, which except for the GetMetadata +// call returning the gRPC unimplemented error, is always returned. +func (s *muxServer) serverDiscovery(ctx context.Context) ([]*tfprotov5.Diagnostic, error) { + s.serverDiscoveryMutex.Lock() + defer s.serverDiscoveryMutex.Unlock() + + // Return early if subsequent concurrent operations reached this logic. + if s.serverDiscoveryComplete { + return nil, nil + } + + logging.MuxTrace(ctx, "starting underlying server discovery via GetMetadata or GetProviderSchema") + + var diags []*tfprotov5.Diagnostic + + for _, server := range s.servers { + ctx := logging.Tfprotov5ProviderServerContext(ctx, server) + ctx = logging.RpcContext(ctx, "GetMetadata") + + logging.MuxTrace(ctx, "calling GetMetadata for discovery") + metadataResp, err := server.GetMetadata(ctx, &tfprotov5.GetMetadataRequest{}) + + // GetMetadata call was successful, populate caches and move on to next + // underlying server. + if err == nil && metadataResp != nil { + // Collect all underlying server diagnostics, but skip early return. + diags = append(diags, metadataResp.Diagnostics...) + + for _, serverDataSource := range metadataResp.DataSources { + if _, ok := s.dataSources[serverDataSource.TypeName]; ok { + diags = append(diags, dataSourceDuplicateError(serverDataSource.TypeName)) + + continue + } + + s.dataSources[serverDataSource.TypeName] = server + } + + for _, serverResource := range metadataResp.Resources { + if _, ok := s.resources[serverResource.TypeName]; ok { + diags = append(diags, resourceDuplicateError(serverResource.TypeName)) + + continue + } + + s.resources[serverResource.TypeName] = server + s.resourceCapabilities[serverResource.TypeName] = metadataResp.ServerCapabilities + } + + continue + } + + // Only continue if the gRPC error was an unimplemented code, otherwise + // return any other gRPC error immediately. + grpcStatus, ok := status.FromError(err) + + if !ok || grpcStatus.Code() != codes.Unimplemented { + return diags, err + } + + logging.MuxTrace(ctx, "calling GetProviderSchema for discovery") + providerSchemaResp, err := server.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) + + if err != nil { + return diags, err + } + + // Collect all underlying server diagnostics, but skip early return. + diags = append(diags, providerSchemaResp.Diagnostics...) + + for typeName := range providerSchemaResp.DataSourceSchemas { + if _, ok := s.dataSources[typeName]; ok { + diags = append(diags, dataSourceDuplicateError(typeName)) + + continue + } + + s.dataSources[typeName] = server + } + + for typeName := range providerSchemaResp.ResourceSchemas { + if _, ok := s.resources[typeName]; ok { + diags = append(diags, resourceDuplicateError(typeName)) + + continue + } + + s.resources[typeName] = server + s.resourceCapabilities[typeName] = providerSchemaResp.ServerCapabilities + } + } + + s.serverDiscoveryComplete = true + + return diags, nil } // NewMuxServer returns a muxed server that will route gRPC requests between diff --git a/tf5muxserver/mux_server_ApplyResourceChange.go b/tf5muxserver/mux_server_ApplyResourceChange.go index 0941ec1..42ef052 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" @@ -14,14 +13,21 @@ import ( // ApplyResourceChange calls the ApplyResourceChange method, passing `req`, on // the provider that returned the resource specified by req.TypeName in its // schema. -func (s muxServer) ApplyResourceChange(ctx context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) { +func (s *muxServer) ApplyResourceChange(ctx context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ApplyResourceChange_test.go b/tf5muxserver/mux_server_ApplyResourceChange_test.go index 6da21b1..b1e76bc 100644 --- a/tf5muxserver/mux_server_ApplyResourceChange_test.go +++ b/tf5muxserver/mux_server_ApplyResourceChange_test.go @@ -39,13 +39,6 @@ func TestMuxServerApplyResourceChange(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ApplyResourceChange(ctx, &tfprotov5.ApplyResourceChangeRequest{ TypeName: "test_resource_server1", }) diff --git a/tf5muxserver/mux_server_ConfigureProvider.go b/tf5muxserver/mux_server_ConfigureProvider.go index 9b2b516..9490cee 100644 --- a/tf5muxserver/mux_server_ConfigureProvider.go +++ b/tf5muxserver/mux_server_ConfigureProvider.go @@ -15,7 +15,7 @@ import ( // time, passing `req`. Any Diagnostic with severity error will abort the // process and return immediately; non-Error severity Diagnostics will be // combined and returned. -func (s muxServer) ConfigureProvider(ctx context.Context, req *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { +func (s *muxServer) ConfigureProvider(ctx context.Context, req *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { rpc := "ConfigureProvider" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) diff --git a/tf5muxserver/mux_server_ConfigureProvider_test.go b/tf5muxserver/mux_server_ConfigureProvider_test.go index 423df04..acecad3 100644 --- a/tf5muxserver/mux_server_ConfigureProvider_test.go +++ b/tf5muxserver/mux_server_ConfigureProvider_test.go @@ -70,13 +70,6 @@ func TestMuxServerConfigureProvider(t *testing.T) { t.Fatalf("error setting up muxer: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - resp, err := muxServer.ProviderServer().ConfigureProvider(ctx, &tfprotov5.ConfigureProviderRequest{}) if err != nil { 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..f5d036b 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 { @@ -56,7 +50,6 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetPro "Provider schema difference: " + schemaDiff(serverResp.Provider, resp.Provider), }) } else { - s.providerSchema = serverResp.Provider resp.Provider = serverResp.Provider } } 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..969c638 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" @@ -14,14 +13,21 @@ import ( // ImportResourceState calls the ImportResourceState method, passing `req`, on // the provider that returned the resource specified by req.TypeName in its // schema. -func (s muxServer) ImportResourceState(ctx context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) { +func (s *muxServer) ImportResourceState(ctx context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.ImportResourceStateResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ImportResourceState_test.go b/tf5muxserver/mux_server_ImportResourceState_test.go index a540912..9dd2b9c 100644 --- a/tf5muxserver/mux_server_ImportResourceState_test.go +++ b/tf5muxserver/mux_server_ImportResourceState_test.go @@ -39,13 +39,6 @@ func TestMuxServerImportResourceState(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ImportResourceState(ctx, &tfprotov5.ImportResourceStateRequest{ TypeName: "test_resource_server1", }) diff --git a/tf5muxserver/mux_server_PlanResourceChange.go b/tf5muxserver/mux_server_PlanResourceChange.go index 85245a4..6454328 100644 --- a/tf5muxserver/mux_server_PlanResourceChange.go +++ b/tf5muxserver/mux_server_PlanResourceChange.go @@ -14,14 +14,21 @@ import ( // PlanResourceChange calls the PlanResourceChange method, passing `req`, on // the provider that returned the resource specified by req.TypeName in its // schema. -func (s muxServer) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { +func (s *muxServer) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.PlanResourceChangeResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_PlanResourceChange_test.go b/tf5muxserver/mux_server_PlanResourceChange_test.go index b6990f6..b64d10a 100644 --- a/tf5muxserver/mux_server_PlanResourceChange_test.go +++ b/tf5muxserver/mux_server_PlanResourceChange_test.go @@ -186,13 +186,6 @@ func TestMuxServerPlanResourceChange_ServerCapabilities_PlanDestroy(t *testing.T t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().PlanResourceChange(ctx, &tfprotov5.PlanResourceChangeRequest{ ProposedNewState: testProposedNewState, TypeName: "test_resource_server1", diff --git a/tf5muxserver/mux_server_PrepareProviderConfig.go b/tf5muxserver/mux_server_PrepareProviderConfig.go index b8c5c6f..d0f6014 100644 --- a/tf5muxserver/mux_server_PrepareProviderConfig.go +++ b/tf5muxserver/mux_server_PrepareProviderConfig.go @@ -9,18 +9,20 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" - "github.com/hashicorp/terraform-plugin-mux/internal/tf5dynamicvalue" ) // PrepareProviderConfig calls the PrepareProviderConfig method on each server // in order, passing `req`. Response diagnostics are appended from all servers. // Response PreparedConfig must be equal across all servers with nil values // skipped. -func (s muxServer) PrepareProviderConfig(ctx context.Context, req *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) { +func (s *muxServer) PrepareProviderConfig(ctx context.Context, req *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) { rpc := "PrepareProviderConfig" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - var resp *tfprotov5.PrepareProviderConfigResponse + + resp := &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: req.Config, // ignored by Terraform anyways + } for _, server := range s.servers { ctx = logging.Tfprotov5ProviderServerContext(ctx, server) @@ -36,33 +38,7 @@ func (s muxServer) PrepareProviderConfig(ctx context.Context, req *tfprotov5.Pre continue } - if resp == nil { - resp = res - continue - } - - if len(res.Diagnostics) > 0 { - // This could implement Diagnostic deduplication if/when - // implemented upstream. - resp.Diagnostics = append(resp.Diagnostics, res.Diagnostics...) - } - - // Do not check equality on missing PreparedConfig or unset PreparedConfig - if res.PreparedConfig == nil { - continue - } - - equal, err := tf5dynamicvalue.Equals(s.providerSchema.ValueType(), res.PreparedConfig, resp.PreparedConfig) - - if err != nil { - return nil, fmt.Errorf("unable to compare PrepareProviderConfig PreparedConfig responses: %w", err) - } - - if !equal { - return nil, fmt.Errorf("got different PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use") - } - - resp.PreparedConfig = res.PreparedConfig + resp.Diagnostics = append(resp.Diagnostics, res.Diagnostics...) } return resp, nil diff --git a/tf5muxserver/mux_server_PrepareProviderConfig_test.go b/tf5muxserver/mux_server_PrepareProviderConfig_test.go index e4936c1..161c0fd 100644 --- a/tf5muxserver/mux_server_PrepareProviderConfig_test.go +++ b/tf5muxserver/mux_server_PrepareProviderConfig_test.go @@ -5,7 +5,6 @@ package tf5muxserver_test import ( "context" - "fmt" "strings" "testing" @@ -92,6 +91,7 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { Detail: "test error details", }, }, + PreparedConfig: &config, }, }, "error-multiple": { @@ -133,6 +133,7 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { Detail: "test error details", }, }, + PreparedConfig: &config, }, }, "warning-once": { @@ -159,6 +160,7 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { Detail: "test warning details", }, }, + PreparedConfig: &config, }, }, "warning-multiple": { @@ -200,6 +202,7 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { Detail: "test warning details", }, }, + PreparedConfig: &config, }, }, "warning-then-error": { @@ -242,6 +245,7 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { Detail: "test error details", }, }, + PreparedConfig: &config, }, }, "no-response": { @@ -250,6 +254,9 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { {}, {}, }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, }, "PreparedConfig-once": { testServers: [3]*tf5testserver.TestServer{ @@ -356,11 +363,13 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { Provider: &configSchema, }, PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ - PreparedConfig: &config2, + PreparedConfig: &config2, // intentionally ignored }, }, }, - expectedError: fmt.Errorf("got different PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use"), + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, }, "PreparedConfig-multiple-equal": { testServers: [3]*tf5testserver.TestServer{ @@ -407,13 +416,6 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { t.Fatalf("error setting up muxer: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - got, err := muxServer.ProviderServer().PrepareProviderConfig(ctx, &tfprotov5.PrepareProviderConfigRequest{ Config: &config, }) diff --git a/tf5muxserver/mux_server_ReadDataSource.go b/tf5muxserver/mux_server_ReadDataSource.go index 72456ac..cacdae5 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" @@ -14,14 +13,21 @@ import ( // ReadDataSource calls the ReadDataSource method, passing `req`, on the // provider that returned the data source specified by req.TypeName in its // schema. -func (s muxServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) { +func (s *muxServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) { 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, diags, err := s.getDataSourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.ReadDataSourceResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ReadDataSource_test.go b/tf5muxserver/mux_server_ReadDataSource_test.go index 7ee529e..66e4435 100644 --- a/tf5muxserver/mux_server_ReadDataSource_test.go +++ b/tf5muxserver/mux_server_ReadDataSource_test.go @@ -39,13 +39,6 @@ func TestMuxServerReadDataSource(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ReadDataSource(ctx, &tfprotov5.ReadDataSourceRequest{ TypeName: "test_data_source_server1", }) diff --git a/tf5muxserver/mux_server_ReadResource.go b/tf5muxserver/mux_server_ReadResource.go index 716531b..b1c3877 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" @@ -13,14 +12,21 @@ import ( // ReadResource calls the ReadResource method, passing `req`, on the provider // that returned the resource specified by req.TypeName in its schema. -func (s muxServer) ReadResource(ctx context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { +func (s *muxServer) ReadResource(ctx context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.ReadResourceResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ReadResource_test.go b/tf5muxserver/mux_server_ReadResource_test.go index 1818992..026be39 100644 --- a/tf5muxserver/mux_server_ReadResource_test.go +++ b/tf5muxserver/mux_server_ReadResource_test.go @@ -39,13 +39,6 @@ func TestMuxServerReadResource(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ReadResource(ctx, &tfprotov5.ReadResourceRequest{ TypeName: "test_resource_server1", }) diff --git a/tf5muxserver/mux_server_StopProvider.go b/tf5muxserver/mux_server_StopProvider.go index e62dd28..9d165bf 100644 --- a/tf5muxserver/mux_server_StopProvider.go +++ b/tf5muxserver/mux_server_StopProvider.go @@ -16,7 +16,7 @@ import ( // with the muxServer, one at a time. All Error fields will be joined // together and returned, but will not prevent the rest of the providers' // StopProvider methods from being called. -func (s muxServer) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { +func (s *muxServer) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { rpc := "StopProvider" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) diff --git a/tf5muxserver/mux_server_StopProvider_test.go b/tf5muxserver/mux_server_StopProvider_test.go index 7773796..9b4f4f4 100644 --- a/tf5muxserver/mux_server_StopProvider_test.go +++ b/tf5muxserver/mux_server_StopProvider_test.go @@ -48,13 +48,6 @@ func TestMuxServerStopProvider(t *testing.T) { t.Fatalf("error setting up muxer: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - resp, err := muxServer.ProviderServer().StopProvider(ctx, &tfprotov5.StopProviderRequest{}) if err != nil { diff --git a/tf5muxserver/mux_server_UpgradeResourceState.go b/tf5muxserver/mux_server_UpgradeResourceState.go index 52c6722..5c40989 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" @@ -14,14 +13,21 @@ import ( // UpgradeResourceState calls the UpgradeResourceState method, passing `req`, // on the provider that returned the resource specified by req.TypeName in its // schema. -func (s muxServer) UpgradeResourceState(ctx context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { +func (s *muxServer) UpgradeResourceState(ctx context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.UpgradeResourceStateResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_UpgradeResourceState_test.go b/tf5muxserver/mux_server_UpgradeResourceState_test.go index b8c9318..7623774 100644 --- a/tf5muxserver/mux_server_UpgradeResourceState_test.go +++ b/tf5muxserver/mux_server_UpgradeResourceState_test.go @@ -39,13 +39,6 @@ func TestMuxServerUpgradeResourceState(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().UpgradeResourceState(ctx, &tfprotov5.UpgradeResourceStateRequest{ TypeName: "test_resource_server1", }) diff --git a/tf5muxserver/mux_server_ValidateDataSourceConfig.go b/tf5muxserver/mux_server_ValidateDataSourceConfig.go index 31ded35..cf8d2e0 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" @@ -14,14 +13,21 @@ import ( // ValidateDataSourceConfig calls the ValidateDataSourceConfig method, passing // `req`, on the provider that returned the data source specified by // req.TypeName in its schema. -func (s muxServer) ValidateDataSourceConfig(ctx context.Context, req *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) { +func (s *muxServer) ValidateDataSourceConfig(ctx context.Context, req *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) { 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, diags, err := s.getDataSourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.ValidateDataSourceConfigResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ValidateDataSourceConfig_test.go b/tf5muxserver/mux_server_ValidateDataSourceConfig_test.go index 8de0551..727c8ee 100644 --- a/tf5muxserver/mux_server_ValidateDataSourceConfig_test.go +++ b/tf5muxserver/mux_server_ValidateDataSourceConfig_test.go @@ -39,13 +39,6 @@ func TestMuxServerValidateDataSourceConfig(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ValidateDataSourceConfig(ctx, &tfprotov5.ValidateDataSourceConfigRequest{ TypeName: "test_data_source_server1", }) diff --git a/tf5muxserver/mux_server_ValidateResourceTypeConfig.go b/tf5muxserver/mux_server_ValidateResourceTypeConfig.go index b8348a1..fcb2630 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" @@ -14,14 +13,21 @@ import ( // ValidateResourceTypeConfig calls the ValidateResourceTypeConfig method, // passing `req`, on the provider that returned the resource specified by // req.TypeName in its schema. -func (s muxServer) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { +func (s *muxServer) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov5.ValidateResourceTypeConfigResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov5ProviderServerContext(ctx, server) diff --git a/tf5muxserver/mux_server_ValidateResourceTypeConfig_test.go b/tf5muxserver/mux_server_ValidateResourceTypeConfig_test.go index 6ca53e0..b70c502 100644 --- a/tf5muxserver/mux_server_ValidateResourceTypeConfig_test.go +++ b/tf5muxserver/mux_server_ValidateResourceTypeConfig_test.go @@ -38,13 +38,6 @@ func TestMuxServerValidateResourceTypeConfig(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ValidateResourceTypeConfig(ctx, &tfprotov5.ValidateResourceTypeConfigRequest{ TypeName: "test_resource_server1", }) diff --git a/tf5muxserver/server_capabilities.go b/tf5muxserver/server_capabilities.go index 042679f..3385d46 100644 --- a/tf5muxserver/server_capabilities.go +++ b/tf5muxserver/server_capabilities.go @@ -5,6 +5,14 @@ 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, +} + // 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/diagnostics.go b/tf6muxserver/diagnostics.go new file mode 100644 index 0000000..b0107a8 --- /dev/null +++ b/tf6muxserver/diagnostics.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tf6muxserver + +import "github.com/hashicorp/terraform-plugin-go/tfprotov6" + +func dataSourceDuplicateError(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 data source type across underlying providers. " + + "Data source 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 data source type: " + typeName, + } +} + +func dataSourceMissingError(typeName string) *tfprotov6.Diagnostic { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Data Source Not Implemented", + Detail: "The combined provider does not implement the requested data source type. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Missing data source type: " + typeName, + } +} + +func diagnosticsHasError(diagnostics []*tfprotov6.Diagnostic) bool { + for _, diagnostic := range diagnostics { + if diagnostic == nil { + continue + } + + if diagnostic.Severity == tfprotov6.DiagnosticSeverityError { + return true + } + } + + return false +} + +func resourceDuplicateError(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 resource type across underlying providers. " + + "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 resource type: " + typeName, + } +} + +func resourceMissingError(typeName string) *tfprotov6.Diagnostic { + return &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Resource Not Implemented", + Detail: "The combined provider does not implement the requested resource type. " + + "This is always an issue in the provider implementation and should be reported to the provider developers.\n\n" + + "Missing resource type: " + typeName, + } +} diff --git a/tf6muxserver/mux_server.go b/tf6muxserver/mux_server.go index 264318b..b8692b5 100644 --- a/tf6muxserver/mux_server.go +++ b/tf6muxserver/mux_server.go @@ -5,8 +5,12 @@ package tf6muxserver import ( "context" + "sync" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var _ tfprotov6.ProviderServer = &muxServer{} @@ -18,23 +22,203 @@ type muxServer struct { // Routing for data source types dataSources map[string]tfprotov6.ProviderServer - // Provider schema is cached during GetProviderSchema for - // ValidateProviderConfig equality checking. - providerSchema *tfprotov6.Schema - // Routing for resource types resources map[string]tfprotov6.ProviderServer - // Resource capabilities are cached during GetProviderSchema + // Resource capabilities are cached during GetMetadata/GetProviderSchema resourceCapabilities map[string]*tfprotov6.ServerCapabilities + // serverDiscoveryComplete is whether the mux server's underlying server + // discovery of resource types has been completed against all servers. + // If false during a resource type specific RPC, the mux server needs to + // pre-emptively call the GetMetadata RPC or GetProviderSchema RPC (as a + // fallback) so it knows which underlying server should receive the RPC. + serverDiscoveryComplete bool + + // serverDiscoveryMutex is a mutex to protect concurrent server discovery + // access from race conditions. + serverDiscoveryMutex sync.RWMutex + // Underlying servers for requests that should be handled by all servers servers []tfprotov6.ProviderServer } // ProviderServer is a function compatible with tf6server.Serve. -func (s muxServer) ProviderServer() tfprotov6.ProviderServer { - return &s +func (s *muxServer) ProviderServer() tfprotov6.ProviderServer { + return s +} + +func (s *muxServer) getDataSourceServer(ctx context.Context, typeName string) (tfprotov6.ProviderServer, []*tfprotov6.Diagnostic, error) { + s.serverDiscoveryMutex.RLock() + server, ok := s.dataSources[typeName] + discoveryComplete := s.serverDiscoveryComplete + s.serverDiscoveryMutex.RUnlock() + + if ok { + return server, nil, nil + } + + if discoveryComplete { + return nil, []*tfprotov6.Diagnostic{ + dataSourceMissingError(typeName), + }, nil + } + + diags, err := s.serverDiscovery(ctx) + + if err != nil || diagnosticsHasError(diags) { + return nil, diags, err + } + + s.serverDiscoveryMutex.RLock() + server, ok = s.dataSources[typeName] + s.serverDiscoveryMutex.RUnlock() + + if !ok { + // Preserve any warning diagnostics + diags = append(diags, dataSourceMissingError(typeName)) + + return nil, diags, nil + } + + return server, diags, nil +} + +func (s *muxServer) getResourceServer(ctx context.Context, typeName string) (tfprotov6.ProviderServer, []*tfprotov6.Diagnostic, error) { + s.serverDiscoveryMutex.RLock() + server, ok := s.resources[typeName] + discoveryComplete := s.serverDiscoveryComplete + s.serverDiscoveryMutex.RUnlock() + + if ok { + return server, nil, nil + } + + if discoveryComplete { + return nil, []*tfprotov6.Diagnostic{ + resourceMissingError(typeName), + }, nil + } + + diags, err := s.serverDiscovery(ctx) + + if err != nil || diagnosticsHasError(diags) { + return nil, diags, err + } + + s.serverDiscoveryMutex.RLock() + server, ok = s.resources[typeName] + s.serverDiscoveryMutex.RUnlock() + + if !ok { + diags = append(diags, resourceMissingError(typeName)) + + return nil, diags, nil + } + + return server, diags, nil +} + +// serverDiscovery will populate the mux server "routing" for resource types by +// calling all underlying server GetMetadata RPC and falling back to +// GetProviderSchema RPC. It is intended to only be called through +// getDataSourceServer and getResourceServer. +// +// The error return represents gRPC errors, which except for the GetMetadata +// call returning the gRPC unimplemented error, is always returned. +func (s *muxServer) serverDiscovery(ctx context.Context) ([]*tfprotov6.Diagnostic, error) { + s.serverDiscoveryMutex.Lock() + defer s.serverDiscoveryMutex.Unlock() + + // Return early if subsequent concurrent operations reached this logic. + if s.serverDiscoveryComplete { + return nil, nil + } + + logging.MuxTrace(ctx, "starting underlying server discovery via GetMetadata or GetProviderSchema") + + var diags []*tfprotov6.Diagnostic + + for _, server := range s.servers { + ctx := logging.Tfprotov6ProviderServerContext(ctx, server) + ctx = logging.RpcContext(ctx, "GetMetadata") + + logging.MuxTrace(ctx, "calling GetMetadata for discovery") + metadataResp, err := server.GetMetadata(ctx, &tfprotov6.GetMetadataRequest{}) + + // GetMetadata call was successful, populate caches and move on to next + // underlying server. + if err == nil && metadataResp != nil { + // Collect all underlying server diagnostics, but skip early return. + diags = append(diags, metadataResp.Diagnostics...) + + for _, serverDataSource := range metadataResp.DataSources { + if _, ok := s.dataSources[serverDataSource.TypeName]; ok { + diags = append(diags, dataSourceDuplicateError(serverDataSource.TypeName)) + + continue + } + + s.dataSources[serverDataSource.TypeName] = server + } + + for _, serverResource := range metadataResp.Resources { + if _, ok := s.resources[serverResource.TypeName]; ok { + diags = append(diags, resourceDuplicateError(serverResource.TypeName)) + + continue + } + + s.resources[serverResource.TypeName] = server + s.resourceCapabilities[serverResource.TypeName] = metadataResp.ServerCapabilities + } + + continue + } + + // Only continue if the gRPC error was an unimplemented code, otherwise + // return any other gRPC error immediately. + grpcStatus, ok := status.FromError(err) + + if !ok || grpcStatus.Code() != codes.Unimplemented { + return diags, err + } + + logging.MuxTrace(ctx, "calling GetProviderSchema for discovery") + providerSchemaResp, err := server.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) + + if err != nil { + return diags, err + } + + // Collect all underlying server diagnostics, but skip early return. + diags = append(diags, providerSchemaResp.Diagnostics...) + + for typeName := range providerSchemaResp.DataSourceSchemas { + if _, ok := s.dataSources[typeName]; ok { + diags = append(diags, dataSourceDuplicateError(typeName)) + + continue + } + + s.dataSources[typeName] = server + } + + for typeName := range providerSchemaResp.ResourceSchemas { + if _, ok := s.resources[typeName]; ok { + diags = append(diags, resourceDuplicateError(typeName)) + + continue + } + + s.resources[typeName] = server + s.resourceCapabilities[typeName] = providerSchemaResp.ServerCapabilities + } + } + + s.serverDiscoveryComplete = true + + return diags, nil } // NewMuxServer returns a muxed server that will route gRPC requests between diff --git a/tf6muxserver/mux_server_ApplyResourceChange.go b/tf6muxserver/mux_server_ApplyResourceChange.go index baefe35..e59d919 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" @@ -14,14 +13,21 @@ import ( // ApplyResourceChange calls the ApplyResourceChange method, passing `req`, on // the provider that returned the resource specified by req.TypeName in its // schema. -func (s muxServer) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { +func (s *muxServer) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.ApplyResourceChangeResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ApplyResourceChange_test.go b/tf6muxserver/mux_server_ApplyResourceChange_test.go index eb37462..b6d06bb 100644 --- a/tf6muxserver/mux_server_ApplyResourceChange_test.go +++ b/tf6muxserver/mux_server_ApplyResourceChange_test.go @@ -39,13 +39,6 @@ func TestMuxServerApplyResourceChange(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ApplyResourceChange(ctx, &tfprotov6.ApplyResourceChangeRequest{ TypeName: "test_resource_server1", }) diff --git a/tf6muxserver/mux_server_ConfigureProvider.go b/tf6muxserver/mux_server_ConfigureProvider.go index 8c9fc64..a341848 100644 --- a/tf6muxserver/mux_server_ConfigureProvider.go +++ b/tf6muxserver/mux_server_ConfigureProvider.go @@ -15,7 +15,7 @@ import ( // time, passing `req`. Any Diagnostic with severity error will abort the // process and return immediately; non-Error severity Diagnostics will be // combined and returned. -func (s muxServer) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { +func (s *muxServer) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { rpc := "ConfigureProvider" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) diff --git a/tf6muxserver/mux_server_ConfigureProvider_test.go b/tf6muxserver/mux_server_ConfigureProvider_test.go index 1e6c42e..fe88b91 100644 --- a/tf6muxserver/mux_server_ConfigureProvider_test.go +++ b/tf6muxserver/mux_server_ConfigureProvider_test.go @@ -70,13 +70,6 @@ func TestMuxServerConfigureProvider(t *testing.T) { t.Fatalf("error setting up muxer: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - resp, err := muxServer.ProviderServer().ConfigureProvider(ctx, &tfprotov6.ConfigureProviderRequest{}) if err != nil { diff --git a/tf6muxserver/mux_server_GetMetadata.go b/tf6muxserver/mux_server_GetMetadata.go new file mode 100644 index 0000000..14727b1 --- /dev/null +++ b/tf6muxserver/mux_server_GetMetadata.go @@ -0,0 +1,90 @@ +// 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) + + s.serverDiscoveryMutex.Lock() + defer s.serverDiscoveryMutex.Unlock() + + 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 { + if datasourceMetadataContainsTypeName(resp.DataSources, datasource.TypeName) { + resp.Diagnostics = append(resp.Diagnostics, dataSourceDuplicateError(datasource.TypeName)) + + continue + } + + s.dataSources[datasource.TypeName] = server + resp.DataSources = append(resp.DataSources, datasource) + } + + for _, resource := range serverResp.Resources { + if resourceMetadataContainsTypeName(resp.Resources, resource.TypeName) { + resp.Diagnostics = append(resp.Diagnostics, resourceDuplicateError(resource.TypeName)) + + continue + } + + s.resources[resource.TypeName] = server + s.resourceCapabilities[resource.TypeName] = serverResp.ServerCapabilities + resp.Resources = append(resp.Resources, resource) + } + } + + return resp, nil +} + +func datasourceMetadataContainsTypeName(metadatas []tfprotov6.DataSourceMetadata, typeName string) bool { + for _, metadata := range metadatas { + if typeName == metadata.TypeName { + return true + } + } + + return false +} + +func resourceMetadataContainsTypeName(metadatas []tfprotov6.ResourceMetadata, typeName string) bool { + for _, metadata := range metadatas { + if typeName == metadata.TypeName { + return true + } + } + + return false +} diff --git a/tf6muxserver/mux_server_GetMetadata_test.go b/tf6muxserver/mux_server_GetMetadata_test.go new file mode 100644 index 0000000..eb9dd14 --- /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 underlying providers. " + + "Data source 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 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 underlying providers. " + + "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 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..7b95c90 100644 --- a/tf6muxserver/mux_server_GetProviderSchema.go +++ b/tf6muxserver/mux_server_GetProviderSchema.go @@ -21,16 +21,13 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetPro ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) + s.serverDiscoveryMutex.Lock() + defer s.serverDiscoveryMutex.Unlock() + 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 { @@ -56,7 +53,6 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetPro "Provider schema difference: " + schemaDiff(serverResp.Provider, resp.Provider), }) } else { - s.providerSchema = serverResp.Provider resp.Provider = serverResp.Provider } } @@ -78,38 +74,29 @@ func (s *muxServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetPro for resourceType, schema := range serverResp.ResourceSchemas { if _, ok := resp.ResourceSchemas[resourceType]; ok { - 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: " + resourceType, - }) - } else { - s.resources[resourceType] = server - resp.ResourceSchemas[resourceType] = schema + resp.Diagnostics = append(resp.Diagnostics, resourceDuplicateError(resourceType)) + + continue } + s.resources[resourceType] = server s.resourceCapabilities[resourceType] = serverResp.ServerCapabilities + resp.ResourceSchemas[resourceType] = schema } for dataSourceType, schema := range serverResp.DataSourceSchemas { if _, ok := resp.DataSourceSchemas[dataSourceType]; ok { - 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: " + dataSourceType, - }) - } else { - s.dataSources[dataSourceType] = server - resp.DataSourceSchemas[dataSourceType] = schema + resp.Diagnostics = append(resp.Diagnostics, dataSourceDuplicateError(dataSourceType)) + + continue } + + s.dataSources[dataSourceType] = server + resp.DataSourceSchemas[dataSourceType] = schema } } + s.serverDiscoveryComplete = true + return resp, nil } diff --git a/tf6muxserver/mux_server_GetProviderSchema_test.go b/tf6muxserver/mux_server_GetProviderSchema_test.go index 5a8eb92..89c430a 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": { @@ -453,15 +454,16 @@ func TestMuxServerGetProviderSchema(t *testing.T) { { 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. " + + Detail: "The combined provider has multiple implementations of the same data source type across underlying providers. " + + "Data source 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 data source type: test_foo", }, }, expectedResourceSchemas: map[string]*tfprotov6.Schema{}, expectedServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, "duplicate-resource-type": { @@ -486,8 +488,8 @@ func TestMuxServerGetProviderSchema(t *testing.T) { { 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. " + + Detail: "The combined provider has multiple implementations of the same resource type across underlying providers. " + + "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 resource type: test_foo", }, @@ -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..e0da44e 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" @@ -14,14 +13,21 @@ import ( // ImportResourceState calls the ImportResourceState method, passing `req`, on // the provider that returned the resource specified by req.TypeName in its // schema. -func (s muxServer) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { +func (s *muxServer) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.ImportResourceStateResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ImportResourceState_test.go b/tf6muxserver/mux_server_ImportResourceState_test.go index dfdfeef..4b2f4d3 100644 --- a/tf6muxserver/mux_server_ImportResourceState_test.go +++ b/tf6muxserver/mux_server_ImportResourceState_test.go @@ -39,13 +39,6 @@ func TestMuxServerImportResourceState(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ImportResourceState(ctx, &tfprotov6.ImportResourceStateRequest{ TypeName: "test_resource_server1", }) diff --git a/tf6muxserver/mux_server_PlanResourceChange.go b/tf6muxserver/mux_server_PlanResourceChange.go index 9d5cfca..13a280b 100644 --- a/tf6muxserver/mux_server_PlanResourceChange.go +++ b/tf6muxserver/mux_server_PlanResourceChange.go @@ -14,14 +14,21 @@ import ( // PlanResourceChange calls the PlanResourceChange method, passing `req`, on // the provider that returned the resource specified by req.TypeName in its // schema. -func (s muxServer) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { +func (s *muxServer) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.PlanResourceChangeResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_PlanResourceChange_test.go b/tf6muxserver/mux_server_PlanResourceChange_test.go index e99cc35..b6c6908 100644 --- a/tf6muxserver/mux_server_PlanResourceChange_test.go +++ b/tf6muxserver/mux_server_PlanResourceChange_test.go @@ -185,13 +185,6 @@ func TestMuxServerPlanResourceChange_ServerCapabilities_PlanDestroy(t *testing.T t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().PlanResourceChange(ctx, &tfprotov6.PlanResourceChangeRequest{ ProposedNewState: testProposedNewState, TypeName: "test_resource_server1", diff --git a/tf6muxserver/mux_server_ReadDataSource.go b/tf6muxserver/mux_server_ReadDataSource.go index bc11ea1..13f7990 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" @@ -14,14 +13,21 @@ import ( // ReadDataSource calls the ReadDataSource method, passing `req`, on the // provider that returned the data source specified by req.TypeName in its // schema. -func (s muxServer) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { +func (s *muxServer) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { 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, diags, err := s.getDataSourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.ReadDataSourceResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ReadDataSource_test.go b/tf6muxserver/mux_server_ReadDataSource_test.go index ee7b19c..b11fcf7 100644 --- a/tf6muxserver/mux_server_ReadDataSource_test.go +++ b/tf6muxserver/mux_server_ReadDataSource_test.go @@ -39,13 +39,6 @@ func TestMuxServerReadDataSource(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ReadDataSource(ctx, &tfprotov6.ReadDataSourceRequest{ TypeName: "test_data_source_server1", }) diff --git a/tf6muxserver/mux_server_ReadResource.go b/tf6muxserver/mux_server_ReadResource.go index 47ad8f0..1150920 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" @@ -13,14 +12,21 @@ import ( // ReadResource calls the ReadResource method, passing `req`, on the provider // that returned the resource specified by req.TypeName in its schema. -func (s muxServer) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { +func (s *muxServer) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.ReadResourceResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ReadResource_test.go b/tf6muxserver/mux_server_ReadResource_test.go index 31ac8f7..81db5e7 100644 --- a/tf6muxserver/mux_server_ReadResource_test.go +++ b/tf6muxserver/mux_server_ReadResource_test.go @@ -39,13 +39,6 @@ func TestMuxServerReadResource(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ReadResource(ctx, &tfprotov6.ReadResourceRequest{ TypeName: "test_resource_server1", }) diff --git a/tf6muxserver/mux_server_StopProvider.go b/tf6muxserver/mux_server_StopProvider.go index cd6a34d..1d1243e 100644 --- a/tf6muxserver/mux_server_StopProvider.go +++ b/tf6muxserver/mux_server_StopProvider.go @@ -16,7 +16,7 @@ import ( // with the muxServer, one at a time. All Error fields will be joined // together and returned, but will not prevent the rest of the providers' // StopProvider methods from being called. -func (s muxServer) StopProvider(ctx context.Context, req *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { +func (s *muxServer) StopProvider(ctx context.Context, req *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { rpc := "StopProvider" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) diff --git a/tf6muxserver/mux_server_StopProvider_test.go b/tf6muxserver/mux_server_StopProvider_test.go index 53cf4ab..a742b8f 100644 --- a/tf6muxserver/mux_server_StopProvider_test.go +++ b/tf6muxserver/mux_server_StopProvider_test.go @@ -48,13 +48,6 @@ func TestMuxServerStopProvider(t *testing.T) { t.Fatalf("error setting up muxer: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - resp, err := muxServer.ProviderServer().StopProvider(ctx, &tfprotov6.StopProviderRequest{}) if err != nil { diff --git a/tf6muxserver/mux_server_UpgradeResourceState.go b/tf6muxserver/mux_server_UpgradeResourceState.go index 3004938..f31df94 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" @@ -14,14 +13,21 @@ import ( // UpgradeResourceState calls the UpgradeResourceState method, passing `req`, // on the provider that returned the resource specified by req.TypeName in its // schema. -func (s muxServer) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { +func (s *muxServer) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.UpgradeResourceStateResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_UpgradeResourceState_test.go b/tf6muxserver/mux_server_UpgradeResourceState_test.go index ff49fb7..abdd8e4 100644 --- a/tf6muxserver/mux_server_UpgradeResourceState_test.go +++ b/tf6muxserver/mux_server_UpgradeResourceState_test.go @@ -39,13 +39,6 @@ func TestMuxServerUpgradeResourceState(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().UpgradeResourceState(ctx, &tfprotov6.UpgradeResourceStateRequest{ TypeName: "test_resource_server1", }) diff --git a/tf6muxserver/mux_server_ValidateDataResourceConfig.go b/tf6muxserver/mux_server_ValidateDataResourceConfig.go index 06584e5..8c89408 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" @@ -14,14 +13,21 @@ import ( // ValidateDataResourceConfig calls the ValidateDataResourceConfig method, passing // `req`, on the provider that returned the data source specified by // req.TypeName in its schema. -func (s muxServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { +func (s *muxServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { 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, diags, err := s.getDataSourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.ValidateDataResourceConfigResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ValidateDataResourceConfig_test.go b/tf6muxserver/mux_server_ValidateDataResourceConfig_test.go index 3808bd2..3ca9702 100644 --- a/tf6muxserver/mux_server_ValidateDataResourceConfig_test.go +++ b/tf6muxserver/mux_server_ValidateDataResourceConfig_test.go @@ -39,13 +39,6 @@ func TestMuxServerValidateDataResourceConfig(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ValidateDataResourceConfig(ctx, &tfprotov6.ValidateDataResourceConfigRequest{ TypeName: "test_data_source_server1", }) diff --git a/tf6muxserver/mux_server_ValidateProviderConfig.go b/tf6muxserver/mux_server_ValidateProviderConfig.go index 7aa72ec..33c2a83 100644 --- a/tf6muxserver/mux_server_ValidateProviderConfig.go +++ b/tf6muxserver/mux_server_ValidateProviderConfig.go @@ -9,18 +9,20 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-mux/internal/logging" - "github.com/hashicorp/terraform-plugin-mux/internal/tf6dynamicvalue" ) // ValidateProviderConfig calls the ValidateProviderConfig method on each server // in order, passing `req`. Response diagnostics are appended from all servers. // Response PreparedConfig must be equal across all servers with nil values // skipped. -func (s muxServer) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { +func (s *muxServer) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { rpc := "ValidateProviderConfig" ctx = logging.InitContext(ctx) ctx = logging.RpcContext(ctx, rpc) - var resp *tfprotov6.ValidateProviderConfigResponse + + resp := &tfprotov6.ValidateProviderConfigResponse{ + PreparedConfig: req.Config, // ignored by Terraform anyways + } for _, server := range s.servers { ctx = logging.Tfprotov6ProviderServerContext(ctx, server) @@ -36,33 +38,7 @@ func (s muxServer) ValidateProviderConfig(ctx context.Context, req *tfprotov6.Va continue } - if resp == nil { - resp = res - continue - } - - if len(res.Diagnostics) > 0 { - // This could implement Diagnostic deduplication if/when - // implemented upstream. - resp.Diagnostics = append(resp.Diagnostics, res.Diagnostics...) - } - - // Do not check equality on missing PreparedConfig or unset PreparedConfig - if res.PreparedConfig == nil { - continue - } - - equal, err := tf6dynamicvalue.Equals(s.providerSchema.ValueType(), res.PreparedConfig, resp.PreparedConfig) - - if err != nil { - return nil, fmt.Errorf("unable to compare ValidateProviderConfig PreparedConfig responses: %w", err) - } - - if !equal { - return nil, fmt.Errorf("got different ValidateProviderConfig PreparedConfig response from multiple servers, not sure which to use") - } - - resp.PreparedConfig = res.PreparedConfig + resp.Diagnostics = append(resp.Diagnostics, res.Diagnostics...) } return resp, nil diff --git a/tf6muxserver/mux_server_ValidateProviderConfig_test.go b/tf6muxserver/mux_server_ValidateProviderConfig_test.go index 6c3b3b9..3f24fa0 100644 --- a/tf6muxserver/mux_server_ValidateProviderConfig_test.go +++ b/tf6muxserver/mux_server_ValidateProviderConfig_test.go @@ -5,7 +5,6 @@ package tf6muxserver_test import ( "context" - "fmt" "strings" "testing" @@ -92,6 +91,7 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { Detail: "test error details", }, }, + PreparedConfig: &config, }, }, "error-multiple": { @@ -133,6 +133,7 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { Detail: "test error details", }, }, + PreparedConfig: &config, }, }, "warning-once": { @@ -159,6 +160,7 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { Detail: "test warning details", }, }, + PreparedConfig: &config, }, }, "warning-multiple": { @@ -200,6 +202,7 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { Detail: "test warning details", }, }, + PreparedConfig: &config, }, }, "warning-then-error": { @@ -241,6 +244,7 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { Detail: "test error details", }, }, + PreparedConfig: &config, }, }, "no-response": { @@ -249,6 +253,9 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { {}, {}, }, + expectedResponse: &tfprotov6.ValidateProviderConfigResponse{ + PreparedConfig: &config, + }, }, "PreparedConfig-once": { testServers: [3]*tf6testserver.TestServer{ @@ -369,11 +376,13 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { Provider: &configSchema, }, ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ - PreparedConfig: &config2, + PreparedConfig: &config2, // intentionally ignored }, }, }, - expectedError: fmt.Errorf("got different ValidateProviderConfig PreparedConfig response from multiple servers, not sure which to use"), + expectedResponse: &tfprotov6.ValidateProviderConfigResponse{ + PreparedConfig: &config, + }, }, "PreparedConfig-multiple-equal": { testServers: [3]*tf6testserver.TestServer{ @@ -424,13 +433,6 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { t.Fatalf("error setting up muxer: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - got, err := muxServer.ProviderServer().ValidateProviderConfig(ctx, &tfprotov6.ValidateProviderConfigRequest{ Config: &config, }) diff --git a/tf6muxserver/mux_server_ValidateResourceConfig.go b/tf6muxserver/mux_server_ValidateResourceConfig.go index 7dd70cb..025a78d 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" @@ -14,14 +13,21 @@ import ( // ValidateResourceConfig calls the ValidateResourceConfig method, // passing `req`, on the provider that returned the resource specified by // req.TypeName in its schema. -func (s muxServer) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { +func (s *muxServer) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { 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, diags, err := s.getResourceServer(ctx, req.TypeName) + + if err != nil { + return nil, err + } + + if diagnosticsHasError(diags) { + return &tfprotov6.ValidateResourceConfigResponse{ + Diagnostics: diags, + }, nil } ctx = logging.Tfprotov6ProviderServerContext(ctx, server) diff --git a/tf6muxserver/mux_server_ValidateResourceConfig_test.go b/tf6muxserver/mux_server_ValidateResourceConfig_test.go index 93c0297..9e1add6 100644 --- a/tf6muxserver/mux_server_ValidateResourceConfig_test.go +++ b/tf6muxserver/mux_server_ValidateResourceConfig_test.go @@ -39,13 +39,6 @@ func TestMuxServerValidateResourceConfig(t *testing.T) { t.Fatalf("unexpected error setting up factory: %s", err) } - // Required to populate routers - _, err = muxServer.GetProviderSchema(ctx, &tfprotov6.GetProviderSchemaRequest{}) - - if err != nil { - t.Fatalf("unexpected error calling GetProviderSchema: %s", err) - } - _, err = muxServer.ProviderServer().ValidateResourceConfig(ctx, &tfprotov6.ValidateResourceConfigRequest{ TypeName: "test_resource_server1", }) diff --git a/tf6muxserver/server_capabilities.go b/tf6muxserver/server_capabilities.go index cac9470..7490e47 100644 --- a/tf6muxserver/server_capabilities.go +++ b/tf6muxserver/server_capabilities.go @@ -5,6 +5,14 @@ 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, +} + // 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()