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 }