github.com/opentofu/opentofu@v1.7.1/internal/tofu/eval_for_each.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tofu
     7  
     8  import (
     9  	"fmt"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/zclconf/go-cty/cty"
    13  
    14  	"github.com/opentofu/opentofu/internal/addrs"
    15  	"github.com/opentofu/opentofu/internal/lang"
    16  	"github.com/opentofu/opentofu/internal/lang/marks"
    17  	"github.com/opentofu/opentofu/internal/tfdiags"
    18  )
    19  
    20  // evaluateForEachExpression is our standard mechanism for interpreting an
    21  // expression given for a "for_each" argument on a resource or a module. This
    22  // should be called during expansion in order to determine the final keys and
    23  // values.
    24  //
    25  // evaluateForEachExpression differs from evaluateForEachExpressionValue by
    26  // returning an error if the count value is not known, and converting the
    27  // cty.Value to a map[string]cty.Value for compatibility with other calls.
    28  func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) {
    29  	forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, false, false)
    30  	// forEachVal might be unknown, but if it is then there should already
    31  	// be an error about it in diags, which we'll return below.
    32  
    33  	if forEachVal.IsNull() || !forEachVal.IsKnown() || markSafeLengthInt(forEachVal) == 0 {
    34  		// we check length, because an empty set return a nil map
    35  		return map[string]cty.Value{}, diags
    36  	}
    37  
    38  	return forEachVal.AsValueMap(), diags
    39  }
    40  
    41  // evaluateForEachExpressionValue is like evaluateForEachExpression
    42  // except that it returns a cty.Value map or set which can be unknown.
    43  // The 'allowTuple' argument is used to support evaluating for_each from tuple
    44  // values, and is currently supported when using for_each in import blocks.
    45  func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool, allowTuple bool) (cty.Value, tfdiags.Diagnostics) {
    46  	var diags tfdiags.Diagnostics
    47  	nullMap := cty.NullVal(cty.Map(cty.DynamicPseudoType))
    48  
    49  	if expr == nil {
    50  		return nullMap, diags
    51  	}
    52  
    53  	refs, moreDiags := lang.ReferencesInExpr(addrs.ParseRef, expr)
    54  	diags = diags.Append(moreDiags)
    55  	scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey)
    56  	var hclCtx *hcl.EvalContext
    57  	if scope != nil {
    58  		hclCtx, moreDiags = scope.EvalContext(refs)
    59  	} else {
    60  		// This shouldn't happen in real code, but it can unfortunately arise
    61  		// in unit tests due to incompletely-implemented mocks. :(
    62  		hclCtx = &hcl.EvalContext{}
    63  	}
    64  	diags = diags.Append(moreDiags)
    65  	if diags.HasErrors() { // Can't continue if we don't even have a valid scope
    66  		return nullMap, diags
    67  	}
    68  
    69  	forEachVal, forEachDiags := expr.Value(hclCtx)
    70  	diags = diags.Append(forEachDiags)
    71  
    72  	// If a whole map is marked, or a set contains marked values (which means the set is then marked)
    73  	// give an error diagnostic as this value cannot be used in for_each
    74  	if forEachVal.HasMark(marks.Sensitive) {
    75  		diags = diags.Append(&hcl.Diagnostic{
    76  			Severity:    hcl.DiagError,
    77  			Summary:     "Invalid for_each argument",
    78  			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.",
    79  			Subject:     expr.Range().Ptr(),
    80  			Expression:  expr,
    81  			EvalContext: hclCtx,
    82  			Extra:       diagnosticCausedBySensitive(true),
    83  		})
    84  	}
    85  
    86  	if diags.HasErrors() {
    87  		return nullMap, diags
    88  	}
    89  	ty := forEachVal.Type()
    90  
    91  	var isAllowedType bool
    92  	var allowedTypesMessage string
    93  	if allowTuple {
    94  		isAllowedType = ty.IsMapType() || ty.IsSetType() || ty.IsObjectType() || ty.IsTupleType()
    95  		allowedTypesMessage = "map, set of strings, or a tuple"
    96  	} else {
    97  		isAllowedType = ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()
    98  		allowedTypesMessage = "map, or set of strings"
    99  	}
   100  
   101  	const errInvalidUnknownDetailMap = "The \"for_each\" map includes keys derived from resource attributes that cannot be determined until apply, and so OpenTofu 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."
   102  	const errInvalidUnknownDetailSet = "The \"for_each\" set includes values derived from resource attributes that cannot be determined until apply, and so OpenTofu 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."
   103  
   104  	switch {
   105  	case forEachVal.IsNull():
   106  		diags = diags.Append(&hcl.Diagnostic{
   107  			Severity:    hcl.DiagError,
   108  			Summary:     "Invalid for_each argument",
   109  			Detail:      fmt.Sprintf(`The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A %s is allowed.`, allowedTypesMessage),
   110  			Subject:     expr.Range().Ptr(),
   111  			Expression:  expr,
   112  			EvalContext: hclCtx,
   113  		})
   114  		return nullMap, diags
   115  	case !forEachVal.IsKnown():
   116  		if !allowUnknown {
   117  			var detailMsg string
   118  			switch {
   119  			case ty.IsSetType():
   120  				detailMsg = errInvalidUnknownDetailSet
   121  			default:
   122  				detailMsg = errInvalidUnknownDetailMap
   123  			}
   124  
   125  			diags = diags.Append(&hcl.Diagnostic{
   126  				Severity:    hcl.DiagError,
   127  				Summary:     "Invalid for_each argument",
   128  				Detail:      detailMsg,
   129  				Subject:     expr.Range().Ptr(),
   130  				Expression:  expr,
   131  				EvalContext: hclCtx,
   132  				Extra:       diagnosticCausedByUnknown(true),
   133  			})
   134  		}
   135  		// ensure that we have a map, and not a DynamicValue
   136  		return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), diags
   137  
   138  	case !(isAllowedType):
   139  		diags = diags.Append(&hcl.Diagnostic{
   140  			Severity:    hcl.DiagError,
   141  			Summary:     "Invalid for_each argument",
   142  			Detail:      fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a %s, and you have provided a value of type %s.`, allowedTypesMessage, ty.FriendlyName()),
   143  			Subject:     expr.Range().Ptr(),
   144  			Expression:  expr,
   145  			EvalContext: hclCtx,
   146  		})
   147  		return nullMap, diags
   148  
   149  	case markSafeLengthInt(forEachVal) == 0:
   150  		// If the map is empty ({}), return an empty map, because cty will
   151  		// return nil when representing {} AsValueMap. This also covers an empty
   152  		// set (toset([]))
   153  		return forEachVal, diags
   154  	}
   155  
   156  	if ty.IsSetType() {
   157  		// since we can't use a set values that are unknown, we treat the
   158  		// entire set as unknown
   159  		if !forEachVal.IsWhollyKnown() {
   160  			if !allowUnknown {
   161  				diags = diags.Append(&hcl.Diagnostic{
   162  					Severity:    hcl.DiagError,
   163  					Summary:     "Invalid for_each argument",
   164  					Detail:      errInvalidUnknownDetailSet,
   165  					Subject:     expr.Range().Ptr(),
   166  					Expression:  expr,
   167  					EvalContext: hclCtx,
   168  					Extra:       diagnosticCausedByUnknown(true),
   169  				})
   170  			}
   171  			return cty.UnknownVal(ty), diags
   172  		}
   173  
   174  		if ty.ElementType() != cty.String {
   175  			diags = diags.Append(&hcl.Diagnostic{
   176  				Severity:    hcl.DiagError,
   177  				Summary:     "Invalid for_each set argument",
   178  				Detail:      fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
   179  				Subject:     expr.Range().Ptr(),
   180  				Expression:  expr,
   181  				EvalContext: hclCtx,
   182  			})
   183  			return cty.NullVal(ty), diags
   184  		}
   185  
   186  		// A set of strings may contain null, which makes it impossible to
   187  		// convert to a map, so we must return an error
   188  		it := forEachVal.ElementIterator()
   189  		for it.Next() {
   190  			item, _ := it.Element()
   191  			if item.IsNull() {
   192  				diags = diags.Append(&hcl.Diagnostic{
   193  					Severity:    hcl.DiagError,
   194  					Summary:     "Invalid for_each set argument",
   195  					Detail:      `The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values.`,
   196  					Subject:     expr.Range().Ptr(),
   197  					Expression:  expr,
   198  					EvalContext: hclCtx,
   199  				})
   200  				return cty.NullVal(ty), diags
   201  			}
   202  		}
   203  	}
   204  
   205  	return forEachVal, nil
   206  }
   207  
   208  // markSafeLengthInt allows calling LengthInt on marked values safely
   209  func markSafeLengthInt(val cty.Value) int {
   210  	v, _ := val.UnmarkDeep()
   211  	return v.LengthInt()
   212  }