Merge #14098: correctly handle new/removed instances from count during refresh

This commit is contained in:
Martin Atkins 2017-05-12 15:48:07 -07:00 committed by GitHub
commit 87e98d6b1a
11 changed files with 834 additions and 13 deletions

View File

@ -0,0 +1,224 @@
package test
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
// TestResourceDataDep_alignedCountScaleOut tests to make sure interpolation
// works (namely without index errors) when a data source and a resource share
// the same count variable during scale-out with an existing state.
func TestResourceDataDep_alignedCountScaleOut(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: func(s *terraform.State) error {
return nil
},
Steps: []resource.TestStep{
{
Config: testResourceDataDepConfig(2),
},
{
Config: testResourceDataDepConfig(4),
Check: resource.TestCheckOutput("out", "value_from_api,value_from_api,value_from_api,value_from_api"),
},
},
})
}
// TestResourceDataDep_alignedCountScaleIn tests to make sure interpolation
// works (namely without index errors) when a data source and a resource share
// the same count variable during scale-in with an existing state.
func TestResourceDataDep_alignedCountScaleIn(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: func(s *terraform.State) error {
return nil
},
Steps: []resource.TestStep{
{
Config: testResourceDataDepConfig(4),
},
{
Config: testResourceDataDepConfig(2),
Check: resource.TestCheckOutput("out", "value_from_api,value_from_api"),
},
},
})
}
// TestDataResourceDep_alignedCountScaleOut functions like
// TestResourceDataDep_alignedCountScaleOut, but with the dependencies swapped
// (resource now depends on data source, a pretty regular use case, but
// included here to check for regressions).
func TestDataResourceDep_alignedCountScaleOut(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: func(s *terraform.State) error {
return nil
},
Steps: []resource.TestStep{
{
Config: testDataResourceDepConfig(2),
},
{
Config: testDataResourceDepConfig(4),
Check: resource.TestCheckOutput("out", "test,test,test,test"),
},
},
})
}
// TestDataResourceDep_alignedCountScaleIn functions like
// TestResourceDataDep_alignedCountScaleIn, but with the dependencies swapped
// (resource now depends on data source, a pretty regular use case, but
// included here to check for regressions).
func TestDataResourceDep_alignedCountScaleIn(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: func(s *terraform.State) error {
return nil
},
Steps: []resource.TestStep{
{
Config: testDataResourceDepConfig(4),
},
{
Config: testDataResourceDepConfig(2),
Check: resource.TestCheckOutput("out", "test,test"),
},
},
})
}
// TestResourceResourceDep_alignedCountScaleOut functions like
// TestResourceDataDep_alignedCountScaleOut, but with a resource-to-resource
// dependency instead, a pretty regular use case, but included here to check
// for regressions.
func TestResourceResourceDep_alignedCountScaleOut(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: func(s *terraform.State) error {
return nil
},
Steps: []resource.TestStep{
{
Config: testResourceResourceDepConfig(2),
},
{
Config: testResourceResourceDepConfig(4),
Check: resource.TestCheckOutput("out", "test,test,test,test"),
},
},
})
}
// TestResourceResourceDep_alignedCountScaleIn functions like
// TestResourceDataDep_alignedCountScaleIn, but with a resource-to-resource
// dependency instead, a pretty regular use case, but included here to check
// for regressions.
func TestResourceResourceDep_alignedCountScaleIn(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
Providers: testAccProviders,
CheckDestroy: func(s *terraform.State) error {
return nil
},
Steps: []resource.TestStep{
{
Config: testResourceResourceDepConfig(4),
},
{
Config: testResourceResourceDepConfig(2),
Check: resource.TestCheckOutput("out", "test,test"),
},
},
})
}
func testResourceDataDepConfig(count int) string {
return fmt.Sprintf(`
variable count {
default = "%d"
}
resource "test_resource" "foo" {
count = "${var.count}"
required = "yes"
required_map = {
"foo" = "bar"
}
}
data "test_data_source" "bar" {
count = "${var.count}"
input = "${test_resource.foo.*.computed_read_only[count.index]}"
}
output "out" {
value = "${join(",", data.test_data_source.bar.*.output)}"
}
`, count)
}
func testDataResourceDepConfig(count int) string {
return fmt.Sprintf(`
variable count {
default = "%d"
}
data "test_data_source" "foo" {
count = "${var.count}"
input = "test"
}
resource "test_resource" "bar" {
count = "${var.count}"
required = "yes"
optional = "${data.test_data_source.foo.*.output[count.index]}"
required_map = {
"foo" = "bar"
}
}
output "out" {
value = "${join(",", test_resource.bar.*.optional)}"
}
`, count)
}
func testResourceResourceDepConfig(count int) string {
return fmt.Sprintf(`
variable count {
default = "%d"
}
resource "test_resource" "foo" {
count = "${var.count}"
required = "yes"
optional = "test"
required_map = {
"foo" = "bar"
}
}
resource "test_resource" "bar" {
count = "${var.count}"
required = "yes"
optional = "${test_resource.foo.*.optional[count.index]}"
required_map = {
"foo" = "bar"
}
}
output "out" {
value = "${join(",", test_resource.bar.*.optional)}"
}
`, count)
}

