core: Allow data resource count to be unknown during refresh
The count for a data resource can potentially depend on a managed resource that isn't recorded in the state yet, in which case references to it will always return unknown. Ideally we'd do the data refreshes during the plan phase as discussed in #17034, which would avoid this problem by planning the managed resources in the same walk, but for now we'll just skip refreshing any data resources with an unknown count during refresh and defer that work to the apply phase, just as we'd do if there were unknown values in the main configuration for the data resource.
This commit is contained in:
parent
95e5ef13a7
commit
3309be9721
|
@ -976,6 +976,65 @@ func TestContext2Refresh_stateBasic(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Refresh_dataCount(t *testing.T) {
|
||||||
|
p := testProvider("test")
|
||||||
|
m := testModule(t, "refresh-data-count")
|
||||||
|
|
||||||
|
// This test is verifying that a data resource count can refer to a
|
||||||
|
// resource attribute that can't be known yet during refresh (because
|
||||||
|
// the resource in question isn't in the state at all). In that case,
|
||||||
|
// we skip the data resource during refresh and process it during the
|
||||||
|
// subsequent plan step instead.
|
||||||
|
//
|
||||||
|
// Normally it's an error for "count" to be computed, but during the
|
||||||
|
// refresh step we allow it because we _expect_ to be working with an
|
||||||
|
// incomplete picture of the world sometimes, particularly when we're
|
||||||
|
// creating object for the first time against an empty state.
|
||||||
|
//
|
||||||
|
// For more information, see:
|
||||||
|
// https://github.com/hashicorp/terraform/issues/21047
|
||||||
|
|
||||||
|
p.GetSchemaReturn = &ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"test": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"things": {Type: cty.List(cty.String), Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DataSources: map[string]*configschema.Block{
|
||||||
|
"test": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
ProviderResolver: providers.ResolverFixed(
|
||||||
|
map[string]providers.Factory{
|
||||||
|
"test": testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Config: m,
|
||||||
|
})
|
||||||
|
|
||||||
|
s, diags := ctx.Refresh()
|
||||||
|
if p.ReadResourceCalled {
|
||||||
|
// The managed resource doesn't exist in the state yet, so there's
|
||||||
|
// nothing to refresh.
|
||||||
|
t.Errorf("ReadResource was called, but should not have been")
|
||||||
|
}
|
||||||
|
if p.ReadDataSourceCalled {
|
||||||
|
// The data resource should've been skipped because its count cannot
|
||||||
|
// be determined yet.
|
||||||
|
t.Errorf("ReadDataSource was called, but should not have been")
|
||||||
|
}
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("refresh errors: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
checkStateString(t, s, `<no state>`)
|
||||||
|
}
|
||||||
|
|
||||||
func TestContext2Refresh_dataOrphan(t *testing.T) {
|
func TestContext2Refresh_dataOrphan(t *testing.T) {
|
||||||
p := testProvider("null")
|
p := testProvider("null")
|
||||||
state := MustShimLegacyState(&State{
|
state := MustShimLegacyState(&State{
|
||||||
|
|
|
@ -23,36 +23,10 @@ import (
|
||||||
// the "count" behavior should not be enabled for this resource at all.
|
// the "count" behavior should not be enabled for this resource at all.
|
||||||
//
|
//
|
||||||
// If error diagnostics are returned then the result is always the meaningless
|
// If error diagnostics are returned then the result is always the meaningless
|
||||||
// placeholder value -1, except in one case: if the count expression evaluates
|
// placeholder value -1.
|
||||||
// to an unknown number value then the result is zero, allowing this situation
|
|
||||||
// to be treated by the caller as special if needed. For example, an early
|
|
||||||
// graph walk may wish to just silently skip resources with unknown counts
|
|
||||||
// to allow them to be dealt with in a later graph walk where more information
|
|
||||||
// is available.
|
|
||||||
func evaluateResourceCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) {
|
func evaluateResourceCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) {
|
||||||
if expr == nil {
|
count, known, diags := evaluateResourceCountExpressionKnown(expr, ctx)
|
||||||
return -1, nil
|
if !known {
|
||||||
}
|
|
||||||
|
|
||||||
var diags tfdiags.Diagnostics
|
|
||||||
var count int
|
|
||||||
|
|
||||||
countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil)
|
|
||||||
diags = diags.Append(countDiags)
|
|
||||||
if diags.HasErrors() {
|
|
||||||
return -1, diags
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -1, diags
|
|
||||||
case !countVal.IsKnown():
|
|
||||||
// Currently this is a rather bad outcome from a UX standpoint, since we have
|
// 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
|
// no real mechanism to deal with this situation and all we can do is produce
|
||||||
// an error message.
|
// an error message.
|
||||||
|
@ -65,11 +39,36 @@ func evaluateResourceCountExpression(expr hcl.Expression, ctx EvalContext) (int,
|
||||||
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.`,
|
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(),
|
Subject: expr.Range().Ptr(),
|
||||||
})
|
})
|
||||||
// We return zero+errors in this one case to allow callers to handle
|
}
|
||||||
// an unknown count as special. This is rarely necessary, but is used
|
return count, diags
|
||||||
// by the validate walk in particular so that it can just skip
|
}
|
||||||
// validation in this case, assuming a later walk will take care of it.
|
|
||||||
return 0, diags
|
// evaluateResourceCountExpressionKnown is like evaluateResourceCountExpression
|
||||||
|
// except that it handles an unknown result by returning count = 0 and
|
||||||
|
// a known = false, rather than by reporting the unknown value as an error
|
||||||
|
// diagnostic.
|
||||||
|
func evaluateResourceCountExpressionKnown(expr hcl.Expression, ctx EvalContext) (count int, known bool, diags tfdiags.Diagnostics) {
|
||||||
|
if expr == nil {
|
||||||
|
return -1, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil)
|
||||||
|
diags = diags.Append(countDiags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return -1, true, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
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 -1, true, diags
|
||||||
|
case !countVal.IsKnown():
|
||||||
|
return 0, false, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
err := gocty.FromCtyValue(countVal, &count)
|
err := gocty.FromCtyValue(countVal, &count)
|
||||||
|
@ -80,7 +79,7 @@ func evaluateResourceCountExpression(expr hcl.Expression, ctx EvalContext) (int,
|
||||||
Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
|
Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err),
|
||||||
Subject: expr.Range().Ptr(),
|
Subject: expr.Range().Ptr(),
|
||||||
})
|
})
|
||||||
return -1, diags
|
return -1, true, diags
|
||||||
}
|
}
|
||||||
if count < 0 {
|
if count < 0 {
|
||||||
diags = diags.Append(&hcl.Diagnostic{
|
diags = diags.Append(&hcl.Diagnostic{
|
||||||
|
@ -89,10 +88,10 @@ func evaluateResourceCountExpression(expr hcl.Expression, ctx EvalContext) (int,
|
||||||
Detail: `The given "count" argument value is unsuitable: negative numbers are not supported.`,
|
Detail: `The given "count" argument value is unsuitable: negative numbers are not supported.`,
|
||||||
Subject: expr.Range().Ptr(),
|
Subject: expr.Range().Ptr(),
|
||||||
})
|
})
|
||||||
return -1, diags
|
return -1, true, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
return count, diags
|
return count, true, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixResourceCountSetTransition is a helper function to fix up the state when a
|
// fixResourceCountSetTransition is a helper function to fix up the state when a
|
||||||
|
|
|
@ -27,11 +27,16 @@ var (
|
||||||
func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
count, countDiags := evaluateResourceCountExpression(n.Config.Count, ctx)
|
count, countKnown, countDiags := evaluateResourceCountExpressionKnown(n.Config.Count, ctx)
|
||||||
diags = diags.Append(countDiags)
|
diags = diags.Append(countDiags)
|
||||||
if countDiags.HasErrors() {
|
if countDiags.HasErrors() {
|
||||||
return nil, diags.Err()
|
return nil, diags.Err()
|
||||||
}
|
}
|
||||||
|
if !countKnown {
|
||||||
|
// If the count isn't known yet, we'll skip refreshing and try expansion
|
||||||
|
// again during the plan walk.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Next we need to potentially rename an instance address in the state
|
// Next we need to potentially rename an instance address in the state
|
||||||
// if we're transitioning whether "count" is set at all.
|
// if we're transitioning whether "count" is set at all.
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
resource "test" "foo" {
|
||||||
|
things = ["foo"]
|
||||||
|
}
|
||||||
|
|
||||||
|
data "test" "foo" {
|
||||||
|
count = length(test.foo.things)
|
||||||
|
}
|
Loading…
Reference in New Issue