core: Use instances.Expander to handle resource count and for_each

This is a minimal integration of instances.Expander used just for resource
count and for_each, for now just forcing modules to always be singletons
because the rest of Terraform Core isn't ready to deal with expanding
module calls yet.

This doesn't integrate super cleanly yet because we still have some
cleanup work to do in the design of the plan walk, to make it explicit
that the nodes in the plan graph represent static configuration objects
rather than expanded instances, including for modules. To make this work
in the meantime, there is some shimming between addrs.Module and
addrs.ModuleInstance to correct for the discontinuities that result from
the fact that Terraform currently assumes that modules are always
singletons.
This commit is contained in:
Martin Atkins 2019-11-21 18:45:43 -08:00
parent 8ea78dfe7d
commit 68b900928d
15 changed files with 321 additions and 222 deletions

View File

@ -488,6 +488,19 @@ func (n *EvalWriteResourceState) Eval(ctx EvalContext) (interface{}, error) {
// while ensuring that any existing instances are preserved, etc.
state.SetResourceMeta(absAddr, eachMode, n.ProviderAddr)
// We'll record our expansion decision in the shared "expander" object
// so that later operations (i.e. DynamicExpand and expression evaluation)
// can refer to it.
expander := ctx.InstanceExpander()
switch eachMode {
case states.EachList:
expander.SetResourceCount(ctx.Path(), n.Addr, count)
case states.EachMap:
expander.SetResourceForEach(ctx.Path(), n.Addr, forEach)
default:
expander.SetResourceSingle(ctx.Path(), n.Addr)
}
return nil, nil
}

View File

@ -153,6 +153,11 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
// analyze the configuration to find references.
&AttachSchemaTransformer{Schemas: b.Schemas},
// Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing
// objects that can belong to modules.
&ModuleExpansionTransformer{Config: b.Config},
// Connect references so ordering is correct
&ReferenceTransformer{},
&AttachDependenciesTransformer{},

View File

