From 2e18b674c88ba5002cca4a7292643e9948d0cbf4 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 15 Feb 2022 15:28:02 -0500 Subject: [PATCH 1/2] Introduce equality checking of PreparedConfig in PrepareProviderConfig and ValidateProviderConfig Reference: https://github.com/hashicorp/terraform-plugin-mux/issues/51 Previously in real world usage: ``` provider_test.go:11: Step 1/1 error: Error running pre-apply refresh: exit status 1 Error: Invalid provider configuration Provider "registry.terraform.io/hashicorp/tf5muxprovider" requires explicit configuration. Add a provider block to the root module and configure the provider's required arguments as described in the provider documentation. Error: Plugin error with provider["registry.terraform.io/hashicorp/tf5muxprovider"], on line 0: (source code not available) The plugin returned an unexpected error from plugin.(*GRPCProvider).ValidateProviderConfig: rpc error: code = Unknown desc = got a PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use provider_test.go:11: Step 1/1 error: Error running pre-apply refresh: exit status 1 Error: Plugin error The plugin returned an unexpected error from plugin6.(*GRPCProvider).ValidateProviderConfig: rpc error: code = Unknown desc = got a ValidateProviderConfig PreparedConfig response from multiple servers, not sure which to use Error: Invalid provider configuration Provider "registry.terraform.io/hashicorp/tf6muxprovider" requires explicit configuration. Add a provider block to the root module and configure the provider's required arguments as described in the provider documentation. ``` Updated unit testing before code changes: ``` --- FAIL: TestMuxServerPrepareProviderConfig (0.00s) --- FAIL: TestMuxServerPrepareProviderConfig/PreparedConfig-multiple-equal (0.00s) mux_server_PrepareProviderConfig_test.go:365: wanted no error, got error: got a PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use FAIL FAIL github.com/hashicorp/terraform-plugin-mux/tf5muxserver 1.244s --- FAIL: TestMuxServerValidateProviderConfig (0.00s) --- FAIL: TestMuxServerValidateProviderConfig/PreparedConfig-multiple-equal (0.00s) mux_server_ValidateProviderConfig_test.go:365: wanted no error, got error: got a ValidateProviderConfig PreparedConfig response from multiple servers, not sure which to use FAIL FAIL github.com/hashicorp/terraform-plugin-mux/tf6muxserver 0.665s ``` Verified those integration tests are now passing by replacing the mux dependency with this branch. Once those tests are published, we can introduce the integration tests into this project's testing workflow. --- .changelog/pending.txt | 7 + tf5muxserver/dynamic_value_equality.go | 38 + tf5muxserver/dynamic_value_equality_test.go | 283 ++++++ .../mux_server_PrepareProviderConfig.go | 27 +- .../mux_server_PrepareProviderConfig_test.go | 58 +- tf5muxserver/schema_type.go | 109 +++ tf5muxserver/schema_type_test.go | 610 +++++++++++++ tf6muxserver/dynamic_value_equality.go | 38 + tf6muxserver/dynamic_value_equality_test.go | 283 ++++++ .../mux_server_ValidateProviderConfig.go | 27 +- .../mux_server_ValidateProviderConfig_test.go | 72 +- tf6muxserver/schema_type.go | 162 ++++ tf6muxserver/schema_type_test.go | 858 ++++++++++++++++++ 13 files changed, 2545 insertions(+), 27 deletions(-) create mode 100644 .changelog/pending.txt create mode 100644 tf5muxserver/dynamic_value_equality.go create mode 100644 tf5muxserver/dynamic_value_equality_test.go create mode 100644 tf5muxserver/schema_type.go create mode 100644 tf5muxserver/schema_type_test.go create mode 100644 tf6muxserver/dynamic_value_equality.go create mode 100644 tf6muxserver/dynamic_value_equality_test.go create mode 100644 tf6muxserver/schema_type.go create mode 100644 tf6muxserver/schema_type_test.go diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 0000000..4e22b37 --- /dev/null +++ b/.changelog/pending.txt @@ -0,0 +1,7 @@ +```release-note:bug +tf5muxserver: Prevent `PrepareProviderConfig` RPC error for multiple `PreparedConfig` responses when combining terraform-plugin-sdk/v2 providers +``` + +```release-note:bug +tf6muxserver: Prevent `ValidateProviderConfig` RPC error for multiple `PreparedConfig` responses when combining terraform-plugin-framework providers +``` diff --git a/tf5muxserver/dynamic_value_equality.go b/tf5muxserver/dynamic_value_equality.go new file mode 100644 index 0000000..0fc9dd6 --- /dev/null +++ b/tf5muxserver/dynamic_value_equality.go @@ -0,0 +1,38 @@ +package tf5muxserver + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// dynamicValueEquals performs equality checking of DynamicValue. +func dynamicValueEquals(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/tf5muxserver/dynamic_value_equality_test.go b/tf5muxserver/dynamic_value_equality_test.go new file mode 100644 index 0000000..0e967fd --- /dev/null +++ b/tf5muxserver/dynamic_value_equality_test.go @@ -0,0 +1,283 @@ +package tf5muxserver + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestDynamicValueEquals(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schemaType tftypes.Type + dynamicValue1 func() (*tfprotov5.DynamicValue, error) + dynamicValue2 func() (*tfprotov5.DynamicValue, error) + expected bool + expectedError error + }{ + "all-missing": { + schemaType: nil, + dynamicValue1: func() (*tfprotov5.DynamicValue, error) { + return nil, nil + }, + dynamicValue2: func() (*tfprotov5.DynamicValue, error) { + return nil, nil + }, + expected: true, + }, + "first-missing": { + schemaType: nil, + dynamicValue1: func() (*tfprotov5.DynamicValue, error) { + return nil, nil + }, + dynamicValue2: func() (*tfprotov5.DynamicValue, error) { + return &tfprotov5.DynamicValue{}, nil + }, + expected: false, + }, + "second-missing": { + schemaType: nil, + dynamicValue1: func() (*tfprotov5.DynamicValue, error) { + return &tfprotov5.DynamicValue{}, nil + }, + dynamicValue2: func() (*tfprotov5.DynamicValue, error) { + return nil, nil + }, + expected: false, + }, + "missing-type": { + schemaType: nil, + dynamicValue1: func() (*tfprotov5.DynamicValue, error) { + dv, err := tfprotov5.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + dynamicValue2: func() (*tfprotov5.DynamicValue, error) { + dv, err := tfprotov5.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + 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: func() (*tfprotov5.DynamicValue, error) { + dv, err := tfprotov5.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + dynamicValue2: func() (*tfprotov5.DynamicValue, error) { + dv, err := tfprotov5.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + 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: func() (*tfprotov5.DynamicValue, error) { + dv, err := tfprotov5.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + dynamicValue2: func() (*tfprotov5.DynamicValue, error) { + dv, err := tfprotov5.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + expected: false, + }, + "String-equal-value": { + schemaType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + dynamicValue1: func() (*tfprotov5.DynamicValue, error) { + dv, err := tfprotov5.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + dynamicValue2: func() (*tfprotov5.DynamicValue, error) { + dv, err := tfprotov5.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + dynamicValue1, err := testCase.dynamicValue1() + + if err != nil { + t.Fatalf("unable to create first DynamicValue: %s", err) + } + + dynamicValue2, err := testCase.dynamicValue2() + + if err != nil { + t.Fatalf("unable to create second DynamicValue: %s", err) + } + + got, err := dynamicValueEquals(testCase.schemaType, dynamicValue1, 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/tf5muxserver/mux_server_PrepareProviderConfig.go b/tf5muxserver/mux_server_PrepareProviderConfig.go index 68e6db1..593ebe0 100644 --- a/tf5muxserver/mux_server_PrepareProviderConfig.go +++ b/tf5muxserver/mux_server_PrepareProviderConfig.go @@ -9,8 +9,9 @@ import ( ) // PrepareProviderConfig calls the PrepareProviderConfig method on each server -// in order, passing `req`. Only one may respond with a non-nil PreparedConfig -// or a non-empty Diagnostics. +// 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) { rpc := "PrepareProviderConfig" ctx = logging.InitContext(ctx) @@ -42,16 +43,22 @@ func (s muxServer) PrepareProviderConfig(ctx context.Context, req *tfprotov5.Pre resp.Diagnostics = append(resp.Diagnostics, res.Diagnostics...) } - if res.PreparedConfig != nil { - // This could check equality to bypass the error, however - // DynamicValue does not implement Equals() and previous mux server - // implementations have not requested the enhancement. - if resp.PreparedConfig != nil { - return nil, fmt.Errorf("got a PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use") - } + // Do not check equality on missing PreparedConfig or unset PreparedConfig + if res.PreparedConfig == nil { + continue + } - resp.PreparedConfig = res.PreparedConfig + equal, err := dynamicValueEquals(schemaType(s.providerSchema), 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 } return resp, nil diff --git a/tf5muxserver/mux_server_PrepareProviderConfig_test.go b/tf5muxserver/mux_server_PrepareProviderConfig_test.go index e21ba86..ffd5844 100644 --- a/tf5muxserver/mux_server_PrepareProviderConfig_test.go +++ b/tf5muxserver/mux_server_PrepareProviderConfig_test.go @@ -32,6 +32,33 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { t.Fatalf("error constructing config: %s", err) } + config2, err := tfprotov5.NewDynamicValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hello": tftypes.String, + }, + }, tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hello": tftypes.String, + }, + }, map[string]tftypes.Value{ + "hello": tftypes.NewValue(tftypes.String, "goodbye"), + })) + + if err != nil { + t.Fatalf("error constructing config: %s", err) + } + + configSchema := tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "hello", + Type: tftypes.String, + }, + }, + }, + } + testCases := map[string]struct { servers []func() tfprotov5.ProviderServer expectedError error @@ -225,6 +252,7 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ PreparedConfig: &config, }, + ProviderSchema: &configSchema, }).ProviderServer, (&tf5testserver.TestServer{}).ProviderServer, (&tf5testserver.TestServer{}).ProviderServer, @@ -239,6 +267,7 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ PreparedConfig: &config, }, + ProviderSchema: &configSchema, }).ProviderServer, (&tf5testserver.TestServer{}).ProviderServer, (&tf5testserver.TestServer{ @@ -250,7 +279,9 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { Detail: "test error details", }, }, + PreparedConfig: &config, }, + ProviderSchema: &configSchema, }).ProviderServer, }, expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ @@ -270,6 +301,7 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ PreparedConfig: &config, }, + ProviderSchema: &configSchema, }).ProviderServer, (&tf5testserver.TestServer{}).ProviderServer, (&tf5testserver.TestServer{ @@ -295,21 +327,43 @@ func TestMuxServerPrepareProviderConfig(t *testing.T) { PreparedConfig: &config, }, }, - "PreparedConfig-multiple": { + "PreparedConfig-multiple-different": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, + ProviderSchema: &configSchema, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config2, + }, + ProviderSchema: &configSchema, + }).ProviderServer, + }, + expectedError: fmt.Errorf("got different PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use"), + }, + "PreparedConfig-multiple-equal": { servers: []func() tfprotov5.ProviderServer{ (&tf5testserver.TestServer{ PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ PreparedConfig: &config, }, + ProviderSchema: &configSchema, }).ProviderServer, (&tf5testserver.TestServer{}).ProviderServer, (&tf5testserver.TestServer{ PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ PreparedConfig: &config, }, + ProviderSchema: &configSchema, }).ProviderServer, }, - expectedError: fmt.Errorf("got a PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use"), + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, }, } diff --git a/tf5muxserver/schema_type.go b/tf5muxserver/schema_type.go new file mode 100644 index 0000000..0d6452e --- /dev/null +++ b/tf5muxserver/schema_type.go @@ -0,0 +1,109 @@ +package tf5muxserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// schemaType returns the Type for a Schema. +// +// This function should be migrated to a (*tfprotov5.Schema).Type() method +// in terraform-plugin-go. +func schemaType(schema *tfprotov5.Schema) tftypes.Type { + if schema == nil { + return tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + } + } + + return schemaBlockType(schema.Block) +} + +// schemaAttributeType returns the Type for a SchemaAttribute. +// +// This function should be migrated to a (*tfprotov5.SchemaAttribute).Type() +// method in terraform-plugin-go. +func schemaAttributeType(attribute *tfprotov5.SchemaAttribute) tftypes.Type { + if attribute == nil { + return nil + } + + return attribute.Type +} + +// schemaBlockType returns the Type for a SchemaBlock. +// +// This function should be migrated to a (*tfprotov5.SchemaBlock).Type() +// method in terraform-plugin-go. +func schemaBlockType(block *tfprotov5.SchemaBlock) tftypes.Type { + if block == nil { + return tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + } + } + + attributeTypes := map[string]tftypes.Type{} + + for _, attribute := range block.Attributes { + if attribute == nil { + continue + } + + attributeType := schemaAttributeType(attribute) + + if attributeType == nil { + continue + } + + attributeTypes[attribute.Name] = attributeType + } + + for _, block := range block.BlockTypes { + if block == nil { + continue + } + + blockType := schemaNestedBlockType(block) + + if blockType == nil { + continue + } + + attributeTypes[block.TypeName] = blockType + } + + return tftypes.Object{ + AttributeTypes: attributeTypes, + } +} + +// schemaNestedBlockType returns the Type for a SchemaNestedBlock. +// +// This function should be migrated to a (*tfprotov5.SchemaNestedBlock).Type() +// method in terraform-plugin-go. +func schemaNestedBlockType(nestedBlock *tfprotov5.SchemaNestedBlock) tftypes.Type { + if nestedBlock == nil { + return nil + } + + switch nestedBlock.Nesting { + case tfprotov5.SchemaNestedBlockNestingModeGroup: + return schemaBlockType(nestedBlock.Block) + case tfprotov5.SchemaNestedBlockNestingModeList: + return tftypes.List{ + ElementType: schemaBlockType(nestedBlock.Block), + } + case tfprotov5.SchemaNestedBlockNestingModeMap: + return tftypes.Map{ + ElementType: schemaBlockType(nestedBlock.Block), + } + case tfprotov5.SchemaNestedBlockNestingModeSet: + return tftypes.Set{ + ElementType: schemaBlockType(nestedBlock.Block), + } + case tfprotov5.SchemaNestedBlockNestingModeSingle: + return schemaBlockType(nestedBlock.Block) + default: + return nil + } +} diff --git a/tf5muxserver/schema_type_test.go b/tf5muxserver/schema_type_test.go new file mode 100644 index 0000000..8b2c7eb --- /dev/null +++ b/tf5muxserver/schema_type_test.go @@ -0,0 +1,610 @@ +package tf5muxserver + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSchemaAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schemaAttribute *tfprotov5.SchemaAttribute + expected tftypes.Type + }{ + "nil": { + schemaAttribute: nil, + expected: nil, + }, + "Bool": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.Bool, + }, + expected: tftypes.Bool, + }, + "DynamicPseudoType": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.DynamicPseudoType, + }, + expected: tftypes.DynamicPseudoType, + }, + "List-String": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.String, + }, + }, + "Map-String": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + expected: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + "Number": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.Number, + }, + expected: tftypes.Number, + }, + "Object-String": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + "Set-String": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + expected: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + "String": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.String, + }, + expected: tftypes.String, + }, + "Tuple-String": { + schemaAttribute: &tfprotov5.SchemaAttribute{ + Type: tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String}, + }, + }, + expected: tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := schemaAttributeType(testCase.schemaAttribute) + + if testCase.expected == nil { + if got == nil { + return + } + + t.Fatalf("expected nil, got: %s", got) + } + + if !testCase.expected.Equal(got) { + t.Errorf("expected %s, got: %s", testCase.expected, got) + } + }) + } +} + +func TestSchemaBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schemaBlock *tfprotov5.SchemaBlock + expected tftypes.Type + }{ + "nil": { + schemaBlock: nil, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, + }, + "Attribute-String": { + schemaBlock: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + "Block-NestingList-String": { + schemaBlock: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := schemaBlockType(testCase.schemaBlock) + + if testCase.expected == nil { + if got == nil { + return + } + + t.Fatalf("expected nil, got: %s", got) + } + + if !testCase.expected.Equal(got) { + t.Errorf("expected %s, got: %s", testCase.expected, got) + } + }) + } +} + +func TestSchemaNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schemaNestedBlock *tfprotov5.SchemaNestedBlock + expected tftypes.Type + }{ + "nil": { + schemaNestedBlock: nil, + expected: nil, + }, + "NestingGroup-Attribute-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeGroup, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + "NestingGroup-Block-NestingList-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeGroup, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + "NestingInvalid": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Nesting: tfprotov5.SchemaNestedBlockNestingModeInvalid, + }, + expected: nil, + }, + "NestingList-missing-Block": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, + }, + }, + "NestingList-Attribute-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + "NestingList-Block-NestingList-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "NestingMap-Attribute-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeMap, + }, + expected: tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + "NestingMap-Block-NestingList-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeMap, + }, + expected: tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "NestingSet-Attribute-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSet, + }, + expected: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + "NestingSet-Block-NestingList-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSet, + }, + expected: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "NestingSingle-Attribute-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + "NestingSingle-Block-NestingList-String": { + schemaNestedBlock: &tfprotov5.SchemaNestedBlock{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeSingle, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := schemaNestedBlockType(testCase.schemaNestedBlock) + + if testCase.expected == nil { + if got == nil { + return + } + + t.Fatalf("expected nil, got: %s", got) + } + + if !testCase.expected.Equal(got) { + t.Errorf("expected %s, got: %s", testCase.expected, got) + } + }) + } +} + +func TestSchemaType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema *tfprotov5.Schema + expected tftypes.Type + }{ + "nil": { + schema: nil, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, + }, + "missing-Block": { + schema: &tfprotov5.Schema{}, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, + }, + "Attribute-String": { + schema: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + "Block-NestingList-String": { + schema: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + Block: &tfprotov5.SchemaBlock{ + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := schemaType(testCase.schema) + + if testCase.expected == nil { + if got == nil { + return + } + + t.Fatalf("expected nil, got: %s", got) + } + + if !testCase.expected.Equal(got) { + t.Errorf("expected %s, got: %s", testCase.expected, got) + } + }) + } +} diff --git a/tf6muxserver/dynamic_value_equality.go b/tf6muxserver/dynamic_value_equality.go new file mode 100644 index 0000000..7abd72b --- /dev/null +++ b/tf6muxserver/dynamic_value_equality.go @@ -0,0 +1,38 @@ +package tf6muxserver + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// dynamicValueEquals performs equality checking of DynamicValue. +func dynamicValueEquals(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/tf6muxserver/dynamic_value_equality_test.go b/tf6muxserver/dynamic_value_equality_test.go new file mode 100644 index 0000000..bd3dc5f --- /dev/null +++ b/tf6muxserver/dynamic_value_equality_test.go @@ -0,0 +1,283 @@ +package tf6muxserver + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestDynamicValueEquals(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schemaType tftypes.Type + dynamicValue1 func() (*tfprotov6.DynamicValue, error) + dynamicValue2 func() (*tfprotov6.DynamicValue, error) + expected bool + expectedError error + }{ + "all-missing": { + schemaType: nil, + dynamicValue1: func() (*tfprotov6.DynamicValue, error) { + return nil, nil + }, + dynamicValue2: func() (*tfprotov6.DynamicValue, error) { + return nil, nil + }, + expected: true, + }, + "first-missing": { + schemaType: nil, + dynamicValue1: func() (*tfprotov6.DynamicValue, error) { + return nil, nil + }, + dynamicValue2: func() (*tfprotov6.DynamicValue, error) { + return &tfprotov6.DynamicValue{}, nil + }, + expected: false, + }, + "second-missing": { + schemaType: nil, + dynamicValue1: func() (*tfprotov6.DynamicValue, error) { + return &tfprotov6.DynamicValue{}, nil + }, + dynamicValue2: func() (*tfprotov6.DynamicValue, error) { + return nil, nil + }, + expected: false, + }, + "missing-type": { + schemaType: nil, + dynamicValue1: func() (*tfprotov6.DynamicValue, error) { + dv, err := tfprotov6.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + dynamicValue2: func() (*tfprotov6.DynamicValue, error) { + dv, err := tfprotov6.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + 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: func() (*tfprotov6.DynamicValue, error) { + dv, err := tfprotov6.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + dynamicValue2: func() (*tfprotov6.DynamicValue, error) { + dv, err := tfprotov6.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + 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: func() (*tfprotov6.DynamicValue, error) { + dv, err := tfprotov6.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + dynamicValue2: func() (*tfprotov6.DynamicValue, error) { + dv, err := tfprotov6.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + expected: false, + }, + "String-equal-value": { + schemaType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + dynamicValue1: func() (*tfprotov6.DynamicValue, error) { + dv, err := tfprotov6.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + dynamicValue2: func() (*tfprotov6.DynamicValue, error) { + dv, err := tfprotov6.NewDynamicValue( + 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"), + }, + ), + ) + return &dv, err + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + dynamicValue1, err := testCase.dynamicValue1() + + if err != nil { + t.Fatalf("unable to create first DynamicValue: %s", err) + } + + dynamicValue2, err := testCase.dynamicValue2() + + if err != nil { + t.Fatalf("unable to create second DynamicValue: %s", err) + } + + got, err := dynamicValueEquals(testCase.schemaType, dynamicValue1, 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/tf6muxserver/mux_server_ValidateProviderConfig.go b/tf6muxserver/mux_server_ValidateProviderConfig.go index 30a71d4..86381f0 100644 --- a/tf6muxserver/mux_server_ValidateProviderConfig.go +++ b/tf6muxserver/mux_server_ValidateProviderConfig.go @@ -9,8 +9,9 @@ import ( ) // ValidateProviderConfig calls the ValidateProviderConfig method on each server -// in order, passing `req`. Only one may respond with a non-nil PreparedConfig -// or a non-empty Diagnostics. +// 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) { rpc := "ValidateProviderConfig" ctx = logging.InitContext(ctx) @@ -42,16 +43,22 @@ func (s muxServer) ValidateProviderConfig(ctx context.Context, req *tfprotov6.Va resp.Diagnostics = append(resp.Diagnostics, res.Diagnostics...) } - if res.PreparedConfig != nil { - // This could check equality to bypass the error, however - // DynamicValue does not implement Equals() and previous mux server - // implementations have not requested the enhancement. - if resp.PreparedConfig != nil { - return nil, fmt.Errorf("got a ValidateProviderConfig PreparedConfig response from multiple servers, not sure which to use") - } + // Do not check equality on missing PreparedConfig or unset PreparedConfig + if res.PreparedConfig == nil { + continue + } - resp.PreparedConfig = res.PreparedConfig + equal, err := dynamicValueEquals(schemaType(s.providerSchema), 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 } return resp, nil diff --git a/tf6muxserver/mux_server_ValidateProviderConfig_test.go b/tf6muxserver/mux_server_ValidateProviderConfig_test.go index 041192d..f3a755c 100644 --- a/tf6muxserver/mux_server_ValidateProviderConfig_test.go +++ b/tf6muxserver/mux_server_ValidateProviderConfig_test.go @@ -32,6 +32,33 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { t.Fatalf("error constructing config: %s", err) } + config2, err := tfprotov6.NewDynamicValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hello": tftypes.String, + }, + }, tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hello": tftypes.String, + }, + }, map[string]tftypes.Value{ + "hello": tftypes.NewValue(tftypes.String, "goodbye"), + })) + + if err != nil { + t.Fatalf("error constructing config: %s", err) + } + + configSchema := tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "hello", + Type: tftypes.String, + }, + }, + }, + } + testCases := map[string]struct { servers []func() tfprotov6.ProviderServer expectedError error @@ -222,6 +249,7 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { "PreparedConfig-once": { servers: []func() tfprotov6.ProviderServer{ (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ PreparedConfig: &config, }, @@ -236,12 +264,16 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { "PreparedConfig-once-and-error": { servers: []func() tfprotov6.ProviderServer{ (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ PreparedConfig: &config, }, }).ProviderServer, - (&tf6testserver.TestServer{}).ProviderServer, (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, + }).ProviderServer, + (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ Diagnostics: []*tfprotov6.Diagnostic{ { @@ -267,12 +299,16 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { "PreparedConfig-once-and-warning": { servers: []func() tfprotov6.ProviderServer{ (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ PreparedConfig: &config, }, }).ProviderServer, - (&tf6testserver.TestServer{}).ProviderServer, (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, + }).ProviderServer, + (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ Diagnostics: []*tfprotov6.Diagnostic{ { @@ -295,21 +331,47 @@ func TestMuxServerValidateProviderConfig(t *testing.T) { PreparedConfig: &config, }, }, - "PreparedConfig-multiple": { + "PreparedConfig-multiple-different": { servers: []func() tfprotov6.ProviderServer{ (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ PreparedConfig: &config, }, }).ProviderServer, - (&tf6testserver.TestServer{}).ProviderServer, (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, + }).ProviderServer, + (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, + ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ + PreparedConfig: &config2, + }, + }).ProviderServer, + }, + expectedError: fmt.Errorf("got different PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use"), + }, + "PreparedConfig-multiple-equal": { + servers: []func() tfprotov6.ProviderServer{ + (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ PreparedConfig: &config, }, }).ProviderServer, + (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, + }).ProviderServer, + (&tf6testserver.TestServer{ + ProviderSchema: &configSchema, + ValidateProviderConfigResponse: &tfprotov6.ValidateProviderConfigResponse{ + PreparedConfig: &config, + }, + }).ProviderServer, + }, + expectedResponse: &tfprotov6.ValidateProviderConfigResponse{ + PreparedConfig: &config, }, - expectedError: fmt.Errorf("got a ValidateProviderConfig PreparedConfig response from multiple servers, not sure which to use"), }, } diff --git a/tf6muxserver/schema_type.go b/tf6muxserver/schema_type.go new file mode 100644 index 0000000..4cd4e16 --- /dev/null +++ b/tf6muxserver/schema_type.go @@ -0,0 +1,162 @@ +package tf6muxserver + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// schemaType returns the Type for a Schema. +// +// This function should be migrated to a (*tfprotov6.Schema).Type() method +// in terraform-plugin-go. +func schemaType(schema *tfprotov6.Schema) tftypes.Type { + if schema == nil { + return tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + } + } + + return schemaBlockType(schema.Block) +} + +// schemaAttributeType returns the Type for a SchemaAttribute. +// +// This function should be migrated to a (*tfprotov6.SchemaAttribute).Type() +// method in terraform-plugin-go. +func schemaAttributeType(attribute *tfprotov6.SchemaAttribute) tftypes.Type { + if attribute == nil { + return nil + } + + if attribute.NestedType != nil { + return schemaObjectType(attribute.NestedType) + } + + return attribute.Type +} + +// schemaBlockType returns the Type for a SchemaBlock. +// +// This function should be migrated to a (*tfprotov6.SchemaBlock).Type() +// method in terraform-plugin-go. +func schemaBlockType(block *tfprotov6.SchemaBlock) tftypes.Type { + if block == nil { + return tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + } + } + + attributeTypes := map[string]tftypes.Type{} + + for _, attribute := range block.Attributes { + if attribute == nil { + continue + } + + attributeType := schemaAttributeType(attribute) + + if attributeType == nil { + continue + } + + attributeTypes[attribute.Name] = attributeType + } + + for _, block := range block.BlockTypes { + if block == nil { + continue + } + + blockType := schemaNestedBlockType(block) + + if blockType == nil { + continue + } + + attributeTypes[block.TypeName] = blockType + } + + return tftypes.Object{ + AttributeTypes: attributeTypes, + } +} + +// schemaNestedBlockType returns the Type for a SchemaNestedBlock. +// +// This function should be migrated to a (*tfprotov6.SchemaNestedBlock).Type() +// method in terraform-plugin-go. +func schemaNestedBlockType(nestedBlock *tfprotov6.SchemaNestedBlock) tftypes.Type { + if nestedBlock == nil { + return nil + } + + switch nestedBlock.Nesting { + case tfprotov6.SchemaNestedBlockNestingModeGroup: + return schemaBlockType(nestedBlock.Block) + case tfprotov6.SchemaNestedBlockNestingModeList: + return tftypes.List{ + ElementType: schemaBlockType(nestedBlock.Block), + } + case tfprotov6.SchemaNestedBlockNestingModeMap: + return tftypes.Map{ + ElementType: schemaBlockType(nestedBlock.Block), + } + case tfprotov6.SchemaNestedBlockNestingModeSet: + return tftypes.Set{ + ElementType: schemaBlockType(nestedBlock.Block), + } + case tfprotov6.SchemaNestedBlockNestingModeSingle: + return schemaBlockType(nestedBlock.Block) + default: + return nil + } +} + +// schemaObjectType returns the Type for a SchemaObject. +// +// This function should be migrated to a (*tfprotov6.SchemaObject).Type() +// method in terraform-plugin-go. +func schemaObjectType(object *tfprotov6.SchemaObject) tftypes.Type { + if object == nil { + return nil + } + + attributeTypes := map[string]tftypes.Type{} + + for _, attribute := range object.Attributes { + if attribute == nil { + continue + } + + attributeType := schemaAttributeType(attribute) + + if attributeType == nil { + continue + } + + attributeTypes[attribute.Name] = attributeType + } + + objectType := tftypes.Object{ + AttributeTypes: attributeTypes, + } + + switch object.Nesting { + case tfprotov6.SchemaObjectNestingModeList: + return tftypes.List{ + ElementType: objectType, + } + case tfprotov6.SchemaObjectNestingModeMap: + return tftypes.Map{ + ElementType: objectType, + } + case tfprotov6.SchemaObjectNestingModeSet: + return tftypes.Set{ + ElementType: objectType, + } + case tfprotov6.SchemaObjectNestingModeSingle: + return objectType + default: + return nil + } +} diff --git a/tf6muxserver/schema_type_test.go b/tf6muxserver/schema_type_test.go new file mode 100644 index 0000000..90b2ff1 --- /dev/null +++ b/tf6muxserver/schema_type_test.go @@ -0,0 +1,858 @@ +package tf6muxserver + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestSchemaAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schemaAttribute *tfprotov6.SchemaAttribute + expected tftypes.Type + }{ + "nil": { + schemaAttribute: nil, + expected: nil, + }, + "Bool": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.Bool, + }, + expected: tftypes.Bool, + }, + "DynamicPseudoType": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.DynamicPseudoType, + }, + expected: tftypes.DynamicPseudoType, + }, + "List-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.String, + }, + }, + "Map-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + expected: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + "NestedList-Bool": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + }, + }, + }, + }, + "NestedList-DynamicPseudoType": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "dynamic", + Type: tftypes.DynamicPseudoType, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "dynamic": tftypes.DynamicPseudoType, + }, + }, + }, + }, + "NestedList-List-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "list", + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "list": tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + "NestedList-Map-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "map", + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "map": tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + "NestedList-Number": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "number", + Type: tftypes.Number, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "number": tftypes.Number, + }, + }, + }, + }, + "NestedList-Object-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + "NestedList-Set-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "set", + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "set": tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + "NestedList-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "string", + Type: tftypes.String, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + }, + "NestedList-Tuple-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "tuple", + Type: tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String}, + }, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeList, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "tuple": tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String}, + }, + }, + }, + }, + }, + "NestedMap-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "string", + Type: tftypes.String, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeMap, + }, + }, + expected: tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + }, + "NestedSet-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "string", + Type: tftypes.String, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeSet, + }, + }, + expected: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + }, + "NestedSingle-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "string", + Type: tftypes.String, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + "Number": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.Number, + }, + expected: tftypes.Number, + }, + "Object-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + }, + "Set-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + expected: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + "String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.String, + }, + expected: tftypes.String, + }, + "Tuple-String": { + schemaAttribute: &tfprotov6.SchemaAttribute{ + Type: tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String}, + }, + }, + expected: tftypes.Tuple{ + ElementTypes: []tftypes.Type{tftypes.String}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := schemaAttributeType(testCase.schemaAttribute) + + if testCase.expected == nil { + if got == nil { + return + } + + t.Fatalf("expected nil, got: %s", got) + } + + if !testCase.expected.Equal(got) { + t.Errorf("expected %s, got: %s", testCase.expected, got) + } + }) + } +} + +func TestSchemaBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schemaBlock *tfprotov6.SchemaBlock + expected tftypes.Type + }{ + "nil": { + schemaBlock: nil, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, + }, + "Attribute-String": { + schemaBlock: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + "Block-NestingList-String": { + schemaBlock: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := schemaBlockType(testCase.schemaBlock) + + if testCase.expected == nil { + if got == nil { + return + } + + t.Fatalf("expected nil, got: %s", got) + } + + if !testCase.expected.Equal(got) { + t.Errorf("expected %s, got: %s", testCase.expected, got) + } + }) + } +} + +func TestSchemaNestedBlockType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schemaNestedBlock *tfprotov6.SchemaNestedBlock + expected tftypes.Type + }{ + "nil": { + schemaNestedBlock: nil, + expected: nil, + }, + "NestingGroup-Attribute-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeGroup, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + "NestingGroup-Block-NestingList-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeGroup, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + "NestingInvalid": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Nesting: tfprotov6.SchemaNestedBlockNestingModeInvalid, + }, + expected: nil, + }, + "NestingList-missing-Block": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, + }, + }, + "NestingList-Attribute-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + "NestingList-Block-NestingList-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "NestingMap-Attribute-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeMap, + }, + expected: tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + "NestingMap-Block-NestingList-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeMap, + }, + expected: tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "NestingSet-Attribute-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + }, + expected: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + "NestingSet-Block-NestingList-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSet, + }, + expected: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "NestingSingle-Attribute-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + "NestingSingle-Block-NestingList-String": { + schemaNestedBlock: &tfprotov6.SchemaNestedBlock{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeSingle, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := schemaNestedBlockType(testCase.schemaNestedBlock) + + if testCase.expected == nil { + if got == nil { + return + } + + t.Fatalf("expected nil, got: %s", got) + } + + if !testCase.expected.Equal(got) { + t.Errorf("expected %s, got: %s", testCase.expected, got) + } + }) + } +} + +func TestSchemaType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema *tfprotov6.Schema + expected tftypes.Type + }{ + "nil": { + schema: nil, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, + }, + "missing-Block": { + schema: &tfprotov6.Schema{}, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{}, + }, + }, + "Attribute-String": { + schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + "Block-NestingList-String": { + schema: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{ + BlockTypes: []*tfprotov6.SchemaNestedBlock{ + { + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "test_string_attribute", + Type: tftypes.String, + }, + }, + }, + Nesting: tfprotov6.SchemaNestedBlockNestingModeList, + TypeName: "test_list_block", + }, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_list_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_string_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := schemaType(testCase.schema) + + if testCase.expected == nil { + if got == nil { + return + } + + t.Fatalf("expected nil, got: %s", got) + } + + if !testCase.expected.Equal(got) { + t.Errorf("expected %s, got: %s", testCase.expected, got) + } + }) + } +} From d8fdec82712e92b85a198ac79418240122050d62 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 15 Feb 2022 15:33:26 -0500 Subject: [PATCH 2/2] Update CHANGELOG for #54 --- .changelog/{pending.txt => 54.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 54.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/54.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/54.txt