diff --git a/.changelog/54.txt b/.changelog/54.txt new file mode 100644 index 0000000..4e22b37 --- /dev/null +++ b/.changelog/54.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) + } + }) + } +}