opentofu/terraform/eval_count.go
Pam Selle 0d586fd056 Unmark values in count before go conversion
When evaluating count values, we need to unmark
the cty value before passing the value for conversion
into a go int value.
2020-12-10 11:38:10 -05:00

133 lines
4.9 KiB
Go

package terraform
import (
"fmt"
"log"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
// evaluateCountExpression is our standard mechanism for interpreting an
// expression given for a "count" argument on a resource or a module. This
// should be called during expansion in order to determine the final count
// value.
//
// evaluateCountExpression differs from evaluateCountExpressionValue by
// returning an error if the count value is not known, and converting the
// cty.Value to an integer.
func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) {
countVal, diags := evaluateCountExpressionValue(expr, ctx)
if !countVal.IsKnown() {
// Currently this is a rather bad outcome from a UX standpoint, since we have
// no real mechanism to deal with this situation and all we can do is produce
// an error message.
// FIXME: In future, implement a built-in mechanism for deferring changes that
// can't yet be predicted, and use it to guide the user through several
// plan/apply steps until the desired configuration is eventually reached.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid count argument",
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.`,
Subject: expr.Range().Ptr(),
})
}
if countVal.IsNull() || !countVal.IsKnown() {
return -1, diags
}
count, _ := countVal.AsBigFloat().Int64()
return int(count), diags
}
// evaluateCountExpressionValue is like evaluateCountExpression
// except that it returns a cty.Value which must be a cty.Number and can be
// unknown.
func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
nullCount := cty.NullVal(cty.Number)
if expr == nil {
return nullCount, nil
}
countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil)
diags = diags.Append(countDiags)
if diags.HasErrors() {
return nullCount, diags
}
// Unmark the count value, sensitive values are allowed in count but not for_each,
// as using it here will not disclose the sensitive value
countVal, _ = countVal.Unmark()
switch {
case countVal.IsNull():
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid count argument",
Detail: `The given "count" argument value is null. An integer is required.`,
Subject: expr.Range().Ptr(),
})
return nullCount, diags
case !countVal.IsKnown():
return cty.UnknownVal(cty.Number), diags
}
var count int
err := gocty.FromCtyValue(countVal, &count)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid count argument",
Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
Subject: expr.Range().Ptr(),
})
return nullCount, diags
}
if count < 0 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid count argument",
Detail: `The given "count" argument value is unsuitable: negative numbers are not supported.`,
Subject: expr.Range().Ptr(),
})
return nullCount, diags
}
return countVal, diags
}
// fixResourceCountSetTransition is a helper function to fix up the state when a
// resource transitions its "count" from being set to unset or vice-versa,
// treating a 0-key and a no-key instance as aliases for one another across
// the transition.
//
// The correct time to call this function is in the DynamicExpand method for
// a node representing a resource, just after evaluating the count with
// evaluateCountExpression, and before any other analysis of the
// state such as orphan detection.
//
// This function calls methods on the given EvalContext to update the current
// state in-place, if necessary. It is a no-op if there is no count transition
// taking place.
//
// Since the state is modified in-place, this function must take a writer lock
// on the state. The caller must therefore not also be holding a state lock,
// or this function will block forever awaiting the lock.
func fixResourceCountSetTransition(ctx EvalContext, addr addrs.ConfigResource, countEnabled bool) {
state := ctx.State()
if state.MaybeFixUpResourceInstanceAddressForCount(addr, countEnabled) {
log.Printf("[TRACE] renamed first %s instance in transient state due to count argument change", addr)
}
refreshState := ctx.RefreshState()
if refreshState != nil && refreshState.MaybeFixUpResourceInstanceAddressForCount(addr, countEnabled) {
log.Printf("[TRACE] renamed first %s instance in transient state due to count argument change", addr)
}
}