rename and cleanup use of count/for_each eval func

Stop evaluating count and for each if they aren't set in the config.
Remove "Resource" from the function names, as they are also now used
with modules.
This commit is contained in:
James Bardin 2020-04-08 17:09:17 -04:00
parent d060a3d0e8
commit b1bc7a792b
11 changed files with 165 additions and 156 deletions

View File

@ -61,7 +61,7 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
configVal := cty.NullVal(cty.DynamicPseudoType) configVal := cty.NullVal(cty.DynamicPseudoType)
if n.Config != nil { if n.Config != nil {
var configDiags tfdiags.Diagnostics var configDiags tfdiags.Diagnostics
forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx) forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx)
keyData := EvalDataForInstanceKey(n.Addr.Key, forEach) keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
configVal, _, configDiags = ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) configVal, _, configDiags = ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData)
diags = diags.Append(configDiags) diags = diags.Append(configDiags)
@ -595,7 +595,7 @@ func (n *EvalApplyProvisioners) apply(ctx EvalContext, provs []*configs.Provisio
// in its result. That's okay because each.value is prohibited for // in its result. That's okay because each.value is prohibited for
// destroy-time provisioners. // destroy-time provisioners.
if n.When != configs.ProvisionerWhenDestroy { if n.When != configs.ProvisionerWhenDestroy {
m, forEachDiags := evaluateResourceForEachExpression(n.ResourceConfig.ForEach, ctx) m, forEachDiags := evaluateForEachExpression(n.ResourceConfig.ForEach, ctx)
diags = diags.Append(forEachDiags) diags = diags.Append(forEachDiags)
forEach = m forEach = m
} }

View File

@ -11,7 +11,7 @@ import (
"github.com/zclconf/go-cty/cty/gocty" "github.com/zclconf/go-cty/cty/gocty"
) )
// evaluateResourceCountExpression is our standard mechanism for interpreting an // evaluateCountExpression is our standard mechanism for interpreting an
// expression given for a "count" argument on a resource. This should be called // expression given for a "count" argument on a resource. This should be called
// from the DynamicExpand of a node representing a resource in order to // from the DynamicExpand of a node representing a resource in order to
// determine the final count value. // determine the final count value.
@ -24,9 +24,9 @@ import (
// //
// 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. // placeholder value -1.
func evaluateResourceCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) { func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) {
count, known, diags := evaluateResourceCountExpressionKnown(expr, ctx) countVal, diags := evaluateCountExpressionValue(expr, ctx)
if !known { if !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.
@ -40,22 +40,29 @@ func evaluateResourceCountExpression(expr hcl.Expression, ctx EvalContext) (int,
Subject: expr.Range().Ptr(), Subject: expr.Range().Ptr(),
}) })
} }
return count, diags
if countVal.IsNull() || !countVal.IsKnown() {
return -1, diags
}
count, _ := countVal.AsBigFloat().Int64()
return int(count), diags
} }
// evaluateResourceCountExpressionKnown is like evaluateResourceCountExpression // evaluateCountExpressionValue is like evaluateCountExpression
// except that it handles an unknown result by returning count = 0 and // except that it returns a cty.Value which must be a cty.Number and can be
// a known = false, rather than by reporting the unknown value as an error // unknown.
// diagnostic. func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) {
func evaluateResourceCountExpressionKnown(expr hcl.Expression, ctx EvalContext) (count int, known bool, diags tfdiags.Diagnostics) { var diags tfdiags.Diagnostics
nullCount := cty.NullVal(cty.Number)
if expr == nil { if expr == nil {
return -1, true, nil return nullCount, nil
} }
countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil) countVal, countDiags := ctx.EvaluateExpr(expr, cty.Number, nil)
diags = diags.Append(countDiags) diags = diags.Append(countDiags)
if diags.HasErrors() { if diags.HasErrors() {
return -1, true, diags return nullCount, diags
} }
switch { switch {
@ -66,11 +73,13 @@ func evaluateResourceCountExpressionKnown(expr hcl.Expression, ctx EvalContext)
Detail: `The given "count" argument value is null. An integer is required.`, Detail: `The given "count" argument value is null. An integer is required.`,
Subject: expr.Range().Ptr(), Subject: expr.Range().Ptr(),
}) })
return -1, true, diags return nullCount, diags
case !countVal.IsKnown(): case !countVal.IsKnown():
return 0, false, diags return cty.UnknownVal(cty.Number), diags
} }
var count int
err := gocty.FromCtyValue(countVal, &count) err := gocty.FromCtyValue(countVal, &count)
if err != nil { if err != nil {
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
@ -79,7 +88,7 @@ func evaluateResourceCountExpressionKnown(expr hcl.Expression, ctx EvalContext)
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, true, diags return nullCount, diags
} }
if count < 0 { if count < 0 {
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
@ -88,10 +97,10 @@ func evaluateResourceCountExpressionKnown(expr hcl.Expression, ctx EvalContext)
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, true, diags return nullCount, diags
} }
return count, true, diags return countVal, 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
@ -101,7 +110,7 @@ func evaluateResourceCountExpressionKnown(expr hcl.Expression, ctx EvalContext)
// //
// The correct time to call this function is in the DynamicExpand method for // The correct time to call this function is in the DynamicExpand method for
// a node representing a resource, just after evaluating the count with // a node representing a resource, just after evaluating the count with
// evaluateResourceCountExpression, and before any other analysis of the // evaluateCountExpression, and before any other analysis of the
// state such as orphan detection. // state such as orphan detection.
// //
// This function calls methods on the given EvalContext to update the current // This function calls methods on the given EvalContext to update the current

