From 5ed8e8e223b339676552030d99c4b0b77bc1dfe6 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 13 Jun 2023 13:35:29 -0400 Subject: [PATCH 1/2] internal/fwserver: Ensure Attribute and Block plan modification returns custom value type implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 Reference: https://github.com/hashicorp/terraform-plugin-framework/pull/715 (precursor) Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/767 (followup) The framework recently was updated to perform stricter value type checking against the defined schema type when setting data. This was to prevent panics or other potentially confusing behaviors with mismatched types. The attribute-based plan modification logic in the framework however was missing some value type conversion, which could generate unavoidable `Value Conversion Error` diagnostics for provider developers when attributes/blocks used both `CustomType` and `PlanModifiers` fields. This changeset ensures that attribute-based plan modification will always return the custom value type implementation of a value after a plan modifier response. All plan modifier responses are currently using the base value type, so the framework logic must handle converting it back. Even if the plan modifier responses were updated to use the `Valuable` interfaces, the framework would still need to perform this logic in case a plan modifier implementation opted to return the base value type, since all base value types implement the `Valuable` interfaces currently. These changes handle custom types on all attribute and block implementations, however a non-trivial amount of internal code refactoring is necessary to fix this same issue for `NestedAttributeObject` and `NestedBlockObject` implementations that contain both `CustomType` and `PlanModifiers` fields. That effort will follow this one separately to reduce review burden. Previously before logic updates: ``` --- FAIL: TestServerPlanResourceChange (0.00s) --- FAIL: TestServerPlanResourceChange/create-attributeplanmodifier-response-attributeplan-custom-type (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/internal/fwserver/server_planresourcechange_test.go:3848: unexpected difference:   &fwserver.PlanResourceChangeResponse{ -  Diagnostics: diag.Diagnostics{ -  diag.withPath{ -  Diagnostic: diag.ErrorDiagnostic{detail: "An unexpected error was encounte"..., summary: "Value Conversion Error"}, -  path: s"test_computed", -  }, -  }, +  Diagnostics: nil,    PlannedPrivate: &{Provider: &{data: {}}},    PlannedState: &tfsdk.State{ -  Raw: s`tftypes.Object["test_computed":tftypes.String, "test_other_computed":tftypes.String, "test_required":tftypes.String]<"test_computed":tftypes.String, "test_other_computed":tftypes.String, "test_required":tftypes.String<"test-config-value">>`, +  Raw: s`tftypes.Object["test_computed":tftypes.String, "test_other_computed":tftypes.String, "test_required":tftypes.String]<"test_computed":tftypes.String<"test-attributeplanmodifier-value">, "test_other_computed":tftypes.String, "test_required":tftypes.`...,    Schema: schema.Schema{Attributes: {"test_computed": schema.StringAttribute{CustomType: s"StringTypeWithSemanticEquals(false)", Computed: true, PlanModifiers: {...}}, "test_other_computed": schema.StringAttribute{Computed: true}, "test_required": schema.StringAttribute{Required: true}}},    },    RequiresReplace: s"[]",   } --- FAIL: TestAttributePlanModifyString (0.00s) --- FAIL: TestAttributePlanModifyString/response-planvalue-custom-type (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/internal/fwserver/attribute_plan_modification_test.go:8315: unexpected difference: &fwserver.ModifyAttributePlanResponse{ - AttributePlan: basetypes.StringValue{state: 2, value: "testvalue"}, + AttributePlan: types.StringValueWithSemanticEquals{StringValue: basetypes.StringValue{state: 2, value: "testvalue"}}, Diagnostics: nil, RequiresReplace: s"[]", Private: nil, } --- FAIL: TestAttributeModifyPlan (0.00s) --- FAIL: TestAttributeModifyPlan/attribute-list-nested-usestateforunknown-custom-type (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/internal/fwserver/attribute_plan_modification_test.go:1733: Unexpected response (-wanted, +got): fwserver.ModifyAttributePlanResponse{ - AttributePlan: types.ListValueWithSemanticEquals{ - ListValue: basetypes.ListValue{ - elements: []attr.Value{ - basetypes.ObjectValue{ - attributes: map[string]attr.Value{...}, - attributeTypes: map[string]attr.Type{...}, - state: 2, - }, - }, - elementType: basetypes.ObjectType{AttrTypes: map[string]attr.Type{"nested_computed": basetypes.StringType{}}}, - state: 2, - }, - }, + AttributePlan: basetypes.ListValue{ + elements: []attr.Value{ + basetypes.ObjectValue{ + attributes: map[string]attr.Value{"nested_computed": basetypes.StringValue{...}}, + attributeTypes: map[string]attr.Type{"nested_computed": basetypes.StringType{}}, + state: 2, + }, + }, + elementType: basetypes.ObjectType{AttrTypes: map[string]attr.Type{"nested_computed": basetypes.StringType{}}}, + state: 2, + }, Diagnostics: nil, RequiresReplace: s"[]", Private: nil, } --- FAIL: TestBlockModifyPlan (0.00s) --- FAIL: TestBlockModifyPlan/block-list-usestateforunknown-custom-type (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/internal/fwserver/block_plan_modification_test.go:3099: Unexpected response (+wanted, -got):   fwserver.ModifyAttributePlanResponse{ -  AttributePlan: types.ListValueWithSemanticEquals{ -  ListValue: basetypes.ListValue{ -  elements: []attr.Value{ -  basetypes.ObjectValue{ -  attributes: map[string]attr.Value{...}, -  attributeTypes: map[string]attr.Type{...}, -  state: 2, -  }, -  }, -  elementType: basetypes.ObjectType{AttrTypes: map[string]attr.Type{"nested_computed": basetypes.StringType{}}}, -  state: 2, -  }, -  }, +  AttributePlan: basetypes.ListValue{ +  elements: []attr.Value{ +  basetypes.ObjectValue{ +  attributes: map[string]attr.Value{"nested_computed": basetypes.StringValue{...}}, +  attributeTypes: map[string]attr.Type{"nested_computed": basetypes.StringType{}}, +  state: 2, +  }, +  }, +  elementType: basetypes.ObjectType{AttrTypes: map[string]attr.Type{"nested_computed": basetypes.StringType{}}}, +  state: 2, +  },    Diagnostics: nil,    RequiresReplace: s"[]",    Private: nil,   } --- FAIL: TestBlockPlanModifyList (0.00s) --- FAIL: TestBlockPlanModifyList/response-planvalue-custom-type (0.00s) /Users/bflad/src/github.com/hashicorp/terraform-plugin-framework/internal/fwserver/block_plan_modification_test.go:3528: unexpected difference: &fwserver.ModifyAttributePlanResponse{ - AttributePlan: basetypes.ListValue{ - elements: []attr.Value{basetypes.StringValue{state: 2, value: "testvalue"}}, - elementType: basetypes.StringType{}, - state: 2, - }, + AttributePlan: types.ListValueWithSemanticEquals{ + ListValue: basetypes.ListValue{ + elements: []attr.Value{basetypes.StringValue{state: 2, value: "testvalue"}}, + elementType: basetypes.StringType{}, + state: 2, + }, + }, Diagnostics: nil, RequiresReplace: s"[]", Private: nil, } ``` --- .../unreleased/BUG FIXES-20230613-133402.yaml | 6 + internal/fwserver/attr_type.go | 144 ++++ internal/fwserver/attr_value.go | 84 ++- .../fwserver/attribute_plan_modification.go | 390 ++++++++++- .../attribute_plan_modification_test.go | 650 ++++++++++++++++++ internal/fwserver/block_plan_modification.go | 195 +++++- .../fwserver/block_plan_modification_test.go | 347 ++++++++++ internal/fwserver/diagnostics.go | 12 + .../server_planresourcechange_test.go | 87 +++ internal/testing/testschema/block.go | 5 + .../testschema/blockwithlistplanmodifiers.go | 5 + .../blockwithobjectplanmodifiers.go | 5 + .../testschema/blockwithsetplanmodifiers.go | 5 + 13 files changed, 1889 insertions(+), 46 deletions(-) create mode 100644 .changes/unreleased/BUG FIXES-20230613-133402.yaml create mode 100644 internal/fwserver/attr_type.go diff --git a/.changes/unreleased/BUG FIXES-20230613-133402.yaml b/.changes/unreleased/BUG FIXES-20230613-133402.yaml new file mode 100644 index 000000000..e9b2c9eb2 --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20230613-133402.yaml @@ -0,0 +1,6 @@ +kind: BUG FIXES +body: 'resource/schema: Prevented `Value Conversion Error` diagnostics for attributes + and blocks implementing both `CustomType` and `PlanModifiers` fields' +time: 2023-06-13T13:34:02.465635-04:00 +custom: + Issue: "754" diff --git a/internal/fwserver/attr_type.go b/internal/fwserver/attr_type.go new file mode 100644 index 000000000..42647b13c --- /dev/null +++ b/internal/fwserver/attr_type.go @@ -0,0 +1,144 @@ +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func coerceBoolTypable(ctx context.Context, schemaPath path.Path, valuable basetypes.BoolValuable) (basetypes.BoolTypable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.BoolTypable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + +func coerceFloat64Typable(ctx context.Context, schemaPath path.Path, valuable basetypes.Float64Valuable) (basetypes.Float64Typable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.Float64Typable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + +func coerceInt64Typable(ctx context.Context, schemaPath path.Path, valuable basetypes.Int64Valuable) (basetypes.Int64Typable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.Int64Typable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + +func coerceListTypable(ctx context.Context, schemaPath path.Path, valuable basetypes.ListValuable) (basetypes.ListTypable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.ListTypable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + +func coerceMapTypable(ctx context.Context, schemaPath path.Path, valuable basetypes.MapValuable) (basetypes.MapTypable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.MapTypable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + +func coerceNumberTypable(ctx context.Context, schemaPath path.Path, valuable basetypes.NumberValuable) (basetypes.NumberTypable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.NumberTypable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + +func coerceObjectTypable(ctx context.Context, schemaPath path.Path, valuable basetypes.ObjectValuable) (basetypes.ObjectTypable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.ObjectTypable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + +func coerceSetTypable(ctx context.Context, schemaPath path.Path, valuable basetypes.SetValuable) (basetypes.SetTypable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.SetTypable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} + +func coerceStringTypable(ctx context.Context, schemaPath path.Path, valuable basetypes.StringValuable) (basetypes.StringTypable, diag.Diagnostics) { + typable, ok := valuable.Type(ctx).(basetypes.StringTypable) + + // Type() of a Valuable should always be a Typable to recreate the Valuable, + // but if for some reason it is not, raise an implementation error instead + // of a panic. + if !ok { + return nil, diag.Diagnostics{ + attributePlanModificationTypableError(schemaPath, valuable), + } + } + + return typable, nil +} diff --git a/internal/fwserver/attr_value.go b/internal/fwserver/attr_value.go index 052c8f3d5..4fb154f90 100644 --- a/internal/fwserver/attr_value.go +++ b/internal/fwserver/attr_value.go @@ -16,8 +16,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) -func coerceListValue(ctx context.Context, schemaPath path.Path, value attr.Value) (types.List, diag.Diagnostics) { - listVal, ok := value.(basetypes.ListValuable) +func coerceListValuable(_ context.Context, schemaPath path.Path, value attr.Value) (basetypes.ListValuable, diag.Diagnostics) { + listValuable, ok := value.(basetypes.ListValuable) if !ok { return types.ListNull(nil), diag.Diagnostics{ @@ -25,11 +25,26 @@ func coerceListValue(ctx context.Context, schemaPath path.Path, value attr.Value } } - return listVal.ToListValue(ctx) + return listValuable, nil } -func coerceMapValue(ctx context.Context, schemaPath path.Path, value attr.Value) (types.Map, diag.Diagnostics) { - mapVal, ok := value.(basetypes.MapValuable) +func coerceListValue(ctx context.Context, schemaPath path.Path, value attr.Value) (types.List, diag.Diagnostics) { + listValuable, diags := coerceListValuable(ctx, schemaPath, value) + + if diags.HasError() { + return types.ListNull(nil), diags + } + + listValue, listValueDiags := listValuable.ToListValue(ctx) + + // Ensure prior warnings are preserved. + diags.Append(listValueDiags...) + + return listValue, diags +} + +func coerceMapValuable(_ context.Context, schemaPath path.Path, value attr.Value) (basetypes.MapValuable, diag.Diagnostics) { + mapValuable, ok := value.(basetypes.MapValuable) if !ok { return types.MapNull(nil), diag.Diagnostics{ @@ -37,11 +52,26 @@ func coerceMapValue(ctx context.Context, schemaPath path.Path, value attr.Value) } } - return mapVal.ToMapValue(ctx) + return mapValuable, nil } -func coerceObjectValue(ctx context.Context, schemaPath path.Path, value attr.Value) (types.Object, diag.Diagnostics) { - objectVal, ok := value.(basetypes.ObjectValuable) +func coerceMapValue(ctx context.Context, schemaPath path.Path, value attr.Value) (types.Map, diag.Diagnostics) { + mapValuable, diags := coerceMapValuable(ctx, schemaPath, value) + + if diags.HasError() { + return types.MapNull(nil), diags + } + + mapValue, mapValueDiags := mapValuable.ToMapValue(ctx) + + // Ensure prior warnings are preserved. + diags.Append(mapValueDiags...) + + return mapValue, diags +} + +func coerceObjectValuable(_ context.Context, schemaPath path.Path, value attr.Value) (basetypes.ObjectValuable, diag.Diagnostics) { + objectValuable, ok := value.(basetypes.ObjectValuable) if !ok { return types.ObjectNull(nil), diag.Diagnostics{ @@ -49,11 +79,26 @@ func coerceObjectValue(ctx context.Context, schemaPath path.Path, value attr.Val } } - return objectVal.ToObjectValue(ctx) + return objectValuable, nil } -func coerceSetValue(ctx context.Context, schemaPath path.Path, value attr.Value) (types.Set, diag.Diagnostics) { - setVal, ok := value.(basetypes.SetValuable) +func coerceObjectValue(ctx context.Context, schemaPath path.Path, value attr.Value) (types.Object, diag.Diagnostics) { + objectValuable, diags := coerceObjectValuable(ctx, schemaPath, value) + + if diags.HasError() { + return types.ObjectNull(nil), diags + } + + objectValue, objectValueDiags := objectValuable.ToObjectValue(ctx) + + // Ensure prior warnings are preserved. + diags.Append(objectValueDiags...) + + return objectValue, diags +} + +func coerceSetValuable(_ context.Context, schemaPath path.Path, value attr.Value) (basetypes.SetValuable, diag.Diagnostics) { + setValuable, ok := value.(basetypes.SetValuable) if !ok { return types.SetNull(nil), diag.Diagnostics{ @@ -61,7 +106,22 @@ func coerceSetValue(ctx context.Context, schemaPath path.Path, value attr.Value) } } - return setVal.ToSetValue(ctx) + return setValuable, nil +} + +func coerceSetValue(ctx context.Context, schemaPath path.Path, value attr.Value) (types.Set, diag.Diagnostics) { + setValuable, diags := coerceSetValuable(ctx, schemaPath, value) + + if diags.HasError() { + return types.SetNull(nil), diags + } + + setValue, setValueDiags := setValuable.ToSetValue(ctx) + + // Ensure prior warnings are preserved. + diags.Append(setValueDiags...) + + return setValue, diags } func listElemObject(ctx context.Context, schemaPath path.Path, list types.List, index int, description fwschemadata.DataDescription) (types.Object, diag.Diagnostics) { diff --git a/internal/fwserver/attribute_plan_modification.go b/internal/fwserver/attribute_plan_modification.go index e044dc101..5c30cacb4 100644 --- a/internal/fwserver/attribute_plan_modification.go +++ b/internal/fwserver/attribute_plan_modification.go @@ -140,7 +140,23 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt // Use response as the planned value may have been modified with list // plan modifiers. - planList, diags := coerceListValue(ctx, req.AttributePath, resp.AttributePlan) + planListValuable, diags := coerceListValuable(ctx, req.AttributePath, resp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + typable, diags := coerceListTypable(ctx, req.AttributePath, planListValuable) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + planList, diags := planListValuable.ToListValue(ctx) resp.Diagnostics.Append(diags...) @@ -209,13 +225,26 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt resp.RequiresReplace.Append(objectResp.RequiresReplace...) } - resp.AttributePlan, diags = types.ListValue(planList.ElementType(ctx), planElements) + respValue, diags := types.ListValue(planList.ElementType(ctx), planElements) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + respValuable, diags := typable.ValueFromList(ctx, respValue) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.AttributePlan = respValuable case fwschema.NestingModeSet: configSet, diags := coerceSetValue(ctx, req.AttributePath, req.AttributeConfig) @@ -227,7 +256,23 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt // Use response as the planned value may have been modified with set // plan modifiers. - planSet, diags := coerceSetValue(ctx, req.AttributePath, resp.AttributePlan) + planSetValuable, diags := coerceSetValuable(ctx, req.AttributePath, resp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + typable, diags := coerceSetTypable(ctx, req.AttributePath, planSetValuable) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + planSet, diags := planSetValuable.ToSetValue(ctx) resp.Diagnostics.Append(diags...) @@ -296,13 +341,26 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt resp.RequiresReplace.Append(objectResp.RequiresReplace...) } - resp.AttributePlan, diags = types.SetValue(planSet.ElementType(ctx), planElements) + respValue, diags := types.SetValue(planSet.ElementType(ctx), planElements) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + respValuable, diags := typable.ValueFromSet(ctx, respValue) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + + resp.AttributePlan = respValuable case fwschema.NestingModeMap: configMap, diags := coerceMapValue(ctx, req.AttributePath, req.AttributeConfig) @@ -314,7 +372,23 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt // Use response as the planned value may have been modified with map // plan modifiers. - planMap, diags := coerceMapValue(ctx, req.AttributePath, resp.AttributePlan) + planMapValuable, diags := coerceMapValuable(ctx, req.AttributePath, resp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + typable, diags := coerceMapTypable(ctx, req.AttributePath, planMapValuable) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + planMap, diags := planMapValuable.ToMapValue(ctx) resp.Diagnostics.Append(diags...) @@ -383,13 +457,26 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt resp.RequiresReplace.Append(objectResp.RequiresReplace...) } - resp.AttributePlan, diags = types.MapValue(planMap.ElementType(ctx), planElements) + respValue, diags := types.MapValue(planMap.ElementType(ctx), planElements) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + respValuable, diags := typable.ValueFromMap(ctx, respValue) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + + resp.AttributePlan = respValuable case fwschema.NestingModeSingle: configObject, diags := coerceObjectValue(ctx, req.AttributePath, req.AttributeConfig) @@ -401,7 +488,23 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt // Use response as the planned value may have been modified with object // plan modifiers. - planObject, diags := coerceObjectValue(ctx, req.AttributePath, resp.AttributePlan) + planObjectValuable, diags := coerceObjectValuable(ctx, req.AttributePath, resp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + typable, diags := coerceObjectTypable(ctx, req.AttributePath, planObjectValuable) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + planObject, diags := planObjectValuable.ToObjectValue(ctx) resp.Diagnostics.Append(diags...) @@ -435,10 +538,30 @@ func AttributeModifyPlan(ctx context.Context, a fwschema.Attribute, req ModifyAt NestedAttributeObjectPlanModify(ctx, nestedAttributeObject, objectReq, objectResp) - resp.AttributePlan = objectResp.AttributePlan resp.Diagnostics.Append(objectResp.Diagnostics...) resp.Private = objectResp.Private resp.RequiresReplace.Append(objectResp.RequiresReplace...) + + respValue, diags := coerceObjectValue(ctx, req.AttributePath, objectResp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + respValuable, diags := typable.ValueFromObject(ctx, respValue) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.AttributePlan = respValuable default: err := fmt.Errorf("unknown attribute nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( @@ -532,6 +655,16 @@ func AttributePlanModifyBool(ctx context.Context, attribute fwxschema.AttributeW return } + typable, diags := coerceBoolTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.BoolRequest{ Config: req.Config, ConfigValue: configValue, @@ -570,8 +703,9 @@ func AttributePlanModifyBool(ctx context.Context, attribute fwxschema.AttributeW }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -583,6 +717,20 @@ func AttributePlanModifyBool(ctx context.Context, attribute fwxschema.AttributeW if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromBool(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -667,6 +815,16 @@ func AttributePlanModifyFloat64(ctx context.Context, attribute fwxschema.Attribu return } + typable, diags := coerceFloat64Typable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.Float64Request{ Config: req.Config, ConfigValue: configValue, @@ -705,8 +863,9 @@ func AttributePlanModifyFloat64(ctx context.Context, attribute fwxschema.Attribu }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -718,6 +877,20 @@ func AttributePlanModifyFloat64(ctx context.Context, attribute fwxschema.Attribu if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromFloat64(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -802,6 +975,16 @@ func AttributePlanModifyInt64(ctx context.Context, attribute fwxschema.Attribute return } + typable, diags := coerceInt64Typable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.Int64Request{ Config: req.Config, ConfigValue: configValue, @@ -840,8 +1023,9 @@ func AttributePlanModifyInt64(ctx context.Context, attribute fwxschema.Attribute }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -853,6 +1037,20 @@ func AttributePlanModifyInt64(ctx context.Context, attribute fwxschema.Attribute if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromInt64(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -937,6 +1135,16 @@ func AttributePlanModifyList(ctx context.Context, attribute fwxschema.AttributeW return } + typable, diags := coerceListTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.ListRequest{ Config: req.Config, ConfigValue: configValue, @@ -975,8 +1183,9 @@ func AttributePlanModifyList(ctx context.Context, attribute fwxschema.AttributeW }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -988,6 +1197,20 @@ func AttributePlanModifyList(ctx context.Context, attribute fwxschema.AttributeW if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromList(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -1072,6 +1295,16 @@ func AttributePlanModifyMap(ctx context.Context, attribute fwxschema.AttributeWi return } + typable, diags := coerceMapTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.MapRequest{ Config: req.Config, ConfigValue: configValue, @@ -1110,8 +1343,9 @@ func AttributePlanModifyMap(ctx context.Context, attribute fwxschema.AttributeWi }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -1123,6 +1357,20 @@ func AttributePlanModifyMap(ctx context.Context, attribute fwxschema.AttributeWi if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromMap(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -1207,6 +1455,16 @@ func AttributePlanModifyNumber(ctx context.Context, attribute fwxschema.Attribut return } + typable, diags := coerceNumberTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.NumberRequest{ Config: req.Config, ConfigValue: configValue, @@ -1245,8 +1503,9 @@ func AttributePlanModifyNumber(ctx context.Context, attribute fwxschema.Attribut }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -1258,6 +1517,20 @@ func AttributePlanModifyNumber(ctx context.Context, attribute fwxschema.Attribut if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromNumber(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -1342,6 +1615,16 @@ func AttributePlanModifyObject(ctx context.Context, attribute fwxschema.Attribut return } + typable, diags := coerceObjectTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.ObjectRequest{ Config: req.Config, ConfigValue: configValue, @@ -1380,8 +1663,9 @@ func AttributePlanModifyObject(ctx context.Context, attribute fwxschema.Attribut }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -1393,6 +1677,20 @@ func AttributePlanModifyObject(ctx context.Context, attribute fwxschema.Attribut if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromObject(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -1477,6 +1775,16 @@ func AttributePlanModifySet(ctx context.Context, attribute fwxschema.AttributeWi return } + typable, diags := coerceSetTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.SetRequest{ Config: req.Config, ConfigValue: configValue, @@ -1515,8 +1823,9 @@ func AttributePlanModifySet(ctx context.Context, attribute fwxschema.AttributeWi }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -1528,6 +1837,20 @@ func AttributePlanModifySet(ctx context.Context, attribute fwxschema.AttributeWi if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromSet(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -1612,6 +1935,16 @@ func AttributePlanModifyString(ctx context.Context, attribute fwxschema.Attribut return } + typable, diags := coerceStringTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.StringRequest{ Config: req.Config, ConfigValue: configValue, @@ -1650,8 +1983,9 @@ func AttributePlanModifyString(ctx context.Context, attribute fwxschema.Attribut }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -1663,12 +1997,26 @@ func AttributePlanModifyString(ctx context.Context, attribute fwxschema.Attribut if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromString(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } func NestedAttributeObjectPlanModify(ctx context.Context, o fwschema.NestedAttributeObject, req planmodifier.ObjectRequest, resp *ModifyAttributePlanResponse) { if objectWithPlanModifiers, ok := o.(fwxschema.NestedAttributeObjectWithPlanModifiers); ok { - for _, objectValidator := range objectWithPlanModifiers.ObjectPlanModifiers() { + for _, objectPlanModifier := range objectWithPlanModifiers.ObjectPlanModifiers() { // Instantiate a new response for each request to prevent plan modifiers // from modifying or removing diagnostics. planModifyResp := &planmodifier.ObjectResponse{ @@ -1680,17 +2028,17 @@ func NestedAttributeObjectPlanModify(ctx context.Context, o fwschema.NestedAttri ctx, "Calling provider defined planmodifier.Object", map[string]interface{}{ - logging.KeyDescription: objectValidator.Description(ctx), + logging.KeyDescription: objectPlanModifier.Description(ctx), }, ) - objectValidator.PlanModifyObject(ctx, req, planModifyResp) + objectPlanModifier.PlanModifyObject(ctx, req, planModifyResp) logging.FrameworkDebug( ctx, "Called provider defined planmodifier.Object", map[string]interface{}{ - logging.KeyDescription: objectValidator.Description(ctx), + logging.KeyDescription: objectPlanModifier.Description(ctx), }, ) diff --git a/internal/fwserver/attribute_plan_modification_test.go b/internal/fwserver/attribute_plan_modification_test.go index 45c7d0a9e..1d4d2e914 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/planmodifiers" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testplanmodifier" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" @@ -292,6 +293,92 @@ func TestAttributeModifyPlan(t *testing.T) { ), }, }, + "attribute-list-nested-usestateforunknown-custom-type": { + attribute: testschema.NestedAttributeWithListPlanModifiers{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_computed": testschema.Attribute{ + Type: types.StringType, + Computed: true, + }, + }, + }, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + Type: testtypes.ListTypeWithSemanticEquals{ + ListType: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + }, + }, + }, + req: ModifyAttributePlanRequest{ + AttributeConfig: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListNull( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributePath: path.Root("test"), + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListUnknown( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributeState: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + }, "attribute-list-nested-nested-usestateforunknown-elements-rearranged": { attribute: testschema.NestedAttribute{ NestedObject: testschema.NestedAttributeObject{ @@ -724,6 +811,92 @@ func TestAttributeModifyPlan(t *testing.T) { ), }, }, + "attribute-set-nested-usestateforunknown-custom-type": { + attribute: testschema.NestedAttributeWithSetPlanModifiers{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_computed": testschema.Attribute{ + Type: types.StringType, + Computed: true, + }, + }, + }, + Computed: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + Type: testtypes.SetTypeWithSemanticEquals{ + SetType: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + }, + }, + }, + req: ModifyAttributePlanRequest{ + AttributeConfig: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetNull( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributePath: path.Root("test"), + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetUnknown( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributeState: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + }, "attribute-set-nested-nested-usestateforunknown": { attribute: testschema.NestedAttribute{ NestedObject: testschema.NestedAttributeObject{ @@ -1297,6 +1470,92 @@ func TestAttributeModifyPlan(t *testing.T) { ), }, }, + "attribute-map-nested-usestateforunknown-custom-type": { + attribute: testschema.NestedAttributeWithMapPlanModifiers{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_computed": testschema.Attribute{ + Type: types.StringType, + Computed: true, + }, + }, + }, + Computed: true, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.UseStateForUnknown(), + }, + Type: testtypes.MapTypeWithSemanticEquals{ + MapType: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + }, + }, + }, + req: ModifyAttributePlanRequest{ + AttributeConfig: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapNull( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributePath: path.Root("test"), + AttributePlan: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapUnknown( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributeState: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + map[string]attr.Value{ + "key1": types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + map[string]attr.Value{ + "key1": types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + }, "attribute-single-nested-private": { attribute: testschema.NestedAttributeWithObjectPlanModifiers{ NestedObject: testschema.NestedAttributeObject{ @@ -1401,6 +1660,68 @@ func TestAttributeModifyPlan(t *testing.T) { ), }, }, + "attribute-single-nested-usestateforunknown-custom-type": { + attribute: testschema.NestedAttributeWithObjectPlanModifiers{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "nested_computed": testschema.Attribute{ + Type: types.StringType, + Computed: true, + }, + }, + }, + Computed: true, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + Type: testtypes.ObjectTypeWithSemanticEquals{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + }, + }, + req: ModifyAttributePlanRequest{ + AttributeConfig: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectNull( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + ), + }, + AttributePath: path.Root("test"), + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectUnknown( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + ), + }, + AttributeState: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + }, + }, "requires-replacement": { attribute: testschema.AttributeWithStringPlanModifiers{ Required: true, @@ -2108,6 +2429,39 @@ func TestAttributePlanModifyBool(t *testing.T) { AttributePlan: types.BoolValue(true), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithBoolPlanModifiers{ + PlanModifiers: []planmodifier.Bool{ + testplanmodifier.Bool{ + PlanModifyBoolMethod: func(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + resp.PlanValue = types.BoolValue(true) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.BoolValueWithSemanticEquals{ + BoolValue: types.BoolNull(), + }, + AttributePlan: testtypes.BoolValueWithSemanticEquals{ + BoolValue: types.BoolUnknown(), + }, + AttributeState: testtypes.BoolValueWithSemanticEquals{ + BoolValue: types.BoolNull(), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.BoolValueWithSemanticEquals{ + BoolValue: types.BoolUnknown(), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.BoolValueWithSemanticEquals{ + BoolValue: types.BoolValue(true), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithBoolPlanModifiers{ PlanModifiers: []planmodifier.Bool{ @@ -2711,6 +3065,39 @@ func TestAttributePlanModifyFloat64(t *testing.T) { AttributePlan: types.Float64Value(1.2), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithFloat64PlanModifiers{ + PlanModifiers: []planmodifier.Float64{ + testplanmodifier.Float64{ + PlanModifyFloat64Method: func(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + resp.PlanValue = types.Float64Value(1.2) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.Float64ValueWithSemanticEquals{ + Float64Value: types.Float64Null(), + }, + AttributePlan: testtypes.Float64ValueWithSemanticEquals{ + Float64Value: types.Float64Unknown(), + }, + AttributeState: testtypes.Float64ValueWithSemanticEquals{ + Float64Value: types.Float64Null(), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.Float64ValueWithSemanticEquals{ + Float64Value: types.Float64Unknown(), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.Float64ValueWithSemanticEquals{ + Float64Value: types.Float64Value(1.2), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithFloat64PlanModifiers{ PlanModifiers: []planmodifier.Float64{ @@ -3314,6 +3701,39 @@ func TestAttributePlanModifyInt64(t *testing.T) { AttributePlan: types.Int64Value(1), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithInt64PlanModifiers{ + PlanModifiers: []planmodifier.Int64{ + testplanmodifier.Int64{ + PlanModifyInt64Method: func(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + resp.PlanValue = types.Int64Value(1) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.Int64ValueWithSemanticEquals{ + Int64Value: types.Int64Null(), + }, + AttributePlan: testtypes.Int64ValueWithSemanticEquals{ + Int64Value: types.Int64Unknown(), + }, + AttributeState: testtypes.Int64ValueWithSemanticEquals{ + Int64Value: types.Int64Null(), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.Int64ValueWithSemanticEquals{ + Int64Value: types.Int64Unknown(), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.Int64ValueWithSemanticEquals{ + Int64Value: types.Int64Value(1), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithInt64PlanModifiers{ PlanModifiers: []planmodifier.Int64{ @@ -3935,6 +4355,39 @@ func TestAttributePlanModifyList(t *testing.T) { AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.PlanValue = types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListNull(types.StringType), + }, + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListUnknown(types.StringType), + }, + AttributeState: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListNull(types.StringType), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListUnknown(types.StringType), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithListPlanModifiers{ PlanModifiers: []planmodifier.List{ @@ -4783,6 +5236,49 @@ func TestAttributePlanModifyMap(t *testing.T) { ), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithMapPlanModifiers{ + PlanModifiers: []planmodifier.Map{ + testplanmodifier.Map{ + PlanModifyMapMethod: func(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + resp.PlanValue = types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapNull(types.StringType), + }, + AttributePlan: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapUnknown(types.StringType), + }, + AttributeState: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapNull(types.StringType), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapUnknown(types.StringType), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.MapValueWithSemanticEquals{ + MapValue: types.MapValueMust( + types.StringType, + map[string]attr.Value{ + "testkey": types.StringValue("testvalue"), + }, + ), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithMapPlanModifiers{ PlanModifiers: []planmodifier.Map{ @@ -5476,6 +5972,39 @@ func TestAttributePlanModifyNumber(t *testing.T) { AttributePlan: types.NumberValue(big.NewFloat(1)), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithNumberPlanModifiers{ + PlanModifiers: []planmodifier.Number{ + testplanmodifier.Number{ + PlanModifyNumberMethod: func(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + resp.PlanValue = types.NumberValue(big.NewFloat(1)) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.NumberValueWithSemanticEquals{ + NumberValue: types.NumberNull(), + }, + AttributePlan: testtypes.NumberValueWithSemanticEquals{ + NumberValue: types.NumberUnknown(), + }, + AttributeState: testtypes.NumberValueWithSemanticEquals{ + NumberValue: types.NumberNull(), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.NumberValueWithSemanticEquals{ + NumberValue: types.NumberUnknown(), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.NumberValueWithSemanticEquals{ + NumberValue: types.NumberValue(big.NewFloat(1)), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithNumberPlanModifiers{ PlanModifiers: []planmodifier.Number{ @@ -6490,6 +7019,61 @@ func TestAttributePlanModifyObject(t *testing.T) { ), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.PlanValue = types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectUnknown(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + AttributeState: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectUnknown(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithObjectPlanModifiers{ PlanModifiers: []planmodifier.Object{ @@ -7241,6 +7825,39 @@ func TestAttributePlanModifySet(t *testing.T) { AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.PlanValue = types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetNull(types.StringType), + }, + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetUnknown(types.StringType), + }, + AttributeState: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetNull(types.StringType), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetUnknown(types.StringType), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithSetPlanModifiers{ PlanModifiers: []planmodifier.Set{ @@ -7844,6 +8461,39 @@ func TestAttributePlanModifyString(t *testing.T) { AttributePlan: types.StringValue("testvalue"), }, }, + "response-planvalue-custom-type": { + attribute: testschema.AttributeWithStringPlanModifiers{ + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.PlanValue = types.StringValue("testvalue") + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringNull(), + }, + AttributePlan: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringUnknown(), + }, + AttributeState: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringNull(), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringUnknown(), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.StringValueWithSemanticEquals{ + StringValue: types.StringValue("testvalue"), + }, + }, + }, "response-private": { attribute: testschema.AttributeWithStringPlanModifiers{ PlanModifiers: []planmodifier.String{ diff --git a/internal/fwserver/block_plan_modification.go b/internal/fwserver/block_plan_modification.go index ab95de950..b5a6dec54 100644 --- a/internal/fwserver/block_plan_modification.go +++ b/internal/fwserver/block_plan_modification.go @@ -60,7 +60,23 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req ModifyAttributeP // Use response as the planned value may have been modified with list // plan modifiers. - planList, diags := coerceListValue(ctx, req.AttributePath, resp.AttributePlan) + planListValuable, diags := coerceListValuable(ctx, req.AttributePath, resp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + typable, diags := coerceListTypable(ctx, req.AttributePath, planListValuable) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + planList, diags := planListValuable.ToListValue(ctx) resp.Diagnostics.Append(diags...) @@ -129,13 +145,26 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req ModifyAttributeP resp.RequiresReplace.Append(objectResp.RequiresReplace...) } - resp.AttributePlan, diags = types.ListValue(planList.ElementType(ctx), planElements) + respValue, diags := types.ListValue(planList.ElementType(ctx), planElements) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + respValuable, diags := typable.ValueFromList(ctx, respValue) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.AttributePlan = respValuable case fwschema.BlockNestingModeSet: configSet, diags := coerceSetValue(ctx, req.AttributePath, req.AttributeConfig) @@ -147,7 +176,23 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req ModifyAttributeP // Use response as the planned value may have been modified with set // plan modifiers. - planSet, diags := coerceSetValue(ctx, req.AttributePath, resp.AttributePlan) + planSetValuable, diags := coerceSetValuable(ctx, req.AttributePath, resp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + typable, diags := coerceSetTypable(ctx, req.AttributePath, planSetValuable) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + planSet, diags := planSetValuable.ToSetValue(ctx) resp.Diagnostics.Append(diags...) @@ -216,13 +261,26 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req ModifyAttributeP resp.RequiresReplace.Append(objectResp.RequiresReplace...) } - resp.AttributePlan, diags = types.SetValue(planSet.ElementType(ctx), planElements) + respValue, diags := types.SetValue(planSet.ElementType(ctx), planElements) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + respValuable, diags := typable.ValueFromSet(ctx, respValue) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } + + resp.AttributePlan = respValuable case fwschema.BlockNestingModeSingle: configObject, diags := coerceObjectValue(ctx, req.AttributePath, req.AttributeConfig) @@ -234,7 +292,23 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req ModifyAttributeP // Use response as the planned value may have been modified with object // plan modifiers. - planObject, diags := coerceObjectValue(ctx, req.AttributePath, resp.AttributePlan) + planObjectValuable, diags := coerceObjectValuable(ctx, req.AttributePath, resp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + typable, diags := coerceObjectTypable(ctx, req.AttributePath, planObjectValuable) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + planObject, diags := planObjectValuable.ToObjectValue(ctx) resp.Diagnostics.Append(diags...) @@ -268,10 +342,30 @@ func BlockModifyPlan(ctx context.Context, b fwschema.Block, req ModifyAttributeP NestedBlockObjectPlanModify(ctx, nestedBlockObject, objectReq, objectResp) - resp.AttributePlan = objectResp.AttributePlan resp.Diagnostics.Append(objectResp.Diagnostics...) resp.Private = objectResp.Private resp.RequiresReplace.Append(objectResp.RequiresReplace...) + + respValue, diags := coerceObjectValue(ctx, req.AttributePath, objectResp.AttributePlan) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + respValuable, diags := typable.ValueFromObject(ctx, respValue) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.AttributePlan = respValuable default: err := fmt.Errorf("unknown block plan modification nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath) resp.Diagnostics.AddAttributeError( @@ -365,6 +459,16 @@ func BlockPlanModifyList(ctx context.Context, block fwxschema.BlockWithListPlanM return } + typable, diags := coerceListTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.ListRequest{ Config: req.Config, ConfigValue: configValue, @@ -403,8 +507,9 @@ func BlockPlanModifyList(ctx context.Context, block fwxschema.BlockWithListPlanM }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -416,6 +521,20 @@ func BlockPlanModifyList(ctx context.Context, block fwxschema.BlockWithListPlanM if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromList(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -500,6 +619,16 @@ func BlockPlanModifyObject(ctx context.Context, block fwxschema.BlockWithObjectP return } + typable, diags := coerceObjectTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.ObjectRequest{ Config: req.Config, ConfigValue: configValue, @@ -538,8 +667,9 @@ func BlockPlanModifyObject(ctx context.Context, block fwxschema.BlockWithObjectP }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -551,6 +681,20 @@ func BlockPlanModifyObject(ctx context.Context, block fwxschema.BlockWithObjectP if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromObject(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } @@ -635,6 +779,16 @@ func BlockPlanModifySet(ctx context.Context, block fwxschema.BlockWithSetPlanMod return } + typable, diags := coerceSetTypable(ctx, req.AttributePath, planValuable) + + resp.Diagnostics.Append(diags...) + + // Only return early on new errors as the resp.Diagnostics may have errors + // from other attributes. + if diags.HasError() { + return + } + planModifyReq := planmodifier.SetRequest{ Config: req.Config, ConfigValue: configValue, @@ -673,8 +827,9 @@ func BlockPlanModifySet(ctx context.Context, block fwxschema.BlockWithSetPlanMod }, ) + // Prepare next request with base type. planModifyReq.PlanValue = planModifyResp.PlanValue - resp.AttributePlan = planModifyResp.PlanValue + resp.Diagnostics.Append(planModifyResp.Diagnostics...) resp.Private = planModifyResp.Private @@ -686,12 +841,26 @@ func BlockPlanModifySet(ctx context.Context, block fwxschema.BlockWithSetPlanMod if planModifyResp.Diagnostics.HasError() { return } + + // A custom value type must be returned in the final response to prevent + // later correctness errors. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/754 + valuable, valueFromDiags := typable.ValueFromSet(ctx, planModifyResp.PlanValue) + + resp.Diagnostics.Append(valueFromDiags...) + + // Only on new errors. + if valueFromDiags.HasError() { + return + } + + resp.AttributePlan = valuable } } func NestedBlockObjectPlanModify(ctx context.Context, o fwschema.NestedBlockObject, req planmodifier.ObjectRequest, resp *ModifyAttributePlanResponse) { if objectWithPlanModifiers, ok := o.(fwxschema.NestedBlockObjectWithPlanModifiers); ok { - for _, objectValidator := range objectWithPlanModifiers.ObjectPlanModifiers() { + for _, objectPlanModifier := range objectWithPlanModifiers.ObjectPlanModifiers() { // Instantiate a new response for each request to prevent plan modifiers // from modifying or removing diagnostics. planModifyResp := &planmodifier.ObjectResponse{ @@ -703,17 +872,17 @@ func NestedBlockObjectPlanModify(ctx context.Context, o fwschema.NestedBlockObje ctx, "Calling provider defined planmodifier.Object", map[string]interface{}{ - logging.KeyDescription: objectValidator.Description(ctx), + logging.KeyDescription: objectPlanModifier.Description(ctx), }, ) - objectValidator.PlanModifyObject(ctx, req, planModifyResp) + objectPlanModifier.PlanModifyObject(ctx, req, planModifyResp) logging.FrameworkDebug( ctx, "Called provider defined planmodifier.Object", map[string]interface{}{ - logging.KeyDescription: objectValidator.Description(ctx), + logging.KeyDescription: objectPlanModifier.Description(ctx), }, ) diff --git a/internal/fwserver/block_plan_modification_test.go b/internal/fwserver/block_plan_modification_test.go index b44b943a8..b979294af 100644 --- a/internal/fwserver/block_plan_modification_test.go +++ b/internal/fwserver/block_plan_modification_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/planmodifiers" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testplanmodifier" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" @@ -650,6 +651,89 @@ func TestBlockModifyPlan(t *testing.T) { ), }, }, + "block-list-usestateforunknown-custom-type": { + block: testschema.BlockWithListPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "nested_computed": testschema.Attribute{ + Type: types.StringType, + Computed: true, + }, + }, + CustomType: testtypes.ListTypeWithSemanticEquals{ + ListType: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + }, + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + }, + req: ModifyAttributePlanRequest{ + AttributeConfig: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListNull( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributePath: path.Root("test"), + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListUnknown( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributeState: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + }, "block-set-null-plan": { block: testschema.BlockWithSetPlanModifiers{ Attributes: map[string]fwschema.Attribute{ @@ -1885,6 +1969,89 @@ func TestBlockModifyPlan(t *testing.T) { ), }, }, + "block-set-usestateforunknown-custom-type": { + block: testschema.BlockWithSetPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "nested_computed": testschema.Attribute{ + Type: types.StringType, + Computed: true, + }, + }, + CustomType: testtypes.SetTypeWithSemanticEquals{ + SetType: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + }, + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + }, + req: ModifyAttributePlanRequest{ + AttributeConfig: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetNull( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributePath: path.Root("test"), + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetUnknown( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + ), + }, + AttributeState: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetValueMust( + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + ), + }, + }, + }, "block-single-null-plan": { block: testschema.BlockWithObjectPlanModifiers{ Attributes: map[string]fwschema.Attribute{ @@ -2146,6 +2313,65 @@ func TestBlockModifyPlan(t *testing.T) { ), }, }, + "block-single-usestateforunknown-custom-type": { + block: testschema.BlockWithObjectPlanModifiers{ + Attributes: map[string]fwschema.Attribute{ + "nested_computed": testschema.Attribute{ + Type: types.StringType, + Computed: true, + }, + }, + CustomType: testtypes.ObjectTypeWithSemanticEquals{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "nested_computed": types.StringType, + }, + }, + }, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.UseStateForUnknown(), + }, + }, + req: ModifyAttributePlanRequest{ + AttributeConfig: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectNull( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + ), + }, + AttributePath: path.Root("test"), + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectUnknown( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + ), + }, + AttributeState: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + }, + expectedResp: ModifyAttributePlanResponse{ + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectValueMust( + map[string]attr.Type{ + "nested_computed": types.StringType, + }, + map[string]attr.Value{ + "nested_computed": types.StringValue("statevalue1"), + }, + ), + }, + }, + }, "block-requires-replacement": { block: testschema.BlockWithListPlanModifiers{ Attributes: map[string]fwschema.Attribute{ @@ -3353,6 +3579,39 @@ func TestBlockPlanModifyList(t *testing.T) { AttributePlan: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), }, }, + "response-planvalue-custom-type": { + block: testschema.BlockWithListPlanModifiers{ + PlanModifiers: []planmodifier.List{ + testplanmodifier.List{ + PlanModifyListMethod: func(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + resp.PlanValue = types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListNull(types.StringType), + }, + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListUnknown(types.StringType), + }, + AttributeState: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListNull(types.StringType), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListUnknown(types.StringType), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.ListValueWithSemanticEquals{ + ListValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + }, "response-private": { block: testschema.BlockWithListPlanModifiers{ PlanModifiers: []planmodifier.List{ @@ -4367,6 +4626,61 @@ func TestBlockPlanModifyObject(t *testing.T) { ), }, }, + "response-planvalue-custom-type": { + block: testschema.BlockWithObjectPlanModifiers{ + PlanModifiers: []planmodifier.Object{ + testplanmodifier.Object{ + PlanModifyObjectMethod: func(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + resp.PlanValue = types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectUnknown(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + AttributeState: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectNull(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectUnknown(map[string]attr.Type{ + "testattr": types.StringType, + }), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.ObjectValueWithSemanticEquals{ + ObjectValue: types.ObjectValueMust( + map[string]attr.Type{ + "testattr": types.StringType, + }, + map[string]attr.Value{ + "testattr": types.StringValue("testvalue"), + }, + ), + }, + }, + }, "response-private": { block: testschema.BlockWithObjectPlanModifiers{ PlanModifiers: []planmodifier.Object{ @@ -5118,6 +5432,39 @@ func TestBlockPlanModifySet(t *testing.T) { AttributePlan: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), }, }, + "response-planvalue-custom-type": { + block: testschema.BlockWithSetPlanModifiers{ + PlanModifiers: []planmodifier.Set{ + testplanmodifier.Set{ + PlanModifySetMethod: func(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + resp.PlanValue = types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}) + }, + }, + }, + }, + request: ModifyAttributePlanRequest{ + AttributePath: path.Root("test"), + AttributeConfig: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetNull(types.StringType), + }, + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetUnknown(types.StringType), + }, + AttributeState: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetNull(types.StringType), + }, + }, + response: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetUnknown(types.StringType), + }, + }, + expected: &ModifyAttributePlanResponse{ + AttributePlan: testtypes.SetValueWithSemanticEquals{ + SetValue: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("testvalue")}), + }, + }, + }, "response-private": { block: testschema.BlockWithSetPlanModifiers{ PlanModifiers: []planmodifier.Set{ diff --git a/internal/fwserver/diagnostics.go b/internal/fwserver/diagnostics.go index c6ebc2ef8..c85f1db77 100644 --- a/internal/fwserver/diagnostics.go +++ b/internal/fwserver/diagnostics.go @@ -13,6 +13,18 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" ) +func attributePlanModificationTypableError(schemaPath path.Path, value attr.Value) diag.Diagnostic { + return diag.NewAttributeErrorDiagnostic( + schemaPath, + "Unexpected Attribute Plan Modifier Type Conversion Error", + "An unexpected issue occurred while trying to get the correct type during attribute plan modification. "+ + "Expected the Valuable implementation Type() method to return a Typable. "+ + "This is likely an implementation error in terraform-plugin-framework and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Value Type: %T\n", value)+ + fmt.Sprintf("Path: %s", schemaPath), + ) +} + func schemaDataValueError(ctx context.Context, value attr.Value, description fwschemadata.DataDescription, err error) diag.Diagnostic { return diag.NewErrorDiagnostic( description.Title()+" Value Error", diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index b5fe1a67d..dceb48720 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testplanmodifier" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -663,6 +664,28 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testSchemaAttributePlanModifierAttributePlanCustomType := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + CustomType: testtypes.StringTypeWithSemanticEquals{}, + PlanModifiers: []planmodifier.String{ + testplanmodifier.String{ + PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + resp.PlanValue = types.StringValue("test-attributeplanmodifier-value") + }, + }, + }, + }, + "test_other_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + testSchemaAttributePlanModifierPrivatePlanRequest := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -1420,6 +1443,70 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-attributeplanmodifier-response-attributeplan-custom-type": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_other_computed": tftypes.String, + "test_required": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_other_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierAttributePlanCustomType, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_other_computed": tftypes.String, + "test_required": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_other_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierAttributePlanCustomType, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_other_computed": tftypes.String, + "test_required": tftypes.String, + }, + }, nil), + Schema: testSchemaAttributePlanModifierAttributePlanCustomType, + }, + ResourceSchema: testSchemaAttributePlanModifierAttributePlanCustomType, + Resource: &testprovider.Resource{}, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_computed": tftypes.String, + "test_other_computed": tftypes.String, + "test_required": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-attributeplanmodifier-value"), + "test_other_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchemaAttributePlanModifierAttributePlanCustomType, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "create-attributeplanmodifier-response-privateplan": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/testing/testschema/block.go b/internal/testing/testschema/block.go index e01cf5769..51bb04d14 100644 --- a/internal/testing/testschema/block.go +++ b/internal/testing/testschema/block.go @@ -13,6 +13,7 @@ import ( var _ fwschema.Block = Block{} type Block struct { + CustomType attr.Type DeprecationMessage string Description string MarkdownDescription string @@ -63,6 +64,10 @@ func (b Block) GetNestingMode() fwschema.BlockNestingMode { // Type satisfies the fwschema.Block interface. func (b Block) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + switch b.GetNestingMode() { case fwschema.BlockNestingModeList: return types.ListType{ diff --git a/internal/testing/testschema/blockwithlistplanmodifiers.go b/internal/testing/testschema/blockwithlistplanmodifiers.go index 1057ab9b9..20572843b 100644 --- a/internal/testing/testschema/blockwithlistplanmodifiers.go +++ b/internal/testing/testschema/blockwithlistplanmodifiers.go @@ -17,6 +17,7 @@ var _ fwxschema.BlockWithListPlanModifiers = BlockWithListPlanModifiers{} type BlockWithListPlanModifiers struct { Attributes map[string]fwschema.Attribute Blocks map[string]fwschema.Block + CustomType attr.Type DeprecationMessage string Description string MarkdownDescription string @@ -74,6 +75,10 @@ func (b BlockWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { // Type satisfies the fwschema.Block interface. func (b BlockWithListPlanModifiers) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + return types.ListType{ ElemType: b.GetNestedObject().Type(), } diff --git a/internal/testing/testschema/blockwithobjectplanmodifiers.go b/internal/testing/testschema/blockwithobjectplanmodifiers.go index 056ce87f6..a4e7ed3d1 100644 --- a/internal/testing/testschema/blockwithobjectplanmodifiers.go +++ b/internal/testing/testschema/blockwithobjectplanmodifiers.go @@ -16,6 +16,7 @@ var _ fwxschema.BlockWithObjectPlanModifiers = BlockWithObjectPlanModifiers{} type BlockWithObjectPlanModifiers struct { Attributes map[string]fwschema.Attribute Blocks map[string]fwschema.Block + CustomType attr.Type DeprecationMessage string Description string MarkdownDescription string @@ -74,5 +75,9 @@ func (b BlockWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Objec // Type satisfies the fwschema.Block interface. func (b BlockWithObjectPlanModifiers) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + return b.GetNestedObject().Type() } diff --git a/internal/testing/testschema/blockwithsetplanmodifiers.go b/internal/testing/testschema/blockwithsetplanmodifiers.go index 5c180dcbc..e3b37247f 100644 --- a/internal/testing/testschema/blockwithsetplanmodifiers.go +++ b/internal/testing/testschema/blockwithsetplanmodifiers.go @@ -17,6 +17,7 @@ var _ fwxschema.BlockWithSetPlanModifiers = BlockWithSetPlanModifiers{} type BlockWithSetPlanModifiers struct { Attributes map[string]fwschema.Attribute Blocks map[string]fwschema.Block + CustomType attr.Type DeprecationMessage string Description string MarkdownDescription string @@ -74,6 +75,10 @@ func (b BlockWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { // Type satisfies the fwschema.Block interface. func (b BlockWithSetPlanModifiers) Type() attr.Type { + if b.CustomType != nil { + return b.CustomType + } + return types.SetType{ ElemType: b.GetNestedObject().Type(), } From a726659f3ab507049048a568137affb0969c4b57 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 13 Jun 2023 13:38:41 -0400 Subject: [PATCH 2/2] internal/fwserver: Add missing copyright header --- internal/fwserver/attr_type.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/fwserver/attr_type.go b/internal/fwserver/attr_type.go index 42647b13c..74ab45fdd 100644 --- a/internal/fwserver/attr_type.go +++ b/internal/fwserver/attr_type.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package fwserver import (