Skip to content

Commit 171e7ef

Browse files
core: Invalid for_each argument messaging improvements
Our original messaging here was largely just lifted from the equivalent message for unknown values in "count", and it didn't really include any specific advice on how to update a configuration to make for_each valid, instead focusing only on the workaround of using the -target planning option. It's tough to pack in a fully-actionable suggestion here since unknown values in for_each keys tends to be a gnarly architectural problem rather than a local quirk -- when data flows between modules it can sometimes be unclear whether it'll end up being used in a context which allows unknown values. I did my best to summarize the advice we've been giving in community forum though, in the hope that more people will be able to address this for themselves without asking for help, until we're one day able to smooth this out better with a mechanism such as "partial apply".
1 parent 1e9075b commit 171e7ef

File tree

2 files changed

+19
-10
lines changed

2 files changed

+19
-10
lines changed

internal/terraform/eval_for_each.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
7878
}
7979
ty := forEachVal.Type()
8080

81+
const errInvalidUnknownDetailMap = "The \"for_each\" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
82+
const errInvalidUnknownDetailSet = "The \"for_each\" set includes values derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
83+
8184
switch {
8285
case forEachVal.IsNull():
8386
diags = diags.Append(&hcl.Diagnostic{
@@ -91,10 +94,18 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
9194
return nullMap, diags
9295
case !forEachVal.IsKnown():
9396
if !allowUnknown {
97+
var detailMsg string
98+
switch {
99+
case ty.IsSetType():
100+
detailMsg = errInvalidUnknownDetailSet
101+
default:
102+
detailMsg = errInvalidUnknownDetailMap
103+
}
104+
94105
diags = diags.Append(&hcl.Diagnostic{
95106
Severity: hcl.DiagError,
96107
Summary: "Invalid for_each argument",
97-
Detail: errInvalidForEachUnknownDetail,
108+
Detail: detailMsg,
98109
Subject: expr.Range().Ptr(),
99110
Expression: expr,
100111
EvalContext: hclCtx,
@@ -129,7 +140,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
129140
diags = diags.Append(&hcl.Diagnostic{
130141
Severity: hcl.DiagError,
131142
Summary: "Invalid for_each argument",
132-
Detail: errInvalidForEachUnknownDetail,
143+
Detail: errInvalidUnknownDetailSet,
133144
Subject: expr.Range().Ptr(),
134145
Expression: expr,
135146
EvalContext: hclCtx,
@@ -172,8 +183,6 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
172183
return forEachVal, nil
173184
}
174185

175-
const errInvalidForEachUnknownDetail = `The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.`
176-
177186
// markSafeLengthInt allows calling LengthInt on marked values safely
178187
func markSafeLengthInt(val cty.Value) int {
179188
v, _ := val.UnmarkDeep()

internal/terraform/eval_for_each_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,12 @@ func TestEvaluateForEachExpression_errors(t *testing.T) {
114114
"unknown string set": {
115115
hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
116116
"Invalid for_each argument",
117-
"depends on resource attributes that cannot be determined until apply",
117+
"set includes values derived from resource attributes that cannot be determined until apply",
118118
},
119119
"unknown map": {
120120
hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
121121
"Invalid for_each argument",
122-
"depends on resource attributes that cannot be determined until apply",
122+
"map includes keys derived from resource attributes that cannot be determined until apply",
123123
},
124124
"marked map": {
125125
hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{
@@ -142,12 +142,12 @@ func TestEvaluateForEachExpression_errors(t *testing.T) {
142142
"set containing unknown value": {
143143
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)})),
144144
"Invalid for_each argument",
145-
"depends on resource attributes that cannot be determined until apply",
145+
"set includes values derived from resource attributes that cannot be determined until apply",
146146
},
147147
"set containing dynamic unknown value": {
148148
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.DynamicPseudoType)})),
149149
"Invalid for_each argument",
150-
"depends on resource attributes that cannot be determined until apply",
150+
"set includes values derived from resource attributes that cannot be determined until apply",
151151
},
152152
"set containing marked values": {
153153
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("beep").Mark(marks.Sensitive), cty.StringVal("boop")})),
@@ -169,10 +169,10 @@ func TestEvaluateForEachExpression_errors(t *testing.T) {
169169
t.Errorf("wrong diagnostic severity %#v; want %#v", got, want)
170170
}
171171
if got, want := diags[0].Description().Summary, test.Summary; got != want {
172-
t.Errorf("wrong diagnostic summary %#v; want %#v", got, want)
172+
t.Errorf("wrong diagnostic summary\ngot: %s\nwant: %s", got, want)
173173
}
174174
if got, want := diags[0].Description().Detail, test.DetailSubstring; !strings.Contains(got, want) {
175-
t.Errorf("wrong diagnostic detail %#v; want %#v", got, want)
175+
t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
176176
}
177177
if fromExpr := diags[0].FromExpr(); fromExpr != nil {
178178
if fromExpr.Expression == nil {

0 commit comments

Comments
 (0)