Skip to content
Open
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
73 changes: 73 additions & 0 deletions resource/schema/boolplanmodifier/use_state_for_unknown_if.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package boolplanmodifier

import (
"context"

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

// UseStateForUnknownIf returns a plan modifier that conditionally copies a known prior state
// value into the planned value. Use this when it is known that an unconfigured
// value will remain the same after a resource update, but only if the given
// condition is met.
//
// To prevent Terraform errors, the framework automatically sets unconfigured
// and Computed attributes to an unknown value "(known after apply)" on update.
// Using this plan modifier will instead display the prior state value in the
// plan, unless a prior plan modifier adjusts the value, but only if the
// condition function returns true.
func UseStateForUnknownIf(f UseStateForUnknownIfFunc, description, markdownDescription string) planmodifier.Bool {
return useStateForUnknownIfModifier{
ifFunc: f,
description: description,
markdownDescription: markdownDescription,
}
}

// useStateForUnknownIfModifier implements the conditional plan modifier.
type useStateForUnknownIfModifier struct {
ifFunc UseStateForUnknownIfFunc
description string
markdownDescription string
}

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

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

// PlanModifyBool implements the plan modification logic.
func (m useStateForUnknownIfModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
// Do nothing if there is no state (resource is being created).
if req.State.Raw.IsNull() {
return
}

// Do nothing if there is a known planned value.
if !req.PlanValue.IsUnknown() {
return
}

// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}

ifFuncResp := &UseStateForUnknownIfFuncResponse{}

m.ifFunc(ctx, req, ifFuncResp)

resp.Diagnostics.Append(ifFuncResp.Diagnostics...)

if ifFuncResp.UseState {
resp.PlanValue = req.StateValue
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package boolplanmodifier

import (
"context"

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

// UseStateForUnknownIfFunc is a conditional function used in the UseStateForUnknownIf
// plan modifier to determine whether the attribute should use the state value for unknown.
type UseStateForUnknownIfFunc func(context.Context, planmodifier.BoolRequest, *UseStateForUnknownIfFuncResponse)

// UseStateForUnknownIfFuncResponse is the response type for a UseStateForUnknownIfFunc.
type UseStateForUnknownIfFuncResponse 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

// UseState should be enabled if the state value should be used for the plan value.
UseState bool
}
207 changes: 207 additions & 0 deletions resource/schema/boolplanmodifier/use_state_for_unknown_if_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package boolplanmodifier_test

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
"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 TestUseStateForUnknownIfModifierPlanModifyBool(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
request planmodifier.BoolRequest
ifFunc boolplanmodifier.UseStateForUnknownIfFunc
expected *planmodifier.BoolResponse
}{
"null-state": {
// when we first create the resource, use the unknown
// value
request: planmodifier.BoolRequest{
State: tfsdk.State{
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"attr": tftypes.Bool,
},
},
nil,
),
},
StateValue: types.BoolNull(),
PlanValue: types.BoolUnknown(),
ConfigValue: types.BoolNull(),
},
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
resp.UseState = true // should never reach here
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolUnknown(),
},
},
"known-plan": {
// this would really only happen if we had a plan
// modifier setting the value before this plan modifier
// got to it
request: planmodifier.BoolRequest{
State: tfsdk.State{
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"attr": tftypes.Bool,
},
},
map[string]tftypes.Value{
"attr": tftypes.NewValue(tftypes.Bool, true),
},
),
},
StateValue: types.BoolValue(true),
PlanValue: types.BoolValue(false),
ConfigValue: types.BoolNull(),
},
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
resp.UseState = true // should never reach here
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolValue(false),
},
},
"non-null-state-value-unknown-plan-if-true": {
// this is the situation we want to preserve the state
// in when condition is true
request: planmodifier.BoolRequest{
State: tfsdk.State{
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"attr": tftypes.Bool,
},
},
map[string]tftypes.Value{
"attr": tftypes.NewValue(tftypes.Bool, true),
},
),
},
StateValue: types.BoolValue(true),
PlanValue: types.BoolUnknown(),
ConfigValue: types.BoolNull(),
},
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
resp.UseState = true
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolValue(true),
},
},
"non-null-state-value-unknown-plan-if-false": {
// this is the situation we want to keep unknown
// when condition is false
request: planmodifier.BoolRequest{
State: tfsdk.State{
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"attr": tftypes.Bool,
},
},
map[string]tftypes.Value{
"attr": tftypes.NewValue(tftypes.Bool, true),
},
),
},
StateValue: types.BoolValue(true),
PlanValue: types.BoolUnknown(),
ConfigValue: types.BoolNull(),
},
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
resp.UseState = false
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolUnknown(),
},
},
"null-state-value-unknown-plan-if-true": {
// Null state values are still known, so we should preserve this as well.
request: planmodifier.BoolRequest{
State: tfsdk.State{
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"attr": tftypes.Bool,
},
},
map[string]tftypes.Value{
"attr": tftypes.NewValue(tftypes.Bool, nil),
},
),
},
StateValue: types.BoolNull(),
PlanValue: types.BoolUnknown(),
ConfigValue: types.BoolNull(),
},
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
resp.UseState = true
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolNull(),
},
},
"unknown-config": {
// this is the situation in which a user is
// interpolating into a field. We want that to still
// show up as unknown, otherwise they'll get apply-time
// errors for changing the value even though we knew it
// was legitimately possible for it to change and the
// provider can't prevent this from happening
request: planmodifier.BoolRequest{
State: tfsdk.State{
Raw: tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"attr": tftypes.Bool,
},
},
map[string]tftypes.Value{
"attr": tftypes.NewValue(tftypes.Bool, true),
},
),
},
StateValue: types.BoolValue(true),
PlanValue: types.BoolUnknown(),
ConfigValue: types.BoolUnknown(),
},
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
resp.UseState = true // should never reach here
},
expected: &planmodifier.BoolResponse{
PlanValue: types.BoolUnknown(),
},
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

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

