Skip to content

Commit

Permalink
resource/schema: New packages which contain type-specific schema plan…
Browse files Browse the repository at this point in the history
… modifier implementations (#565)

Reference: #132

When developers migrate from `tfsdk.Schema` to `resource/schema.Schema`, they will need to also migrate to the type-specific plan modifiers. New packages have been introduced under `resource/schema`, such as `resource/schema/stringplanmodifier`. The existing plan modifiers are deprecated, similar to the `tfsdk` schema handling.
  • Loading branch information
bflad committed Nov 30, 2022
1 parent 8cde922 commit 4db7ec6
Show file tree
Hide file tree
Showing 93 changed files with 7,215 additions and 5 deletions.
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
}
31 changes: 31 additions & 0 deletions resource/schema/boolplanmodifier/requires_replace_if_configured.go
@@ -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
}

0 comments on commit 4db7ec6

Please sign in to comment.