From dfb09a654c378345544cf936831d0fa3b4918a20 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Mon, 13 Jun 2022 13:37:50 -0400 Subject: [PATCH] tfsdk: Prevent configuration handling error when Schema contains Blocks (#371) Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/328 Previously the framework could return errors similar to the following: ``` Error: Error parsing config The provider had a problem parsing the config. Report this to the provider developer: AttributeName("output").ElementKeyInt(0): error decoding object; expected 0 attributes, got 5 ``` New unit testing failures before fix: ``` --- FAIL: TestBlockAttributeType (0.00s) --- FAIL: TestBlockAttributeType/NestingMode-Set (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/tfsdk/block_test.go:103: unexpected difference: types.SetType( - s`types.SetType[types.ObjectType["test_attribute":types.StringType]]`, + s`types.SetType[types.ObjectType["test_attribute":types.StringType, "test_block":types.SetType[types.ObjectType["test_block_attribute":types.StringType]]]]`, ) --- FAIL: TestBlockAttributeType/NestingMode-List (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/tfsdk/block_test.go:103: unexpected difference: types.ListType( - s`types.ListType[types.ObjectType["test_attribute":types.StringType]]`, + s`types.ListType[types.ObjectType["test_attribute":types.StringType, "test_block":types.ListType[types.ObjectType["test_block_attribute":types.StringType]]]]`, ) --- FAIL: TestBlockTerraformType (0.00s) --- FAIL: TestBlockTerraformType/NestingMode-List (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/tfsdk/block_test.go:199: unexpected difference: tftypes.List( - s`tftypes.List[tftypes.Object["test_attribute":tftypes.String]]`, + s`tftypes.List[tftypes.Object["test_attribute":tftypes.String, "test_block":tftypes.List[tftypes.Object["test_block_attribute":tftypes.String]]]]`, ) --- FAIL: TestBlockTerraformType/NestingMode-Set (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/tfsdk/block_test.go:199: unexpected difference: tftypes.Set( - s`tftypes.Set[tftypes.Object["test_attribute":tftypes.String]]`, + s`tftypes.Set[tftypes.Object["test_attribute":tftypes.String, "test_block":tftypes.Set[tftypes.Object["test_block_attribute":tftypes.String]]]]`, ) ``` --- .changelog/371.txt | 3 + tfsdk/attribute_test.go | 365 ++++++++++++++++++++++++++++++++++++++++ tfsdk/block.go | 2 +- tfsdk/block_test.go | 203 ++++++++++++++++++++++ 4 files changed, 572 insertions(+), 1 deletion(-) create mode 100644 .changelog/371.txt create mode 100644 tfsdk/attribute_test.go create mode 100644 tfsdk/block_test.go diff --git a/.changelog/371.txt b/.changelog/371.txt new file mode 100644 index 000000000..046059f4d --- /dev/null +++ b/.changelog/371.txt @@ -0,0 +1,3 @@ +```release-note:bug +tfsdk: Prevented configuration handling error when `Schema` contained `Blocks` +``` diff --git a/tfsdk/attribute_test.go b/tfsdk/attribute_test.go new file mode 100644 index 000000000..f963b93e4 --- /dev/null +++ b/tfsdk/attribute_test.go @@ -0,0 +1,365 @@ +package tfsdk + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestAttributeAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute Attribute + expected attr.Type + }{ + "Attributes-ListNestedAttributes": { + attribute: Attribute{ + Attributes: ListNestedAttributes(map[string]Attribute{ + "test_nested_attribute": { + Required: true, + Type: types.StringType, + }, + }), + Required: true, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_nested_attribute": types.StringType, + }, + }, + }, + }, + "Attributes-MapNestedAttributes": { + attribute: Attribute{ + Attributes: MapNestedAttributes(map[string]Attribute{ + "test_nested_attribute": { + Required: true, + Type: types.StringType, + }, + }), + Required: true, + }, + expected: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_nested_attribute": types.StringType, + }, + }, + }, + }, + "Attributes-SetNestedAttributes": { + attribute: Attribute{ + Attributes: SetNestedAttributes(map[string]Attribute{ + "test_nested_attribute": { + Required: true, + Type: types.StringType, + }, + }), + Required: true, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_nested_attribute": types.StringType, + }, + }, + }, + }, + "Attributes-SingleNestedAttributes": { + attribute: Attribute{ + Attributes: SingleNestedAttributes(map[string]Attribute{ + "test_nested_attribute": { + Required: true, + Type: types.StringType, + }, + }), + Required: true, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_nested_attribute": types.StringType, + }, + }, + }, + "Type-BoolType": { + attribute: Attribute{ + Required: true, + Type: types.BoolType, + }, + expected: types.BoolType, + }, + "Type-Float64Type": { + attribute: Attribute{ + Required: true, + Type: types.Float64Type, + }, + expected: types.Float64Type, + }, + "Type-Int64Type": { + attribute: Attribute{ + Required: true, + Type: types.Int64Type, + }, + expected: types.Int64Type, + }, + "Type-ListType": { + attribute: Attribute{ + Required: true, + Type: types.ListType{ + ElemType: types.StringType, + }, + }, + expected: types.ListType{ + ElemType: types.StringType, + }, + }, + "Type-MapType": { + attribute: Attribute{ + Required: true, + Type: types.MapType{ + ElemType: types.StringType, + }, + }, + expected: types.MapType{ + ElemType: types.StringType, + }, + }, + "Type-ObjectType": { + attribute: Attribute{ + Required: true, + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + "Type-NumberType": { + attribute: Attribute{ + Required: true, + Type: types.NumberType, + }, + expected: types.NumberType, + }, + "Type-SetType": { + attribute: Attribute{ + Required: true, + Type: types.SetType{ + ElemType: types.StringType, + }, + }, + expected: types.SetType{ + ElemType: types.StringType, + }, + }, + "Type-StringType": { + attribute: Attribute{ + Required: true, + Type: types.StringType, + }, + expected: types.StringType, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.attributeType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAttributeTerraformType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute Attribute + expected tftypes.Type + }{ + "Attributes-ListNestedAttributes": { + attribute: Attribute{ + Attributes: ListNestedAttributes(map[string]Attribute{ + "test_nested_attribute": { + Required: true, + Type: types.StringType, + }, + }), + Required: true, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_nested_attribute": tftypes.String, + }, + }, + }, + }, + "Attributes-MapNestedAttributes": { + attribute: Attribute{ + Attributes: MapNestedAttributes(map[string]Attribute{ + "test_nested_attribute": { + Required: true, + Type: types.StringType, + }, + }), + Required: true, + }, + expected: tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_nested_attribute": tftypes.String, + }, + }, + }, + }, + "Attributes-SetNestedAttributes": { + attribute: Attribute{ + Attributes: SetNestedAttributes(map[string]Attribute{ + "test_nested_attribute": { + Required: true, + Type: types.StringType, + }, + }), + Required: true, + }, + expected: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_nested_attribute": tftypes.String, + }, + }, + }, + }, + "Attributes-SingleNestedAttributes": { + attribute: Attribute{ + Attributes: SingleNestedAttributes(map[string]Attribute{ + "test_nested_attribute": { + Required: true, + Type: types.StringType, + }, + }), + Required: true, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_nested_attribute": tftypes.String, + }, + }, + }, + "Type-BoolType": { + attribute: Attribute{ + Required: true, + Type: types.BoolType, + }, + expected: tftypes.Bool, + }, + "Type-Float64Type": { + attribute: Attribute{ + Required: true, + Type: types.Float64Type, + }, + expected: tftypes.Number, + }, + "Type-Int64Type": { + attribute: Attribute{ + Required: true, + Type: types.Int64Type, + }, + expected: tftypes.Number, + }, + "Type-ListType": { + attribute: Attribute{ + Required: true, + Type: types.ListType{ + ElemType: types.StringType, + }, + }, + expected: tftypes.List{ + ElementType: tftypes.String, + }, + }, + "Type-MapType": { + attribute: Attribute{ + Required: true, + Type: types.MapType{ + ElemType: types.StringType, + }, + }, + expected: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + "Type-NumberType": { + attribute: Attribute{ + Required: true, + Type: types.NumberType, + }, + expected: tftypes.Number, + }, + "Type-ObjectType": { + attribute: Attribute{ + Required: true, + Type: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_object_attribute": types.StringType, + }, + }, + }, + expected: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_object_attribute": tftypes.String, + }, + }, + }, + "Type-SetType": { + attribute: Attribute{ + Required: true, + Type: types.SetType{ + ElemType: types.StringType, + }, + }, + expected: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + "Type-StringType": { + attribute: Attribute{ + Required: true, + Type: types.StringType, + }, + expected: tftypes.String, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.terraformType(context.Background()) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/tfsdk/block.go b/tfsdk/block.go index 30d5c7309..c761a865a 100644 --- a/tfsdk/block.go +++ b/tfsdk/block.go @@ -134,7 +134,7 @@ func (b Block) attributeType() attr.Type { attrType.AttrTypes[attrName] = attr.attributeType() } - for blockName, block := range b.Attributes { + for blockName, block := range b.Blocks { attrType.AttrTypes[blockName] = block.attributeType() } diff --git a/tfsdk/block_test.go b/tfsdk/block_test.go new file mode 100644 index 000000000..ed9786230 --- /dev/null +++ b/tfsdk/block_test.go @@ -0,0 +1,203 @@ +package tfsdk + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestBlockAttributeType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block Block + expected attr.Type + }{ + "NestingMode-List": { + block: Block{ + Attributes: map[string]Attribute{ + "test_attribute": { + Required: true, + Type: types.StringType, + }, + }, + Blocks: map[string]Block{ + "test_block": { + Attributes: map[string]Attribute{ + "test_block_attribute": { + Required: true, + Type: types.StringType, + }, + }, + NestingMode: BlockNestingModeList, + }, + }, + NestingMode: BlockNestingModeList, + }, + expected: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attribute": types.StringType, + "test_block": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_block_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + "NestingMode-Set": { + block: Block{ + Attributes: map[string]Attribute{ + "test_attribute": { + Required: true, + Type: types.StringType, + }, + }, + Blocks: map[string]Block{ + "test_block": { + Attributes: map[string]Attribute{ + "test_block_attribute": { + Required: true, + Type: types.StringType, + }, + }, + NestingMode: BlockNestingModeSet, + }, + }, + NestingMode: BlockNestingModeSet, + }, + expected: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_attribute": types.StringType, + "test_block": types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test_block_attribute": types.StringType, + }, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.attributeType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBlockTerraformType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + block Block + expected tftypes.Type + }{ + "NestingMode-List": { + block: Block{ + Attributes: map[string]Attribute{ + "test_attribute": { + Required: true, + Type: types.StringType, + }, + }, + Blocks: map[string]Block{ + "test_block": { + Attributes: map[string]Attribute{ + "test_block_attribute": { + Required: true, + Type: types.StringType, + }, + }, + NestingMode: BlockNestingModeList, + }, + }, + NestingMode: BlockNestingModeList, + }, + expected: tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + "test_block": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_block_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "NestingMode-Set": { + block: Block{ + Attributes: map[string]Attribute{ + "test_attribute": { + Required: true, + Type: types.StringType, + }, + }, + Blocks: map[string]Block{ + "test_block": { + Attributes: map[string]Attribute{ + "test_block_attribute": { + Required: true, + Type: types.StringType, + }, + }, + NestingMode: BlockNestingModeSet, + }, + }, + NestingMode: BlockNestingModeSet, + }, + expected: tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + "test_block": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_block_attribute": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.block.terraformType(context.Background()) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +}