package terraform import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" "github.com/zclconf/go-cty/cty" ) // evaluateResourceForEachExpression interprets a "for_each" argument on a resource. // // Returns a cty.Value map, and diagnostics if necessary. It will return nil if // the expression is nil, and is used to distinguish between an unset for_each and an // empty map func evaluateResourceForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) { forEachMap, known, diags := evaluateResourceForEachExpressionKnown(expr, ctx) if !known { // Attach a diag as we do with count, with the same downsides diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid for_each argument", Detail: `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.`, Subject: expr.Range().Ptr(), }) } return forEachMap, diags } // evaluateResourceForEachExpressionKnown is like evaluateResourceForEachExpression // except that it handles an unknown result by returning an empty map and // a known = false, rather than by reporting the unknown value as an error // diagnostic. func evaluateResourceForEachExpressionKnown(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, known bool, diags tfdiags.Diagnostics) { if expr == nil { return nil, true, nil } forEachVal, forEachDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) diags = diags.Append(forEachDiags) if diags.HasErrors() { return nil, true, diags } 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(), }) return nil, true, diags case !forEachVal.IsKnown(): return map[string]cty.Value{}, false, diags } if !forEachVal.CanIterateElements() || forEachVal.Type().IsListType() || forEachVal.Type().IsTupleType() { 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.`, forEachVal.Type().FriendlyName()), Subject: expr.Range().Ptr(), }) return nil, true, diags } // If the map is empty ({}), return an empty map, because cty will return nil when representing {} AsValueMap // This also covers an empty set (toset([])) if forEachVal.LengthInt() == 0 { return map[string]cty.Value{}, true, diags } if forEachVal.Type().IsSetType() { if forEachVal.Type().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(), }) return nil, true, diags } // A set may contain unknown values that must be // discovered by checking with IsWhollyKnown (which iterates through the // structure), while for maps in cty, keys can never be unknown or null, // thus the earlier IsKnown check suffices for maps if !forEachVal.IsWhollyKnown() { return map[string]cty.Value{}, false, diags } } return forEachVal.AsValueMap(), true, nil }