opentofu/terraform/eval_for_each.go
Martin Atkins d03a774def core: Fix typo in for_each sensitive error message
Co-authored-by: Alisdair McDiarmid <alisdair@users.noreply.github.com>
2020-10-29 09:07:48 -07:00

171 lines
6.1 KiB
Go

package terraform
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/lang"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// evaluateForEachExpression is our standard mechanism for interpreting an
// expression given for a "for_each" argument on a resource or a module. This
// should be called during expansion in order to determine the final keys and
// values.
//
// evaluateForEachExpression differs from evaluateForEachExpressionValue by
// returning an error if the count value is not known, and converting the
// cty.Value to a map[string]cty.Value for compatibility with other calls.
func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) {
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, false)
// forEachVal might be unknown, but if it is then there should already
// be an error about it in diags, which we'll return below.
if forEachVal.IsNull() || !forEachVal.IsKnown() || forEachVal.LengthInt() == 0 {
// we check length, because an empty set return a nil map
return map[string]cty.Value{}, diags
}
return forEachVal.AsValueMap(), diags
}
// evaluateForEachExpressionValue is like evaluateForEachExpression
// except that it returns a cty.Value map or set which can be unknown.
func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
nullMap := cty.NullVal(cty.Map(cty.DynamicPseudoType))
if expr == nil {
return nullMap, diags
}
refs, moreDiags := lang.ReferencesInExpr(expr)
diags = diags.Append(moreDiags)
scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey)
var hclCtx *hcl.EvalContext
if scope != nil {
hclCtx, moreDiags = scope.EvalContext(refs)
} else {
// This shouldn't happen in real code, but it can unfortunately arise
// in unit tests due to incompletely-implemented mocks. :(
hclCtx = &hcl.EvalContext{}
}
diags = diags.Append(moreDiags)
if diags.HasErrors() { // Can't continue if we don't even have a valid scope
return nullMap, diags
}
forEachVal, forEachDiags := expr.Value(hclCtx)
diags = diags.Append(forEachDiags)
if forEachVal.ContainsMarked() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: "Sensitive variables, or values derived from sensitive variables, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
}
if diags.HasErrors() {
return nullMap, diags
}
ty := forEachVal.Type()
switch {
case forEachVal.IsNull():
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`,
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
return nullMap, diags
case !forEachVal.IsKnown():
if !allowUnknown {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: errInvalidForEachUnknownDetail,
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
}
// ensure that we have a map, and not a DynamicValue
return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), diags
case !(ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()):
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type %s.`, ty.FriendlyName()),
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
return nullMap, diags
case forEachVal.LengthInt() == 0:
// If the map is empty ({}), return an empty map, because cty will
// return nil when representing {} AsValueMap. This also covers an empty
// set (toset([]))
return forEachVal, diags
}
if ty.IsSetType() {
// since we can't use a set values that are unknown, we treat the
// entire set as unknown
if !forEachVal.IsWhollyKnown() {
if !allowUnknown {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: errInvalidForEachUnknownDetail,
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
}
return cty.UnknownVal(ty), diags
}
if ty.ElementType() != cty.String {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each set argument",
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
return cty.NullVal(ty), diags
}
// A set of strings may contain null, which makes it impossible to
// convert to a map, so we must return an error
it := forEachVal.ElementIterator()
for it.Next() {
item, _ := it.Element()
if item.IsNull() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each set argument",
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values.`),
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
return cty.NullVal(ty), diags
}
}
}
return forEachVal, nil
}
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.`