github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/eval_for_each.go (about)

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