View File

@ -133,7 +133,7 @@ func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
// Should be caught during validation, so we don't bother with a pretty error here // Should be caught during validation, so we don't bother with a pretty error here
return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type) return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
} }
forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx) forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx)
keyData := EvalDataForInstanceKey(n.Addr.Key, forEach) keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
diags = diags.Append(configDiags) diags = diags.Append(configDiags)

View File

@ -8,14 +8,14 @@ import (
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
// evaluateResourceForEachExpression interprets a "for_each" argument on a resource. // evaluateForEachExpression interprets a "for_each" argument on a resource.
// //
// Returns a cty.Value map, and diagnostics if necessary. It will return nil if // Returns a cty.Value map, and diagnostics if necessary. It will return nil if
// the expression is nil, and is used to distinguish between an unset for_each and an // the expression is nil, and is used to distinguish between an unset for_each and an
// empty map // empty map
func evaluateResourceForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) { func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) {
forEachMap, known, diags := evaluateResourceForEachExpressionKnown(expr, ctx) forEachVal, diags := evaluateForEachExpressionValue(expr, ctx)
if !known { if !forEachVal.IsKnown() {
// Attach a diag as we do with count, with the same downsides // Attach a diag as we do with count, with the same downsides
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
@ -24,23 +24,31 @@ func evaluateResourceForEachExpression(expr hcl.Expression, ctx EvalContext) (fo
Subject: expr.Range().Ptr(), Subject: expr.Range().Ptr(),
}) })
} }
return forEachMap, diags
if forEachVal.IsNull() || !forEachVal.IsKnown() || forEachVal.LengthInt() == 0 {
// we check length, because an empty set return a nil map
return map[string]cty.Value{}, diags
}
return forEachVal.AsValueMap(), diags
} }
// evaluateResourceForEachExpressionKnown is like evaluateResourceForEachExpression // evaluateForEachExpressionValue is like evaluateForEachExpression
// except that it handles an unknown result by returning an empty map and // except that it returns a cty.Value map or set which can be unknown.
// a known = false, rather than by reporting the unknown value as an error func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) {
// diagnostic. var diags tfdiags.Diagnostics
func evaluateResourceForEachExpressionKnown(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, known bool, diags tfdiags.Diagnostics) { nullMap := cty.NullVal(cty.Map(cty.DynamicPseudoType))
if expr == nil { if expr == nil {
return nil, true, nil return nullMap, diags
} }
forEachVal, forEachDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) forEachVal, forEachDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
diags = diags.Append(forEachDiags) diags = diags.Append(forEachDiags)
if diags.HasErrors() { if diags.HasErrors() {
return nil, true, diags return nullMap, diags
} }
ty := forEachVal.Type()
switch { switch {
case forEachVal.IsNull(): case forEachVal.IsNull():
@ -50,44 +58,42 @@ func evaluateResourceForEachExpressionKnown(expr hcl.Expression, ctx EvalContext
Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`, Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`,
Subject: expr.Range().Ptr(), Subject: expr.Range().Ptr(),
}) })
return nil, true, diags return nullMap, diags
case !forEachVal.IsKnown(): case !forEachVal.IsKnown():
return map[string]cty.Value{}, false, diags // ensure that we have a map, and not a DynamicValue
} return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), diags
if !forEachVal.CanIterateElements() || forEachVal.Type().IsListType() || forEachVal.Type().IsTupleType() { case !(ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()):
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Invalid for_each argument", Summary: "Invalid for_each argument",
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type %s.`, forEachVal.Type().FriendlyName()), Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type %s.`, ty.FriendlyName()),
Subject: expr.Range().Ptr(), Subject: expr.Range().Ptr(),
}) })
return nil, true, diags return nullMap, diags
case forEachVal.LengthInt() == 0:
// If the map is empty ({}), return an empty map, because cty will
// return nil when representing {} AsValueMap This also covers an empty
// set (toset([]))
return forEachVal, diags
} }
// If the map is empty ({}), return an empty map, because cty will return nil when representing {} AsValueMap if ty.IsSetType() {
// This also covers an empty set (toset([])) if ty.ElementType() != cty.String {
if forEachVal.LengthInt() == 0 {
return map[string]cty.Value{}, true, diags
}
if forEachVal.Type().IsSetType() {
if forEachVal.Type().ElementType() != cty.String {
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Invalid for_each set argument", Summary: "Invalid for_each set argument",
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()), Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
Subject: expr.Range().Ptr(), Subject: expr.Range().Ptr(),
}) })
return nil, true, diags return cty.NullVal(ty), diags
} }
// A set may contain unknown values that must be // since we can't use a set values that are unknown, we treat the
// discovered by checking with IsWhollyKnown (which iterates through the // entire set as unknown
// structure), while for maps in cty, keys can never be unknown or null,
// thus the earlier IsKnown check suffices for maps
if !forEachVal.IsWhollyKnown() { if !forEachVal.IsWhollyKnown() {
return map[string]cty.Value{}, false, diags return cty.UnknownVal(ty), diags
} }
// A set of strings may contain null, which makes it impossible to // A set of strings may contain null, which makes it impossible to
@ -102,10 +108,10 @@ func evaluateResourceForEachExpressionKnown(expr hcl.Expression, ctx EvalContext
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values.`), Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values.`),
Subject: expr.Range().Ptr(), Subject: expr.Range().Ptr(),
}) })
return nil, true, diags return cty.NullVal(ty), diags
} }
} }
} }
return forEachVal.AsValueMap(), true, nil return forEachVal, nil
} }

