Merge #14098: correctly handle new/removed instances from count during refresh
This commit is contained in:
commit
87e98d6b1a
|
@ -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)
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/config/module"
|
||||
"github.com/hashicorp/terraform/dag"
|
||||
|
@ -56,8 +58,16 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
|
|||
}
|
||||
}
|
||||
|
||||
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
|
||||
return &NodeRefreshableResource{
|
||||
concreteManagedResource := func(a *NodeAbstractResource) dag.Vertex {
|
||||
return &NodeRefreshableManagedResource{
|
||||
NodeAbstractCountResource: &NodeAbstractCountResource{
|
||||
NodeAbstractResource: a,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
concreteManagedResourceInstance := func(a *NodeAbstractResource) dag.Vertex {
|
||||
return &NodeRefreshableManagedResourceInstance{
|
||||
NodeAbstractResource: a,
|
||||
}
|
||||
}
|
||||
|
@ -71,13 +81,25 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
|
|||
}
|
||||
|
||||
steps := []GraphTransformer{
|
||||
// Creates all the resources represented in the state
|
||||
&StateTransformer{
|
||||
Concrete: concreteResource,
|
||||
State: b.State,
|
||||
},
|
||||
// Creates all the managed resources that aren't in the state, but only if
|
||||
// we have a state already. No resources in state means there's not
|
||||
// anything to refresh.
|
||||
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{
|
||||
Concrete: concreteDataResource,
|
||||
Module: b.Module,
|
||||
|
@ -86,6 +108,15 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
|
|||
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
|
||||
&AttachStateTransformer{State: b.State},
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
steps := []GraphTransformer{
|
||||
// Expand the count.
|
||||
|
@ -42,6 +53,15 @@ func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, er
|
|||
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
|
||||
&AttachStateTransformer{State: state},
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -4,21 +4,99 @@ import (
|
|||
"fmt"
|
||||
|
||||
"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.
|
||||
type NodeRefreshableResource struct {
|
||||
type NodeRefreshableManagedResourceInstance struct {
|
||||
*NodeAbstractResource
|
||||
}
|
||||
|
||||
// GraphNodeDestroyer
|
||||
func (n *NodeRefreshableResource) DestroyAddr() *ResourceAddress {
|
||||
func (n *NodeRefreshableManagedResourceInstance) DestroyAddr() *ResourceAddress {
|
||||
return n.Addr
|
||||
}
|
||||
|
||||
// GraphNodeEvalable
|
||||
func (n *NodeRefreshableResource) EvalTree() EvalNode {
|
||||
func (n *NodeRefreshableManagedResourceInstance) EvalTree() EvalNode {
|
||||
// Eval info is different depending on what kind of resource this is
|
||||
switch mode := n.Addr.Mode; mode {
|
||||
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
|
||||
|
||||
// stateId is the ID to put into the state
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource "aws_instance" "foo" {
|
||||
count = 3
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
data "aws_instance" "foo" {
|
||||
count = 3
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
resource "aws_instance" "foo" {
|
||||
count = 3
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue