github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/terraform/eval_count.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "log" 6 7 "github.com/hashicorp/hcl/v2" 8 "github.com/hashicorp/terraform/internal/addrs" 9 "github.com/hashicorp/terraform/internal/tfdiags" 10 "github.com/zclconf/go-cty/cty" 11 "github.com/zclconf/go-cty/cty/gocty" 12 ) 13 14 // evaluateCountExpression is our standard mechanism for interpreting an 15 // expression given for a "count" argument on a resource or a module. This 16 // should be called during expansion in order to determine the final count 17 // value. 18 // 19 // evaluateCountExpression differs from evaluateCountExpressionValue by 20 // returning an error if the count value is not known, and converting the 21 // cty.Value to an integer. 22 func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) { 23 countVal, diags := evaluateCountExpressionValue(expr, ctx) 24 if !countVal.IsKnown() { 25 // Currently this is a rather bad outcome from a UX standpoint, since we have 26 // no real mechanism to deal with this situation and all we can do is produce 27 // an error message. 28 // FIXME: In future, implement a built-in mechanism for deferring changes that 29 // can't yet be predicted, and use it to guide the user through several 30 // plan/apply steps until the desired configuration is eventually reached. 31 diags = diags.Append(&hcl.Diagnostic{ 32 Severity: hcl.DiagError, 33 Summary: "Invalid count argument", 34 Detail: `The "count" 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 count depends on.`, 35 Subject: expr.Range().Ptr(), 36 }) 37 } 38 39 if countVal.IsNull() || !countVal.IsKnown() { 40 return -1, diags 41 } 42 43 count, _ := countVal.AsBigFloat().Int64() 44 return int(count), diags 45 } 46 47 // evaluateCountExpressionValue is like evaluateCountExpression 48 // except that it returns a cty.Value which must be a cty.Number and can be 49 // unknown. 50 func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) { 51 var diags tfdiags.Diagnostics 52 nullCount := cty.NullVal(cty.Number) 53 if expr == nil { 54 return nullCount, nil 55 } 56 57 countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil) 58 diags = diags.Append(countDiags) 59 if diags.HasErrors() { 60 return nullCount, diags 61 } 62 63 // Unmark the count value, sensitive values are allowed in count but not for_each, 64 // as using it here will not disclose the sensitive value 65 countVal, _ = countVal.Unmark() 66 67 switch { 68 case countVal.IsNull(): 69 diags = diags.Append(&hcl.Diagnostic{ 70 Severity: hcl.DiagError, 71 Summary: "Invalid count argument", 72 Detail: `The given "count" argument value is null. An integer is required.`, 73 Subject: expr.Range().Ptr(), 74 }) 75 return nullCount, diags 76 77 case !countVal.IsKnown(): 78 return cty.UnknownVal(cty.Number), diags 79 } 80 81 var count int 82 err := gocty.FromCtyValue(countVal, &count) 83 if err != nil { 84 diags = diags.Append(&hcl.Diagnostic{ 85 Severity: hcl.DiagError, 86 Summary: "Invalid count argument", 87 Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), 88 Subject: expr.Range().Ptr(), 89 }) 90 return nullCount, diags 91 } 92 if count < 0 { 93 diags = diags.Append(&hcl.Diagnostic{ 94 Severity: hcl.DiagError, 95 Summary: "Invalid count argument", 96 Detail: `The given "count" argument value is unsuitable: negative numbers are not supported.`, 97 Subject: expr.Range().Ptr(), 98 }) 99 return nullCount, diags 100 } 101 102 return countVal, diags 103 } 104 105 // fixResourceCountSetTransition is a helper function to fix up the state when a 106 // resource transitions its "count" from being set to unset or vice-versa, 107 // treating a 0-key and a no-key instance as aliases for one another across 108 // the transition. 109 // 110 // The correct time to call this function is in the DynamicExpand method for 111 // a node representing a resource, just after evaluating the count with 112 // evaluateCountExpression, and before any other analysis of the 113 // state such as orphan detection. 114 // 115 // This function calls methods on the given EvalContext to update the current 116 // state in-place, if necessary. It is a no-op if there is no count transition 117 // taking place. 118 // 119 // Since the state is modified in-place, this function must take a writer lock 120 // on the state. The caller must therefore not also be holding a state lock, 121 // or this function will block forever awaiting the lock. 122 func fixResourceCountSetTransition(ctx EvalContext, addr addrs.ConfigResource, countEnabled bool) { 123 state := ctx.State() 124 if state.MaybeFixUpResourceInstanceAddressForCount(addr, countEnabled) { 125 log.Printf("[TRACE] renamed first %s instance in transient state due to count argument change", addr) 126 } 127 128 refreshState := ctx.RefreshState() 129 if refreshState != nil && refreshState.MaybeFixUpResourceInstanceAddressForCount(addr, countEnabled) { 130 log.Printf("[TRACE] renamed first %s instance in transient state due to count argument change", addr) 131 } 132 }