diff --git a/go.mod b/go.mod index a082332..9a8134f 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.6 require ( github.com/hashicorp/go-memdb v1.3.4 github.com/hashicorp/terraform-json v0.22.1 - github.com/hashicorp/terraform-plugin-framework v1.10.0 + github.com/hashicorp/terraform-plugin-framework v1.10.1-0.20240723171809-2ccf8f265f8d github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 github.com/hashicorp/terraform-plugin-framework-timetypes v0.4.0 github.com/hashicorp/terraform-plugin-go v0.23.0 diff --git a/go.sum b/go.sum index a565801..d14f263 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVW github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= -github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc= -github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= +github.com/hashicorp/terraform-plugin-framework v1.10.1-0.20240723171809-2ccf8f265f8d h1:3QAP7iiGP+dr1z6kcZg08Z2wAZKN3CqJIHryCROqw1s= +github.com/hashicorp/terraform-plugin-framework v1.10.1-0.20240723171809-2ccf8f265f8d/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaKFhm4h2TgvMUlNzFAtUqlcOWnWPm+9E= github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY= github.com/hashicorp/terraform-plugin-framework-timetypes v0.4.0 h1:XLI93Oqw2/KTzYjgCXrUnm8LBkGAiHC/mDQg5g5Vob4= diff --git a/internal/framework5provider/provider.go b/internal/framework5provider/provider.go index 9315def..3cc243a 100644 --- a/internal/framework5provider/provider.go +++ b/internal/framework5provider/provider.go @@ -75,6 +75,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewUserResource, NewFloat32PrecisionResource, NewFloat64PrecisionResource, + NewTFSDKReflectionResource, } } diff --git a/internal/framework5provider/tfsdk_reflection_resource.go b/internal/framework5provider/tfsdk_reflection_resource.go new file mode 100644 index 0000000..2515b54 --- /dev/null +++ b/internal/framework5provider/tfsdk_reflection_resource.go @@ -0,0 +1,324 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = TFSDKReflectionResource{} + +func NewTFSDKReflectionResource() resource.Resource { + return &TFSDKReflectionResource{} +} + +// TFSDKReflectionResource is a smoke test for reflection logic on objects using the `tfsdk` field tags. +type TFSDKReflectionResource struct{} + +func (r TFSDKReflectionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tfsdk_reflection" +} + +var nestedObjType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, +} + +func (r TFSDKReflectionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "config_bool": schema.BoolAttribute{ + Required: true, + }, + "computed_bool": schema.BoolAttribute{ + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "config_dynamic": schema.DynamicAttribute{ + Required: true, + }, + "computed_dynamic": schema.DynamicAttribute{ + Computed: true, + PlanModifiers: []planmodifier.Dynamic{ + dynamicplanmodifier.UseStateForUnknown(), + }, + }, + "config_float64": schema.Float64Attribute{ + Required: true, + }, + "computed_float64": schema.Float64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Float64{ + float64planmodifier.UseStateForUnknown(), + }, + }, + "config_int64": schema.Int64Attribute{ + Required: true, + }, + "computed_int64": schema.Int64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "config_list": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + }, + "computed_list": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + "config_map": schema.MapAttribute{ + ElementType: types.StringType, + Required: true, + }, + "computed_map": schema.MapAttribute{ + ElementType: types.StringType, + Computed: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.UseStateForUnknown(), + }, + }, + "config_object": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Required: true, + }, + "computed_object": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + }, + "config_string": schema.StringAttribute{ + Required: true, + }, + "computed_string": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "config_set_nested_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested_string": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + "computed_list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested_string": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + }, + } +} + +func (r TFSDKReflectionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data TFSDKReflectionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + data.ComputedBool = types.BoolValue(true) + data.ComputedDynamic = types.DynamicValue(types.StringValue("dynamic string")) + data.ComputedFloat64 = types.Float64Value(1.2) + data.ComputedInt64 = types.Int64Value(100) + data.ComputedString = types.StringValue("computed string") + + data.ComputedList = types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("computed"), + types.StringValue("list"), + }, + ) + data.ComputedMap = types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "key6": types.StringValue("val6"), + "key7": types.StringValue("val7"), + }, + ) + + data.ComputedObject = types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("computed string"), + }, + ) + + data.ComputedListNestedBlock = types.ListValueMust( + nestedObjType, + []attr.Value{ + types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("computed string one"), + }, + ), + types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("computed string two"), + }, + ), + }, + ) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r TFSDKReflectionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data TFSDKReflectionResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r TFSDKReflectionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data TFSDKReflectionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r TFSDKReflectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type TFSDKReflectionResourceModel struct { + ConfigDynamic types.Dynamic `tfsdk:"config_dynamic"` + ComputedDynamic types.Dynamic `tfsdk:"computed_dynamic"` + + primitiveAttrs + CollectionAttrs + objectAttrs + CollectionBlocks + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint + *EmbedIgnore `tfsdk:"-"` + embedIgnore `tfsdk:"-"` +} + +type embedIgnore struct { + Field1 string +} + +type EmbedIgnore struct { + Field2 string +} + +type primitiveAttrs struct { + ConfigBool *bool `tfsdk:"config_bool"` + ComputedBool types.Bool `tfsdk:"computed_bool"` + + ConfigFloat64 *float64 `tfsdk:"config_float64"` + ComputedFloat64 types.Float64 `tfsdk:"computed_float64"` + + ConfigInt64 *int64 `tfsdk:"config_int64"` + ComputedInt64 types.Int64 `tfsdk:"computed_int64"` + + ConfigString *string `tfsdk:"config_string"` + ComputedString types.String `tfsdk:"computed_string"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type CollectionAttrs struct { + ConfigList []string `tfsdk:"config_list"` + ComputedList types.List `tfsdk:"computed_list"` + + MapAttrs + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type MapAttrs struct { + ConfigMap map[string]string `tfsdk:"config_map"` + ComputedMap types.Map `tfsdk:"computed_map"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type objectAttrs struct { + ConfigObject types.Object `tfsdk:"config_object"` + ComputedObject types.Object `tfsdk:"computed_object"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type CollectionBlocks struct { + setBlock + listBlock + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type setBlock struct { + ConfigSetNestedBlock types.Set `tfsdk:"config_set_nested_block"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} +type listBlock struct { + ComputedListNestedBlock types.List `tfsdk:"computed_list_nested_block"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} diff --git a/internal/framework5provider/tfsdk_reflection_resource_test.go b/internal/framework5provider/tfsdk_reflection_resource_test.go new file mode 100644 index 0000000..40c5fe3 --- /dev/null +++ b/internal/framework5provider/tfsdk_reflection_resource_test.go @@ -0,0 +1,180 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestTFSDKReflectionResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_tfsdk_reflection" "test" { + config_bool = true + config_dynamic = "hello" + config_float64 = 2.1 + config_int64 = 201 + config_string = "hello" + + config_list = ["one", "two"] + config_map = { + key1 = "val1" + key2 = "val2" + key3 = "val3" + } + + config_object = { + nested_string = "hello" + } + + config_set_nested_block { + nested_string = "one" + } + config_set_nested_block { + nested_string = "two" + } + + # Blocks themselves are not computed, but the attributes within are. Since it's hardcoded + # in the resource Create function, this configuration ensures no plan consistency errors + computed_list_nested_block{} + computed_list_nested_block{} + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_bool"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_dynamic"), knownvalue.StringExact("dynamic string")), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_float64"), knownvalue.Float64Exact(1.2)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_int64"), knownvalue.Int64Exact(100)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_string"), knownvalue.StringExact("computed string")), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_list"), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("computed"), + knownvalue.StringExact("list"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_map"), + knownvalue.MapExact( + map[string]knownvalue.Check{ + "key6": knownvalue.StringExact("val6"), + "key7": knownvalue.StringExact("val7"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_object"), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_list_nested_block"), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string one"), + }, + ), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string two"), + }, + ), + }, + ), + ), + }, + }, + { + Config: `resource "framework_tfsdk_reflection" "test" { + config_bool = false + config_dynamic = "world" + config_float64 = 3.4 + config_int64 = 301 + config_string = "world" + + config_list = ["three", "four", "five"] + config_map = { + key4 = "val4" + key5 = "val5" + } + + config_object = { + nested_string = "world" + } + + config_set_nested_block { + nested_string = "three" + } + config_set_nested_block { + nested_string = "four" + } + + # Blocks themselves are not computed, but the attributes within are. Since it's hardcoded + # in the resource Create function, this configuration ensures no plan consistency errors + computed_list_nested_block{} + computed_list_nested_block{} + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_bool"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_dynamic"), knownvalue.StringExact("dynamic string")), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_float64"), knownvalue.Float64Exact(1.2)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_int64"), knownvalue.Int64Exact(100)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_string"), knownvalue.StringExact("computed string")), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_list"), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("computed"), + knownvalue.StringExact("list"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_map"), + knownvalue.MapExact( + map[string]knownvalue.Check{ + "key6": knownvalue.StringExact("val6"), + "key7": knownvalue.StringExact("val7"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_object"), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_list_nested_block"), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string one"), + }, + ), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string two"), + }, + ), + }, + ), + ), + }, + }, + }, + }) +} diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index 99841b2..d37e19c 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -74,6 +74,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewUserResource, NewFloat32PrecisionResource, NewFloat64PrecisionResource, + NewTFSDKReflectionResource, } } diff --git a/internal/framework6provider/tfsdk_reflection_resource.go b/internal/framework6provider/tfsdk_reflection_resource.go new file mode 100644 index 0000000..fbebcf6 --- /dev/null +++ b/internal/framework6provider/tfsdk_reflection_resource.go @@ -0,0 +1,396 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = TFSDKReflectionResource{} + +func NewTFSDKReflectionResource() resource.Resource { + return &TFSDKReflectionResource{} +} + +// TFSDKReflectionResource is a smoke test for reflection logic on objects using the `tfsdk` field tags. +type TFSDKReflectionResource struct{} + +func (r TFSDKReflectionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_tfsdk_reflection" +} + +var nestedObjType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, +} + +func (r TFSDKReflectionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "config_bool": schema.BoolAttribute{ + Required: true, + }, + "computed_bool": schema.BoolAttribute{ + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "config_dynamic": schema.DynamicAttribute{ + Required: true, + }, + "computed_dynamic": schema.DynamicAttribute{ + Computed: true, + PlanModifiers: []planmodifier.Dynamic{ + dynamicplanmodifier.UseStateForUnknown(), + }, + }, + "config_float64": schema.Float64Attribute{ + Required: true, + }, + "computed_float64": schema.Float64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Float64{ + float64planmodifier.UseStateForUnknown(), + }, + }, + "config_int64": schema.Int64Attribute{ + Required: true, + }, + "computed_int64": schema.Int64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "config_list": schema.ListAttribute{ + ElementType: types.StringType, + Required: true, + }, + "computed_list": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + "config_map": schema.MapAttribute{ + ElementType: types.StringType, + Required: true, + }, + "computed_map": schema.MapAttribute{ + ElementType: types.StringType, + Computed: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.UseStateForUnknown(), + }, + }, + "config_map_nested": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested_string": schema.StringAttribute{ + Required: true, + }, + }, + }, + Required: true, + }, + "computed_map_nested": schema.MapNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "nested_string": schema.StringAttribute{ + Computed: true, + }, + }, + }, + Computed: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.UseStateForUnknown(), + }, + }, + "config_object": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Required: true, + }, + "computed_object": schema.ObjectAttribute{ + AttributeTypes: map[string]attr.Type{ + "nested_string": types.StringType, + }, + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + }, + "config_single_nested": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested_string": schema.StringAttribute{ + Required: true, + }, + }, + Required: true, + }, + "computed_single_nested": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "nested_string": schema.StringAttribute{ + Computed: true, + }, + }, + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + }, + "config_string": schema.StringAttribute{ + Required: true, + }, + "computed_string": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "config_set_nested_block": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested_string": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + "computed_list_nested_block": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "nested_string": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + }, + } +} + +func (r TFSDKReflectionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data TFSDKReflectionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + data.ComputedBool = types.BoolValue(true) + data.ComputedDynamic = types.DynamicValue(types.StringValue("dynamic string")) + data.ComputedFloat64 = types.Float64Value(1.2) + data.ComputedInt64 = types.Int64Value(100) + data.ComputedString = types.StringValue("computed string") + + data.ComputedList = types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("computed"), + types.StringValue("list"), + }, + ) + data.ComputedMap = types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "key6": types.StringValue("val6"), + "key7": types.StringValue("val7"), + }, + ) + data.ComputedMapNested = types.MapValueMust( + nestedObjType, + map[string]attr.Value{ + "key6": types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("val6"), + }, + ), + "key7": types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("val7"), + }, + ), + }, + ) + + data.ComputedObject = types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("computed string"), + }, + ) + + data.ComputedSingleNested = types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("computed string"), + }, + ) + + data.ComputedListNestedBlock = types.ListValueMust( + nestedObjType, + []attr.Value{ + types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("computed string one"), + }, + ), + types.ObjectValueMust( + nestedObjType.AttrTypes, + map[string]attr.Value{ + "nested_string": types.StringValue("computed string two"), + }, + ), + }, + ) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r TFSDKReflectionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data TFSDKReflectionResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r TFSDKReflectionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data TFSDKReflectionResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r TFSDKReflectionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type TFSDKReflectionResourceModel struct { + ConfigDynamic types.Dynamic `tfsdk:"config_dynamic"` + ComputedDynamic types.Dynamic `tfsdk:"computed_dynamic"` + + primitiveAttrs + CollectionAttrs + objectAttrs + CollectionBlocks + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint + *EmbedIgnore `tfsdk:"-"` + embedIgnore `tfsdk:"-"` +} + +type embedIgnore struct { + Field1 string +} + +type EmbedIgnore struct { + Field2 string +} + +type primitiveAttrs struct { + ConfigBool *bool `tfsdk:"config_bool"` + ComputedBool types.Bool `tfsdk:"computed_bool"` + + ConfigFloat64 *float64 `tfsdk:"config_float64"` + ComputedFloat64 types.Float64 `tfsdk:"computed_float64"` + + ConfigInt64 *int64 `tfsdk:"config_int64"` + ComputedInt64 types.Int64 `tfsdk:"computed_int64"` + + ConfigString *string `tfsdk:"config_string"` + ComputedString types.String `tfsdk:"computed_string"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type CollectionAttrs struct { + ConfigList []string `tfsdk:"config_list"` + ComputedList types.List `tfsdk:"computed_list"` + + MapAttrs + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type MapAttrs struct { + ConfigMap map[string]string `tfsdk:"config_map"` + ComputedMap types.Map `tfsdk:"computed_map"` + + ConfigMapNested types.Map `tfsdk:"config_map_nested"` + ComputedMapNested types.Map `tfsdk:"computed_map_nested"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type objectAttrs struct { + ConfigObject types.Object `tfsdk:"config_object"` + ComputedObject types.Object `tfsdk:"computed_object"` + + ConfigSingleNested types.Object `tfsdk:"config_single_nested"` + ComputedSingleNested types.Object `tfsdk:"computed_single_nested"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type CollectionBlocks struct { + setBlock + listBlock + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} + +type setBlock struct { + ConfigSetNestedBlock types.Set `tfsdk:"config_set_nested_block"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} +type listBlock struct { + ComputedListNestedBlock types.List `tfsdk:"computed_list_nested_block"` + + ExplicitIgnore string `tfsdk:"-"` + implicitIgnore string //nolint +} diff --git a/internal/framework6provider/tfsdk_reflection_resource_test.go b/internal/framework6provider/tfsdk_reflection_resource_test.go new file mode 100644 index 0000000..b33d8bf --- /dev/null +++ b/internal/framework6provider/tfsdk_reflection_resource_test.go @@ -0,0 +1,251 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestTFSDKReflectionResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "framework_tfsdk_reflection" "test" { + config_bool = true + config_dynamic = "hello" + config_float64 = 2.1 + config_int64 = 201 + config_string = "hello" + + config_list = ["one", "two"] + config_map = { + key1 = "val1" + key2 = "val2" + key3 = "val3" + } + config_map_nested = { + key1 = { + nested_string = "val1" + } + key2 = { + nested_string = "val2" + } + key3 = { + nested_string = "val3" + } + } + + config_object = { + nested_string = "hello" + } + config_single_nested = { + nested_string = "hello" + } + + config_set_nested_block { + nested_string = "one" + } + config_set_nested_block { + nested_string = "two" + } + + # Blocks themselves are not computed, but the attributes within are. Since it's hardcoded + # in the resource Create function, this configuration ensures no plan consistency errors + computed_list_nested_block{} + computed_list_nested_block{} + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_bool"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_dynamic"), knownvalue.StringExact("dynamic string")), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_float64"), knownvalue.Float64Exact(1.2)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_int64"), knownvalue.Int64Exact(100)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_string"), knownvalue.StringExact("computed string")), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_list"), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("computed"), + knownvalue.StringExact("list"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_map"), + knownvalue.MapExact( + map[string]knownvalue.Check{ + "key6": knownvalue.StringExact("val6"), + "key7": knownvalue.StringExact("val7"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_map_nested"), + knownvalue.MapExact( + map[string]knownvalue.Check{ + "key6": knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("val6"), + }, + ), + "key7": knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("val7"), + }, + ), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_object"), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_single_nested"), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_list_nested_block"), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string one"), + }, + ), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string two"), + }, + ), + }, + ), + ), + }, + }, + { + Config: `resource "framework_tfsdk_reflection" "test" { + config_bool = false + config_dynamic = "world" + config_float64 = 3.4 + config_int64 = 301 + config_string = "world" + + config_list = ["three", "four", "five"] + config_map = { + key4 = "val4" + key5 = "val5" + } + config_map_nested = { + key4 = { + nested_string = "val4" + } + key5 = { + nested_string = "val5" + } + } + + config_object = { + nested_string = "world" + } + config_single_nested = { + nested_string = "world" + } + + config_set_nested_block { + nested_string = "three" + } + config_set_nested_block { + nested_string = "four" + } + + # Blocks themselves are not computed, but the attributes within are. Since it's hardcoded + # in the resource Create function, this configuration ensures no plan consistency errors + computed_list_nested_block{} + computed_list_nested_block{} + }`, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_bool"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_dynamic"), knownvalue.StringExact("dynamic string")), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_float64"), knownvalue.Float64Exact(1.2)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_int64"), knownvalue.Int64Exact(100)), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_string"), knownvalue.StringExact("computed string")), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_list"), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("computed"), + knownvalue.StringExact("list"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_map"), + knownvalue.MapExact( + map[string]knownvalue.Check{ + "key6": knownvalue.StringExact("val6"), + "key7": knownvalue.StringExact("val7"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_map_nested"), + knownvalue.MapExact( + map[string]knownvalue.Check{ + "key6": knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("val6"), + }, + ), + "key7": knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("val7"), + }, + ), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_object"), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_single_nested"), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string"), + }, + ), + ), + statecheck.ExpectKnownValue("framework_tfsdk_reflection.test", tfjsonpath.New("computed_list_nested_block"), + knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string one"), + }, + ), + knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "nested_string": knownvalue.StringExact("computed string two"), + }, + ), + }, + ), + ), + }, + }, + }, + }) +}