Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions internal/generic/collection_plan_modifiers.go
Original file line number Diff line number Diff line change
@@ -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
}
54 changes: 54 additions & 0 deletions internal/generic/custom-plan-modifiers.md
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions internal/generic/numeric_plan_modifiers.go
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions internal/generic/object_plan_modifiers.go
Original file line number Diff line number Diff line change
@@ -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
}
57 changes: 57 additions & 0 deletions internal/generic/string_plan_modifiers.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 9 additions & 2 deletions internal/provider/generators/shared/codegen/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,8 +756,15 @@
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 {
Expand Down Expand Up @@ -916,7 +923,7 @@
uniqueItems = *property.UniqueItems
}

if uniqueItems && !insertionOrder {

Check failure on line 926 in internal/provider/generators/shared/codegen/emitter.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofmt)
return aggregateSet
}

Expand Down
Loading