-
Notifications
You must be signed in to change notification settings - Fork 230
/
testing_sets.go
361 lines (335 loc) · 13.2 KB
/
testing_sets.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// These test helpers were developed by the AWS provider team at HashiCorp.
package resource
import (
"fmt"
"regexp"
"strings"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
sentinelIndex = "*"
)
// TestCheckTypeSetElemNestedAttrs ensures a subset map of values is stored in
// state for the given name and key combination of attributes nested under a
// list or set block. Use this TestCheckFunc in preference over non-set
// variants to simplify testing code and ensure compatibility with indicies,
// which can easily change with schema changes. State value checking is only
// recommended for testing Computed attributes and attribute defaults.
//
// For managed resources, the name parameter is a combination of the resource
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the sentinel value '*' to replace the element indexing into
// a list or set. The sentinel value can be used for each list or set index, if
// there are multiple lists or sets in the attribute path.
//
// The values parameter is the map of attribute names to attribute values
// expected to be nested under the list or set.
//
// You may check for unset nested attributes, however this will also match keys
// set to an empty string. Use a map with at least 1 non-empty value.
//
// map[string]string{
// "key1": "value",
// "key2": "",
// }
//
// If the values map is not granular enough, it is possible to match an element
// you were not intending to in the set. Provide the most complete mapping of
// attributes possible to be sure the unique element exists.
func TestCheckTypeSetElemNestedAttrs(name, attr string, values map[string]string) TestCheckFunc {
return func(s *terraform.State) error {
is, err := primaryInstanceState(s, name)
if err != nil {
return err
}
attrParts := strings.Split(attr, ".")
if attrParts[len(attrParts)-1] != sentinelIndex {
return fmt.Errorf("%q does not end with the special value %q", attr, sentinelIndex)
}
// account for cases where the user is trying to see if the value is unset/empty
// there may be ambiguous scenarios where a field was deliberately unset vs set
// to the empty string, this will match both, which may be a false positive.
var matchCount int
for _, v := range values {
if v != "" {
matchCount++
}
}
if matchCount == 0 {
return fmt.Errorf("%#v has no non-empty values", values)
}
if testCheckTypeSetElemNestedAttrsInState(is, attrParts, matchCount, values) {
return nil
}
return fmt.Errorf("%q no TypeSet element %q, with nested attrs %#v in state: %#v", name, attr, values, is.Attributes)
}
}
// TestMatchTypeSetElemNestedAttrs ensures a subset map of values, compared by
// regular expressions, is stored in state for the given name and key
// combination of attributes nested under a list or set block. Use this
// TestCheckFunc in preference over non-set variants to simplify testing code
// and ensure compatibility with indicies, which can easily change with schema
// changes. State value checking is only recommended for testing Computed
// attributes and attribute defaults.
//
// For managed resources, the name parameter is a combination of the resource
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the sentinel value '*' to replace the element indexing into
// a list or set. The sentinel value can be used for each list or set index, if
// there are multiple lists or sets in the attribute path.
//
// The values parameter is the map of attribute names to regular expressions
// for matching attribute values expected to be nested under the list or set.
//
// You may check for unset nested attributes, however this will also match keys
// set to an empty string. Use a map with at least 1 non-empty value.
//
// map[string]*regexp.Regexp{
// "key1": regexp.MustCompile(`^value`),
// "key2": regexp.MustCompile(`^$`),
// }
//
// If the values map is not granular enough, it is possible to match an element
// you were not intending to in the set. Provide the most complete mapping of
// attributes possible to be sure the unique element exists.
func TestMatchTypeSetElemNestedAttrs(name, attr string, values map[string]*regexp.Regexp) TestCheckFunc {
return func(s *terraform.State) error {
is, err := primaryInstanceState(s, name)
if err != nil {
return err
}
attrParts := strings.Split(attr, ".")
if attrParts[len(attrParts)-1] != sentinelIndex {
return fmt.Errorf("%q does not end with the special value %q", attr, sentinelIndex)
}
// account for cases where the user is trying to see if the value is unset/empty
// there may be ambiguous scenarios where a field was deliberately unset vs set
// to the empty string, this will match both, which may be a false positive.
var matchCount int
for _, v := range values {
if v != nil {
matchCount++
}
}
if matchCount == 0 {
return fmt.Errorf("%#v has no non-empty values", values)
}
if testCheckTypeSetElemNestedAttrsInState(is, attrParts, matchCount, values) {
return nil
}
return fmt.Errorf("%q no TypeSet element %q, with the regex provided, match in state: %#v", name, attr, is.Attributes)
}
}
// TestCheckTypeSetElemAttr is a TestCheckFunc that accepts a resource
// name, an attribute path, which should use the sentinel value '*' for indexing
// into a TypeSet. The function verifies that an element matches the provided
// value.
//
// Use this function over SDK provided TestCheckFunctions when validating a
// TypeSet where its elements are a simple value
// TestCheckTypeSetElemAttr ensures a specific value is stored in state for the
// given name and key combination under a list or set. Use this TestCheckFunc
// in preference over non-set variants to simplify testing code and ensure
// compatibility with indicies, which can easily change with schema changes.
// State value checking is only recommended for testing Computed attributes and
// attribute defaults.
//
// For managed resources, the name parameter is a combination of the resource
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the sentinel value '*' to replace the element indexing into
// a list or set. The sentinel value can be used for each list or set index, if
// there are multiple lists or sets in the attribute path.
//
// The value parameter is the stringified data to check at the given key. Use
// the following attribute type rules to set the value:
//
// - Boolean: "false" or "true".
// - Float/Integer: Stringified number, such as "1.2" or "123".
// - String: No conversion necessary.
func TestCheckTypeSetElemAttr(name, attr, value string) TestCheckFunc {
return func(s *terraform.State) error {
is, err := primaryInstanceState(s, name)
if err != nil {
return err
}
err = testCheckTypeSetElem(is, attr, value)
if err != nil {
return fmt.Errorf("%q error: %s", name, err)
}
return nil
}
}
// TestCheckTypeSetElemAttrPair ensures value equality in state between the
// first given name and key combination and the second name and key
// combination. State value checking is only recommended for testing Computed
// attributes and attribute defaults.
//
// For managed resources, the name parameter is a combination of the resource
// type, a period (.), and the name label. The name for the below example
// configuration would be "myprovider_thing.example".
//
// resource "myprovider_thing" "example" { ... }
//
// For data sources, the name parameter is a combination of the keyword "data",
// a period (.), the data source type, a period (.), and the name label. The
// name for the below example configuration would be
// "data.myprovider_thing.example".
//
// data "myprovider_thing" "example" { ... }
//
// The first and second names may use any combination of managed resources
// and/or data sources.
//
// The key parameter is an attribute path in Terraform CLI 0.11 and earlier
// "flatmap" syntax. Keys start with the attribute name of a top-level
// attribute. Use the sentinel value '*' to replace the element indexing into
// a list or set. The sentinel value can be used for each list or set index, if
// there are multiple lists or sets in the attribute path.
func TestCheckTypeSetElemAttrPair(nameFirst, keyFirst, nameSecond, keySecond string) TestCheckFunc {
return func(s *terraform.State) error {
isFirst, err := primaryInstanceState(s, nameFirst)
if err != nil {
return err
}
isSecond, err := primaryInstanceState(s, nameSecond)
if err != nil {
return err
}
vSecond, okSecond := isSecond.Attributes[keySecond]
if !okSecond {
return fmt.Errorf("%s: Attribute %q not set, cannot be checked against TypeSet", nameSecond, keySecond)
}
return testCheckTypeSetElemPair(isFirst, keyFirst, vSecond)
}
}
func testCheckTypeSetElem(is *terraform.InstanceState, attr, value string) error {
attrParts := strings.Split(attr, ".")
if attrParts[len(attrParts)-1] != sentinelIndex {
return fmt.Errorf("%q does not end with the special value %q", attr, sentinelIndex)
}
for stateKey, stateValue := range is.Attributes {
if stateValue == value {
stateKeyParts := strings.Split(stateKey, ".")
if len(stateKeyParts) == len(attrParts) {
for i := range attrParts {
if attrParts[i] != stateKeyParts[i] && attrParts[i] != sentinelIndex {
break
}
if i == len(attrParts)-1 {
return nil
}
}
}
}
}
return fmt.Errorf("no TypeSet element %q, with value %q in state: %#v", attr, value, is.Attributes)
}
func testCheckTypeSetElemPair(is *terraform.InstanceState, attr, value string) error {
attrParts := strings.Split(attr, ".")
for stateKey, stateValue := range is.Attributes {
if stateValue == value {
stateKeyParts := strings.Split(stateKey, ".")
if len(stateKeyParts) == len(attrParts) {
for i := range attrParts {
if attrParts[i] != stateKeyParts[i] && attrParts[i] != sentinelIndex {
break
}
if i == len(attrParts)-1 {
return nil
}
}
}
}
}
return fmt.Errorf("no TypeSet element %q, with value %q in state: %#v", attr, value, is.Attributes)
}
// testCheckTypeSetElemNestedAttrsInState is a helper function
// to determine if nested attributes and their values are equal to those
// in the instance state. Currently, the function accepts a "values" param of type
// map[string]string or map[string]*regexp.Regexp.
// Returns true if all attributes match, else false.
func testCheckTypeSetElemNestedAttrsInState(is *terraform.InstanceState, attrParts []string, matchCount int, values interface{}) bool {
matches := make(map[string]int)
for stateKey, stateValue := range is.Attributes {
stateKeyParts := strings.Split(stateKey, ".")
// a Set/List item with nested attrs would have a flatmap address of
// at least length 3
// foo.0.name = "bar"
if len(stateKeyParts) < 3 || len(attrParts) > len(stateKeyParts) {
continue
}
var pathMatch bool
for i := range attrParts {
if attrParts[i] != stateKeyParts[i] && attrParts[i] != sentinelIndex {
break
}
if i == len(attrParts)-1 {
pathMatch = true
}
}
if !pathMatch {
continue
}
id := stateKeyParts[len(attrParts)-1]
nestedAttr := strings.Join(stateKeyParts[len(attrParts):], ".")
var match bool
switch t := values.(type) {
case map[string]string:
if v, keyExists := t[nestedAttr]; keyExists && v == stateValue {
match = true
}
case map[string]*regexp.Regexp:
if v, keyExists := t[nestedAttr]; keyExists && v != nil && v.MatchString(stateValue) {
match = true
}
}
if match {
matches[id] = matches[id] + 1
if matches[id] == matchCount {
return true
}
}
}
return false
}