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..74ab45fdd --- /dev/null +++ b/internal/fwserver/attr_type.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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(), }