Skip to content

Commit 518987f

Browse files
committed
non-number primitives
1 parent 7fb5005 commit 518987f

9 files changed

+672
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
// UseNonNullStateForUnknown returns a plan modifier that copies a known, non-null, prior state
13+
// value into the planned value. Use this when it is known that an unconfigured value will remain the
14+
// same after the attribute is updated to a non-null value.
15+
//
16+
// To prevent Terraform errors, the framework automatically sets unconfigured
17+
// and Computed attributes to an unknown value "(known after apply)" on update.
18+
// Using this plan modifier will instead display the non-null prior state value in the
19+
// plan, unless a prior plan modifier adjusts the value.
20+
//
21+
// This plan modifier can be a useful alternative to [UseStateForUnknown] when the attribute is
22+
// a child of a nested attribute that can be null after the resource is created.
23+
func UseNonNullStateForUnknown() planmodifier.Bool {
24+
return useNonNullStateForUnknown{}
25+
}
26+
27+
type useNonNullStateForUnknown struct{}
28+
29+
func (m useNonNullStateForUnknown) Description(_ context.Context) string {
30+
return "Once set to a non-null value, the value of this attribute in state will not change."
31+
}
32+
33+
func (m useNonNullStateForUnknown) MarkdownDescription(_ context.Context) string {
34+
return "Once set to a non-null value, the value of this attribute in state will not change."
35+
}
36+
37+
func (m useNonNullStateForUnknown) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
38+
// Do nothing if the state value is null.
39+
if req.StateValue.IsNull() {
40+
return
41+
}
42+
43+
// Do nothing if there is a known planned value.
44+
if !req.PlanValue.IsUnknown() {
45+
return
46+
}
47+
48+
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
49+
if req.ConfigValue.IsUnknown() {
50+
return
51+
}
52+
53+
resp.PlanValue = req.StateValue
54+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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 TestUseNonNullStateForUnknownModifierPlanModifyBool(t *testing.T) {
19+
t.Parallel()
20+
21+
testCases := map[string]struct {
22+
request planmodifier.BoolRequest
23+
expected *planmodifier.BoolResponse
24+
}{
25+
"null-state": {
26+
// when we first create the resource, the state value will be null,
27+
// so use the unknown value
28+
request: planmodifier.BoolRequest{
29+
State: tfsdk.State{
30+
Raw: tftypes.NewValue(
31+
tftypes.Object{
32+
AttributeTypes: map[string]tftypes.Type{
33+
"attr": tftypes.Bool,
34+
},
35+
},
36+
nil,
37+
),
38+
},
39+
StateValue: types.BoolNull(),
40+
PlanValue: types.BoolUnknown(),
41+
ConfigValue: types.BoolNull(),
42+
},
43+
expected: &planmodifier.BoolResponse{
44+
PlanValue: types.BoolUnknown(),
45+
},
46+
},
47+
"known-plan": {
48+
// this would really only happen if we had a plan
49+
// modifier setting the value before this plan modifier
50+
// got to it
51+
//
52+
// but we still want to preserve that value, in this
53+
// case
54+
request: planmodifier.BoolRequest{
55+
State: tfsdk.State{
56+
Raw: tftypes.NewValue(
57+
tftypes.Object{
58+
AttributeTypes: map[string]tftypes.Type{
59+
"attr": tftypes.Bool,
60+
},
61+
},
62+
map[string]tftypes.Value{
63+
"attr": tftypes.NewValue(tftypes.Bool, false),
64+
},
65+
),
66+
},
67+
StateValue: types.BoolValue(false),
68+
PlanValue: types.BoolValue(true),
69+
ConfigValue: types.BoolNull(),
70+
},
71+
expected: &planmodifier.BoolResponse{
72+
PlanValue: types.BoolValue(true),
73+
},
74+
},
75+
"non-null-state-value-unknown-plan": {
76+
// this is the situation we want to preserve the state in
77+
request: planmodifier.BoolRequest{
78+
State: tfsdk.State{
79+
Raw: tftypes.NewValue(
80+
tftypes.Object{
81+
AttributeTypes: map[string]tftypes.Type{
82+
"attr": tftypes.Bool,
83+
},
84+
},
85+
map[string]tftypes.Value{
86+
"attr": tftypes.NewValue(tftypes.Bool, true),
87+
},
88+
),
89+
},
90+
StateValue: types.BoolValue(true),
91+
PlanValue: types.BoolUnknown(),
92+
ConfigValue: types.BoolNull(),
93+
},
94+
expected: &planmodifier.BoolResponse{
95+
PlanValue: types.BoolValue(true),
96+
},
97+
},
98+
"null-state-value-unknown-plan": {
99+
// Null state values should not be preserved
100+
request: planmodifier.BoolRequest{
101+
State: tfsdk.State{
102+
Raw: tftypes.NewValue(
103+
tftypes.Object{
104+
AttributeTypes: map[string]tftypes.Type{
105+
"attr": tftypes.Bool,
106+
},
107+
},
108+
map[string]tftypes.Value{
109+
"attr": tftypes.NewValue(tftypes.Bool, nil),
110+
},
111+
),
112+
},
113+
StateValue: types.BoolNull(),
114+
PlanValue: types.BoolUnknown(),
115+
ConfigValue: types.BoolNull(),
116+
},
117+
expected: &planmodifier.BoolResponse{
118+
PlanValue: types.BoolUnknown(),
119+
},
120+
},
121+
"unknown-config": {
122+
// this is the situation in which a user is
123+
// interpolating into a field. We want that to still
124+
// show up as unknown, otherwise they'll get apply-time
125+
// errors for changing the value even though we knew it
126+
// was legitimately possible for it to change and the
127+
// provider can't prevent this from happening
128+
request: planmodifier.BoolRequest{
129+
State: tfsdk.State{
130+
Raw: tftypes.NewValue(
131+
tftypes.Object{
132+
AttributeTypes: map[string]tftypes.Type{
133+
"attr": tftypes.Bool,
134+
},
135+
},
136+
map[string]tftypes.Value{
137+
"attr": tftypes.NewValue(tftypes.Bool, true),
138+
},
139+
),
140+
},
141+
StateValue: types.BoolValue(true),
142+
PlanValue: types.BoolUnknown(),
143+
ConfigValue: types.BoolUnknown(),
144+
},
145+
expected: &planmodifier.BoolResponse{
146+
PlanValue: types.BoolUnknown(),
147+
},
148+
},
149+
}
150+
151+
for name, testCase := range testCases {
152+
t.Run(name, func(t *testing.T) {
153+
t.Parallel()
154+
155+
resp := &planmodifier.BoolResponse{
156+
PlanValue: testCase.request.PlanValue,
157+
}
158+
159+
boolplanmodifier.UseNonNullStateForUnknown().PlanModifyBool(context.Background(), testCase.request, resp)
160+
161+
if diff := cmp.Diff(testCase.expected, resp); diff != "" {
162+
t.Errorf("unexpected difference: %s", diff)
163+
}
164+
})
165+
}
166+
}

