diff --git a/internal/generic/collection_plan_modifiers.go b/internal/generic/collection_plan_modifiers.go new file mode 100644 index 0000000000..1bf78159bf --- /dev/null +++ b/internal/generic/collection_plan_modifiers.go @@ -0,0 +1,78 @@ +// Package generic provides custom plan modifiers to address shadow drift issues. +// This file contains collection type plan modifiers. +package generic + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// CustomUseStateForUnknownList returns a plan modifier for list attributes. +func CustomUseStateForUnknownList() planmodifier.List { + return customUseStateForUnknownListModifier{} +} + +type customUseStateForUnknownListModifier struct{} + +func (m customUseStateForUnknownListModifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownListModifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownListModifier) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} + +// CustomUseStateForUnknownSet returns a plan modifier for set attributes. +func CustomUseStateForUnknownSet() planmodifier.Set { + return customUseStateForUnknownSetModifier{} +} + +type customUseStateForUnknownSetModifier struct{} + +func (m customUseStateForUnknownSetModifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownSetModifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownSetModifier) PlanModifySet(ctx context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} + +// CustomUseStateForUnknownMap returns a plan modifier for map attributes. +func CustomUseStateForUnknownMap() planmodifier.Map { + return customUseStateForUnknownMapModifier{} +} + +type customUseStateForUnknownMapModifier struct{} + +func (m customUseStateForUnknownMapModifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownMapModifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownMapModifier) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} diff --git a/internal/generic/custom-plan-modifiers.md b/internal/generic/custom-plan-modifiers.md new file mode 100644 index 0000000000..4105678e9c --- /dev/null +++ b/internal/generic/custom-plan-modifiers.md @@ -0,0 +1,54 @@ +# Custom Plan Modifiers + +This package provides custom plan modifiers to address shadow drift issues in the AWSCC provider. These replace the framework's `UseStateForUnknown` to avoid false positive drift detection. + +**Reference**: https://github.com/hashicorp/terraform-provider-awscc/issues/2726 + +## File Structure + +| File | Types | Functions | +|------|-------|-----------| +| `string_plan_modifiers.go` | String, Bool | `CustomUseStateForUnknownString()`, `CustomUseStateForUnknownBool()` | +| `numeric_plan_modifiers.go` | Int64, Float64, Number | `CustomUseStateForUnknownInt64()`, `CustomUseStateForUnknownFloat64()`, `CustomUseStateForUnknownNumber()` | +| `collection_plan_modifiers.go` | List, Set, Map | `CustomUseStateForUnknownList()`, `CustomUseStateForUnknownSet()`, `CustomUseStateForUnknownMap()` | +| `object_plan_modifiers.go` | Object | `CustomUseStateForUnknownObject()` | + +## Usage + +```go +import "github.com/hashicorp/terraform-provider-awscc/internal/generic" + +// String attribute +"name": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + generic.CustomUseStateForUnknownString(), + }, +}, + +// Integer attribute +"port": schema.Int64Attribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + generic.CustomUseStateForUnknownInt64(), + }, +}, + +// Object attribute +"configuration": schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Object{ + generic.CustomUseStateForUnknownObject(), + }, +}, +``` + +## How It Works + +These custom modifiers prevent shadow drift by: +1. Using the state value when configuration is null +2. Preventing the framework from marking attributes as "unknown" +3. Avoiding false positive drift detection in computed attributes diff --git a/internal/generic/numeric_plan_modifiers.go b/internal/generic/numeric_plan_modifiers.go new file mode 100644 index 0000000000..b41e7a488b --- /dev/null +++ b/internal/generic/numeric_plan_modifiers.go @@ -0,0 +1,78 @@ +// Package generic provides custom plan modifiers to address shadow drift issues. +// This file contains numeric type plan modifiers. +package generic + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// CustomUseStateForUnknownInt64 returns a plan modifier for int64 attributes. +func CustomUseStateForUnknownInt64() planmodifier.Int64 { + return customUseStateForUnknownInt64Modifier{} +} + +type customUseStateForUnknownInt64Modifier struct{} + +func (m customUseStateForUnknownInt64Modifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownInt64Modifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownInt64Modifier) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} + +// CustomUseStateForUnknownFloat64 returns a plan modifier for float64 attributes. +func CustomUseStateForUnknownFloat64() planmodifier.Float64 { + return customUseStateForUnknownFloat64Modifier{} +} + +type customUseStateForUnknownFloat64Modifier struct{} + +func (m customUseStateForUnknownFloat64Modifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownFloat64Modifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownFloat64Modifier) PlanModifyFloat64(ctx context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} + +// CustomUseStateForUnknownNumber returns a plan modifier for number attributes. +func CustomUseStateForUnknownNumber() planmodifier.Number { + return customUseStateForUnknownNumberModifier{} +} + +type customUseStateForUnknownNumberModifier struct{} + +func (m customUseStateForUnknownNumberModifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownNumberModifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownNumberModifier) PlanModifyNumber(ctx context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} diff --git a/internal/generic/object_plan_modifiers.go b/internal/generic/object_plan_modifiers.go new file mode 100644 index 0000000000..f24aea6d36 --- /dev/null +++ b/internal/generic/object_plan_modifiers.go @@ -0,0 +1,32 @@ +// Package generic provides custom plan modifiers to address shadow drift issues. +// This file contains complex object type plan modifiers. +package generic + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// CustomUseStateForUnknownObject returns a plan modifier for object attributes. +func CustomUseStateForUnknownObject() planmodifier.Object { + return customUseStateForUnknownObjectModifier{} +} + +type customUseStateForUnknownObjectModifier struct{} + +func (m customUseStateForUnknownObjectModifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownObjectModifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownObjectModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} diff --git a/internal/generic/string_plan_modifiers.go b/internal/generic/string_plan_modifiers.go new file mode 100644 index 0000000000..034f7abedc --- /dev/null +++ b/internal/generic/string_plan_modifiers.go @@ -0,0 +1,57 @@ +// Package generic provides custom plan modifiers to address shadow drift issues. +// These replace the framework's UseStateForUnknown to avoid false positive drift detection. +// Reference: https://github.com/hashicorp/terraform-provider-awscc/issues/2726 +package generic + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// CustomUseStateForUnknownString returns a plan modifier that prevents shadow drift +// by using the state value when configuration is null. +func CustomUseStateForUnknownString() planmodifier.String { + return customUseStateForUnknownModifier{} +} + +type customUseStateForUnknownModifier struct{} + +func (m customUseStateForUnknownModifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownModifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} + +// CustomUseStateForUnknownBool returns a plan modifier for boolean attributes. +func CustomUseStateForUnknownBool() planmodifier.Bool { + return customUseStateForUnknownBoolModifier{} +} + +type customUseStateForUnknownBoolModifier struct{} + +func (m customUseStateForUnknownBoolModifier) Description(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownBoolModifier) MarkdownDescription(ctx context.Context) string { + return "If configuration is null, use the state value to avoid shadow drift." +} + +func (m customUseStateForUnknownBoolModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + if !req.ConfigValue.IsNull() { + return + } + // Use state value to prevent framework's "unknown" marking + resp.PlanValue = req.StateValue +} diff --git a/internal/provider/generators/shared/codegen/emitter.go b/internal/provider/generators/shared/codegen/emitter.go index c1fdc87354..2bad3b56e9 100644 --- a/internal/provider/generators/shared/codegen/emitter.go +++ b/internal/provider/generators/shared/codegen/emitter.go @@ -756,8 +756,15 @@ func (e Emitter) emitAttribute(tfType string, attributeNameMap map[string]string if computed && !parentComputedOnly { // Computed. // If our parent is Computed-only (and hence we are) then we don't need our own plan modifier. - planModifiers = append(planModifiers, fmt.Sprintf("%s.UseStateForUnknown()", fwPlanModifierPackage)) - features.FrameworkPlanModifierPackages = append(features.FrameworkPlanModifierPackages, fwPlanModifierPackage) + + if computedAndOptional { + // Optional+Computed attributes: Use custom modifier to prevent false drift detection + planModifiers = append(planModifiers, fmt.Sprintf("generic.CustomUseStateForUnknown%s()", fwPlanModifierType)) + } else { + // Read-only computed attributes: Use framework modifier (existing behavior) + planModifiers = append(planModifiers, fmt.Sprintf("%s.UseStateForUnknown()", fwPlanModifierPackage)) + features.FrameworkPlanModifierPackages = append(features.FrameworkPlanModifierPackages, fwPlanModifierPackage) + } } if createOnly {