Skip to content

Commit f21b22f

Browse files
committed
Port TypeSet helpers from AWS
1 parent cb228de commit f21b22f

File tree

2 files changed

+2749
-0
lines changed

2 files changed

+2749
-0
lines changed

helper/resource/testing_sets.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// These test helpers were developed by the AWS provider team at HashiCorp.
2+
3+
package resource
4+
5+
import (
6+
"fmt"
7+
"regexp"
8+
"strings"
9+
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
11+
)
12+
13+
const (
14+
sentinelIndex = "*"
15+
)
16+
17+
// TestCheckTypeSetElemNestedAttrs is a TestCheckFunc that accepts a resource
18+
// name, an attribute path, which should use the sentinel value '*' for indexing
19+
// into a TypeSet. The function verifies that an element matches the whole value
20+
// map.
21+
//
22+
// You may check for unset keys, however this will also match keys set to empty
23+
// string. Please provide a map with at least 1 non-empty value.
24+
//
25+
// map[string]string{
26+
// "key1": "value",
27+
// "key2": "",
28+
// }
29+
//
30+
// Use this function over SDK provided TestCheckFunctions when validating a
31+
// TypeSet where its elements are a nested object with their own attrs/values.
32+
//
33+
// Please note, if the provided value map is not granular enough, there exists
34+
// the possibility you match an element you were not intending to, in the TypeSet.
35+
// Provide a full mapping of attributes to be sure the unique element exists.
36+
func TestCheckTypeSetElemNestedAttrs(name, attr string, values map[string]string) TestCheckFunc {
37+
return func(s *terraform.State) error {
38+
is, err := primaryInstanceState(s, name)
39+
if err != nil {
40+
return err
41+
}
42+
43+
attrParts := strings.Split(attr, ".")
44+
if attrParts[len(attrParts)-1] != sentinelIndex {
45+
return fmt.Errorf("%q does not end with the special value %q", attr, sentinelIndex)
46+
}
47+
// account for cases where the user is trying to see if the value is unset/empty
48+
// there may be ambiguous scenarios where a field was deliberately unset vs set
49+
// to the empty string, this will match both, which may be a false positive.
50+
var matchCount int
51+
for _, v := range values {
52+
if v != "" {
53+
matchCount++
54+
}
55+
}
56+
if matchCount == 0 {
57+
return fmt.Errorf("%#v has no non-empty values", values)
58+
}
59+
60+
if testCheckTypeSetElemNestedAttrsInState(is, attrParts, matchCount, values) {
61+
return nil
62+
}
63+
return fmt.Errorf("%q no TypeSet element %q, with nested attrs %#v in state: %#v", name, attr, values, is.Attributes)
64+
}
65+
}
66+
67+
// TestMatchTypeSetElemNestedAttrs is a TestCheckFunc similar to TestCheckTypeSetElemNestedAttrs
68+
// with the exception that it verifies that an element matches a *regexp.Regexp.
69+
//
70+
// You may check for unset keys, however this will also match keys set to empty
71+
// string. Please provide a map with at least 1 non-empty value e.g.
72+
//
73+
// map[string]*regexp.Regexp{
74+
// "key1": regexp.MustCompile("value"),
75+
// "key2": regexp.MustCompile(""),
76+
// }
77+
//
78+
func TestMatchTypeSetElemNestedAttrs(name, attr string, values map[string]*regexp.Regexp) TestCheckFunc {
79+
return func(s *terraform.State) error {
80+
is, err := primaryInstanceState(s, name)
81+
if err != nil {
82+
return err
83+
}
84+
85+
attrParts := strings.Split(attr, ".")
86+
if attrParts[len(attrParts)-1] != sentinelIndex {
87+
return fmt.Errorf("%q does not end with the special value %q", attr, sentinelIndex)
88+
}
89+
// account for cases where the user is trying to see if the value is unset/empty
90+
// there may be ambiguous scenarios where a field was deliberately unset vs set
91+
// to the empty string, this will match both, which may be a false positive.
92+
var matchCount int
93+
for _, v := range values {
94+
if v != nil {
95+
matchCount++
96+
}
97+
}
98+
if matchCount == 0 {
99+
return fmt.Errorf("%#v has no non-empty values", values)
100+
}
101+
102+
if testCheckTypeSetElemNestedAttrsInState(is, attrParts, matchCount, values) {
103+
return nil
104+
}
105+
return fmt.Errorf("%q no TypeSet element %q, with the regex provided, match in state: %#v", name, attr, is.Attributes)
106+
}
107+
}
108+
109+
// TestCheckTypeSetElemAttr is a TestCheckFunc that accepts a resource
110+
// name, an attribute path, which should use the sentinel value '*' for indexing
111+
// into a TypeSet. The function verifies that an element matches the provided
112+
// value.
113+
//
114+
// Use this function over SDK provided TestCheckFunctions when validating a
115+
// TypeSet where its elements are a simple value
116+
func TestCheckTypeSetElemAttr(name, attr, value string) TestCheckFunc {
117+
return func(s *terraform.State) error {
118+
is, err := primaryInstanceState(s, name)
119+
if err != nil {
120+
return err
121+
}
122+
123+
err = testCheckTypeSetElem(is, attr, value)
124+
if err != nil {
125+
return fmt.Errorf("%q error: %s", name, err)
126+
}
127+
128+
return nil
129+
}
130+
}
131+
132+
// TestCheckTypeSetElemAttrPair is a TestCheckFunc that verifies a pair of name/key
133+
// combinations are equal where the first uses the sentinel value to index into a
134+
// TypeSet.
135+
//
136+
// E.g., TestCheckTypeSetElemAttrPair("aws_autoscaling_group.bar", "availability_zones.*", "data.aws_availability_zones.available", "names.0")
137+
// E.g., TestCheckTypeSetElemAttrPair("aws_spot_fleet_request.bar", "launch_specification.*.instance_type", "data.data.aws_ec2_instance_type_offering.available", "instance_type")
138+
func TestCheckTypeSetElemAttrPair(nameFirst, keyFirst, nameSecond, keySecond string) TestCheckFunc {
139+
return func(s *terraform.State) error {
140+
isFirst, err := primaryInstanceState(s, nameFirst)
141+
if err != nil {
142+
return err
143+
}
144+
145+
isSecond, err := primaryInstanceState(s, nameSecond)
146+
if err != nil {
147+
return err
148+
}
149+
150+
vSecond, okSecond := isSecond.Attributes[keySecond]
151+
if !okSecond {
152+
return fmt.Errorf("%s: Attribute %q not set, cannot be checked against TypeSet", nameSecond, keySecond)
153+
}
154+
155+
return testCheckTypeSetElemPair(isFirst, keyFirst, vSecond)
156+
}
157+
}
158+
159+
func testCheckTypeSetElem(is *terraform.InstanceState, attr, value string) error {
160+
attrParts := strings.Split(attr, ".")
161+
if attrParts[len(attrParts)-1] != sentinelIndex {
162+
return fmt.Errorf("%q does not end with the special value %q", attr, sentinelIndex)
163+
}
164+
for stateKey, stateValue := range is.Attributes {
165+
if stateValue == value {
166+
stateKeyParts := strings.Split(stateKey, ".")
167+
if len(stateKeyParts) == len(attrParts) {
168+
for i := range attrParts {
169+
if attrParts[i] != stateKeyParts[i] && attrParts[i] != sentinelIndex {
170+
break
171+
}
172+
if i == len(attrParts)-1 {
173+
return nil
174+
}
175+
}
176+
}
177+
}
178+
}
179+
180+
return fmt.Errorf("no TypeSet element %q, with value %q in state: %#v", attr, value, is.Attributes)
181+
}
182+
183+
func testCheckTypeSetElemPair(is *terraform.InstanceState, attr, value string) error {
184+
attrParts := strings.Split(attr, ".")
185+
for stateKey, stateValue := range is.Attributes {
186+
if stateValue == value {
187+
stateKeyParts := strings.Split(stateKey, ".")
188+
if len(stateKeyParts) == len(attrParts) {
189+
for i := range attrParts {
190+
if attrParts[i] != stateKeyParts[i] && attrParts[i] != sentinelIndex {
191+
break
192+
}
193+
if i == len(attrParts)-1 {
194+
return nil
195+
}
196+
}
197+
}
198+
}
199+
}
200+
201+
return fmt.Errorf("no TypeSet element %q, with value %q in state: %#v", attr, value, is.Attributes)
202+
}
203+
204+
// testCheckTypeSetElemNestedAttrsInState is a helper function
205+
// to determine if nested attributes and their values are equal to those
206+
// in the instance state. Currently, the function accepts a "values" param of type
207+
// map[string]string or map[string]*regexp.Regexp.
208+
// Returns true if all attributes match, else false.
209+
func testCheckTypeSetElemNestedAttrsInState(is *terraform.InstanceState, attrParts []string, matchCount int, values interface{}) bool {
210+
matches := make(map[string]int)
211+
212+
for stateKey, stateValue := range is.Attributes {
213+
stateKeyParts := strings.Split(stateKey, ".")
214+
// a Set/List item with nested attrs would have a flatmap address of
215+
// at least length 3
216+
// foo.0.name = "bar"
217+
if len(stateKeyParts) < 3 {
218+
continue
219+
}
220+
var pathMatch bool
221+
for i := range attrParts {
222+
if attrParts[i] != stateKeyParts[i] && attrParts[i] != sentinelIndex {
223+
break
224+
}
225+
if i == len(attrParts)-1 {
226+
pathMatch = true
227+
}
228+
}
229+
if !pathMatch {
230+
continue
231+
}
232+
id := stateKeyParts[len(attrParts)-1]
233+
nestedAttr := strings.Join(stateKeyParts[len(attrParts):], ".")
234+
235+
var match bool
236+
switch t := values.(type) {
237+
case map[string]string:
238+
if v, keyExists := t[nestedAttr]; keyExists && v == stateValue {
239+
match = true
240+
}
241+
case map[string]*regexp.Regexp:
242+
if v, keyExists := t[nestedAttr]; keyExists && v != nil && v.MatchString(stateValue) {
243+
match = true
244+
}
245+
}
246+
if match {
247+
matches[id] = matches[id] + 1
248+
if matches[id] == matchCount {
249+
return true
250+
}
251+
}
252+
}
253+
return false
254+
}

0 commit comments

Comments
 (0)