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