Bumps [github.com/hashicorp/terraform-plugin-sdk/v2](https://github.com/hashicorp/terraform-plugin-sdk) from 2.26.1 to 2.27.0. - [Release notes](https://github.com/hashicorp/terraform-plugin-sdk/releases) - [Changelog](https://github.com/hashicorp/terraform-plugin-sdk/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/terraform-plugin-sdk/compare/v2.26.1...v2.27.0) --- updated-dependencies: - dependency-name: github.com/hashicorp/terraform-plugin-sdk/v2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
362 lines
13 KiB
Go
362 lines
13 KiB
Go
// 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
|
|
}
|