View File

@ -12,7 +12,7 @@ import (
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
func TestEvaluateResourceForEachExpression_valid(t *testing.T) { func TestEvaluateForEachExpression_valid(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
Expr hcl.Expression Expr hcl.Expression
ForEachMap map[string]cty.Value ForEachMap map[string]cty.Value
@ -58,7 +58,7 @@ func TestEvaluateResourceForEachExpression_valid(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
ctx := &MockEvalContext{} ctx := &MockEvalContext{}
ctx.installSimpleEval() ctx.installSimpleEval()
forEachMap, diags := evaluateResourceForEachExpression(test.Expr, ctx) forEachMap, diags := evaluateForEachExpression(test.Expr, ctx)
if len(diags) != 0 { if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
@ -75,7 +75,7 @@ func TestEvaluateResourceForEachExpression_valid(t *testing.T) {
} }
} }
func TestEvaluateResourceForEachExpression_errors(t *testing.T) { func TestEvaluateForEachExpression_errors(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
Expr hcl.Expression Expr hcl.Expression
Summary, DetailSubstring string Summary, DetailSubstring string
@ -131,7 +131,7 @@ func TestEvaluateResourceForEachExpression_errors(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
ctx := &MockEvalContext{} ctx := &MockEvalContext{}
ctx.installSimpleEval() ctx.installSimpleEval()
_, diags := evaluateResourceForEachExpression(test.Expr, ctx) _, diags := evaluateForEachExpression(test.Expr, ctx)
if len(diags) != 1 { if len(diags) != 1 {
t.Fatalf("got %d diagnostics; want 1", diags) t.Fatalf("got %d diagnostics; want 1", diags)
@ -149,7 +149,7 @@ func TestEvaluateResourceForEachExpression_errors(t *testing.T) {
} }
} }
func TestEvaluateResourceForEachExpressionKnown(t *testing.T) { func TestEvaluateForEachExpressionKnown(t *testing.T) {
tests := map[string]hcl.Expression{ tests := map[string]hcl.Expression{
"unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))), "unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
"unknown map": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))), "unknown map": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
@ -159,23 +159,15 @@ func TestEvaluateResourceForEachExpressionKnown(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
ctx := &MockEvalContext{} ctx := &MockEvalContext{}
ctx.installSimpleEval() ctx.installSimpleEval()
forEachMap, known, diags := evaluateResourceForEachExpressionKnown(expr, ctx) forEachVal, diags := evaluateForEachExpressionValue(expr, ctx)
if len(diags) != 0 { if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
} }
if known { if forEachVal.IsKnown() {
t.Errorf("got %v known, want false", known) t.Error("got known, want unknown")
} }
if len(forEachMap) != 0 {
t.Errorf(
"expected empty map\ngot: %s",
spew.Sdump(forEachMap),
)
}
}) })
} }
} }

