terraform: OrphanResourceCountTransformer for orphaning extranneous
instances
This commit is contained in:
parent
97b7915b8f
commit
091264e4ba
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The concrete resource factory we'll use for oprhans
|
||||||
|
concreteResourceOrphan := func(a *NodeAbstractResource) dag.Vertex {
|
||||||
|
return &NodePlannableResourceOrphan{
|
||||||
|
NodeAbstractResource: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start creating the steps
|
// Start creating the steps
|
||||||
steps := make([]GraphTransformer, 0, 5)
|
steps := []GraphTransformer{
|
||||||
|
// Expand the count.
|
||||||
|
&ResourceCountTransformer{
|
||||||
|
Concrete: concreteResource,
|
||||||
|
Count: count,
|
||||||
|
Addr: n.ResourceAddr(),
|
||||||
|
},
|
||||||
|
|
||||||
// Expand counts.
|
// Add the count orphans
|
||||||
steps = append(steps, &ResourceCountTransformer{
|
&OrphanResourceCountTransformer{
|
||||||
Concrete: concreteResource,
|
Concrete: concreteResourceOrphan,
|
||||||
Count: count,
|
Count: count,
|
||||||
Addr: n.ResourceAddr(),
|
Addr: n.ResourceAddr(),
|
||||||
})
|
State: state,
|
||||||
|
},
|
||||||
|
|
||||||
// Always end with the root being added
|
// TODO: deposed
|
||||||
steps = append(steps, &RootTransformer{})
|
// 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}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
`
|
Loading…
Reference in New Issue