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
|
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},
|
||||||
|
|
||||||
|
|
|
@ -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
|
// 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},
|
||||||
|
|
||||||
|
|
|
@ -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"
|
"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
|
||||||
|
|
|
@ -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