From bd5d97f9f582f6b9a7f0df6291287fcccc285516 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 20 Sep 2016 09:32:34 -0700 Subject: [PATCH] terraform: transform to attach resource configs --- terraform/context_apply_test.go | 61 ++++++++++++++- terraform/graph_builder_apply.go | 3 + terraform/node_resource_apply.go | 5 ++ terraform/transform_attach_config_resource.go | 74 +++++++++++++++++++ terraform/transform_destroy_edge.go | 64 ++++++++++++++++ terraform/transform_diff.go | 28 ------- 6 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 terraform/transform_attach_config_resource.go create mode 100644 terraform/transform_destroy_edge.go diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 0cee63051..841530aee 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -2789,7 +2789,7 @@ func TestContext2Apply_Provisioner_ConnInfo(t *testing.T) { } } -func TestContext2Apply_destroy(t *testing.T) { +func TestContext2Apply_destroyX(t *testing.T) { m := testModule(t, "apply-destroy") h := new(HookRecordApplyOrder) p := testProvider("aws") @@ -2849,6 +2849,65 @@ func TestContext2Apply_destroy(t *testing.T) { } } +func TestContext2Apply_destroyOrder(t *testing.T) { + m := testModule(t, "apply-destroy") + h := new(HookRecordApplyOrder) + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + // First plan and apply a create operation + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Next, plan and apply config-less to force a destroy with "apply" + h.Active = true + ctx = testContext2(t, &ContextOpts{ + State: state, + Module: module.NewEmptyTree(), + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err = ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Test that things were destroyed + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyDestroyStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } + + // Test that things were destroyed _in the right order_ + expected2 := []string{"aws_instance.bar", "aws_instance.foo"} + actual2 := h.IDs + if !reflect.DeepEqual(actual2, expected2) { + t.Fatalf("expected: %#v\n\ngot:%#v", expected2, actual2) + } +} + // https://github.com/hashicorp/terraform/issues/2767 func TestContext2Apply_destroyModulePrefix(t *testing.T) { m := testModule(t, "apply-destroy-module-resource-prefix") diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go index 4889bda55..d54a70948 100644 --- a/terraform/graph_builder_apply.go +++ b/terraform/graph_builder_apply.go @@ -54,6 +54,9 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { State: b.State, }, + // Attach the configuration to any resources + &AttachResourceConfigTransformer{Module: b.Module}, + // Create orphan output nodes &OrphanOutputTransformer{Module: b.Module, State: b.State}, diff --git a/terraform/node_resource_apply.go b/terraform/node_resource_apply.go index 894b651f1..7b8d7b0ed 100644 --- a/terraform/node_resource_apply.go +++ b/terraform/node_resource_apply.go @@ -91,6 +91,11 @@ func (n *NodeApplyableResource) ResourceAddr() *ResourceAddress { return n.Addr } +// GraphNodeAttachResource +func (n *NodeApplyableResource) AttachResourceConfig(c *config.Resource) { + n.Config = c +} + // GraphNodeAttachResourceState func (n *NodeApplyableResource) AttachResourceState(s *ResourceState) { n.ResourceState = s diff --git a/terraform/transform_attach_config_resource.go b/terraform/transform_attach_config_resource.go new file mode 100644 index 000000000..449d1ac16 --- /dev/null +++ b/terraform/transform_attach_config_resource.go @@ -0,0 +1,74 @@ +package terraform + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// GraphNodeAttachResourceConfig is an interface that must be implemented by nodes +// that want resource configurations attached. +type GraphNodeAttachResourceConfig interface { + // ResourceAddr is the address to the resource + ResourceAddr() *ResourceAddress + + // Sets the configuration + AttachResourceConfig(*config.Resource) +} + +// AttachResourceConfigTransformer goes through the graph and attaches +// resource configuration structures to nodes that implement the interfaces +// above. +// +// The attached configuration structures are directly from the configuration. +// If they're going to be modified, a copy should be made. +type AttachResourceConfigTransformer struct { + Module *module.Tree // Module is the root module for the config +} + +func (t *AttachResourceConfigTransformer) Transform(g *Graph) error { + // Go through and find GraphNodeAttachResource + for _, v := range g.Vertices() { + // Only care about GraphNodeAttachResource implementations + arn, ok := v.(GraphNodeAttachResourceConfig) + if !ok { + continue + } + + // Determine what we're looking for + addr := arn.ResourceAddr() + log.Printf("[TRACE] Attach resource request: %s", addr) + + // Get the configuration. + path := normalizeModulePath(addr.Path) + path = path[1:] + tree := t.Module.Child(path) + if tree == nil { + continue + } + + // Go through the resource configs to find the matching config + for _, r := range tree.Config().Resources { + // Get a resource address so we can compare + a, err := parseResourceAddressConfig(r) + if err != nil { + panic(fmt.Sprintf( + "Error parsing config address, this is a bug: %#v", r)) + } + a.Path = addr.Path + + // If this is not the same resource, then continue + if !a.Equals(addr) { + continue + } + + log.Printf("[TRACE] Attaching resource config: %#v", r) + arn.AttachResourceConfig(r) + break + } + } + + return nil +} diff --git a/terraform/transform_destroy_edge.go b/terraform/transform_destroy_edge.go new file mode 100644 index 000000000..d1e9d194e --- /dev/null +++ b/terraform/transform_destroy_edge.go @@ -0,0 +1,64 @@ +package terraform + +// GraphNodeDestroyer must be implemented by nodes that destroy resources. +type GraphNodeDestroyer interface { + // ResourceAddr is the address of the resource that is being + // destroyed by this node. If this returns nil, then this node + // is not destroying anything. + DestroyAddr() *ResourceAddress +} + +// DestroyEdgeTransformer is a GraphTransformer that creates the proper +// references for destroy resources. Destroy resources are more complex +// in that they must be depend on the destruction of resources that +// in turn depend on the CREATION of the node being destroy. +// +// That is complicated. Visually: +// +// B_d -> A_d -> A -> B +// +// Notice that A destroy depends on B destroy, while B create depends on +// A create. They're inverted. This must be done for example because often +// dependent resources will block parent resources from deleting. Concrete +// example: VPC with subnets, the VPC can't be deleted while there are +// still subnets. +type DestroyEdgeTransformer struct{} + +func (t *DestroyEdgeTransformer) Transform(g *Graph) error { + // Build a map of what is being destroyed (by address string) to + // the list of destroyers. In general there will only be one destroyer + // but to make it more robust we support multiple. + destroyers := make(map[string][]GraphNodeDestroyer) + for _, v := range g.Vertices() { + dn, ok := v.(GraphNodeDestroyer) + if !ok { + continue + } + + addr := dn.DestroyAddr() + if addr == nil { + continue + } + + key := addr.String() + destroyers[key] = append(destroyers[key], dn) + } + + // If we aren't destroying anything, there will be no edges to make + // so just exit early and avoid future work. + if len(destroyers) == 0 { + return nil + } + + // Go through the all destroyers and find what they're destroying. + // Use this to find the dependencies, look up if any of them are being + // destroyed, and to make the proper edge. + for _, ds := range destroyers { + for _, d := range ds { + // TODO + println(d) + } + } + + return nil +} diff --git a/terraform/transform_diff.go b/terraform/transform_diff.go index 21fd0bbc6..81bebab9a 100644 --- a/terraform/transform_diff.go +++ b/terraform/transform_diff.go @@ -69,34 +69,6 @@ func (t *DiffTransformer) Transform(g *Graph) error { } } - // NOTE: Lots of room for performance optimizations below. For - // resource-heavy diffs this part alone is probably pretty slow. - - // Annotate all nodes with their config and state - for _, n := range nodes { - // Grab the configuration at this path. - if t := t.Module.Child(n.Addr.Path); t != nil { - for _, r := range t.Config().Resources { - // Get a resource address so we can compare - addr, err := parseResourceAddressConfig(r) - if err != nil { - panic(fmt.Sprintf( - "Error parsing config address, this is a bug: %#v", r)) - } - addr.Path = n.Addr.Path - - // If this is not the same resource, then continue - if !addr.Equals(n.Addr) { - continue - } - - // Same resource! Mark it and exit - n.Config = r - break - } - } - } - // Add all the nodes to the graph for _, n := range nodes { g.Add(n)