Skip to content

Commit a4585e0

Browse files
committed
Implement UseStateForUnknownIf plan modifier
Signed-off-by: Zhiwei Liang <[email protected]>
1 parent e39d577 commit a4585e0

36 files changed

+3551
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package boolplanmodifier
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
10+
)
11+
12+
// UseStateForUnknownIf returns a plan modifier that conditionally copies a known prior state
13+
// value into the planned value. Use this when it is known that an unconfigured
14+
// value will remain the same after a resource update, but only if the given
15+
// condition is met.
16+
//
17+
// To prevent Terraform errors, the framework automatically sets unconfigured
18+
// and Computed attributes to an unknown value "(known after apply)" on update.
19+
// Using this plan modifier will instead display the prior state value in the
20+
// plan, unless a prior plan modifier adjusts the value, but only if the
21+
// condition function returns true.
22+
func UseStateForUnknownIf(f UseStateForUnknownIfFunc, description, markdownDescription string) planmodifier.Bool {
23+
return useStateForUnknownIfModifier{
24+
ifFunc: f,
25+
description: description,
26+
markdownDescription: markdownDescription,
27+
}
28+
}
29+
30+
// useStateForUnknownIfModifier implements the conditional plan modifier.
31+
type useStateForUnknownIfModifier struct {
32+
ifFunc UseStateForUnknownIfFunc
33+
description string
34+
markdownDescription string
35+
}
36+
37+
// Description returns a human-readable description of the plan modifier.
38+
func (m useStateForUnknownIfModifier) Description(_ context.Context) string {
39+
return m.description
40+
}
41+
42+
// MarkdownDescription returns a markdown description of the plan modifier.
43+
func (m useStateForUnknownIfModifier) MarkdownDescription(_ context.Context) string {
44+
return m.markdownDescription
45+
}
46+
47+
// PlanModifyBool implements the plan modification logic.
48+
func (m useStateForUnknownIfModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
49+
// Do nothing if there is no state (resource is being created).
50+
if req.State.Raw.IsNull() {
51+
return
52+
}
53+
54+
// Do nothing if there is a known planned value.
55+
if !req.PlanValue.IsUnknown() {
56+
return
57+
}
58+
59+
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
60+
if req.ConfigValue.IsUnknown() {
61+
return
62+
}
63+
64+
ifFuncResp := &UseStateForUnknownIfFuncResponse{}
65+
66+
m.ifFunc(ctx, req, ifFuncResp)
67+
68+
resp.Diagnostics.Append(ifFuncResp.Diagnostics...)
69+
70+
if ifFuncResp.UseState {
71+
resp.PlanValue = req.StateValue
72+
}
73+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package boolplanmodifier
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
11+
)
12+
13+
// UseStateForUnknownIfFunc is a conditional function used in the UseStateForUnknownIf
14+
// plan modifier to determine whether the attribute should use the state value for unknown.
15+
type UseStateForUnknownIfFunc func(context.Context, planmodifier.BoolRequest, *UseStateForUnknownIfFuncResponse)
16+
17+
// UseStateForUnknownIfFuncResponse is the response type for a UseStateForUnknownIfFunc.
18+
type UseStateForUnknownIfFuncResponse struct {
19+
// Diagnostics report errors or warnings related to this logic. An empty
20+
// or unset slice indicates success, with no warnings or errors generated.
21+
Diagnostics diag.Diagnostics
22+
23+
// UseState should be enabled if the state value should be used for the plan value.
24+
UseState bool
25+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package boolplanmodifier_test
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
13+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
"github.com/hashicorp/terraform-plugin-go/tftypes"
16+
)
17+
18+
func TestUseStateForUnknownIfModifierPlanModifyBool(t *testing.T) {
19+
t.Parallel()
20+
21+
testCases := map[string]struct {
22+
request planmodifier.BoolRequest
23+
ifFunc boolplanmodifier.UseStateForUnknownIfFunc
24+
expected *planmodifier.BoolResponse
25+
}{
26+
"null-state": {
27+
// when we first create the resource, use the unknown
28+
// value
29+
request: planmodifier.BoolRequest{
30+
State: tfsdk.State{
31+
Raw: tftypes.NewValue(
32+
tftypes.Object{
33+
AttributeTypes: map[string]tftypes.Type{
34+
"attr": tftypes.Bool,
35+
},
36+
},
37+
nil,
38+
),
39+
},
40+
StateValue: types.BoolNull(),
41+
PlanValue: types.BoolUnknown(),
42+
ConfigValue: types.BoolNull(),
43+
},
44+
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
45+
resp.UseState = true // should never reach here
46+
},
47+
expected: &planmodifier.BoolResponse{
48+
PlanValue: types.BoolUnknown(),
49+
},
50+
},
51+
"known-plan": {
52+
// this would really only happen if we had a plan
53+
// modifier setting the value before this plan modifier
54+
// got to it
55+
request: planmodifier.BoolRequest{
56+
State: tfsdk.State{
57+
Raw: tftypes.NewValue(
58+
tftypes.Object{
59+
AttributeTypes: map[string]tftypes.Type{
60+
"attr": tftypes.Bool,
61+
},
62+
},
63+
map[string]tftypes.Value{
64+
"attr": tftypes.NewValue(tftypes.Bool, true),
65+
},
66+
),
67+
},
68+
StateValue: types.BoolValue(true),
69+
PlanValue: types.BoolValue(false),
70+
ConfigValue: types.BoolNull(),
71+
},
72+
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
73+
resp.UseState = true // should never reach here
74+
},
75+
expected: &planmodifier.BoolResponse{
76+
PlanValue: types.BoolValue(false),
77+
},
78+
},
79+
"non-null-state-value-unknown-plan-if-true": {
80+
// this is the situation we want to preserve the state
81+
// in when condition is true
82+
request: planmodifier.BoolRequest{
83+
State: tfsdk.State{
84+
Raw: tftypes.NewValue(
85+
tftypes.Object{
86+
AttributeTypes: map[string]tftypes.Type{
87+
"attr": tftypes.Bool,
88+
},
89+
},
90+
map[string]tftypes.Value{
91+
"attr": tftypes.NewValue(tftypes.Bool, true),
92+
},
93+
),
94+
},
95+
StateValue: types.BoolValue(true),
96+
PlanValue: types.BoolUnknown(),
97+
ConfigValue: types.BoolNull(),
98+
},
99+
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
100+
resp.UseState = true
101+
},
102+
expected: &planmodifier.BoolResponse{
103+
PlanValue: types.BoolValue(true),
104+
},
105+
},
106+
"non-null-state-value-unknown-plan-if-false": {
107+
// this is the situation we want to keep unknown
108+
// when condition is false
109+
request: planmodifier.BoolRequest{
110+
State: tfsdk.State{
111+
Raw: tftypes.NewValue(
112+
tftypes.Object{
113+
AttributeTypes: map[string]tftypes.Type{
114+
"attr": tftypes.Bool,
115+
},
116+
},
117+
map[string]tftypes.Value{
118+
"attr": tftypes.NewValue(tftypes.Bool, true),
119+
},
120+
),
121+
},
122+
StateValue: types.BoolValue(true),
123+
PlanValue: types.BoolUnknown(),
124+
ConfigValue: types.BoolNull(),
125+
},
126+
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
127+
resp.UseState = false
128+
},
129+
expected: &planmodifier.BoolResponse{
130+
PlanValue: types.BoolUnknown(),
131+
},
132+
},
133+
"null-state-value-unknown-plan-if-true": {
134+
// Null state values are still known, so we should preserve this as well.
135+
request: planmodifier.BoolRequest{
136+
State: tfsdk.State{
137+
Raw: tftypes.NewValue(
138+
tftypes.Object{
139+
AttributeTypes: map[string]tftypes.Type{
140+
"attr": tftypes.Bool,
141+
},
142+
},
143+
map[string]tftypes.Value{
144+
"attr": tftypes.NewValue(tftypes.Bool, nil),
145+
},
146+
),
147+
},
148+
StateValue: types.BoolNull(),
149+
PlanValue: types.BoolUnknown(),
150+
ConfigValue: types.BoolNull(),
151+
},
152+
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
153+
resp.UseState = true
154+
},
155+
expected: &planmodifier.BoolResponse{
156+
PlanValue: types.BoolNull(),
157+
},
158+
},
159+
"unknown-config": {
160+
// this is the situation in which a user is
161+
// interpolating into a field. We want that to still
162+
// show up as unknown, otherwise they'll get apply-time
163+
// errors for changing the value even though we knew it
164+
// was legitimately possible for it to change and the
165+
// provider can't prevent this from happening
166+
request: planmodifier.BoolRequest{
167+
State: tfsdk.State{
168+
Raw: tftypes.NewValue(
169+
tftypes.Object{
170+
AttributeTypes: map[string]tftypes.Type{
171+
"attr": tftypes.Bool,
172+
},
173+
},
174+
map[string]tftypes.Value{
175+
"attr": tftypes.NewValue(tftypes.Bool, true),
176+
},
177+
),
178+
},
179+
StateValue: types.BoolValue(true),
180+
PlanValue: types.BoolUnknown(),
181+
ConfigValue: types.BoolUnknown(),
182+
},
183+
ifFunc: func(ctx context.Context, req planmodifier.BoolRequest, resp *boolplanmodifier.UseStateForUnknownIfFuncResponse) {
184+
resp.UseState = true // should never reach here
185+
},
186+
expected: &planmodifier.BoolResponse{
187+
PlanValue: types.BoolUnknown(),
188+
},
189+
},
190+
}
191+
192+
for name, testCase := range testCases {
193+
t.Run(name, func(t *testing.T) {
194+
t.Parallel()
195+
196+
resp := &planmodifier.BoolResponse{
197+
PlanValue: testCase.request.PlanValue,
198+
}
199+
200+
boolplanmodifier.UseStateForUnknownIf(testCase.ifFunc, "test description", "test markdown description").PlanModifyBool(context.Background(), testCase.request, resp)
201+
202+
if diff := cmp.Diff(testCase.expected, resp); diff != "" {
203+
t.Errorf("unexpected difference: %s", diff)
204+
}
205+
})
206+
}
207+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package dynamicplanmodifier
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
10+
)
11+
12+
// UseStateForUnknownIf returns a plan modifier that conditionally copies a known prior state
13+
// value into the planned value. Use this when it is known that an unconfigured
14+
// value will remain the same after a resource update, but only if the given
15+
// condition is met.
16+
//
17+
// To prevent Terraform errors, the framework automatically sets unconfigured
18+
// and Computed attributes to an unknown value "(known after apply)" on update.
19+
// Using this plan modifier will instead display the prior state value in the
20+
// plan, unless a prior plan modifier adjusts the value, but only if the
21+
// condition function returns true.
22+
func UseStateForUnknownIf(f UseStateForUnknownIfFunc, description, markdownDescription string) planmodifier.Dynamic {
23+
return useStateForUnknownIfModifier{
24+
ifFunc: f,
25+
description: description,
26+
markdownDescription: markdownDescription,
27+
}
28+
}
29+
30+
// useStateForUnknownIfModifier implements the conditional plan modifier.
31+
type useStateForUnknownIfModifier struct {
32+
ifFunc UseStateForUnknownIfFunc
33+
description string
34+
markdownDescription string
35+
}
36+
37+
// Description returns a human-readable description of the plan modifier.
38+
func (m useStateForUnknownIfModifier) Description(_ context.Context) string {
39+
return m.description
40+
}
41+
42+
// MarkdownDescription returns a markdown description of the plan modifier.
43+
func (m useStateForUnknownIfModifier) MarkdownDescription(_ context.Context) string {
44+
return m.markdownDescription
45+
}
46+
47+
// PlanModifyDynamic implements the plan modification logic.
48+
func (m useStateForUnknownIfModifier) PlanModifyDynamic(ctx context.Context, req planmodifier.DynamicRequest, resp *planmodifier.DynamicResponse) {
49+
// Do nothing if there is no state (resource is being created).
50+
if req.State.Raw.IsNull() {
51+
return
52+
}
53+
54+
// Do nothing if there is a known planned value.
55+
if !req.PlanValue.IsUnknown() {
56+
return
57+
}
58+
59+
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
60+
if req.ConfigValue.IsUnknown() {
61+
return
62+
}
63+
64+
ifFuncResp := &UseStateForUnknownIfFuncResponse{}
65+
66+
m.ifFunc(ctx, req, ifFuncResp)
67+
68+
resp.Diagnostics.Append(ifFuncResp.Diagnostics...)
69+
70+
if ifFuncResp.UseState {
71+
resp.PlanValue = req.StateValue
72+
}
73+
}

0 commit comments

Comments
 (0)