From 2608c5f282d760cb090ec3ab666a3e783a96e6d3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Nov 2016 10:37:56 -0800 Subject: [PATCH] terraform: transform for adding orphan resources + tests --- .../transform-orphan-count-empty/main.tf | 1 + .../transform-orphan-count/main.tf | 1 + terraform/transform_orphan_resource.go | 74 ++++++ terraform/transform_orphan_resource_test.go | 246 ++++++++++++++++++ 4 files changed, 322 insertions(+) create mode 100644 terraform/test-fixtures/transform-orphan-count-empty/main.tf create mode 100644 terraform/test-fixtures/transform-orphan-count/main.tf create mode 100644 terraform/transform_orphan_resource.go create mode 100644 terraform/transform_orphan_resource_test.go diff --git a/terraform/test-fixtures/transform-orphan-count-empty/main.tf b/terraform/test-fixtures/transform-orphan-count-empty/main.tf new file mode 100644 index 000000000..e8045d6fc --- /dev/null +++ b/terraform/test-fixtures/transform-orphan-count-empty/main.tf @@ -0,0 +1 @@ +# Purposefully empty diff --git a/terraform/test-fixtures/transform-orphan-count/main.tf b/terraform/test-fixtures/transform-orphan-count/main.tf new file mode 100644 index 000000000..954d7a569 --- /dev/null +++ b/terraform/test-fixtures/transform-orphan-count/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "foo" { count = 3 } diff --git a/terraform/transform_orphan_resource.go b/terraform/transform_orphan_resource.go new file mode 100644 index 000000000..11721462e --- /dev/null +++ b/terraform/transform_orphan_resource.go @@ -0,0 +1,74 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// OrphanResourceTransformer is a GraphTransformer that adds resource +// orphans to the graph. A resource orphan is a resource that is +// represented in the state but not in the configuration. +// +// This only adds orphans that have no representation at all in the +// configuration. +type OrphanResourceTransformer struct { + Concrete ConcreteResourceNodeFunc + + // State is the global state. We require the global state to + // properly find module orphans at our path. + State *State + + // Module is the root module. We'll look up the proper configuration + // using the graph path. + Module *module.Tree +} + +func (t *OrphanResourceTransformer) Transform(g *Graph) error { + if t.State == nil { + // If the entire state is nil, there can't be any orphans + return nil + } + + // Go through the modules and for each module transform in order + // to add the orphan. + for _, ms := range t.State.Modules { + if err := t.transform(g, ms); err != nil { + return err + } + } + + return nil +} + +func (t *OrphanResourceTransformer) transform(g *Graph, ms *ModuleState) error { + // Get the configuration for this path. The configuration might be + // nil if the module was removed from the configuration. This is okay, + // this just means that every resource is an orphan. + var c *config.Config + if m := t.Module.Child(ms.Path[1:]); m != nil { + c = m.Config() + } + + // Go through the orphans and add them all to the state + for _, key := range ms.Orphans(c) { + // Build the abstract resource + addr, err := parseResourceAddressInternal(key) + if err != nil { + return err + } + addr.Path = ms.Path[1:] + + // 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_resource_test.go b/terraform/transform_orphan_resource_test.go new file mode 100644 index 000000000..ce29eecf1 --- /dev/null +++ b/terraform/transform_orphan_resource_test.go @@ -0,0 +1,246 @@ +package terraform + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestOrphanResourceTransformer(t *testing.T) { + mod := testModule(t, "transform-orphan-basic") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + + // The orphan + "aws_instance.db": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &OrphanResourceTransformer{ + Concrete: testOrphanResourceConcreteFunc, + State: state, Module: mod, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanResourceBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanResourceTransformer_countGood(t *testing.T) { + mod := testModule(t, "transform-orphan-count") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "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 := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &OrphanResourceTransformer{ + Concrete: testOrphanResourceConcreteFunc, + State: state, Module: mod, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanResourceCountStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanResourceTransformer_countBad(t *testing.T) { + mod := testModule(t, "transform-orphan-count-empty") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "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 := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &OrphanResourceTransformer{ + Concrete: testOrphanResourceConcreteFunc, + State: state, Module: mod, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanResourceCountBadStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestOrphanResourceTransformer_modules(t *testing.T) { + mod := testModule(t, "transform-orphan-modules") + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: RootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.foo": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + + &ModuleState{ + Path: []string{"root", "child"}, + Resources: map[string]*ResourceState{ + "aws_instance.web": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "foo", + }, + }, + }, + }, + }, + } + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &OrphanResourceTransformer{ + Concrete: testOrphanResourceConcreteFunc, + State: state, Module: mod, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanResourceModulesStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testTransformOrphanResourceBasicStr = ` +aws_instance.db (orphan) +aws_instance.web +` + +const testTransformOrphanResourceCountStr = ` +aws_instance.foo +` + +const testTransformOrphanResourceCountBadStr = ` +aws_instance.foo[0] (orphan) +aws_instance.foo[1] (orphan) +` + +const testTransformOrphanResourceModulesStr = ` +aws_instance.foo +module.child.aws_instance.web (orphan) +` + +func testOrphanResourceConcreteFunc(a *NodeAbstractResource) dag.Vertex { + return &testOrphanResourceConcrete{a} +} + +type testOrphanResourceConcrete struct { + *NodeAbstractResource +} + +func (n *testOrphanResourceConcrete) Name() string { + return fmt.Sprintf("%s (orphan)", n.NodeAbstractResource.Name()) +}