boolplanmodifier.UseStateForUnknownIf(testCase.ifFunc, "test description", "test markdown description").PlanModifyBool(context.Background(), testCase.request, resp)

if diff := cmp.Diff(testCase.expected, resp); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}
73 changes: 73 additions & 0 deletions resource/schema/dynamicplanmodifier/use_state_for_unknown_if.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package dynamicplanmodifier

import (
"context"

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

// UseStateForUnknownIf returns a plan modifier that conditionally copies a known prior state
// value into the planned value. Use this when it is known that an unconfigured
// value will remain the same after a resource update, but only if the given
// condition is met.
//
// To prevent Terraform errors, the framework automatically sets unconfigured
// and Computed attributes to an unknown value "(known after apply)" on update.
// Using this plan modifier will instead display the prior state value in the
// plan, unless a prior plan modifier adjusts the value, but only if the
// condition function returns true.
func UseStateForUnknownIf(f UseStateForUnknownIfFunc, description, markdownDescription string) planmodifier.Dynamic {
return useStateForUnknownIfModifier{
ifFunc: f,
description: description,
markdownDescription: markdownDescription,
}
}

// useStateForUnknownIfModifier implements the conditional plan modifier.
type useStateForUnknownIfModifier struct {
ifFunc UseStateForUnknownIfFunc
description string
markdownDescription string
}

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

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

// PlanModifyDynamic implements the plan modification logic.
func (m useStateForUnknownIfModifier) PlanModifyDynamic(ctx context.Context, req planmodifier.DynamicRequest, resp *planmodifier.DynamicResponse) {
// Do nothing if there is no state (resource is being created).
if req.State.Raw.IsNull() {
return
}

// Do nothing if there is a known planned value.
if !req.PlanValue.IsUnknown() {
return
}

// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}

ifFuncResp := &UseStateForUnknownIfFuncResponse{}

m.ifFunc(ctx, req, ifFuncResp)

resp.Diagnostics.Append(ifFuncResp.Diagnostics...)

if ifFuncResp.UseState {
resp.PlanValue = req.StateValue
}
}
Loading