terraform: graph tainted resources into the graph

This commit is contained in:
Mitchell Hashimoto 2014-09-19 21:29:48 -06:00
parent 5e0765c24a
commit f89c2c5ff0
4 changed files with 209 additions and 9 deletions

View File

@ -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,14 +754,11 @@ 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 variables of the resource provisioners
for _, p := range m.Resource.Provisioners {
@ -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(

View File

@ -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

View File

@ -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

View File

@ -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}"
]
}