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 }