github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-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/muratcelep/terraform/not-internal/lang"
     8  	"github.com/muratcelep/terraform/not-internal/lang/marks"
     9  	"github.com/muratcelep/terraform/not-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  		})
    74  	}
    75  
    76  	if diags.HasErrors() {
    77  		return nullMap, diags
    78  	}
    79  	ty := forEachVal.Type()
    80  
    81  	switch {
    82  	case forEachVal.IsNull():
    83  		diags = diags.Append(&hcl.Diagnostic{
    84  			Severity:    hcl.DiagError,
    85  			Summary:     "Invalid for_each argument",
    86  			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.`,
    87  			Subject:     expr.Range().Ptr(),
    88  			Expression:  expr,
    89  			EvalContext: hclCtx,
    90  		})
    91  		return nullMap, diags
    92  	case !forEachVal.IsKnown():
    93  		if !allowUnknown {
    94  			diags = diags.Append(&hcl.Diagnostic{
    95  				Severity:    hcl.DiagError,
    96  				Summary:     "Invalid for_each argument",
    97  				Detail:      errInvalidForEachUnknownDetail,
    98  				Subject:     expr.Range().Ptr(),
    99  				Expression:  expr,
   100  				EvalContext: hclCtx,
   101  			})
   102  		}
   103  		// ensure that we have a map, and not a DynamicValue
   104  		return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), diags
   105  
   106  	case !(ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()):
   107  		diags = diags.Append(&hcl.Diagnostic{
   108  			Severity:    hcl.DiagError,
   109  			Summary:     "Invalid for_each argument",
   110  			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()),
   111  			Subject:     expr.Range().Ptr(),
   112  			Expression:  expr,
   113  			EvalContext: hclCtx,
   114  		})
   115  		return nullMap, diags
   116  
   117  	case markSafeLengthInt(forEachVal) == 0:
   118  		// If the map is empty ({}), return an empty map, because cty will
   119  		// return nil when representing {} AsValueMap. This also covers an empty
   120  		// set (toset([]))
   121  		return forEachVal, diags
   122  	}
   123  
   124  	if ty.IsSetType() {
   125  		// since we can't use a set values that are unknown, we treat the
   126  		// entire set as unknown
   127  		if !forEachVal.IsWhollyKnown() {
   128  			if !allowUnknown {
   129  				diags = diags.Append(&hcl.Diagnostic{
   130  					Severity:    hcl.DiagError,
   131  					Summary:     "Invalid for_each argument",
   132  					Detail:      errInvalidForEachUnknownDetail,
   133  					Subject:     expr.Range().Ptr(),
   134  					Expression:  expr,
   135  					EvalContext: hclCtx,
   136  				})
   137  			}
   138  			return cty.UnknownVal(ty), diags
   139  		}
   140  
   141  		if ty.ElementType() != cty.String {
   142  			diags = diags.Append(&hcl.Diagnostic{
   143  				Severity:    hcl.DiagError,
   144  				Summary:     "Invalid for_each set argument",
   145  				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()),
   146  				Subject:     expr.Range().Ptr(),
   147  				Expression:  expr,
   148  				EvalContext: hclCtx,
   149  			})
   150  			return cty.NullVal(ty), diags
   151  		}
   152  
   153  		// A set of strings may contain null, which makes it impossible to
   154  		// convert to a map, so we must return an error
   155  		it := forEachVal.ElementIterator()
   156  		for it.Next() {
   157  			item, _ := it.Element()
   158  			if item.IsNull() {
   159  				diags = diags.Append(&hcl.Diagnostic{
   160  					Severity:    hcl.DiagError,
   161  					Summary:     "Invalid for_each set argument",
   162  					Detail:      `The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values.`,
   163  					Subject:     expr.Range().Ptr(),
   164  					Expression:  expr,
   165  					EvalContext: hclCtx,
   166  				})
   167  				return cty.NullVal(ty), diags
   168  			}
   169  		}
   170  	}
   171  
   172  	return forEachVal, nil
   173  }
   174  
   175  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.`
   176  
   177  // markSafeLengthInt allows calling LengthInt on marked values safely
   178  func markSafeLengthInt(val cty.Value) int {
   179  	v, _ := val.UnmarkDeep()
   180  	return v.LengthInt()
   181  }