github.com/opentofu/opentofu@v1.7.1/internal/tofu/eval_count.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/opentofu/opentofu/internal/tfdiags" 13 "github.com/zclconf/go-cty/cty" 14 "github.com/zclconf/go-cty/cty/gocty" 15 ) 16 17 // evaluateCountExpression is our standard mechanism for interpreting an 18 // expression given for a "count" argument on a resource or a module. This 19 // should be called during expansion in order to determine the final count 20 // value. 21 // 22 // evaluateCountExpression differs from evaluateCountExpressionValue by 23 // returning an error if the count value is not known, and converting the 24 // cty.Value to an integer. 25 func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) { 26 countVal, diags := evaluateCountExpressionValue(expr, ctx) 27 if !countVal.IsKnown() { 28 // Currently this is a rather bad outcome from a UX standpoint, since we have 29 // no real mechanism to deal with this situation and all we can do is produce 30 // an error message. 31 // FIXME: In future, implement a built-in mechanism for deferring changes that 32 // can't yet be predicted, and use it to guide the user through several 33 // plan/apply steps until the desired configuration is eventually reached. 34 diags = diags.Append(&hcl.Diagnostic{ 35 Severity: hcl.DiagError, 36 Summary: "Invalid count argument", 37 Detail: `The "count" value depends on resource attributes that cannot be determined until apply, so OpenTofu cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count depends on.`, 38 Subject: expr.Range().Ptr(), 39 40 // TODO: Also populate Expression and EvalContext in here, but 41 // we can't easily do that right now because the hcl.EvalContext 42 // (which is not the same as the ctx we have in scope here) is 43 // hidden away inside evaluateCountExpressionValue. 44 Extra: diagnosticCausedByUnknown(true), 45 }) 46 } 47 48 if countVal.IsNull() || !countVal.IsKnown() { 49 return -1, diags 50 } 51 52 count, _ := countVal.AsBigFloat().Int64() 53 return int(count), diags 54 } 55 56 // evaluateCountExpressionValue is like evaluateCountExpression 57 // except that it returns a cty.Value which must be a cty.Number and can be 58 // unknown. 59 func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) { 60 var diags tfdiags.Diagnostics 61 nullCount := cty.NullVal(cty.Number) 62 if expr == nil { 63 return nullCount, nil 64 } 65 66 countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil) 67 diags = diags.Append(countDiags) 68 if diags.HasErrors() { 69 return nullCount, diags 70 } 71 72 // Unmark the count value, sensitive values are allowed in count but not for_each, 73 // as using it here will not disclose the sensitive value 74 countVal, _ = countVal.Unmark() 75 76 switch { 77 case countVal.IsNull(): 78 diags = diags.Append(&hcl.Diagnostic{ 79 Severity: hcl.DiagError, 80 Summary: "Invalid count argument", 81 Detail: `The given "count" argument value is null. An integer is required.`, 82 Subject: expr.Range().Ptr(), 83 }) 84 return nullCount, diags 85 86 case !countVal.IsKnown(): 87 return cty.UnknownVal(cty.Number), diags 88 } 89 90 var count int 91 err := gocty.FromCtyValue(countVal, &count) 92 if err != nil { 93 diags = diags.Append(&hcl.Diagnostic{ 94 Severity: hcl.DiagError, 95 Summary: "Invalid count argument", 96 Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), 97 Subject: expr.Range().Ptr(), 98 }) 99 return nullCount, diags 100 } 101 if count < 0 { 102 diags = diags.Append(&hcl.Diagnostic{ 103 Severity: hcl.DiagError, 104 Summary: "Invalid count argument", 105 Detail: `The given "count" argument value is unsuitable: must be greater than or equal to zero.`, 106 Subject: expr.Range().Ptr(), 107 }) 108 return nullCount, diags 109 } 110 111 return countVal, diags 112 }