View File

@ -96,7 +96,7 @@ func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) {
objTy := schema.ImpliedType() objTy := schema.ImpliedType()
priorVal := cty.NullVal(objTy) // for data resources, prior is always null because we start fresh every time priorVal := cty.NullVal(objTy) // for data resources, prior is always null because we start fresh every time
forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx) forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx)
keyData := EvalDataForInstanceKey(n.Addr.Key, forEach) keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
var configDiags tfdiags.Diagnostics var configDiags tfdiags.Diagnostics

View File

@ -466,36 +466,32 @@ func (n *EvalWriteResourceState) Eval(ctx EvalContext) (interface{}, error) {
// can refer to it. Since this node represents the abstract module, we need // can refer to it. Since this node represents the abstract module, we need
// to expand the module here to create all resources. // to expand the module here to create all resources.
expander := ctx.InstanceExpander() expander := ctx.InstanceExpander()
count, countDiags := evaluateResourceCountExpression(n.Config.Count, ctx)
diags = diags.Append(countDiags)
if countDiags.HasErrors() {
return nil, diags.Err()
}
eachMode := states.NoEach switch {
if count >= 0 { // -1 signals "count not set" case n.Config.Count != nil:
eachMode = states.EachList count, countDiags := evaluateCountExpression(n.Config.Count, ctx)
} diags = diags.Append(countDiags)
if countDiags.HasErrors() {
return nil, diags.Err()
}
forEach, forEachDiags := evaluateResourceForEachExpression(n.Config.ForEach, ctx) state.SetResourceMeta(n.Addr, states.EachList, n.ProviderAddr)
diags = diags.Append(forEachDiags)
if forEachDiags.HasErrors() {
return nil, diags.Err()
}
if forEach != nil {
eachMode = states.EachMap
}
// This method takes care of all of the business logic of updating this
// while ensuring that any existing instances are preserved, etc.
state.SetResourceMeta(n.Addr, eachMode, n.ProviderAddr)
switch eachMode {
case states.EachList:
expander.SetResourceCount(n.Addr.Module, n.Addr.Resource, count) expander.SetResourceCount(n.Addr.Module, n.Addr.Resource, count)
case states.EachMap:
case n.Config.ForEach != nil:
forEach, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx)
diags = diags.Append(forEachDiags)
if forEachDiags.HasErrors() {
return nil, diags.Err()
}
// This method takes care of all of the business logic of updating this
// while ensuring that any existing instances are preserved, etc.
state.SetResourceMeta(n.Addr, states.EachMap, n.ProviderAddr)
expander.SetResourceForEach(n.Addr.Module, n.Addr.Resource, forEach) expander.SetResourceForEach(n.Addr.Module, n.Addr.Resource, forEach)
default: default:
state.SetResourceMeta(n.Addr, states.NoEach, n.ProviderAddr)
expander.SetResourceSingle(n.Addr.Module, n.Addr.Resource) expander.SetResourceSingle(n.Addr.Module, n.Addr.Resource)
} }

