Files
opentf/internal/lang/evalchecks/eval_for_each_test.go
Andrei Ciobanu 1bab9aff46 Ephemeral todos handling (#3177)
Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
2025-09-10 07:45:23 -04:00

1276 lines
45 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package evalchecks
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// returns a mock ref function for the unit tests
func mockRefsFunc() ContextFunc {
return func(_ []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
evalContext := hcl.EvalContext{}
return &evalContext, diags
}
}
func TestEvaluateForEachExpression_multi_errors(t *testing.T) {
tests := map[string]struct {
Expr hcl.Expression
Wanted []struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}
}{
"marked list": {
hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Sensitive)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type list",
false,
},
},
},
"ephemeral marked list": {
hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Ephemeral)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments. If used, the ephemeral value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type list",
false,
},
},
},
"marked tuple": {
hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Sensitive)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type tuple",
false,
},
},
},
"ephemeral marked tuple": {
hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}).Mark(marks.Ephemeral)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments. If used, the ephemeral value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type tuple",
false,
},
},
},
"marked string": {
hcltest.MockExprLiteral(cty.StringVal("a").Mark(marks.Sensitive)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type string",
false,
},
},
},
"ephemeral marked string": {
hcltest.MockExprLiteral(cty.StringVal("a").Mark(marks.Ephemeral)),
[]struct {
Summary string
DetailSubstring string
CausedBySensitive bool
}{
{
"Invalid for_each argument",
"Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments. If used, the ephemeral value could be exposed as a resource instance key.",
true,
},
{
"Invalid for_each argument",
"must be a map, or set of strings, and you have provided a value of type string",
false,
},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
_, diags := EvaluateForEachExpression(test.Expr, mockRefsFunc(), nil)
if len(diags) != len(test.Wanted) {
t.Fatalf("Expected diagnostics: %s\nGot: %s\n", spew.Sdump(test.Wanted), spew.Sdump(diags))
}
for idx := range test.Wanted {
if got, want := diags[idx].Severity(), tfdiags.Error; got != want {
t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
}
if got, want := diags[idx].Description().Summary, test.Wanted[idx].Summary; got != want {
t.Errorf("wrong diagnostic summary\ngot: %s\nwant: %s", got, want)
}
if got, want := diags[idx].Description().Detail, test.Wanted[idx].DetailSubstring; !strings.Contains(got, want) {
t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
}
if fromExpr := diags[idx].FromExpr(); fromExpr != nil {
if fromExpr.Expression == nil {
t.Errorf("diagnostic does not refer to an expression")
}
if fromExpr.EvalContext == nil {
t.Errorf("diagnostic does not refer to an EvalContext")
}
} else {
t.Errorf("diagnostic does not support FromExpr\ngot: %s", spew.Sdump(diags[idx]))
}
if got, want := tfdiags.DiagnosticCausedByConfidentialValues(diags[idx]), test.Wanted[idx].CausedBySensitive; got != want {
t.Errorf("wrong result from tfdiags.DiagnosticCausedByConfidentialValues\ngot: %#v\nwant: %#v", got, want)
}
}
})
}
}
func TestEvaluateForEachExpressionValueTuple(t *testing.T) {
// Tests for cases where allowTuple is true. This can't be added to the tests on TestEvaluateForEach since they are called with allowTuple as false.
tests := map[string]struct {
Value cty.Value
expectedErrs []expectedErr
}{
"valid tuple": {
Value: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}),
},
"empty tuple": {
Value: cty.EmptyTupleVal,
},
"null tuple": {
Value: cty.NullVal(cty.Tuple([]cty.Type{})),
expectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "the given \"for_each\" argument value is null",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
},
"sensitive tuple": {
Value: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Sensitive),
expectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
},
},
"ephemeral tuple": {
Value: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Ephemeral),
expectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
},
},
"unknown tuple": {
Value: cty.UnknownVal(cty.Tuple([]cty.Type{cty.String})),
expectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "tuple includes values derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
expr := hcltest.MockExprLiteral(test.Value)
_, diags := EvaluateForEachExpressionValue(expr, mockRefsFunc(), false, true, nil)
assertDiagnosticsMatch(t, diags, test.expectedErrs, "plan")
})
}
}
func TestForEachCommandLineExcludeSuggestion(t *testing.T) {
noKeyResourceAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "happycloud_virtual_machine",
Name: "boop",
}.Absolute(addrs.RootModuleInstance)
stringKeyResourceAddr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "happycloud_virtual_machine",
Name: "boop",
}.Absolute(addrs.RootModuleInstance.Child("foo", addrs.StringKey("bleep bloop")))
tests := []struct {
excludableAddr addrs.Targetable
goos string
want string
}{
{
nil,
"linux",
`Alternatively, you could use the -target option to first apply only the resources that for_each depends on, and then apply normally to converge.`,
},
{
nil,
"windows",
`Alternatively, you could use the -target option to first apply only the resources that for_each depends on, and then apply normally to converge.`,
},
{
nil,
"darwin",
`Alternatively, you could use the -target option to first apply only the resources that for_each depends on, and then apply normally to converge.`,
},
{
noKeyResourceAddr,
"linux",
`Alternatively, you could use the planning option -exclude=happycloud_virtual_machine.boop to first apply without this object, and then apply normally to converge.`,
},
{
noKeyResourceAddr,
"windows",
`Alternatively, you could use the planning option -exclude=happycloud_virtual_machine.boop to first apply without this object, and then apply normally to converge.`,
},
{
noKeyResourceAddr,
"darwin",
`Alternatively, you could use the planning option -exclude=happycloud_virtual_machine.boop to first apply without this object, and then apply normally to converge.`,
},
{
stringKeyResourceAddr,
"linux",
`Alternatively, you could use the planning option -exclude='module.foo["bleep bloop"].happycloud_virtual_machine.boop' to first apply without this object, and then apply normally to converge.`,
},
{
stringKeyResourceAddr,
"windows",
`Alternatively, you could use the planning option -exclude="module.foo[\"bleep bloop\"].happycloud_virtual_machine.boop" to first apply without this object, and then apply normally to converge.`,
},
{
stringKeyResourceAddr,
"darwin",
`Alternatively, you could use the planning option -exclude='module.foo["bleep bloop"].happycloud_virtual_machine.boop' to first apply without this object, and then apply normally to converge.`,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%v on %s", test.excludableAddr, test.goos), func(t *testing.T) {
got := forEachCommandLineExcludeSuggestionImpl(test.excludableAddr, test.goos)
if got != test.want {
t.Errorf("wrong suggestion\ngot: %s\nwant: %s", got, test.want)
}
})
}
}
type expectedErr struct {
Summary string
Detail string
CausedByUnknown bool
CausedBySensitive bool
}
func assertDiagnosticsMatch(t *testing.T, gotDiags []tfdiags.Diagnostic, wantDiags []expectedErr, phase string) {
t.Helper()
if len(gotDiags) != len(wantDiags) {
t.Errorf("got %d errors in %s phase; expected %d", len(gotDiags), phase, len(wantDiags))
}
for i := range max(len(gotDiags), len(wantDiags)) {
if i >= len(gotDiags) {
t.Errorf("on %s phase, got no error on position %d, while expecting error:\n%s\n%s", phase, i, wantDiags[i].Summary, wantDiags[i].Detail)
} else if i >= len(wantDiags) {
t.Errorf("on %s phase on position %d, while expecting no error, got:\n%s\n%s", phase, i, gotDiags[i].Description().Summary, gotDiags[i].Description().Detail)
} else {
assertDiagnosticMatch(t, phase, gotDiags[i], wantDiags[i])
}
}
}
func assertDiagnosticMatch(t *testing.T, phase string, gotDiag tfdiags.Diagnostic, wantDiag expectedErr) {
t.Helper()
if got, want := gotDiag.Severity(), tfdiags.Error; got != want {
t.Errorf("wrong diagnostic severity on %s phase %#v; want %#v", phase, got, want)
}
if got, want := gotDiag.Description().Summary, wantDiag.Summary; got != want {
t.Errorf("wrong diagnostic summary on %s phase\ngot: %s\nwant: %s", phase, got, want)
}
if got, want := gotDiag.Description().Detail, wantDiag.Detail; !strings.Contains(got, want) {
t.Errorf("wrong diagnostic detail on %s phase, \ngot: %s\nwant substring: %s", phase, got, want)
}
if got, want := tfdiags.DiagnosticCausedByUnknown(gotDiag), wantDiag.CausedByUnknown; got != want {
t.Errorf("wrong result from tfdiags.DiagnosticCausedByUnknown on %s phase\ngot: %#v\nwant: %#v", phase, got, want)
}
if got, want := tfdiags.DiagnosticCausedByConfidentialValues(gotDiag), wantDiag.CausedBySensitive; got != want {
t.Errorf("wrong result from tfdiags.DiagnosticCausedByConfidentialValues on %s phase\ngot: %#v\nwant: %#v", phase, got, want)
}
if fromExpr := gotDiag.FromExpr(); fromExpr != nil {
if fromExpr.Expression == nil {
t.Errorf("diagnostic does not refer to an expression")
}
if fromExpr.EvalContext == nil {
t.Errorf("diagnostic does not refer to an EvalContext")
}
} else {
t.Errorf("diagnostic does not support FromExpr\ngot: %s", spew.Sdump(gotDiag))
}
}
func TestEvaluateForEach(t *testing.T) {
t.Parallel()
tests := map[string]struct {
Input cty.Value
ValidateExpectedErrs []expectedErr
ValidateReturnValue cty.Value
PlanExpectedErrs []expectedErr
PlanReturnValue map[string]cty.Value
}{
"empty_set": {
Input: cty.SetValEmpty(cty.String),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.SetValEmpty(cty.String),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{},
},
"empty_set_dynamic": {
Input: cty.SetValEmpty(cty.DynamicPseudoType),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.SetValEmpty(cty.DynamicPseudoType),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{},
},
"set_of_strings": {
Input: cty.SetVal([]cty.Value{cty.StringVal("a")}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.SetVal([]cty.Value{cty.StringVal("a")}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.StringVal("a")},
},
"set_of_bool": {
Input: cty.SetVal([]cty.Value{cty.True}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "but you have provided a set containing type bool.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Set(cty.Bool)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "but you have provided a set containing type bool.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"set_of_null": {
Input: cty.SetVal([]cty.Value{cty.NullVal(cty.String)}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "sets must not contain null values",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "sets must not contain null values.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"set_of_unknown_strings": {
Input: cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.UnknownVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set includes values derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"set_of_unknown_dynamic": {
Input: cty.SetVal([]cty.Value{cty.DynamicVal}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.UnknownVal(cty.Set(cty.DynamicPseudoType)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set includes values derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
// # Tuples
"empty_tuple": {
Input: cty.EmptyTupleVal,
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.EmptyTuple),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"tuple_of_strings": {
Input: cty.TupleVal([]cty.Value{cty.StringVal("a")}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.String})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"tuple_of_bool": {
Input: cty.TupleVal([]cty.Value{cty.True}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.Bool})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"tuple_of_null": {
Input: cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.String})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"tuple_of_unknown_strings": {
Input: cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String)}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.String})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"tuple_of_unknown_dynamic": {
Input: cty.TupleVal([]cty.Value{cty.DynamicVal}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.DynamicPseudoType})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"empty_map": {
Input: cty.MapValEmpty(cty.String),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.MapValEmpty(cty.String),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{},
},
"map_of_null": {
Input: cty.MapVal(map[string]cty.Value{"a": cty.NullVal(cty.String)}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.MapVal(map[string]cty.Value{"a": cty.NullVal(cty.String)}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.NullVal(cty.String)},
},
"map_of_unknown_string": {
Input: cty.MapVal(map[string]cty.Value{"a": cty.UnknownVal(cty.String)}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.MapVal(map[string]cty.Value{"a": cty.UnknownVal(cty.String)}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.UnknownVal(cty.String)},
},
"map_of_bool": {
Input: cty.MapVal(map[string]cty.Value{"a": cty.True}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.MapVal(map[string]cty.Value{"a": cty.True}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.True},
},
"map_of_unknown_bool": {
Input: cty.MapVal(map[string]cty.Value{"a": cty.UnknownVal(cty.Bool)}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.MapVal(map[string]cty.Value{"a": cty.UnknownVal(cty.Bool)}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.UnknownVal(cty.Bool)},
},
"map_of_string": {
Input: cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b")}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b")}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.StringVal("b")},
},
"empty_object": {
Input: cty.EmptyObjectVal,
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.EmptyObjectVal,
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{},
},
"object_with_null_values": {
Input: cty.ObjectVal(map[string]cty.Value{"a": cty.NullVal(cty.String)}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.ObjectVal(map[string]cty.Value{"a": cty.NullVal(cty.String)}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.NullVal(cty.String)},
},
"object_with_unknown_string_values": {
Input: cty.ObjectVal(map[string]cty.Value{"a": cty.UnknownVal(cty.String)}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.ObjectVal(map[string]cty.Value{"a": cty.UnknownVal(cty.String)}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.UnknownVal(cty.String)},
},
"object_with_bool_values": {
Input: cty.ObjectVal(map[string]cty.Value{"a": cty.BoolVal(true)}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.ObjectVal(map[string]cty.Value{"a": cty.True}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.True},
},
"object_with_unknown_bool_values": {
Input: cty.ObjectVal(map[string]cty.Value{"a": cty.UnknownVal(cty.Bool)}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.ObjectVal(map[string]cty.Value{"a": cty.UnknownVal(cty.Bool)}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.UnknownVal(cty.Bool)},
},
"object_with_string_values": {
Input: cty.ObjectVal(map[string]cty.Value{"a": cty.StringVal("b")}),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.ObjectVal(map[string]cty.Value{"a": cty.StringVal("b")}),
PlanExpectedErrs: nil,
PlanReturnValue: map[string]cty.Value{"a": cty.StringVal("b")},
},
// Other
"null_set_string": {
Input: cty.NullVal(cty.Set(cty.String)),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument value is null.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument value is null.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"null_object": {
Input: cty.NullVal(cty.Object(map[string]cty.Type{
"route_addrs": cty.String,
"cidr": cty.String,
})),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument value is null.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Object(map[string]cty.Type{"cidr": cty.String, "route_addrs": cty.String})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument value is null.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"null_tuple": {
Input: cty.NullVal(cty.Tuple([]cty.Type{})),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument value is null.",
CausedByUnknown: false,
CausedBySensitive: false,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument value is null.",
CausedByUnknown: false,
CausedBySensitive: false,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"sensitive_tuple": {
Input: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Sensitive),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"sensitive_set": {
Input: cty.SetVal([]cty.Value{cty.StringVal("a")}).Mark(marks.Sensitive),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
ValidateReturnValue: cty.NullVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
PlanReturnValue: map[string]cty.Value{},
},
"sensitive_set_elements": {
Input: cty.SetVal([]cty.Value{cty.StringVal("a").Mark(marks.Sensitive)}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
ValidateReturnValue: cty.NullVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
PlanReturnValue: map[string]cty.Value{},
},
"ephemeral_tuple": {
Input: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Ephemeral),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"ephemeral_set": {
Input: cty.SetVal([]cty.Value{cty.StringVal("a")}).Mark(marks.Ephemeral),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
ValidateReturnValue: cty.NullVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
PlanReturnValue: map[string]cty.Value{},
},
"ephemeral_set_elements": {
Input: cty.SetVal([]cty.Value{cty.StringVal("a").Mark(marks.Ephemeral)}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
ValidateReturnValue: cty.NullVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "Ephemeral values, or values derived from ephemeral values, cannot be used as for_each arguments",
CausedByUnknown: false,
CausedBySensitive: true,
}},
PlanReturnValue: map[string]cty.Value{},
},
"string": {
Input: cty.StringVal("i am definitely a set"),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type string",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.String),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type string",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"number": {
Input: cty.MustParseNumberVal("1e+50"),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type number",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Number),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type number",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"bool": {
Input: cty.True,
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type bool",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Bool),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type bool",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"list": {
Input: cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")}),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type list of string",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.List(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "must be a map, or set of strings, and you have provided a value of type list of string",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
// Top-Level Unknowns (basically all the above with unknown on the top-level)
"unknown_empty_set": {
Input: cty.UnknownVal(cty.Set(cty.String)),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.UnknownVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set includes values derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_set_of_object": {
Input: cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
"route_addrs": cty.String,
"cidr": cty.String,
}))),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set containing type object.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{"cidr": cty.String, "route_addrs": cty.String}))),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set includes values derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
{
Summary: "Invalid for_each argument",
Detail: "set containing type object.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_set_of_strings": {
Input: cty.UnknownVal(cty.Set(cty.String)),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.UnknownVal(cty.Set(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set includes values derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_set_of_bool": {
Input: cty.UnknownVal(cty.Set(cty.Bool)),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set containing type bool.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.UnknownVal(cty.Set(cty.Bool)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set includes values derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
{
Summary: "Invalid for_each argument",
Detail: "set containing type bool.",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_set_of_dynamic": {
Input: cty.UnknownVal(cty.Set(cty.DynamicPseudoType)),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.UnknownVal(cty.Set(cty.DynamicPseudoType)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "set includes values derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_empty_map": {
Input: cty.UnknownVal(cty.Map(cty.String)),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.UnknownVal(cty.Map(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "map includes keys derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_map_of_strings": {
Input: cty.UnknownVal(cty.Map(cty.String)),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.UnknownVal(cty.Map(cty.String)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "map includes keys derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_of_unknowns": {
Input: cty.DynamicVal,
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.DynamicVal,
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "value includes keys or set values from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_map_of_bool": {
Input: cty.UnknownVal(cty.Map(cty.Bool)),
ValidateExpectedErrs: nil,
ValidateReturnValue: cty.UnknownVal(cty.Map(cty.Bool)),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "map includes keys derived from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_tuple_of_strings": {
Input: cty.UnknownVal(cty.Tuple([]cty.Type{cty.String})),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.String})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "value includes keys or set values from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_tuple_of_dynamic": {
Input: cty.UnknownVal(cty.Tuple([]cty.Type{cty.DynamicPseudoType})),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.DynamicPseudoType})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "value includes keys or set values from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
"unknown_tuple_of_bool": {
Input: cty.UnknownVal(cty.Tuple([]cty.Type{cty.Bool})),
ValidateExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
ValidateReturnValue: cty.NullVal(cty.Tuple([]cty.Type{cty.Bool})),
PlanExpectedErrs: []expectedErr{
{
Summary: "Invalid for_each argument",
Detail: "value includes keys or set values from resource attributes that cannot be determined until apply",
CausedByUnknown: true,
CausedBySensitive: false,
},
{
Summary: "Invalid for_each argument",
Detail: "argument must be a map, or set of strings, and you have provided a value of type tuple",
CausedByUnknown: false,
CausedBySensitive: false,
},
},
PlanReturnValue: map[string]cty.Value{},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
// Validate Phase
allowUnknown := true
allowTuple := false
expr := hcltest.MockExprLiteral(test.Input)
validateReturn, validateDiags := EvaluateForEachExpressionValue(expr, mockRefsFunc(), allowUnknown, allowTuple, nil)
if !validateReturn.RawEquals(test.ValidateReturnValue) {
t.Errorf("got %#v in validate phase; want %#v", validateReturn, test.ValidateReturnValue)
}
if test.ValidateExpectedErrs != nil || len(validateDiags) > 0 {
assertDiagnosticsMatch(t, validateDiags, test.ValidateExpectedErrs, "validate")
}
// Plan Phase
planReturn, planDiags := EvaluateForEachExpression(expr, mockRefsFunc(), nil)
if !reflect.DeepEqual(planReturn, test.PlanReturnValue) {
t.Errorf("got %#v in plan phase; want %#v", planReturn, test.PlanReturnValue)
}
if test.PlanExpectedErrs != nil || len(planDiags) > 0 {
assertDiagnosticsMatch(t, planDiags, test.PlanExpectedErrs, "plan")
}
})
}
}