Skip to content

Commit def82d2

Browse files
committed
collections + object
1 parent 8155af3 commit def82d2

12 files changed

+904
-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 listplanmodifier
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.List {
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) PlanModifyList(ctx context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) {
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: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package listplanmodifier_test
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/terraform-plugin-framework/attr"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
14+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
15+
"github.com/hashicorp/terraform-plugin-framework/types"
16+
"github.com/hashicorp/terraform-plugin-go/tftypes"
17+
)
18+
19+
func TestUseNonNullStateForUnknownModifierPlanModifyList(t *testing.T) {
20+
t.Parallel()
21+
22+
testCases := map[string]struct {
23+
request planmodifier.ListRequest
24+
expected *planmodifier.ListResponse
25+
}{
26+
"null-state": {
27+
// when we first create the resource, the state value will be null,
28+
// so use the unknown value
29+
request: planmodifier.ListRequest{
30+
State: tfsdk.State{
31+
Raw: tftypes.NewValue(
32+
tftypes.Object{
33+
AttributeTypes: map[string]tftypes.Type{
34+
"attr": tftypes.List{ElementType: tftypes.String},
35+
},
36+
},
37+
nil,
38+
),
39+
},
40+
StateValue: types.ListNull(types.StringType),
41+
PlanValue: types.ListUnknown(types.StringType),
42+
ConfigValue: types.ListNull(types.StringType),
43+
},
44+
expected: &planmodifier.ListResponse{
45+
PlanValue: types.ListUnknown(types.StringType),
46+
},
47+
},
48+
"known-plan": {
49+
// this would really only happen if we had a plan
50+
// modifier setting the value before this plan modifier
51+
// got to it
52+
//
53+
// but we still want to preserve that value, in this
54+
// case
55+
request: planmodifier.ListRequest{
56+
State: tfsdk.State{
57+
Raw: tftypes.NewValue(
58+
tftypes.Object{
59+
AttributeTypes: map[string]tftypes.Type{
60+
"attr": tftypes.List{ElementType: tftypes.String},
61+
},
62+
},
63+
map[string]tftypes.Value{
64+
"attr": tftypes.NewValue(
65+
tftypes.List{ElementType: tftypes.String},
66+
[]tftypes.Value{
67+
tftypes.NewValue(tftypes.String, "other"),
68+
},
69+
),
70+
},
71+
),
72+
},
73+
StateValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("other")}),
74+
PlanValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}),
75+
ConfigValue: types.ListNull(types.StringType),
76+
},
77+
expected: &planmodifier.ListResponse{
78+
PlanValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}),
79+
},
80+
},
81+
"non-null-state-value-unknown-plan": {
82+
// this is the situation we want to preserve the state in
83+
request: planmodifier.ListRequest{
84+
State: tfsdk.State{
85+
Raw: tftypes.NewValue(
86+
tftypes.Object{
87+
AttributeTypes: map[string]tftypes.Type{
88+
"attr": tftypes.List{ElementType: tftypes.String},
89+
},
90+
},
91+
map[string]tftypes.Value{
92+
"attr": tftypes.NewValue(
93+
tftypes.List{ElementType: tftypes.String},
94+
[]tftypes.Value{
95+
tftypes.NewValue(tftypes.String, "test"),
96+
},
97+
),
98+
},
99+
),
100+
},
101+
StateValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}),
102+
PlanValue: types.ListUnknown(types.StringType),
103+
ConfigValue: types.ListNull(types.StringType),
104+
},
105+
expected: &planmodifier.ListResponse{
106+
PlanValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}),
107+
},
108+
},
109+
"null-state-value-unknown-plan": {
110+
// Null state values should not be preserved
111+
request: planmodifier.ListRequest{
112+
State: tfsdk.State{
113+
Raw: tftypes.NewValue(
114+
tftypes.Object{
115+
AttributeTypes: map[string]tftypes.Type{
116+
"attr": tftypes.List{ElementType: tftypes.String},
117+
},
118+
},
119+
map[string]tftypes.Value{
120+
"attr": tftypes.NewValue(
121+
tftypes.List{ElementType: tftypes.String},
122+
nil,
123+
),
124+
},
125+
),
126+
},
127+
StateValue: types.ListNull(types.StringType),
128+
PlanValue: types.ListUnknown(types.StringType),
129+
ConfigValue: types.ListNull(types.StringType),
130+
},
131+
expected: &planmodifier.ListResponse{
132+
PlanValue: types.ListUnknown(types.StringType),
133+
},
134+
},
135+
"unknown-config": {
136+
// this is the situation in which a user is
137+
// interpolating into a field. We want that to still
138+
// show up as unknown, otherwise they'll get apply-time
139+
// errors for changing the value even though we knew it
140+
// was legitimately possible for it to change and the
141+
// provider can't prevent this from happening
142+
request: planmodifier.ListRequest{
143+
StateValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}),
144+
PlanValue: types.ListUnknown(types.StringType),
145+
ConfigValue: types.ListUnknown(types.StringType),
146+
},
147+
expected: &planmodifier.ListResponse{
148+
PlanValue: types.ListUnknown(types.StringType),
149+
},
150+
},
151+
}
152+
153+
for name, testCase := range testCases {
154+
t.Run(name, func(t *testing.T) {
155+
t.Parallel()
156+
157+
resp := &planmodifier.ListResponse{
158+
PlanValue: testCase.request.PlanValue,
159+
}
160+
161+
listplanmodifier.UseNonNullStateForUnknown().PlanModifyList(context.Background(), testCase.request, resp)
162+
163+
if diff := cmp.Diff(testCase.expected, resp); diff != "" {
164+
t.Errorf("unexpected difference: %s", diff)
165+
}
166+
})
167+
}
168+
}

resource/schema/listplanmodifier/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.List {
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 mapplanmodifier
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.Map {
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) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) {
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)