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