Skip to content

Commit 972929c

Browse files
committed
map and set plan modifiers
1 parent 411df8d commit 972929c

16 files changed

+1250
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mapplanmodifier
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
11+
)
12+
13+
// WillHaveSizeAtLeast returns a plan modifier that will add a refinement to an unknown planned value
14+
// which promises that:
15+
// - The final value will not be null.
16+
// - The final size of the map value will be at least the provided minimum value.
17+
//
18+
// This unknown value refinement allows Terraform to validate more of the configuration during plan
19+
// and evaluate conditional logic in meta-arguments such as "count".
20+
func WillHaveSizeAtLeast(minVal int) planmodifier.Map {
21+
return willHaveSizeAtLeastModifier{
22+
min: minVal,
23+
}
24+
}
25+
26+
type willHaveSizeAtLeastModifier struct {
27+
min int
28+
}
29+
30+
func (m willHaveSizeAtLeastModifier) Description(_ context.Context) string {
31+
return fmt.Sprintf("Promises the value of this attribute will contain at least %d elements once it becomes known", m.min)
32+
}
33+
34+
func (m willHaveSizeAtLeastModifier) MarkdownDescription(_ context.Context) string {
35+
return fmt.Sprintf("Promises the value of this attribute will contain at least %d elements once it becomes known", m.min)
36+
}
37+
38+
func (m willHaveSizeAtLeastModifier) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) {
39+
// Do nothing if there is a known planned value.
40+
if !req.PlanValue.IsUnknown() {
41+
return
42+
}
43+
44+
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
45+
if req.ConfigValue.IsUnknown() {
46+
return
47+
}
48+
49+
resp.PlanValue = req.PlanValue.RefineWithLengthLowerBound(int64(m.min))
50+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mapplanmodifier_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/mapplanmodifier"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
)
16+
17+
func TestWillHaveSizeAtLeastModifierPlanModifyMap(t *testing.T) {
18+
t.Parallel()
19+
20+
testCases := map[string]struct {
21+
minVal int
22+
request planmodifier.MapRequest
23+
expected *planmodifier.MapResponse
24+
}{
25+
"known-plan": {
26+
minVal: 5,
27+
request: planmodifier.MapRequest{
28+
StateValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world")}),
29+
PlanValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world"), "key3": types.StringValue("!")}),
30+
ConfigValue: types.MapNull(types.StringType),
31+
},
32+
expected: &planmodifier.MapResponse{
33+
PlanValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world"), "key3": types.StringValue("!")}),
34+
},
35+
},
36+
"unknown-config": {
37+
// this is the situation in which a user is
38+
// interpolating into a field. We want that to still
39+
// show up as unknown (with no refinement), otherwise they'll
40+
// get apply-time errors for changing the value even though
41+
// we knew it was legitimately possible for it to change and the
42+
// provider can't prevent this from happening
43+
minVal: 5,
44+
request: planmodifier.MapRequest{
45+
StateValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world"), "key3": types.StringValue("!")}),
46+
PlanValue: types.MapUnknown(types.StringType),
47+
ConfigValue: types.MapUnknown(types.StringType),
48+
},
49+
expected: &planmodifier.MapResponse{
50+
PlanValue: types.MapUnknown(types.StringType),
51+
},
52+
},
53+
"unknown-plan-null-state": {
54+
minVal: 5,
55+
request: planmodifier.MapRequest{
56+
StateValue: types.MapNull(types.StringType),
57+
PlanValue: types.MapUnknown(types.StringType),
58+
ConfigValue: types.MapNull(types.StringType),
59+
},
60+
expected: &planmodifier.MapResponse{
61+
PlanValue: types.MapUnknown(types.StringType).RefineWithLengthLowerBound(5),
62+
},
63+
},
64+
"unknown-plan-non-null-state": {
65+
minVal: 3,
66+
request: planmodifier.MapRequest{
67+
StateValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world"), "key3": types.StringValue("!")}),
68+
PlanValue: types.MapUnknown(types.StringType),
69+
ConfigValue: types.MapNull(types.StringType),
70+
},
71+
expected: &planmodifier.MapResponse{
72+
PlanValue: types.MapUnknown(types.StringType).RefineWithLengthLowerBound(3),
73+
},
74+
},
75+
"unknown-plan-preserve-existing-refinement": {
76+
minVal: 2,
77+
request: planmodifier.MapRequest{
78+
StateValue: types.MapNull(types.StringType),
79+
PlanValue: types.MapUnknown(types.StringType).RefineWithLengthUpperBound(6),
80+
ConfigValue: types.MapNull(types.StringType),
81+
},
82+
expected: &planmodifier.MapResponse{
83+
PlanValue: types.MapUnknown(types.StringType).RefineWithLengthUpperBound(6).RefineWithLengthLowerBound(2),
84+
},
85+
},
86+
}
87+
88+
for name, testCase := range testCases {
89+
name, testCase := name, testCase
90+
91+
t.Run(name, func(t *testing.T) {
92+
t.Parallel()
93+
94+
resp := &planmodifier.MapResponse{
95+
PlanValue: testCase.request.PlanValue,
96+
}
97+
98+
mapplanmodifier.WillHaveSizeAtLeast(testCase.minVal).PlanModifyMap(context.Background(), testCase.request, resp)
99+
100+
if diff := cmp.Diff(testCase.expected, resp); diff != "" {
101+
t.Errorf("unexpected difference: %s", diff)
102+
}
103+
})
104+
}
105+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mapplanmodifier
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
11+
)
12+
13+
// WillHaveSizeAtMost returns a plan modifier that will add a refinement to an unknown planned value
14+
// which promises that:
15+
// - The final value will not be null.
16+
// - The final size of the map value will be at most the provided maximum value.
17+
//
18+
// This unknown value refinement allows Terraform to validate more of the configuration during plan
19+
// and evaluate conditional logic in meta-arguments such as "count".
20+
func WillHaveSizeAtMost(maxVal int) planmodifier.Map {
21+
return willHaveSizeAtMostModifier{
22+
max: maxVal,
23+
}
24+
}
25+
26+
type willHaveSizeAtMostModifier struct {
27+
max int
28+
}
29+
30+
func (m willHaveSizeAtMostModifier) Description(_ context.Context) string {
31+
return fmt.Sprintf("Promises the value of this attribute will contain at most %d elements once it becomes known", m.max)
32+
}
33+
34+
func (m willHaveSizeAtMostModifier) MarkdownDescription(_ context.Context) string {
35+
return fmt.Sprintf("Promises the value of this attribute will contain at most %d elements once it becomes known", m.max)
36+
}
37+
38+
func (m willHaveSizeAtMostModifier) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) {
39+
// Do nothing if there is a known planned value.
40+
if !req.PlanValue.IsUnknown() {
41+
return
42+
}
43+
44+
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
45+
if req.ConfigValue.IsUnknown() {
46+
return
47+
}
48+
49+
resp.PlanValue = req.PlanValue.RefineWithLengthUpperBound(int64(m.max))
50+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mapplanmodifier_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/mapplanmodifier"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
)
16+
17+
func TestWillHaveSizeAtMostModifierPlanModifyMap(t *testing.T) {
18+
t.Parallel()
19+
20+
testCases := map[string]struct {
21+
maxVal int
22+
request planmodifier.MapRequest
23+
expected *planmodifier.MapResponse
24+
}{
25+
"known-plan": {
26+
maxVal: 10,
27+
request: planmodifier.MapRequest{
28+
StateValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world")}),
29+
PlanValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world"), "key3": types.StringValue("!")}),
30+
ConfigValue: types.MapNull(types.StringType),
31+
},
32+
expected: &planmodifier.MapResponse{
33+
PlanValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world"), "key3": types.StringValue("!")}),
34+
},
35+
},
36+
"unknown-config": {
37+
// this is the situation in which a user is
38+
// interpolating into a field. We want that to still
39+
// show up as unknown (with no refinement), otherwise they'll
40+
// get apply-time errors for changing the value even though
41+
// we knew it was legitimately possible for it to change and the
42+
// provider can't prevent this from happening
43+
maxVal: 10,
44+
request: planmodifier.MapRequest{
45+
StateValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world"), "key3": types.StringValue("!")}),
46+
PlanValue: types.MapUnknown(types.StringType),
47+
ConfigValue: types.MapUnknown(types.StringType),
48+
},
49+
expected: &planmodifier.MapResponse{
50+
PlanValue: types.MapUnknown(types.StringType),
51+
},
52+
},
53+
"unknown-plan-null-state": {
54+
maxVal: 10,
55+
request: planmodifier.MapRequest{
56+
StateValue: types.MapNull(types.StringType),
57+
PlanValue: types.MapUnknown(types.StringType),
58+
ConfigValue: types.MapNull(types.StringType),
59+
},
60+
expected: &planmodifier.MapResponse{
61+
PlanValue: types.MapUnknown(types.StringType).RefineWithLengthUpperBound(10),
62+
},
63+
},
64+
"unknown-plan-non-null-state": {
65+
maxVal: 4,
66+
request: planmodifier.MapRequest{
67+
StateValue: types.MapValueMust(types.StringType, map[string]attr.Value{"key1": types.StringValue("hello"), "key2": types.StringValue("world"), "key3": types.StringValue("!")}),
68+
PlanValue: types.MapUnknown(types.StringType),
69+
ConfigValue: types.MapNull(types.StringType),
70+
},
71+
expected: &planmodifier.MapResponse{
72+
PlanValue: types.MapUnknown(types.StringType).RefineWithLengthUpperBound(4),
73+
},
74+
},
75+
"unknown-plan-preserve-existing-refinement": {
76+
maxVal: 6,
77+
request: planmodifier.MapRequest{
78+
StateValue: types.MapNull(types.StringType),
79+
PlanValue: types.MapUnknown(types.StringType).RefineWithLengthLowerBound(2),
80+
ConfigValue: types.MapNull(types.StringType),
81+
},
82+
expected: &planmodifier.MapResponse{
83+
PlanValue: types.MapUnknown(types.StringType).RefineWithLengthLowerBound(2).RefineWithLengthUpperBound(6),
84+
},
85+
},
86+
}
87+
88+
for name, testCase := range testCases {
89+
name, testCase := name, testCase
90+
91+
t.Run(name, func(t *testing.T) {
92+
t.Parallel()
93+
94+
resp := &planmodifier.MapResponse{
95+
PlanValue: testCase.request.PlanValue,
96+
}
97+
98+
mapplanmodifier.WillHaveSizeAtMost(testCase.maxVal).PlanModifyMap(context.Background(), testCase.request, resp)
99+
100+
if diff := cmp.Diff(testCase.expected, resp); diff != "" {
101+
t.Errorf("unexpected difference: %s", diff)
102+
}
103+
})
104+
}
105+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package mapplanmodifier
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
11+
)
12+
13+
// WillHaveSizeBetween returns a plan modifier that will add a refinement to an unknown planned value
14+
// which promises that:
15+
// - The final value will not be null.
16+
// - The final size of the map value will be at least the provided minimum value.
17+
// - The final size of the map value will be at most the provided maximum value.
18+
//
19+
// This unknown value refinement allows Terraform to validate more of the configuration during plan
20+
// and evaluate conditional logic in meta-arguments such as "count".
21+
func WillHaveSizeBetween(minVal, maxVal int) planmodifier.Map {
22+
return willHaveSizeBetweenModifier{
23+
min: minVal,
24+
max: maxVal,
25+
}
26+
}
27+
28+
type willHaveSizeBetweenModifier struct {
29+
min int
30+
max int
31+
}
32+
33+
func (m willHaveSizeBetweenModifier) Description(_ context.Context) string {
34+
return fmt.Sprintf("Promises the value of this attribute will contain at least %d elements and at most %d elements once it becomes known", m.min, m.max)
35+
}
36+
37+
func (m willHaveSizeBetweenModifier) MarkdownDescription(_ context.Context) string {
38+
return fmt.Sprintf("Promises the value of this attribute will contain at least %d elements and at most %d elements once it becomes known", m.min, m.max)
39+
}
40+
41+
func (m willHaveSizeBetweenModifier) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) {
42+
// Do nothing if there is a known planned value.
43+
if !req.PlanValue.IsUnknown() {
44+
return
45+
}
46+
47+
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
48+
if req.ConfigValue.IsUnknown() {
49+
return
50+
}
51+
52+
resp.PlanValue = req.PlanValue.
53+
RefineWithLengthLowerBound(int64(m.min)).
54+
RefineWithLengthUpperBound(int64(m.max))
55+
}

0 commit comments

Comments
 (0)