terraform: OrphanResourceCountTransformer for orphaning extranneous

instances
This commit is contained in:
Mitchell Hashimoto 2016-11-07 13:23:06 -08:00
parent 97b7915b8f
commit 091264e4ba
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
3 changed files with 446 additions and 10 deletions

View File

@ -29,6 +29,11 @@ func (n *NodePlannableResource) EvalTree() EvalNode {
// GraphNodeDynamicExpandable // GraphNodeDynamicExpandable
func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { func (n *NodePlannableResource) 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 // Expand the resource count which must be available by now from EvalTree
count, err := n.Config.Count() count, err := n.Config.Count()
if err != nil { if err != nil {
@ -39,25 +44,45 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
concreteResource := func(a *NodeAbstractResource) dag.Vertex { concreteResource := func(a *NodeAbstractResource) dag.Vertex {
// Add the config and state since we don't do that via transforms // Add the config and state since we don't do that via transforms
a.Config = n.Config a.Config = n.Config
a.ResourceState = n.ResourceState
return &NodePlannableResourceInstance{ return &NodePlannableResourceInstance{
NodeAbstractResource: a, NodeAbstractResource: a,
} }
} }
// Start creating the steps // The concrete resource factory we'll use for oprhans
steps := make([]GraphTransformer, 0, 5) concreteResourceOrphan := func(a *NodeAbstractResource) dag.Vertex {
return &NodePlannableResourceOrphan{
NodeAbstractResource: a,
}
}
// Expand counts. // Start creating the steps
steps = append(steps, &ResourceCountTransformer{ steps := []GraphTransformer{
// Expand the count.
&ResourceCountTransformer{
Concrete: concreteResource, Concrete: concreteResource,
Count: count, Count: count,
Addr: n.ResourceAddr(), Addr: n.ResourceAddr(),
}) },
// Always end with the root being added // Add the count orphans
steps = append(steps, &RootTransformer{}) &OrphanResourceCountTransformer{
Concrete: concreteResourceOrphan,
Count: count,
Addr: n.ResourceAddr(),
State: state,
},
// TODO: deposed
// TODO: targeting
// Attach the state
&AttachStateTransformer{State: state},
// Make sure there is a single root
&RootTransformer{},
}
// Build the graph // Build the graph
b := &BasicGraphBuilder{Steps: steps, Validate: true} b := &BasicGraphBuilder{Steps: steps, Validate: true}

View File

@ -0,0 +1,99 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/dag"
)
// OrphanResourceCountTransformer is a GraphTransformer that adds orphans
// for an expanded count to the graph. The determination of this depends
// on the count argument given.
//
// Orphans are found by comparing the count to what is found in the state.
// This tranform assumes that if an element in the state is within the count
// bounds given, that it is not an orphan.
type OrphanResourceCountTransformer struct {
Concrete ConcreteResourceNodeFunc
Count int // Actual count of the resource
Addr *ResourceAddress // Addr of the resource to look for orphans
State *State // Full global state
}
func (t *OrphanResourceCountTransformer) Transform(g *Graph) error {
log.Printf("[TRACE] OrphanResourceCount: Starting...")
// Grab the module in the state just for this resource address
ms := t.State.ModuleByPath(normalizeModulePath(t.Addr.Path))
if ms == nil {
// If no state, there can't be orphans
return nil
}
// Go through the orphans and add them all to the state
for key, _ := range ms.Resources {
// Build the address
addr, err := parseResourceAddressInternal(key)
if err != nil {
return err
}
addr.Path = ms.Path[1:]
// Copy the address for comparison. If we aren't looking at
// the same resource, then just ignore it.
addrCopy := addr.Copy()
addrCopy.Index = -1
if !addrCopy.Equals(t.Addr) {
continue
}
log.Printf("[TRACE] OrphanResourceCount: Checking: %s", addr)
idx := addr.Index
// If we have zero and the index here is 0 or 1, then we
// change the index to a high number so that we treat it as
// an orphan.
if t.Count <= 0 && idx <= 0 {
idx = t.Count + 1
}
// If we have a count greater than 0 and we're at the zero index,
// we do a special case check to see if our state also has a
// -1 index value. If so, this is an orphan because our rules are
// that if both a -1 and 0 are in the state, the 0 is destroyed.
if t.Count > 0 && idx == -1 {
key := &ResourceStateKey{
Name: addr.Name,
Type: addr.Type,
Mode: addr.Mode,
Index: 0,
}
if _, ok := ms.Resources[key.String()]; ok {
// We have a -1 index, too. Make an arbitrarily high
// index so that we always mark this as an orphan.
log.Printf("[WARN] OrphanResourceCount: %q both -1 and 0 index found, orphaning -1", addr)
idx = t.Count + 1
}
}
// If the index is within the count bounds, it is not an orphan
if idx < t.Count {
continue
}
// Build the abstract node and the concrete one
abstract := &NodeAbstractResource{Addr: addr}
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
// Add it to the graph
g.Add(node)
}
return nil
}

View File

@ -0,0 +1,312 @@
package terraform
import (
"strings"
"testing"
)
func TestOrphanResourceCountTransformer(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 1,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_zero(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 0,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountZeroStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_oneNoIndex(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 1,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountOneNoIndexStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_oneIndex(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 1,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountOneIndexStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_zeroAndNone(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 1,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountZeroAndNoneStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
const testTransformOrphanResourceCountBasicStr = `
aws_instance.foo[2] (orphan)
`
const testTransformOrphanResourceCountZeroStr = `
aws_instance.foo (orphan)
aws_instance.foo[2] (orphan)
`
const testTransformOrphanResourceCountOneNoIndexStr = `
aws_instance.foo[2] (orphan)
`
const testTransformOrphanResourceCountOneIndexStr = `
aws_instance.foo[1] (orphan)
`
const testTransformOrphanResourceCountZeroAndNoneStr = `
aws_instance.foo (orphan)
`