diff --git a/terraform/node_resource_plan.go b/terraform/node_resource_plan.go index 4bde0b17c2..82656f58e6 100644 --- a/terraform/node_resource_plan.go +++ b/terraform/node_resource_plan.go @@ -29,6 +29,11 @@ func (n *NodePlannableResource) EvalTree() EvalNode { // GraphNodeDynamicExpandable 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 count, err := n.Config.Count() if err != nil { @@ -39,25 +44,45 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { concreteResource := func(a *NodeAbstractResource) dag.Vertex { // Add the config and state since we don't do that via transforms a.Config = n.Config - a.ResourceState = n.ResourceState return &NodePlannableResourceInstance{ 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 - steps := make([]GraphTransformer, 0, 5) + steps := []GraphTransformer{ + // Expand the count. + &ResourceCountTransformer{ + Concrete: concreteResource, + Count: count, + Addr: n.ResourceAddr(), + }, - // Expand counts. - steps = append(steps, &ResourceCountTransformer{ - Concrete: concreteResource, - Count: count, - Addr: n.ResourceAddr(), - }) + // Add the count orphans + &OrphanResourceCountTransformer{ + Concrete: concreteResourceOrphan, + Count: count, + Addr: n.ResourceAddr(), + State: state, + }, - // Always end with the root being added - steps = append(steps, &RootTransformer{}) + // TODO: deposed + // TODO: targeting + + // Attach the state + &AttachStateTransformer{State: state}, + + // Make sure there is a single root + &RootTransformer{}, + } // Build the graph b := &BasicGraphBuilder{Steps: steps, Validate: true} diff --git a/terraform/transform_orphan_count.go b/terraform/transform_orphan_count.go new file mode 100644 index 0000000000..602532b855 --- /dev/null +++ b/terraform/transform_orphan_count.go @@ -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 +} diff --git a/terraform/transform_orphan_count_test.go b/terraform/transform_orphan_count_test.go new file mode 100644 index 0000000000..aececf0ddb --- /dev/null +++ b/terraform/transform_orphan_count_test.go @@ -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) +`