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  }