@ -656,15 +656,18 @@ const testApplyGraphBuilderStr = `
meta.count-boundary (EachMode fixup)
module.child.test_object.other
test_object.other
module.child
module.child.test_object.create
module.child.test_object.create (prepare state)
module.child.test_object.create (prepare state)
module.child
provider["registry.terraform.io/-/test"]
provisioner.test
module.child.test_object.other
module.child.test_object.create
module.child.test_object.other (prepare state)
module.child.test_object.other (prepare state)
module.child
provider["registry.terraform.io/-/test"]
provider["registry.terraform.io/-/test"]
provider["registry.terraform.io/-/test"] (close)

View File

@ -137,6 +137,11 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
// analyze the configuration to find references.
&AttachSchemaTransformer{Schemas: b.Schemas},
// Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing
// objects that can belong to modules.
&ModuleExpansionTransformer{Config: b.Config},
// Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on.
&ReferenceTransformer{},

View File

@ -162,6 +162,11 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
// analyze the configuration to find references.
&AttachSchemaTransformer{Schemas: b.Schemas},
// Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing
// objects that can belong to modules.
&ModuleExpansionTransformer{Config: b.Config},
// Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on.
&ReferenceTransformer{},

View File

@ -53,6 +53,19 @@ func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, er
// if we're transitioning whether "count" is set at all.
fixResourceCountSetTransition(ctx, n.ResourceAddr(), count != -1)
// 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(ctx.Path(), n.ResourceAddr().Resource, count)
case forEachMap != nil:
expander.SetResourceForEach(ctx.Path(), n.ResourceAddr().Resource, forEachMap)
default:
expander.SetResourceSingle(ctx.Path(), n.ResourceAddr().Resource)
}
instanceAddrs := expander.ExpandResource(ctx.Path().Module(), n.ResourceAddr().Resource)
// Our graph transformers require access to the full state, so we'll
// temporarily lock it while we work on this.
state := ctx.State().Lock()
@ -85,21 +98,19 @@ func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, er
steps := []GraphTransformer{
// Expand the count.
&ResourceCountTransformer{
Concrete: concreteResource,
Schema: n.Schema,
Count: count,
ForEach: forEachMap,
Addr: n.ResourceAddr(),
Concrete: concreteResource,
Schema: n.Schema,
Addr: n.ResourceAddr(),
InstanceAddrs: instanceAddrs,
},
// Add the count orphans. As these are orphaned refresh nodes, we add them
// directly as NodeDestroyableDataResource.
&OrphanResourceCountTransformer{
Concrete: concreteResourceDestroyable,
Count: count,
ForEach: forEachMap,
Addr: n.ResourceAddr(),
State: state,
Concrete: concreteResourceDestroyable,
Addr: n.ResourceAddr(),
InstanceAddrs: instanceAddrs,
State: state,
},
// Attach the state

View File

@ -6,6 +6,7 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/instances"
)
func TestNodeRefreshableDataResourceDynamicExpand_scaleOut(t *testing.T) {
@ -49,8 +50,9 @@ func TestNodeRefreshableDataResourceDynamicExpand_scaleOut(t *testing.T) {
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: addrs.RootModuleInstance,
StateState: state.SyncWrapper(),
PathPath: addrs.RootModuleInstance,
StateState: state.SyncWrapper(),
InstanceExpanderExpander: instances.NewExpander(),
// DynamicExpand will call EvaluateExpr to evaluate the "count"
// expression, which is just a literal number 3 in the fixture config
@ -136,8 +138,9 @@ func TestNodeRefreshableDataResourceDynamicExpand_scaleIn(t *testing.T) {
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: addrs.RootModuleInstance,
StateState: state.SyncWrapper(),
PathPath: addrs.RootModuleInstance,
StateState: state.SyncWrapper(),
InstanceExpanderExpander: instances.NewExpander(),
// DynamicExpand will call EvaluateExpr to evaluate the "count"
// expression, which is just a literal number 3 in the fixture config

View File

@ -0,0 +1,96 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
)
// nodeExpandModule represents a module call in the configuration that
// might expand into multiple module instances depending on how it is
// configured.
type nodeExpandModule struct {
CallerAddr addrs.ModuleInstance
Call addrs.ModuleCall
Config *configs.Module
}
var (
_ GraphNodeSubPath = (*nodeExpandModule)(nil)
_ RemovableIfNotTargeted = (*nodeExpandModule)(nil)
_ GraphNodeEvalable = (*nodeExpandModule)(nil)
_ GraphNodeReferencer = (*nodeExpandModule)(nil)
)
func (n *nodeExpandModule) Name() string {
return n.CallerAddr.Child(n.Call.Name, addrs.NoKey).String()
}
// GraphNodeSubPath implementation
func (n *nodeExpandModule) Path() addrs.ModuleInstance {
// Notice that the node represents the module call and so we report
// the parent module as the path. The module call we're representing
// might expand into multiple child module instances during our work here.
return n.CallerAddr
}
// GraphNodeReferencer implementation
func (n *nodeExpandModule) References() []*addrs.Reference {
// Expansion only uses the count and for_each expressions, so this
// particular graph node only refers to those.
// Individual variable values in the module call definition might also
// refer to other objects, but that's handled by
// NodeApplyableModuleVariable.
//
// Because our Path method returns the module instance that contains
// our call, these references will be correctly interpreted as being
// in the calling module's namespace, not the namespaces of any of the
// child module instances we might expand to during our evaluation.
var ret []*addrs.Reference
// TODO: Once count and for_each are actually supported, analyze their
// expressions for references here.
/*
if n.Config.Count != nil {
ret = append(ret, n.Config.Count.References()...)
}
if n.Config.ForEach != nil {
ret = append(ret, n.Config.ForEach.References()...)
}
*/
return ret
}
// RemovableIfNotTargeted implementation
func (n *nodeExpandModule) RemoveIfNotTargeted() bool {
// We need to add this so that this node will be removed if
// it isn't targeted or a dependency of a target.
return true
}
// GraphNodeEvalable
func (n *nodeExpandModule) EvalTree() EvalNode {
return &evalPrepareModuleExpansion{
CallerAddr: n.CallerAddr,
Call: n.Call,
Config: n.Config,
}
}
type evalPrepareModuleExpansion struct {
CallerAddr addrs.ModuleInstance
Call addrs.ModuleCall
Config *configs.Module
}
func (n *evalPrepareModuleExpansion) Eval(ctx EvalContext) (interface{}, error) {
// Modules don't support any of the repetition arguments yet, so their
// expansion type is always "single". We just record this here to make
// the expander data structure consistent for now.
// FIXME: Once the rest of Terraform Core is ready to support expanding
// modules, evaluate the "count" and "for_each" arguments here in a
// similar way as in EvalWriteResourceState.
log.Printf("[TRACE] evalPrepareModuleExpansion: %s is a singleton", n.CallerAddr.Child(n.Call.Name, addrs.NoKey))
ctx.InstanceExpander().SetModuleSingle(n.CallerAddr, n.Call)
return nil, nil
}

View File

@ -71,19 +71,25 @@ func (n *NodePlannableResource) ModifyCreateBeforeDestroy(v bool) error {
func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
var diags tfdiags.Diagnostics
// Our instance expander should already have been informed about the
// expansion of this resource and of all of its containing modules, so
// it can tell us which instance addresses we need to process.
module := ctx.Path().Module()
expander := ctx.InstanceExpander()
instanceAddrs := expander.ExpandResource(module, n.ResourceAddr().Resource)
// We need to potentially rename an instance address in the state
// if we're transitioning whether "count" is set at all.
//
// FIXME: We're re-evaluating count here, even though the InstanceExpander
// has already dealt with our expansion above, because we need it to
// call fixResourceCountSetTransition; the expander API and that function
// are not compatible yet.
count, countDiags := evaluateResourceCountExpression(n.Config.Count, ctx)
diags = diags.Append(countDiags)
if countDiags.HasErrors() {
return nil, diags.Err()
}
forEachMap, forEachDiags := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
if forEachDiags.HasErrors() {
return nil, diags.Err()
}
// Next we need to potentially rename an instance address in the state
// if we're transitioning whether "count" is set at all.
fixResourceCountSetTransition(ctx, n.ResourceAddr(), count != -1)
// Our graph transformers require access to the full state, so we'll
@ -126,20 +132,18 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
steps := []GraphTransformer{
// Expand the count or for_each (if present)
&ResourceCountTransformer{
Concrete: concreteResource,
Schema: n.Schema,
Count: count,
ForEach: forEachMap,
Addr: n.ResourceAddr(),
Concrete: concreteResource,
Schema: n.Schema,
Addr: n.ResourceAddr(),
InstanceAddrs: instanceAddrs,
},
// Add the count/for_each orphans
&OrphanResourceCountTransformer{
Concrete: concreteResourceOrphan,
Count: count,
ForEach: forEachMap,
Addr: n.ResourceAddr(),
State: state,
Concrete: concreteResourceOrphan,
Addr: n.ResourceAddr(),
InstanceAddrs: instanceAddrs,
State: state,
},
// Attach the state

View File

@ -58,6 +58,19 @@ func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph,
// if we're transitioning whether "count" is set at all.
fixResourceCountSetTransition(ctx, n.ResourceAddr(), count != -1)
// 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(ctx.Path(), n.ResourceAddr().Resource, count)
case forEachMap != nil:
expander.SetResourceForEach(ctx.Path(), n.ResourceAddr().Resource, forEachMap)
default:
expander.SetResourceSingle(ctx.Path(), n.ResourceAddr().Resource)
}
instanceAddrs := expander.ExpandResource(ctx.Path().Module(), n.ResourceAddr().Resource)
// Our graph transformers require access to the full state, so we'll
// temporarily lock it while we work on this.
state := ctx.State().Lock()
@ -79,21 +92,19 @@ func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph,
steps := []GraphTransformer{
// Expand the count.
&ResourceCountTransformer{
Concrete: concreteResource,
Schema: n.Schema,
Count: count,
ForEach: forEachMap,
Addr: n.ResourceAddr(),
Concrete: concreteResource,
Schema: n.Schema,
Addr: n.ResourceAddr(),
InstanceAddrs: instanceAddrs,
},
// Add the count orphans to make sure these resources are accounted for
// during a scale in.
&OrphanResourceCountTransformer{
Concrete: concreteResource,
Count: count,
ForEach: forEachMap,
Addr: n.ResourceAddr(),
State: state,
Concrete: concreteResource,
Addr: n.ResourceAddr(),
InstanceAddrs: instanceAddrs,
State: state,
},
// Attach the state

View File

@ -8,6 +8,7 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/instances"
)
func TestNodeRefreshableManagedResourceDynamicExpand_scaleOut(t *testing.T) {
@ -49,8 +50,9 @@ func TestNodeRefreshableManagedResourceDynamicExpand_scaleOut(t *testing.T) {
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: addrs.RootModuleInstance,
StateState: state,
PathPath: addrs.RootModuleInstance,
StateState: state,
InstanceExpanderExpander: instances.NewExpander(),
// DynamicExpand will call EvaluateExpr to evaluate the "count"
// expression, which is just a literal number 3 in the fixture config
@ -130,8 +132,9 @@ func TestNodeRefreshableManagedResourceDynamicExpand_scaleIn(t *testing.T) {
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: addrs.RootModuleInstance,
StateState: state,
PathPath: addrs.RootModuleInstance,
StateState: state,
InstanceExpanderExpander: instances.NewExpander(),
// DynamicExpand will call EvaluateExpr to evaluate the "count"
// expression, which is just a literal number 3 in the fixture config

View File

@ -0,0 +1,82 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/dag"
)
// ModuleExpansionTransformer is a GraphTransformer that adds graph nodes
// representing the possible expansion of each module call in the configuration,
// and ensures that any nodes representing objects declared within a module
// are dependent on the expansion node so that they will be visited only
// after the module expansion has been decided.
//
// This transform must be applied only after all nodes representing objects
// that can be contained within modules have already been added.
type ModuleExpansionTransformer struct {
Config *configs.Config
}
func (t *ModuleExpansionTransformer) Transform(g *Graph) error {
// The root module is always a singleton and so does not need expansion
// processing, but any descendent modules do. We'll process them
// recursively using t.transform.
for _, cfg := range t.Config.Children {
err := t.transform(g, cfg, nil)
if err != nil {
return err
}
}
return nil
}
func (t *ModuleExpansionTransformer) transform(g *Graph, c *configs.Config, parentNode dag.Vertex) error {
// FIXME: We're using addrs.ModuleInstance to represent the paths here
// because the rest of Terraform Core is expecting that, but in practice
// thus is representing a path through the static module instances (not
// expanded yet), and so as we weave in support for repetition of module
// calls we'll need to make the plan processing actually use addrs.Module
// to represent that our graph nodes are actually representing unexpanded
// static configuration objects, not instances.
fullAddr := c.Path.UnkeyedInstanceShim()
callerAddr, callAddr := fullAddr.Call()
v := &nodeExpandModule{
CallerAddr: callerAddr,
Call: callAddr,
Config: c.Module,
}
g.Add(v)
log.Printf("[TRACE] ModuleExpansionTransformer: Added %s as %T", fullAddr, v)
if parentNode != nil {
log.Printf("[TRACE] ModuleExpansionTransformer: %s must wait for expansion of %s", dag.VertexName(v), dag.VertexName(parentNode))
g.Connect(dag.BasicEdge(v, parentNode))
}
// Connect any node that reports this module as its Path to ensure that
// the module expansion will be handled before that node.
// FIXME: Again, there is some Module vs. ModuleInstance muddling here
// for legacy reasons, which we'll need to clean up as part of further
// work to properly support "count" and "for_each" for modules. Nodes
// in the plan graph actually belong to modules, not to module instances.
for _, childV := range g.Vertices() {
pather, ok := childV.(GraphNodeSubPath)
if !ok {
continue
}
if pather.Path().Equal(fullAddr) {
log.Printf("[TRACE] ModuleExpansionTransformer: %s must wait for expansion of %s", dag.VertexName(childV), fullAddr)
g.Connect(dag.BasicEdge(childV, v))
}
}
// Also visit child modules, recursively.
for _, cc := range c.Children {
return t.transform(g, cc, v)
}
return nil
}

View File

@ -6,7 +6,6 @@ import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/states"
"github.com/zclconf/go-cty/cty"
)
// OrphanResourceCountTransformer is a GraphTransformer that adds orphans
@ -19,155 +18,43 @@ import (
type OrphanResourceCountTransformer struct {
Concrete ConcreteResourceInstanceNodeFunc
Count int // Actual count of the resource, or -1 if count is not set at all
ForEach map[string]cty.Value // The ForEach map on the resource
Addr addrs.AbsResource // Addr of the resource to look for orphans
State *states.State // Full global state
Addr addrs.AbsResource // Addr of the resource to look for orphans
InstanceAddrs []addrs.AbsResourceInstance // Addresses that currently exist in config
State *states.State // Full global state
}
func (t *OrphanResourceCountTransformer) Transform(g *Graph) error {
// FIXME: This is currently assuming that all of the instances of
// this resource belong to a single module instance, which is true
// at the time of writing this because Terraform Core doesn't support
// repetition of module calls yet, but this will need to be corrected
// in order to support count and for_each on module calls, where
// our t.InstanceAddrs may contain resource instances from many different
// module instances.
rs := t.State.Resource(t.Addr)
if rs == nil {
return nil // Resource doesn't exist in state, so nothing to do!
}
haveKeys := make(map[addrs.InstanceKey]struct{})
// This is an O(n*m) analysis, which we accept for now because the
// number of instances of a single resource ought to always be small in any
// reasonable Terraform configuration.
Have:
for key := range rs.Instances {
haveKeys[key] = struct{}{}
}
// if for_each is set, use that transformer
if t.ForEach != nil {
return t.transformForEach(haveKeys, g)
}
if t.Count < 0 {
return t.transformNoCount(haveKeys, g)
}
if t.Count == 0 {
return t.transformZeroCount(haveKeys, g)
}
return t.transformCount(haveKeys, g)
}
func (t *OrphanResourceCountTransformer) transformForEach(haveKeys map[addrs.InstanceKey]struct{}, g *Graph) error {
// If there is a NoKey node, add this to the graph first,
// so that we can create edges to it in subsequent (StringKey) nodes.
// This is because the last item determines the resource mode for the whole resource,
// (see SetResourceInstanceCurrent for more information) and we need to evaluate
// an orphaned (NoKey) resource before the in-memory state is updated
// to deal with a new for_each resource
_, hasNoKeyNode := haveKeys[addrs.NoKey]
var noKeyNode dag.Vertex
if hasNoKeyNode {
abstract := NewNodeAbstractResourceInstance(t.Addr.Instance(addrs.NoKey))
noKeyNode = abstract
if f := t.Concrete; f != nil {
noKeyNode = f(abstract)
thisAddr := t.Addr.Instance(key)
for _, wantAddr := range t.InstanceAddrs {
if wantAddr.Equal(thisAddr) {
continue Have
}
}
g.Add(noKeyNode)
}
// If thisAddr is not in t.InstanceAddrs then we've found an "orphan"
for key := range haveKeys {
// If the key is no-key, we have already added it, so skip
if key == addrs.NoKey {
continue
}
s, _ := key.(addrs.StringKey)
// If the key is present in our current for_each, carry on
if _, ok := t.ForEach[string(s)]; ok {
continue
}
abstract := NewNodeAbstractResourceInstance(t.Addr.Instance(key))
abstract := NewNodeAbstractResourceInstance(thisAddr)
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
log.Printf("[TRACE] OrphanResourceCount(non-zero): adding %s as %T", t.Addr, node)
g.Add(node)
// Add edge to noKeyNode if it exists
if hasNoKeyNode {
g.Connect(dag.BasicEdge(node, noKeyNode))
}
}
return nil
}
func (t *OrphanResourceCountTransformer) transformCount(haveKeys map[addrs.InstanceKey]struct{}, g *Graph) error {
// Due to the logic in Transform, we only get in here if our count is
// at least one.
_, have0Key := haveKeys[addrs.IntKey(0)]
for key := range haveKeys {
if key == addrs.NoKey && !have0Key {
// If we have no 0-key then we will accept a no-key instance
// as an alias for it.
continue
}
i, isInt := key.(addrs.IntKey)
if isInt && int(i) < t.Count {
continue
}
abstract := NewNodeAbstractResourceInstance(t.Addr.Instance(key))
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
log.Printf("[TRACE] OrphanResourceCount(non-zero): adding %s as %T", t.Addr, node)
g.Add(node)
}
return nil
}
func (t *OrphanResourceCountTransformer) transformZeroCount(haveKeys map[addrs.InstanceKey]struct{}, g *Graph) error {
// This case is easy: we need to orphan any keys we have at all.
for key := range haveKeys {
abstract := NewNodeAbstractResourceInstance(t.Addr.Instance(key))
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
log.Printf("[TRACE] OrphanResourceCount(zero): adding %s as %T", t.Addr, node)
g.Add(node)
}
return nil
}
func (t *OrphanResourceCountTransformer) transformNoCount(haveKeys map[addrs.InstanceKey]struct{}, g *Graph) error {
// Negative count indicates that count is not set at all, in which
// case we expect to have a single instance with no key set at all.
// However, we'll also accept an instance with key 0 set as an alias
// for it, in case the user has just deleted the "count" argument and
// so wants to keep the first instance in the set.
_, haveNoKey := haveKeys[addrs.NoKey]
_, have0Key := haveKeys[addrs.IntKey(0)]
keepKey := addrs.NoKey
if have0Key && !haveNoKey {
// If we don't have a no-key instance then we can use the 0-key instance
// instead.
keepKey = addrs.IntKey(0)
}
for key := range haveKeys {
if key == keepKey {
continue
}
abstract := NewNodeAbstractResourceInstance(t.Addr.Instance(key))
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
log.Printf("[TRACE] OrphanResourceCount(no-count): adding %s as %T", t.Addr, node)
log.Printf("[TRACE] OrphanResourceCountTransformer: adding %s as %T", thisAddr, node)
g.Add(node)
}

View File

@ -1,5 +1,10 @@
package terraform
// FIXME: Update these tests for the new OrphanResourceCountTransformer
// interface that expects to be given a list of instance addresses that
// exist in config.
/*
import (
"strings"
"testing"
@ -433,3 +438,4 @@ aws_instance.foo (orphan)
aws_instance.foo["bar"] (orphan)
aws_instance.foo (orphan)
`
*/