View File

@ -1,6 +1,8 @@
package terraform package terraform
import ( import (
"log"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag" "github.com/hashicorp/terraform/dag"
@ -56,8 +58,16 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
} }
} }
concreteResource := func(a *NodeAbstractResource) dag.Vertex { concreteManagedResource := func(a *NodeAbstractResource) dag.Vertex {
return &NodeRefreshableResource{ return &NodeRefreshableManagedResource{
NodeAbstractCountResource: &NodeAbstractCountResource{
NodeAbstractResource: a,
},
}
}
concreteManagedResourceInstance := func(a *NodeAbstractResource) dag.Vertex {
return &NodeRefreshableManagedResourceInstance{
NodeAbstractResource: a, NodeAbstractResource: a,
} }
} }
@ -71,13 +81,25 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
} }
steps := []GraphTransformer{ steps := []GraphTransformer{
// Creates all the resources represented in the state // Creates all the managed resources that aren't in the state, but only if
&StateTransformer{ // we have a state already. No resources in state means there's not
Concrete: concreteResource, // anything to refresh.
State: b.State, func() GraphTransformer {
}, if b.State.HasResources() {
return &ConfigTransformer{
Concrete: concreteManagedResource,
Module: b.Module,
Unique: true,
ModeFilter: true,
Mode: config.ManagedResourceMode,
}
}
log.Println("[TRACE] No managed resources in state during refresh, skipping managed resource transformer")
return nil
}(),
// Creates all the data resources that aren't in the state // Creates all the data resources that aren't in the state. This will also
// add any orphans from scaling in as destroy nodes.
&ConfigTransformer{ &ConfigTransformer{
Concrete: concreteDataResource, Concrete: concreteDataResource,
Module: b.Module, Module: b.Module,
@ -86,6 +108,15 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
Mode: config.DataResourceMode, Mode: config.DataResourceMode,
}, },
// Add any fully-orphaned resources from config (ones that have been
// removed completely, not ones that are just orphaned due to a scaled-in
// count.
&OrphanResourceTransformer{
Concrete: concreteManagedResourceInstance,
State: b.State,
Module: b.Module,
},
// Attach the state // Attach the state
&AttachStateTransformer{State: b.State}, &AttachStateTransformer{State: b.State},

View File

@ -0,0 +1,96 @@
package terraform
import "testing"
func TestRefreshGraphBuilder_configOrphans(t *testing.T) {
m := testModule(t, "refresh-config-orphan")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "baz",
},
},
},
"data.aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"data.aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
"data.aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "baz",
},
},
},
},
},
},
}
b := &RefreshGraphBuilder{
Module: m,
State: state,
Providers: []string{"aws"},
}
g, err := b.Build(rootModulePath)
if err != nil {
t.Fatalf("Error building graph: %s", err)
}
actual := g.StringWithNodeTypes()
expected := `aws_instance.foo - *terraform.NodeRefreshableManagedResource
provider.aws - *terraform.NodeApplyableProvider
data.aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
provider.aws - *terraform.NodeApplyableProvider
data.aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
provider.aws - *terraform.NodeApplyableProvider
data.aws_instance.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
provider.aws - *terraform.NodeApplyableProvider
provider.aws - *terraform.NodeApplyableProvider
provider.aws (close) - *terraform.graphNodeCloseProvider
aws_instance.foo - *terraform.NodeRefreshableManagedResource
data.aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
}

View File

@ -33,6 +33,17 @@ func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, er
} }
} }
// We also need a destroyable resource for orphans that are a result of a
// scaled-in count.
concreteResourceDestroyable := func(a *NodeAbstractResource) dag.Vertex {
// Add the config since we don't do that via transforms
a.Config = n.Config
return &NodeDestroyableDataResource{
NodeAbstractResource: a,
}
}
// Start creating the steps // Start creating the steps
steps := []GraphTransformer{ steps := []GraphTransformer{
// Expand the count. // Expand the count.
@ -42,6 +53,15 @@ func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, er
Addr: n.ResourceAddr(), Addr: n.ResourceAddr(),
}, },
// Add the count orphans. As these are orphaned refresh nodes, we add them
// directly as NodeDestroyableDataResource.
&OrphanResourceCountTransformer{
Concrete: concreteResourceDestroyable,
Count: count,
Addr: n.ResourceAddr(),
State: state,
},
// Attach the state // Attach the state
&AttachStateTransformer{State: state}, &AttachStateTransformer{State: state},

View File

@ -0,0 +1,154 @@
package terraform
import (
"sync"
"testing"
)
func TestNodeRefreshableDataResourceDynamicExpand_scaleOut(t *testing.T) {
var stateLock sync.RWMutex
addr, err := ParseResourceAddress("data.aws_instance.foo")
if err != nil {
t.Fatalf("bad: %s", err)
}
m := testModule(t, "refresh-data-scale-inout")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"data.aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"data.aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
},
},
},
}
n := &NodeRefreshableDataResource{
NodeAbstractCountResource: &NodeAbstractCountResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: addr,
Config: m.Config().Resources[0],
},
},
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: []string{"root"},
StateState: state,
StateLock: &stateLock,
})
actual := g.StringWithNodeTypes()
expected := `data.aws_instance.foo[0] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableDataResourceInstance
root - terraform.graphNodeRoot
data.aws_instance.foo[0] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableDataResourceInstance
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
}
func TestNodeRefreshableDataResourceDynamicExpand_scaleIn(t *testing.T) {
var stateLock sync.RWMutex
addr, err := ParseResourceAddress("data.aws_instance.foo")
if err != nil {
t.Fatalf("bad: %s", err)
}
m := testModule(t, "refresh-data-scale-inout")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"data.aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"data.aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
"data.aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "baz",
},
},
},
"data.aws_instance.foo.3": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "qux",
},
},
},
},
},
},
}
n := &NodeRefreshableDataResource{
NodeAbstractCountResource: &NodeAbstractCountResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: addr,
Config: m.Config().Resources[0],
},
},
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: []string{"root"},
StateState: state,
StateLock: &stateLock,
})
actual := g.StringWithNodeTypes()
expected := `data.aws_instance.foo[0] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[3] - *terraform.NodeDestroyableDataResource
root - terraform.graphNodeRoot
data.aws_instance.foo[0] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[3] - *terraform.NodeDestroyableDataResource
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
}

View File

@ -4,21 +4,99 @@ import (
"fmt" "fmt"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
) )
// NodeRefreshableResource represents a resource that is "applyable": // NodeRefreshableManagedResource represents a resource that is expanabled into
// NodeRefreshableManagedResourceInstance. Resource count orphans are also added.
type NodeRefreshableManagedResource struct {
*NodeAbstractCountResource
}
// GraphNodeDynamicExpandable
func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
// Grab the state which we read
state, lock := ctx.State()
lock.RLock()
defer lock.RUnlock()
// Expand the resource count which must be available by now from EvalTree
count, err := n.Config.Count()
if err != nil {
return nil, err
}
// The concrete resource factory we'll use
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
// Add the config and state since we don't do that via transforms
a.Config = n.Config
return &NodeRefreshableManagedResourceInstance{
NodeAbstractResource: a,
}
}
// Start creating the steps
steps := []GraphTransformer{
// Expand the count.
&ResourceCountTransformer{
Concrete: concreteResource,
Count: count,
Addr: n.ResourceAddr(),
},
// Switch up any node missing state to a plannable resource. This helps
// catch cases where data sources depend on the counts from this resource
// during a scale out.
&ResourceRefreshPlannableTransformer{
State: state,
},
// Add the count orphans to make sure these resources are accounted for
// during a scale in.
&OrphanResourceCountTransformer{
Concrete: concreteResource,
Count: count,
Addr: n.ResourceAddr(),
State: state,
},
// Attach the state
&AttachStateTransformer{State: state},
// Targeting
&TargetsTransformer{ParsedTargets: n.Targets},
// Connect references so ordering is correct
&ReferenceTransformer{},
// Make sure there is a single root
&RootTransformer{},
}
// Build the graph
b := &BasicGraphBuilder{
Steps: steps,
Validate: true,
Name: "NodeRefreshableManagedResource",
}
return b.Build(ctx.Path())
}
// NodeRefreshableManagedResourceInstance represents a resource that is "applyable":
// it is ready to be applied and is represented by a diff. // it is ready to be applied and is represented by a diff.
type NodeRefreshableResource struct { type NodeRefreshableManagedResourceInstance struct {
*NodeAbstractResource *NodeAbstractResource
} }
// GraphNodeDestroyer // GraphNodeDestroyer
func (n *NodeRefreshableResource) DestroyAddr() *ResourceAddress { func (n *NodeRefreshableManagedResourceInstance) DestroyAddr() *ResourceAddress {
return n.Addr return n.Addr
} }
// GraphNodeEvalable // GraphNodeEvalable
func (n *NodeRefreshableResource) EvalTree() EvalNode { func (n *NodeRefreshableManagedResourceInstance) EvalTree() EvalNode {
// Eval info is different depending on what kind of resource this is // Eval info is different depending on what kind of resource this is
switch mode := n.Addr.Mode; mode { switch mode := n.Addr.Mode; mode {
case config.ManagedResourceMode: case config.ManagedResourceMode:
@ -44,7 +122,7 @@ func (n *NodeRefreshableResource) EvalTree() EvalNode {
} }
} }
func (n *NodeRefreshableResource) evalTreeManagedResource() EvalNode { func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResource() EvalNode {
addr := n.NodeAbstractResource.Addr addr := n.NodeAbstractResource.Addr
// stateId is the ID to put into the state // stateId is the ID to put into the state

View File

@ -0,0 +1,154 @@
package terraform
import (
"sync"
"testing"
)
func TestNodeRefreshableManagedResourceDynamicExpand_scaleOut(t *testing.T) {
var stateLock sync.RWMutex
addr, err := ParseResourceAddress("aws_instance.foo")
if err != nil {
t.Fatalf("bad: %s", err)
}
m := testModule(t, "refresh-resource-scale-inout")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
},
},
},
}
n := &NodeRefreshableManagedResource{
NodeAbstractCountResource: &NodeAbstractCountResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: addr,
Config: m.Config().Resources[0],
},
},
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: []string{"root"},
StateState: state,
StateLock: &stateLock,
})
actual := g.StringWithNodeTypes()
expected := `aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[2] - *terraform.NodePlannableResourceInstance
root - terraform.graphNodeRoot
aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[2] - *terraform.NodePlannableResourceInstance
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
}
func TestNodeRefreshableManagedResourceDynamicExpand_scaleIn(t *testing.T) {
var stateLock sync.RWMutex
addr, err := ParseResourceAddress("aws_instance.foo")
if err != nil {
t.Fatalf("bad: %s", err)
}
m := testModule(t, "refresh-resource-scale-inout")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "baz",
},
},
},
"aws_instance.foo.3": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "qux",
},
},
},
},
},
},
}
n := &NodeRefreshableManagedResource{
NodeAbstractCountResource: &NodeAbstractCountResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: addr,
Config: m.Config().Resources[0],
},
},
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: []string{"root"},
StateState: state,
StateLock: &stateLock,
})
actual := g.StringWithNodeTypes()
expected := `aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[3] - *terraform.NodeRefreshableManagedResourceInstance
root - terraform.graphNodeRoot
aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[3] - *terraform.NodeRefreshableManagedResourceInstance
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
}

View File

@ -0,0 +1,3 @@
resource "aws_instance" "foo" {
count = 3
}

View File

@ -0,0 +1,3 @@
data "aws_instance" "foo" {
count = 3
}

View File

@ -0,0 +1,3 @@
resource "aws_instance" "foo" {
count = 3
}

View File

@ -0,0 +1,55 @@
package terraform
import (
"fmt"
"log"
)
// ResourceRefreshPlannableTransformer is a GraphTransformer that replaces any
// nodes that don't have state yet exist in config with
// NodePlannableResourceInstance.
//
// This transformer is used when expanding count on managed resource nodes
// during the refresh phase to ensure that data sources that have
// interpolations that depend on resources existing in the graph can be walked
// properly.
type ResourceRefreshPlannableTransformer struct {
// The full global state.
State *State
}
// Transform implements GraphTransformer for
// ResourceRefreshPlannableTransformer.
func (t *ResourceRefreshPlannableTransformer) Transform(g *Graph) error {
nextVertex:
for _, v := range g.Vertices() {
addr := v.(*NodeRefreshableManagedResourceInstance).Addr
// Find the state for this address, if there is one
filter := &StateFilter{State: t.State}
results, err := filter.Filter(addr.String())
if err != nil {
return err
}
// Check to see if we have a state for this resource. If we do, skip this
// node.
for _, result := range results {
if _, ok := result.Value.(*ResourceState); ok {
continue nextVertex
}
}
// If we don't, convert this resource to a NodePlannableResourceInstance node
// with all of the data we need to make it happen.
log.Printf("[TRACE] No state for %s, converting to NodePlannableResourceInstance", addr.String())
new := &NodePlannableResourceInstance{
NodeAbstractResource: v.(*NodeRefreshableManagedResourceInstance).NodeAbstractResource,
}
// Replace the node in the graph
if !g.Replace(v, new) {
return fmt.Errorf("ResourceRefreshPlannableTransformer: Could not replace node %#v with %#v", v, new)
}
}
return nil
}