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 }