View File

@ -375,7 +375,9 @@ func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) {
mode := cfg.Mode mode := cfg.Mode
keyData := EvalDataForNoInstanceKey keyData := EvalDataForNoInstanceKey
if n.Config.Count != nil {
switch {
case n.Config.Count != nil:
// If the config block has count, we'll evaluate with an unknown // If the config block has count, we'll evaluate with an unknown
// number as count.index so we can still type check even though // number as count.index so we can still type check even though
// we won't expand count until the plan phase. // we won't expand count until the plan phase.
@ -387,9 +389,8 @@ func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) {
// of this will happen when we DynamicExpand during the plan walk. // of this will happen when we DynamicExpand during the plan walk.
countDiags := n.validateCount(ctx, n.Config.Count) countDiags := n.validateCount(ctx, n.Config.Count)
diags = diags.Append(countDiags) diags = diags.Append(countDiags)
}
if n.Config.ForEach != nil { case n.Config.ForEach != nil:
keyData = InstanceKeyEvalData{ keyData = InstanceKeyEvalData{
EachKey: cty.UnknownVal(cty.String), EachKey: cty.UnknownVal(cty.String),
EachValue: cty.UnknownVal(cty.DynamicPseudoType), EachValue: cty.UnknownVal(cty.DynamicPseudoType),
@ -608,10 +609,10 @@ func (n *EvalValidateResource) validateCount(ctx EvalContext, expr hcl.Expressio
} }
func (n *EvalValidateResource) validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) { func (n *EvalValidateResource) validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
_, known, forEachDiags := evaluateResourceForEachExpressionKnown(expr, ctx) val, forEachDiags := evaluateForEachExpressionValue(expr, ctx)
// If the value isn't known then that's the best we can do for now, but // If the value isn't known then that's the best we can do for now, but
// we'll check more thoroughly during the plan walk // we'll check more thoroughly during the plan walk
if !known { if !val.IsKnown() {
return diags return diags
} }

View File

@ -65,43 +65,46 @@ func (n *NodeRefreshableDataResource) Path() addrs.ModuleInstance {
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, countKnown, countDiags := evaluateResourceCountExpressionKnown(n.Config.Count, ctx) expander := ctx.InstanceExpander()
diags = diags.Append(countDiags)
if countDiags.HasErrors() {
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
}
forEachMap, forEachKnown, forEachDiags := evaluateResourceForEachExpressionKnown(n.Config.ForEach, ctx) switch {
diags = diags.Append(forEachDiags) case n.Config.Count != nil:
if forEachDiags.HasErrors() { count, countDiags := evaluateCountExpressionValue(n.Config.Count, ctx)
return nil, diags.Err() diags = diags.Append(countDiags)
} if countDiags.HasErrors() {
if !forEachKnown { return nil, diags.Err()
// If the for_each isn't known yet, we'll skip refreshing and try expansion }
// again during the plan walk. if !count.IsKnown() {
return nil, nil // If the count isn't known yet, we'll skip refreshing and try expansion
// again during the plan walk.
return nil, nil
}
c, _ := count.AsBigFloat().Int64()
expander.SetResourceCount(n.Addr.Module, n.Addr.Resource, int(c))
case n.Config.ForEach != nil:
forEachVal, forEachDiags := evaluateForEachExpressionValue(n.Config.ForEach, ctx)
diags = diags.Append(forEachDiags)
if forEachDiags.HasErrors() {
return nil, diags.Err()
}
if !forEachVal.IsKnown() {
// If the for_each isn't known yet, we'll skip refreshing and try expansion
// again during the plan walk.
return nil, nil
}
expander.SetResourceForEach(n.Addr.Module, n.Addr.Resource, forEachVal.AsValueMap())
default:
expander.SetResourceSingle(n.Addr.Module, n.Addr.Resource)
} }
// 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.
fixResourceCountSetTransition(ctx, n.ResourceAddr(), count != -1) fixResourceCountSetTransition(ctx, n.ResourceAddr(), n.Config.Count != nil)
// Inform our instance expander about our expansion results above,
// and then use it to calculate the instance addresses we'll expand for.
expander := ctx.InstanceExpander()
switch {
case count >= 0:
expander.SetResourceCount(n.Addr.Module, n.Addr.Resource, count)
case forEachMap != nil:
expander.SetResourceForEach(n.Addr.Module, n.Addr.Resource, forEachMap)
default:
expander.SetResourceSingle(n.Addr.Module, n.Addr.Resource)
}
instanceAddrs := expander.ExpandResource(n.Addr) instanceAddrs := expander.ExpandResource(n.Addr)
// Our graph transformers require access to the full state, so we'll // Our graph transformers require access to the full state, so we'll

View File

@ -229,14 +229,14 @@ func (n *evalPrepareModuleExpansion) Eval(ctx EvalContext) (interface{}, error)
switch { switch {
case n.ModuleCall.Count != nil: case n.ModuleCall.Count != nil:
count, diags := evaluateResourceCountExpression(n.ModuleCall.Count, ctx) count, diags := evaluateCountExpression(n.ModuleCall.Count, ctx)
if diags.HasErrors() { if diags.HasErrors() {
return nil, diags.Err() return nil, diags.Err()
} }
expander.SetModuleCount(module, call, count) expander.SetModuleCount(module, call, count)
case n.ModuleCall.ForEach != nil: case n.ModuleCall.ForEach != nil:
forEach, diags := evaluateResourceForEachExpression(n.ModuleCall.ForEach, ctx) forEach, diags := evaluateForEachExpression(n.ModuleCall.ForEach, ctx)
if diags.HasErrors() { if diags.HasErrors() {
return nil, diags.Err() return nil, diags.Err()
} }

View File

@ -89,32 +89,34 @@ func (n *NodeRefreshableManagedResource) Path() addrs.ModuleInstance {
func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph, error) { func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
count, countDiags := evaluateResourceCountExpression(n.Config.Count, ctx) expander := ctx.InstanceExpander()
diags = diags.Append(countDiags) // Inform our instance expander about our expansion results, and then use
if countDiags.HasErrors() { // it to calculate the instance addresses we'll expand for.
return nil, diags.Err() switch {
} case n.Config.Count != nil:
count, countDiags := evaluateCountExpression(n.Config.Count, ctx)
diags = diags.Append(countDiags)
if countDiags.HasErrors() {
return nil, diags.Err()
}
forEachMap, forEachDiags := evaluateResourceForEachExpression(n.Config.ForEach, ctx) expander.SetResourceCount(n.Addr.Module, n.Addr.Resource, count)
if forEachDiags.HasErrors() {
return nil, diags.Err() case n.Config.ForEach != nil:
forEachMap, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx)
if forEachDiags.HasErrors() {
return nil, diags.Err()
}
expander.SetResourceForEach(n.Addr.Module, n.Addr.Resource, forEachMap)
default:
expander.SetResourceSingle(n.Addr.Module, n.Addr.Resource)
} }
// 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.
fixResourceCountSetTransition(ctx, n.Addr.Config(), count != -1) fixResourceCountSetTransition(ctx, n.Addr.Config(), n.Config.Count != nil)
// Inform our instance expander about our expansion results above,
// and then use it to calculate the instance addresses we'll expand for.
expander := ctx.InstanceExpander()
switch {
case count >= 0:
expander.SetResourceCount(n.Addr.Module, n.Addr.Resource, count)
case forEachMap != nil:
expander.SetResourceForEach(n.Addr.Module, n.Addr.Resource, forEachMap)
default:
expander.SetResourceSingle(n.Addr.Module, n.Addr.Resource)
}
instanceAddrs := expander.ExpandResource(n.Addr) instanceAddrs := expander.ExpandResource(n.Addr)
// Our graph transformers require access to the full state, so we'll // Our graph transformers require access to the full state, so we'll