View File

@ -1,10 +1,11 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/dag"
"github.com/zclconf/go-cty/cty"
)
// ResourceCountTransformer is a GraphTransformer that expands the count
@ -15,33 +16,12 @@ type ResourceCountTransformer struct {
Concrete ConcreteResourceInstanceNodeFunc
Schema *configschema.Block
// Count is either the number of indexed instances to create, or -1 to
// indicate that count is not set at all and thus a no-key instance should
// be created.
Count int
ForEach map[string]cty.Value
Addr addrs.AbsResource
Addr addrs.AbsResource
InstanceAddrs []addrs.AbsResourceInstance
}
func (t *ResourceCountTransformer) Transform(g *Graph) error {
if t.Count < 0 && t.ForEach == nil {
// Negative count indicates that count is not set at all.
addr := t.Addr.Instance(addrs.NoKey)
abstract := NewNodeAbstractResourceInstance(addr)
abstract.Schema = t.Schema
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
g.Add(node)
return nil
}
// Add nodes related to the for_each expression
for key := range t.ForEach {
addr := t.Addr.Instance(addrs.StringKey(key))
for _, addr := range t.InstanceAddrs {
abstract := NewNodeAbstractResourceInstance(addr)
abstract.Schema = t.Schema
var node dag.Vertex = abstract
@ -49,23 +29,8 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
node = f(abstract)
}
log.Printf("[TRACE] ResourceCountTransformer: adding %s as %T", addr, node)
g.Add(node)
}
// For each count, build and add the node
for i := 0; i < t.Count; i++ {
key := addrs.IntKey(i)
addr := t.Addr.Instance(key)
abstract := NewNodeAbstractResourceInstance(addr)
abstract.Schema = t.Schema
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
g.Add(node)
}
return nil
}