resource/schema/boolplanmodifier/use_state_for_unknown.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import (
1717
// and Computed attributes to an unknown value "(known after apply)" on update.
1818
// Using this plan modifier will instead display the prior state value in the
1919
// plan, unless a prior plan modifier adjusts the value.
20+
//
21+
// Null is also a known value in Terraform and will be copied to the planned value
22+
// by this plan modifier. For use-cases like a child attribute of a nested attribute or
23+
// if null is desired to be marked as unknown in the case of an update, use [UseNonNullStateForUnknown].
2024
func UseStateForUnknown() planmodifier.Bool {
2125
return useStateForUnknownModifier{}
2226
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
// UseNonNullStateForUnknown returns a plan modifier that copies a known, non-null, prior state
13+
// value into the planned value. Use this when it is known that an unconfigured value will remain the
14+
// same after the attribute is updated to a non-null value.
15+
//
16+
// To prevent Terraform errors, the framework automatically sets unconfigured
17+
// and Computed attributes to an unknown value "(known after apply)" on update.
18+
// Using this plan modifier will instead display the non-null prior state value in the
19+
// plan, unless a prior plan modifier adjusts the value.
20+
//
21+
// This plan modifier can be a useful alternative to [UseStateForUnknown] when the attribute is
22+
// a child of a nested attribute that can be null after the resource is created.
23+
func UseNonNullStateForUnknown() planmodifier.Dynamic {
24+
return useNonNullStateForUnknown{}
25+
}
26+
27+
type useNonNullStateForUnknown struct{}
28+
29+
func (m useNonNullStateForUnknown) Description(_ context.Context) string {
30+
return "Once set to a non-null value, the value of this attribute in state will not change."
31+
}
32+
33+
func (m useNonNullStateForUnknown) MarkdownDescription(_ context.Context) string {
34+
return "Once set to a non-null value, the value of this attribute in state will not change."
35+
}
36+
37+
func (m useNonNullStateForUnknown) PlanModifyDynamic(ctx context.Context, req planmodifier.DynamicRequest, resp *planmodifier.DynamicResponse) {
38+
// Do nothing if the state value is null.
39+
if req.StateValue.IsNull() {
40+
return
41+
}
42+
43+
// Do nothing if there is a known planned value.
44+
if !req.PlanValue.IsUnknown() {
45+
return
46+
}
47+
48+
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
49+
if req.ConfigValue.IsUnknown() {
50+
return
51+
}
52+
53+
resp.PlanValue = req.StateValue
54+
}

0 commit comments

Comments
 (0)