Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: New refresh graph building behaviour #14098

Merged
merged 5 commits into from
May 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions builtin/providers/test/resource_data_dep_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
49 changes: 40 additions & 9 deletions terraform/graph_builder_refresh.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package terraform

import (
"log"

"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
Expand Down Expand Up @@ -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,
}
}
Expand All @@ -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 data resources that aren't in the 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. This will also
// add any orphans from scaling in as destroy nodes.
&ConfigTransformer{
Concrete: concreteDataResource,
Module: b.Module,
Expand All @@ -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},

Expand Down
96 changes: 96 additions & 0 deletions terraform/graph_builder_refresh_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading