From f89c2c5ff0f80b011a3ec6001bf6ace237fed010 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Sep 2014 21:29:48 -0600 Subject: [PATCH] terraform: graph tainted resources into the graph --- terraform/graph.go | 86 +++++++++++-- terraform/graph_test.go | 113 ++++++++++++++++++ terraform/resource.go | 1 + terraform/test-fixtures/graph-tainted/main.tf | 18 +++ 4 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 terraform/test-fixtures/graph-tainted/main.tf diff --git a/terraform/graph.go b/terraform/graph.go index b8c16c9e5..d303fcc81 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -106,15 +106,19 @@ func Graph(opts *GraphOpts) (*depgraph.Graph, error) { g := new(depgraph.Graph) // First, build the initial resource graph. This only has the resources - // and no dependencies. + // and no dependencies. This only adds resources that are in the config + // and not "orphans" (that are in the state, but not in the config). graphAddConfigResources(g, opts.Config, opts.State) // Add explicit dependsOn dependencies to the graph graphAddExplicitDeps(g) - // Next, add the state orphans if we have any if opts.State != nil { + // Next, add the state orphans if we have any graphAddOrphans(g, opts.Config, opts.State) + + // Add tainted resources if we have any. + graphAddTainted(g, opts.State) } // Map the provider configurations to all of the resources @@ -750,15 +754,12 @@ func graphAddVariableDeps(g *depgraph.Graph) { var vars map[string]config.InterpolatedVariable switch m := n.Meta.(type) { case *GraphNodeResource: - // Ignore orphan nodes - if m.Orphan { - continue + if m.Config != nil { + // Handle the resource variables + vars = m.Config.RawConfig.Variables + nounAddVariableDeps(g, n, vars, false) } - // Handle the resource variables - vars = m.Config.RawConfig.Variables - nounAddVariableDeps(g, n, vars, false) - // Handle the variables of the resource provisioners for _, p := range m.Resource.Provisioners { vars = p.RawConfig.Variables @@ -778,6 +779,73 @@ func graphAddVariableDeps(g *depgraph.Graph) { } } +// graphAddTainted adds the tainted instances to the graph. +func graphAddTainted(g *depgraph.Graph, s *State) { + // TODO: Handle other modules + mod := s.ModuleByPath(rootModulePath) + if mod == nil { + return + } + + var nlist []*depgraph.Noun + for k, rs := range mod.Resources { + // If we have no tainted resources, continue on + if len(rs.Tainted) == 0 { + continue + } + + // Find the untainted resource of this in the noun list + var untainted *depgraph.Noun + for _, n := range g.Nouns { + if n.Name == k { + untainted = n + break + } + } + + for i, _ := range rs.Tainted { + name := fmt.Sprintf("%s (tainted #%d)", k, i+1) + + // Add each of the tainted resources to the graph, and encode + // a dependency from the non-tainted resource to this so that + // tainted resources are always destroyed first. + noun := &depgraph.Noun{ + Name: name, + Meta: &GraphNodeResource{ + Index: -1, + Type: rs.Type, + Resource: &Resource{ + Id: k, + State: rs, + Config: NewResourceConfig(nil), + Diff: &InstanceDiff{Destroy: true}, + Tainted: true, + TaintedIndex: i, + }, + }, + } + + // Append it to the list so we handle it later + nlist = append(nlist, noun) + + // If we have an untainted version, then make sure to add + // the dependency. + if untainted != nil { + dep := &depgraph.Dependency{ + Name: name, + Source: untainted, + Target: noun, + } + + untainted.Deps = append(untainted.Deps, dep) + } + } + } + + // Add the nouns to the graph + g.Nouns = append(g.Nouns, nlist...) +} + // nounAddVariableDeps updates the dependencies of a noun given // a set of associated variable values func nounAddVariableDeps( diff --git a/terraform/graph_test.go b/terraform/graph_test.go index 69adb5a00..8501b7c3a 100644 --- a/terraform/graph_test.go +++ b/terraform/graph_test.go @@ -112,6 +112,81 @@ func TestGraph_state(t *testing.T) { } } +func TestGraph_tainted(t *testing.T) { + config := testConfig(t, "graph-tainted") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + Tainted: []*InstanceState{ + &InstanceState{ + ID: "bar", + }, + }, + }, + }, + }, + }, + } + + g, err := Graph(&GraphOpts{Config: config, State: state}) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTerraformGraphTaintedStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestGraph_taintedMulti(t *testing.T) { + config := testConfig(t, "graph-tainted") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + Tainted: []*InstanceState{ + &InstanceState{ + ID: "bar", + }, + &InstanceState{ + ID: "baz", + }, + }, + }, + }, + }, + }, + } + + g, err := Graph(&GraphOpts{Config: config, State: state}) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTerraformGraphTaintedMultiStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + func TestGraphFull(t *testing.T) { rpAws := new(MockResourceProvider) rpOS := new(MockResourceProvider) @@ -715,6 +790,44 @@ root root -> openstack_floating_ip.random ` +const testTerraformGraphTaintedStr = ` +root: root +aws_instance.web + aws_instance.web -> aws_instance.web (tainted #1) + aws_instance.web -> aws_security_group.firewall + aws_instance.web -> provider.aws +aws_instance.web (tainted #1) + aws_instance.web (tainted #1) -> provider.aws +aws_security_group.firewall + aws_security_group.firewall -> provider.aws +provider.aws +root + root -> aws_instance.web + root -> aws_instance.web (tainted #1) + root -> aws_security_group.firewall +` + +const testTerraformGraphTaintedMultiStr = ` +root: root +aws_instance.web + aws_instance.web -> aws_instance.web (tainted #1) + aws_instance.web -> aws_instance.web (tainted #2) + aws_instance.web -> aws_security_group.firewall + aws_instance.web -> provider.aws +aws_instance.web (tainted #1) + aws_instance.web (tainted #1) -> provider.aws +aws_instance.web (tainted #2) + aws_instance.web (tainted #2) -> provider.aws +aws_security_group.firewall + aws_security_group.firewall -> provider.aws +provider.aws +root + root -> aws_instance.web + root -> aws_instance.web (tainted #1) + root -> aws_instance.web (tainted #2) + root -> aws_security_group.firewall +` + const testTerraformGraphCountOrphanStr = ` root: root aws_instance.web diff --git a/terraform/resource.go b/terraform/resource.go index 18277db10..225cf5f59 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -33,6 +33,7 @@ type Resource struct { State *ResourceState Provisioners []*ResourceProvisionerConfig Tainted bool + TaintedIndex int } // Vars returns the mapping of variables that should be replaced in diff --git a/terraform/test-fixtures/graph-tainted/main.tf b/terraform/test-fixtures/graph-tainted/main.tf new file mode 100644 index 000000000..da7eb0a79 --- /dev/null +++ b/terraform/test-fixtures/graph-tainted/main.tf @@ -0,0 +1,18 @@ +variable "foo" { + default = "bar" + description = "bar" +} + +provider "aws" { + foo = "${openstack_floating_ip.random.value}" +} + +resource "aws_security_group" "firewall" {} + +resource "aws_instance" "web" { + ami = "${var.foo}" + security_groups = [ + "foo", + "${aws_security_group.firewall.foo}" + ] +}