Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource/schema: New packages which contain type-specific schema plan modifier implementations #565

Merged
merged 2 commits into from Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changelog/565.txt
@@ -0,0 +1,15 @@
```release-note:feature
resource/schema: New packages, such as `stringplanmodifier` which contain type-specific schema plan modifier implementations
```

```release-note:note
resource: The `RequiresReplace()` plan modifier has been deprecated. Use a type-specific plan modifier instead, such as `resource/schema/stringplanmodifier.RequiresReplace()` or `resource/schema/stringplanmodifier.RequiresReplaceIfConfigured()`
```

```release-note:note
resource: The `RequiresReplaceIf()` plan modifier has been deprecated. Use a type-specific plan modifier instead, such as `resource/schema/stringplanmodifier.RequiresReplaceIf()`
```

```release-note:note
resource: The `UseStateForUnknown()` plan modifier has been deprecated. Use a type-specific plan modifier instead, such as `resource/schema/stringplanmodifier.UseStateForUnknown()`
```
2 changes: 2 additions & 0 deletions resource/schema/boolplanmodifier/doc.go
@@ -0,0 +1,2 @@
// Package boolplanmodifier provides plan modifiers for types.Bool attributes.
package boolplanmodifier
27 changes: 27 additions & 0 deletions resource/schema/boolplanmodifier/requires_replace.go
@@ -0,0 +1,27 @@
package boolplanmodifier

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

// RequiresReplace returns a plan modifier that conditionally requires
// resource replacement if:
//
// - The resource is planned for update.
// - The plan and state values are not equal.
//
// Use RequiresReplaceIfConfigured if the resource replacement should
// only occur if there is a configuration value (ignore unconfigured drift
// detection changes). Use RequiresReplaceIf if the resource replacement
// should check provider-defined conditional logic.
func RequiresReplace() planmodifier.Bool {
return RequiresReplaceIf(
func(_ context.Context, _ planmodifier.BoolRequest, resp *RequiresReplaceIfFuncResponse) {
resp.RequiresReplace = true
},
"If the value of this attribute changes, Terraform will destroy and recreate the resource.",
"If the value of this attribute changes, Terraform will destroy and recreate the resource.",
)
}
70 changes: 70 additions & 0 deletions resource/schema/boolplanmodifier/requires_replace_if.go
@@ -0,0 +1,70 @@
package boolplanmodifier

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

// RequiresReplaceIf returns a plan modifier that conditionally requires
// resource replacement if:
//
// - The resource is planned for update.
// - The plan and state values are not equal.
// - The given function returns true. Returning false will not unset any
// prior resource replacement.
//
// Use RequiresReplace if the resource replacement should always occur on value
// changes. Use RequiresReplaceIfConfigured if the resource replacement should
// occur on value changes, but only if there is a configuration value (ignore
// unconfigured drift detection changes).
func RequiresReplaceIf(f RequiresReplaceIfFunc, description, markdownDescription string) planmodifier.Bool {
return requiresReplaceIfModifier{
ifFunc: f,
description: description,
markdownDescription: markdownDescription,
}
}

// requiresReplaceIfModifier is an plan modifier that sets RequiresReplace
// on the attribute if a given function is true.
type requiresReplaceIfModifier struct {
ifFunc RequiresReplaceIfFunc
description string
markdownDescription string
}

// Description returns a human-readable description of the plan modifier.
func (m requiresReplaceIfModifier) Description(_ context.Context) string {
return m.description
}

// MarkdownDescription returns a markdown description of the plan modifier.
func (m requiresReplaceIfModifier) MarkdownDescription(_ context.Context) string {
return m.markdownDescription
}

// PlanModifyBool implements the plan modification logic.
func (m requiresReplaceIfModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
// Do not replace on resource creation.
if req.State.Raw.IsNull() {
return
}

// Do not replace on resource destroy.
if req.Plan.Raw.IsNull() {
return
}

// Do not replace if the plan and state values are equal.
if req.PlanValue.Equal(req.StateValue) {
return
}

ifFuncResp := &RequiresReplaceIfFuncResponse{}

m.ifFunc(ctx, req, ifFuncResp)

resp.Diagnostics.Append(ifFuncResp.Diagnostics...)
resp.RequiresReplace = ifFuncResp.RequiresReplace
}
@@ -0,0 +1,31 @@
package boolplanmodifier

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

// RequiresReplaceIfConfigured returns a plan modifier that conditionally requires
// resource replacement if:
//
// - The resource is planned for update.
// - The plan and state values are not equal.
// - The configuration value is not null.
//
// Use RequiresReplace if the resource replacement should occur regardless of
// the presence of a configuration value. Use RequiresReplaceIf if the resource
// replacement should check provider-defined conditional logic.
func RequiresReplaceIfConfigured() planmodifier.Bool {
return RequiresReplaceIf(
func(_ context.Context, req planmodifier.BoolRequest, resp *RequiresReplaceIfFuncResponse) {
if req.ConfigValue.IsNull() {
return
}

resp.RequiresReplace = true
},
"If the value of this attribute is configured and changes, Terraform will destroy and recreate the resource.",
"If the value of this attribute is configured and changes, Terraform will destroy and recreate the resource.",
)
}
@@ -0,0 +1,167 @@
package boolplanmodifier_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func TestRequiresReplaceIfConfiguredModifierPlanModifyBool(t *testing.T) {
t.Parallel()

testSchema := schema.Schema{
Attributes: map[string]schema.Attribute{
"testattr": schema.BoolAttribute{},
},
}

nullPlan := tfsdk.Plan{
Schema: testSchema,
Raw: tftypes.NewValue(
testSchema.Type().TerraformType(context.Background()),
nil,
),
}

nullState := tfsdk.State{
Schema: testSchema,
Raw: tftypes.NewValue(
testSchema.Type().TerraformType(context.Background()),
nil,
),
}

testPlan := func(value types.Bool) tfsdk.Plan {
tfValue, err := value.ToTerraformValue(context.Background())

if err != nil {
panic("ToTerraformValue error: " + err.Error())
}

return tfsdk.Plan{
Schema: testSchema,
Raw: tftypes.NewValue(
testSchema.Type().TerraformType(context.Background()),
map[string]tftypes.Value{
"testattr": tfValue,
},
),
}
}

testState := func(value types.Bool) tfsdk.State {
tfValue, err := value.ToTerraformValue(context.Background())

if err != nil {
panic("ToTerraformValue error: " + err.Error())
}

return tfsdk.State{
Schema: testSchema,
Raw: tftypes.NewValue(
testSchema.Type().TerraformType(context.Background()),
map[string]tftypes.Value{
"testattr": tfValue,
},
),
}
}

testCases := map[string]struct {
request planmodifier.BoolRequest
expected *planmodifier.BoolResponse
}{
"state-null": {
// resource creation
request: planmodifier.BoolRequest{
ConfigValue: types.BoolValue(true),
Plan: testPlan(types.BoolValue(true)),
PlanValue: types.BoolValue(true),
State: nullState,
StateValue: types.BoolNull(),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolValue(true),
RequiresReplace: false,
},
},
"plan-null": {
// resource destroy
request: planmodifier.BoolRequest{
ConfigValue: types.BoolNull(),
Plan: nullPlan,
PlanValue: types.BoolNull(),
State: testState(types.BoolValue(true)),
StateValue: types.BoolValue(true),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolNull(),
RequiresReplace: false,
},
},
"planvalue-statevalue-different-configured": {
request: planmodifier.BoolRequest{
ConfigValue: types.BoolValue(false),
Plan: testPlan(types.BoolValue(false)),
PlanValue: types.BoolValue(false),
State: testState(types.BoolValue(true)),
StateValue: types.BoolValue(true),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolValue(false),
RequiresReplace: true,
},
},
"planvalue-statevalue-different-unconfigured": {
request: planmodifier.BoolRequest{
ConfigValue: types.BoolNull(),
Plan: testPlan(types.BoolValue(false)),
PlanValue: types.BoolValue(false),
State: testState(types.BoolValue(true)),
StateValue: types.BoolValue(true),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolValue(false),
RequiresReplace: false,
},
},
"planvalue-statevalue-equal": {
request: planmodifier.BoolRequest{
ConfigValue: types.BoolValue(true),
Plan: testPlan(types.BoolValue(true)),
PlanValue: types.BoolValue(true),
State: testState(types.BoolValue(true)),
StateValue: types.BoolValue(true),
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolValue(true),
RequiresReplace: false,
},
},
}

for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()

resp := &planmodifier.BoolResponse{
PlanValue: testCase.request.PlanValue,
}

boolplanmodifier.RequiresReplaceIfConfigured().PlanModifyBool(context.Background(), testCase.request, resp)

if diff := cmp.Diff(testCase.expected, resp); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
22 changes: 22 additions & 0 deletions resource/schema/boolplanmodifier/requires_replace_if_func.go
@@ -0,0 +1,22 @@
package boolplanmodifier

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

// RequiresReplaceIfFunc is a conditional function used in the RequiresReplaceIf
// plan modifier to determine whether the attribute requires replacement.
type RequiresReplaceIfFunc func(context.Context, planmodifier.BoolRequest, *RequiresReplaceIfFuncResponse)

// RequiresReplaceIfFuncResponse is the response type for a RequiresReplaceIfFunc.
type RequiresReplaceIfFuncResponse struct {
// Diagnostics report errors or warnings related to this logic. An empty
// or unset slice indicates success, with no warnings or errors generated.
Diagnostics diag.Diagnostics

// RequiresReplace should be enabled if the resource should be replaced.
RequiresReplace bool
}