mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-18 12:42:58 -06:00
36d0a50427
This is part of a general effort to move all of Terraform's non-library package surface under internal in order to reinforce that these are for internal use within Terraform only. If you were previously importing packages under this prefix into an external codebase, you could pin to an earlier release tag as an interim solution until you've make a plan to achieve the same functionality some other way.
181 lines
6.5 KiB
Go
181 lines
6.5 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/terraform/internal/lang"
|
|
"github.com/hashicorp/terraform/internal/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() || markSafeLengthInt(forEachVal) == 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 a whole map is marked, or a set contains marked values (which means the set is then marked)
|
|
// give an error diagnostic as this value cannot be used in for_each
|
|
if forEachVal.IsMarked() {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid for_each argument",
|
|
Detail: "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.",
|
|
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 markSafeLengthInt(forEachVal) == 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: `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.`
|
|
|
|
// markSafeLengthInt allows calling LengthInt on marked values safely
|
|
func markSafeLengthInt(val cty.Value) int {
|
|
v, _ := val.UnmarkDeep()
|
|
return v.LengthInt()
|
|
}
|