From f76713acd33f9e948c788b3e8cdf11b71d738cfa Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 9 Jun 2022 16:48:08 -0700 Subject: [PATCH 1/2] tfsdk: Prevent configuration handling error when Schema contained Blocks 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/pending.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/pending.txt create mode 100644 tfsdk/attribute_test.go create mode 100644 tfsdk/block_test.go diff --git a/.changelog/pending.txt b/.changelog/pending.txt new file mode 100644 index 000000000..046059f4d --- /dev/null +++ b/.changelog/pending.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) + } + }) + } +} From 1c0b8e846a7f8c6828506d7c29559a5e7ec8c845 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Thu, 9 Jun 2022 16:49:34 -0700 Subject: [PATCH 2/2] Update CHANGELOG for #371 --- .changelog/{pending.txt => 371.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{pending.txt => 371.txt} (100%) diff --git a/.changelog/pending.txt b/.changelog/371.txt similarity index 100% rename from .changelog/pending.txt rename to .changelog/371.txt