mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
90ea7b0bc5
By observing the sorts of questions people ask in the community, and the ways they ask them, we've inferred that various different people have been confused by Terraform reporting that a value won't be known until apply or that a value is sensitive as part of an error message when that message doesn't actually relate to the known-ness and sensitivity of any value. Quite reasonably, someone who sees Terraform discussing an unfamiliar concept like unknown values can assume that it must be somehow relevant to the problem being discussed, and so in that sense Terraform's current error messages are giving "too much information": information that isn't actually helpful in understanding the problem being described, and in the worst case is a distraction from understanding the problem being described. With that in mind then, here we introduce an explicit annotation on diagnostic objects that are directly talking about unknown values or sensitive values, and then the diagnostic renderer will react to that to avoid using the terminology "known only after apply" or "sensitive" in the generated diagnostic annotations unless we're rendering a message that is explicitly related to one of those topics. This ends up being a bit of a cross-cutting concern because the code that generates these diagnostics and the code that renders them are in separate packages and are not directly aware of each other. With that in mind, the logic for actually deciding for a particular diagnostic whether it's flagged in one of these special ways lives inside the tfdiags package as an intermediation point, which both the diagnostic generator (in the core package) and the diagnostic renderer can both depend on.
233 lines
7.6 KiB
Go
233 lines
7.6 KiB
Go
package terraform
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hcltest"
|
|
"github.com/hashicorp/terraform/internal/lang/marks"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestEvaluateForEachExpression_valid(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Expr hcl.Expression
|
|
ForEachMap map[string]cty.Value
|
|
}{
|
|
"empty set": {
|
|
hcltest.MockExprLiteral(cty.SetValEmpty(cty.String)),
|
|
map[string]cty.Value{},
|
|
},
|
|
"multi-value string set": {
|
|
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
|
|
map[string]cty.Value{
|
|
"a": cty.StringVal("a"),
|
|
"b": cty.StringVal("b"),
|
|
},
|
|
},
|
|
"empty map": {
|
|
hcltest.MockExprLiteral(cty.MapValEmpty(cty.Bool)),
|
|
map[string]cty.Value{},
|
|
},
|
|
"map": {
|
|
hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"a": cty.BoolVal(true),
|
|
"b": cty.BoolVal(false),
|
|
})),
|
|
map[string]cty.Value{
|
|
"a": cty.BoolVal(true),
|
|
"b": cty.BoolVal(false),
|
|
},
|
|
},
|
|
"map containing unknown values": {
|
|
hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"a": cty.UnknownVal(cty.Bool),
|
|
"b": cty.UnknownVal(cty.Bool),
|
|
})),
|
|
map[string]cty.Value{
|
|
"a": cty.UnknownVal(cty.Bool),
|
|
"b": cty.UnknownVal(cty.Bool),
|
|
},
|
|
},
|
|
"map containing sensitive values, but strings are literal": {
|
|
hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"a": cty.BoolVal(true).Mark(marks.Sensitive),
|
|
"b": cty.BoolVal(false),
|
|
})),
|
|
map[string]cty.Value{
|
|
"a": cty.BoolVal(true).Mark(marks.Sensitive),
|
|
"b": cty.BoolVal(false),
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ctx := &MockEvalContext{}
|
|
ctx.installSimpleEval()
|
|
forEachMap, diags := evaluateForEachExpression(test.Expr, ctx)
|
|
|
|
if len(diags) != 0 {
|
|
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
|
}
|
|
|
|
if !reflect.DeepEqual(forEachMap, test.ForEachMap) {
|
|
t.Errorf(
|
|
"wrong map value\ngot: %swant: %s",
|
|
spew.Sdump(forEachMap), spew.Sdump(test.ForEachMap),
|
|
)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluateForEachExpression_errors(t *testing.T) {
|
|
tests := map[string]struct {
|
|
Expr hcl.Expression
|
|
Summary, DetailSubstring string
|
|
CausedByUnknown, CausedBySensitive bool
|
|
}{
|
|
"null set": {
|
|
hcltest.MockExprLiteral(cty.NullVal(cty.Set(cty.String))),
|
|
"Invalid for_each argument",
|
|
`the given "for_each" argument value is null`,
|
|
false, false,
|
|
},
|
|
"string": {
|
|
hcltest.MockExprLiteral(cty.StringVal("i am definitely a set")),
|
|
"Invalid for_each argument",
|
|
"must be a map, or set of strings, and you have provided a value of type string",
|
|
false, false,
|
|
},
|
|
"list": {
|
|
hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")})),
|
|
"Invalid for_each argument",
|
|
"must be a map, or set of strings, and you have provided a value of type list",
|
|
false, false,
|
|
},
|
|
"tuple": {
|
|
hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
|
|
"Invalid for_each argument",
|
|
"must be a map, or set of strings, and you have provided a value of type tuple",
|
|
false, false,
|
|
},
|
|
"unknown string set": {
|
|
hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
|
|
"Invalid for_each argument",
|
|
"set includes values derived from resource attributes that cannot be determined until apply",
|
|
true, false,
|
|
},
|
|
"unknown map": {
|
|
hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
|
|
"Invalid for_each argument",
|
|
"map includes keys derived from resource attributes that cannot be determined until apply",
|
|
true, false,
|
|
},
|
|
"marked map": {
|
|
hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
|
|
"a": cty.BoolVal(true),
|
|
"b": cty.BoolVal(false),
|
|
}).Mark(marks.Sensitive)),
|
|
"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.",
|
|
false, true,
|
|
},
|
|
"set containing booleans": {
|
|
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.BoolVal(true)})),
|
|
"Invalid for_each set argument",
|
|
"supports maps and sets of strings, but you have provided a set containing type bool",
|
|
false, false,
|
|
},
|
|
"set containing null": {
|
|
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.NullVal(cty.String)})),
|
|
"Invalid for_each set argument",
|
|
"must not contain null values",
|
|
false, false,
|
|
},
|
|
"set containing unknown value": {
|
|
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)})),
|
|
"Invalid for_each argument",
|
|
"set includes values derived from resource attributes that cannot be determined until apply",
|
|
true, false,
|
|
},
|
|
"set containing dynamic unknown value": {
|
|
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.DynamicPseudoType)})),
|
|
"Invalid for_each argument",
|
|
"set includes values derived from resource attributes that cannot be determined until apply",
|
|
true, false,
|
|
},
|
|
"set containing marked values": {
|
|
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("beep").Mark(marks.Sensitive), cty.StringVal("boop")})),
|
|
"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.",
|
|
false, true,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ctx := &MockEvalContext{}
|
|
ctx.installSimpleEval()
|
|
_, diags := evaluateForEachExpression(test.Expr, ctx)
|
|
|
|
if len(diags) != 1 {
|
|
t.Fatalf("got %d diagnostics; want 1", diags)
|
|
}
|
|
if got, want := diags[0].Severity(), tfdiags.Error; got != want {
|
|
t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
|
|
}
|
|
if got, want := diags[0].Description().Summary, test.Summary; got != want {
|
|
t.Errorf("wrong diagnostic summary\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if got, want := diags[0].Description().Detail, test.DetailSubstring; !strings.Contains(got, want) {
|
|
t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
|
|
}
|
|
if fromExpr := diags[0].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[0]))
|
|
}
|
|
|
|
if got, want := tfdiags.DiagnosticCausedByUnknown(diags[0]), test.CausedByUnknown; got != want {
|
|
t.Errorf("wrong result from tfdiags.DiagnosticCausedByUnknown\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
if got, want := tfdiags.DiagnosticCausedBySensitive(diags[0]), test.CausedBySensitive; got != want {
|
|
t.Errorf("wrong result from tfdiags.DiagnosticCausedBySensitive\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvaluateForEachExpressionKnown(t *testing.T) {
|
|
tests := map[string]hcl.Expression{
|
|
"unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
|
|
"unknown map": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
|
|
}
|
|
|
|
for name, expr := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
ctx := &MockEvalContext{}
|
|
ctx.installSimpleEval()
|
|
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true)
|
|
|
|
if len(diags) != 0 {
|
|
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
|
|
}
|
|
|
|
if forEachVal.IsKnown() {
|
|
t.Error("got known, want unknown")
|
|
}
|
|
})
|
|
}
|
|
}
|