diff --git a/terraform/context.go b/terraform/context.go index abf8c22188..3cef509457 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -488,28 +488,79 @@ func (c *Context) Plan() (*Plan, error) { c.diffLock.Unlock() // Used throughout below + X_newApply := experiment.Enabled(experiment.X_newApply) X_newDestroy := experiment.Enabled(experiment.X_newDestroy) + newGraphEnabled := (c.destroy && X_newDestroy) || (!c.destroy && X_newApply) - // Build the graph. We have a branch here since for the pure-destroy - // plan (c.destroy) we use a much simpler graph builder that simply - // walks the state and reverses edges. - var graph *Graph - var err error - if c.destroy && X_newDestroy { - graph, err = (&DestroyPlanGraphBuilder{ - Module: c.module, - State: c.state, - Targets: c.targets, - }).Build(RootModulePath) - } else { - graph, err = c.Graph(&ContextGraphOpts{Validate: true}) + // Build the original graph. This is before the new graph builders + // coming in 0.8. We do this for shadow graphing. + oldGraph, err := c.Graph(&ContextGraphOpts{Validate: true}) + if err != nil && newGraphEnabled { + // If we had an error graphing but we're using the new graph, + // just set it to nil and let it go. There are some features that + // may work with the new graph that don't with the old. + oldGraph = nil + err = nil } if err != nil { return nil, err } + // Build the new graph. We do this no matter wht so we can shadow it. + var newGraph *Graph + err = nil + if c.destroy { + newGraph, err = (&DestroyPlanGraphBuilder{ + Module: c.module, + State: c.state, + Targets: c.targets, + }).Build(RootModulePath) + } else { + newGraph, err = (&PlanGraphBuilder{ + Module: c.module, + State: c.state, + Providers: c.components.ResourceProviders(), + Targets: c.targets, + }).Build(RootModulePath) + } + if err != nil && !newGraphEnabled { + // If we had an error graphing but we're not using this graph, just + // set it to nil and record it as a shadow error. + c.shadowErr = multierror.Append(c.shadowErr, fmt.Errorf( + "Error building new graph: %s", err)) + + newGraph = nil + err = nil + } + if err != nil { + return nil, err + } + + // Determine what is the real and what is the shadow. The logic here + // is straightforward though the if statements are not: + // + // * If the new graph, shadow with experiment in both because the + // experiment has less nodes so the original can't shadow. + // * If not the new graph, shadow with the experiment + // + real := oldGraph + shadow := newGraph + if newGraphEnabled { + log.Printf("[WARN] terraform: real graph is experiment, shadow is experiment") + real = shadow + } else { + log.Printf("[WARN] terraform: real graph is original, shadow is experiment") + } + + // Special case here: if we're using destroy don't shadow it because + // the new destroy graph behaves a bit differently on purpose by not + // setting the module destroy flag. + if c.destroy && !newGraphEnabled { + shadow = nil + } + // Do the walk - walker, err := c.walk(graph, graph, operation) + walker, err := c.walk(real, shadow, operation) if err != nil { return nil, err } @@ -528,7 +579,7 @@ func (c *Context) Plan() (*Plan, error) { // We don't do the reverification during the new destroy plan because // it will use a different apply process. - if !(c.destroy && X_newDestroy) { + if !newGraphEnabled { // Now that we have a diff, we can build the exact graph that Apply will use // and catch any possible cycles during the Plan phase. if _, err := c.Graph(&ContextGraphOpts{Validate: true}); err != nil { @@ -814,7 +865,12 @@ func (c *Context) walk( // // This must be done BEFORE appending shadowWalkErr since the // shadowWalkErr may include expected errors. - if c.shadowErr != nil && contextFailOnShadowError { + // + // We only do this if we don't have a real error. In the case of + // a real error, we can't guarantee what nodes were and weren't + // traversed in parallel scenarios so we can't guarantee no + // shadow errors. + if c.shadowErr != nil && contextFailOnShadowError && realErr == nil { panic(multierror.Prefix(c.shadowErr, "shadow graph:")) } diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index e568008f0f..751e3d9bb5 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -3488,6 +3488,8 @@ func TestContext2Apply_destroyOrder(t *testing.T) { t.Fatalf("err: %s", err) } + t.Logf("State 1: %s", state) + // Next, plan and apply config-less to force a destroy with "apply" h.Active = true ctx = testContext2(t, &ContextOpts{ @@ -3697,8 +3699,10 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { }) // First plan and apply a create operation - if _, err := ctx.Plan(); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("plan err: %s", err) + } else { + t.Logf("Step 1 plan: %s", p) } state, err = ctx.Apply() @@ -3732,6 +3736,8 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { t.Fatalf("destroy plan err: %s", err) } + t.Logf("Step 2 plan: %s", plan) + var buf bytes.Buffer if err := WritePlan(plan, &buf); err != nil { t.Fatalf("plan write err: %s", err) @@ -3755,6 +3761,8 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { if err != nil { t.Fatalf("destroy apply err: %s", err) } + + t.Logf("Step 2 state: %s", state) } //Test that things were destroyed @@ -3765,7 +3773,7 @@ module.child: `) if actual != expected { - t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual) + t.Fatalf("expected:\n\n%s\n\nactual:\n\n%s", expected, actual) } } @@ -5120,8 +5128,10 @@ func TestContext2Apply_targetedModuleDep(t *testing.T) { Targets: []string{"aws_instance.foo"}, }) - if _, err := ctx.Plan(); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) + } else { + t.Logf("Diff: %s", p) } state, err := ctx.Apply() diff --git a/terraform/context_plan_test.go b/terraform/context_plan_test.go index c791b8b5c8..f2b4736235 100644 --- a/terraform/context_plan_test.go +++ b/terraform/context_plan_test.go @@ -11,7 +11,7 @@ import ( "testing" ) -func TestContext2Plan(t *testing.T) { +func TestContext2Plan_basic(t *testing.T) { m := testModule(t, "plan-good") p := testProvider("aws") p.DiffFn = testDiffFn @@ -626,7 +626,7 @@ func TestContext2Plan_moduleVar(t *testing.T) { } } -func TestContext2Plan_moduleVarWrongType(t *testing.T) { +func TestContext2Plan_moduleVarWrongTypeBasic(t *testing.T) { m := testModule(t, "plan-module-wrong-var-type") p := testProvider("aws") p.DiffFn = testDiffFn diff --git a/terraform/diff.go b/terraform/diff.go index b3a8fe5de0..a474efad1c 100644 --- a/terraform/diff.go +++ b/terraform/diff.go @@ -32,6 +32,30 @@ type Diff struct { Modules []*ModuleDiff } +// Prune cleans out unused structures in the diff without affecting +// the behavior of the diff at all. +// +// This is not safe to call concurrently. This is safe to call on a +// nil Diff. +func (d *Diff) Prune() { + if d == nil { + return + } + + // Prune all empty modules + newModules := make([]*ModuleDiff, 0, len(d.Modules)) + for _, m := range d.Modules { + // If the module isn't empty, we keep it + if !m.Empty() { + newModules = append(newModules, m) + } + } + if len(newModules) == 0 { + newModules = nil + } + d.Modules = newModules +} + // AddModule adds the module with the given path to the diff. // // This should be the preferred method to add module diffs since it @@ -212,6 +236,10 @@ func (d *ModuleDiff) ChangeType() DiffChangeType { // Empty returns true if the diff has no changes within this module. func (d *ModuleDiff) Empty() bool { + if d.Destroy { + return false + } + if len(d.Resources) == 0 { return true } diff --git a/terraform/diff_test.go b/terraform/diff_test.go index fca0fbcbe3..655b165f96 100644 --- a/terraform/diff_test.go +++ b/terraform/diff_test.go @@ -103,6 +103,53 @@ func TestDiffEqual(t *testing.T) { } } +func TestDiffPrune(t *testing.T) { + cases := map[string]struct { + D1, D2 *Diff + }{ + "nil": { + nil, + nil, + }, + + "empty": { + new(Diff), + new(Diff), + }, + + "empty module": { + &Diff{ + Modules: []*ModuleDiff{ + &ModuleDiff{Path: []string{"root", "foo"}}, + }, + }, + &Diff{}, + }, + + "destroy module": { + &Diff{ + Modules: []*ModuleDiff{ + &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, + }, + }, + &Diff{ + Modules: []*ModuleDiff{ + &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + tc.D1.Prune() + if !tc.D1.Equal(tc.D2) { + t.Fatalf("bad:\n\n%#v\n\n%#v", tc.D1, tc.D2) + } + }) + } +} + func TestModuleDiff_ChangeType(t *testing.T) { cases := []struct { Diff *ModuleDiff diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index 4172b71bed..161bd1038b 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -129,7 +129,7 @@ func (b *BuiltinGraphBuilder) Build(path []string) (*Graph, error) { func (b *BuiltinGraphBuilder) Steps(path []string) []GraphTransformer { steps := []GraphTransformer{ // Create all our resources from the configuration and state - &ConfigTransformer{Module: b.Root}, + &ConfigTransformerOld{Module: b.Root}, &OrphanTransformer{ State: b.State, Module: b.Root, diff --git a/terraform/graph_builder_import.go b/terraform/graph_builder_import.go index 6d87d487dc..8bd11fb5c2 100644 --- a/terraform/graph_builder_import.go +++ b/terraform/graph_builder_import.go @@ -38,7 +38,7 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer { steps := []GraphTransformer{ // Create all our resources from the configuration and state - &ConfigTransformer{Module: mod}, + &ConfigTransformerOld{Module: mod}, // Add the import steps &ImportStateTransformer{Targets: b.ImportTargets}, diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go new file mode 100644 index 0000000000..43e709f7fb --- /dev/null +++ b/terraform/graph_builder_plan.go @@ -0,0 +1,125 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// PlanGraphBuilder implements GraphBuilder and is responsible for building +// a graph for planning (creating a Terraform Diff). +// +// The primary difference between this graph and others: +// +// * Based on the config since it represents the target state +// +// * Ignores lifecycle options since no lifecycle events occur here. This +// simplifies the graph significantly since complex transforms such as +// create-before-destroy can be completely ignored. +// +type PlanGraphBuilder struct { + // Module is the root module for the graph to build. + Module *module.Tree + + // State is the current state + State *State + + // Providers is the list of providers supported. + Providers []string + + // Targets are resources to target + Targets []string + + // DisableReduce, if true, will not reduce the graph. Great for testing. + DisableReduce bool +} + +// See GraphBuilder +func (b *PlanGraphBuilder) Build(path []string) (*Graph, error) { + return (&BasicGraphBuilder{ + Steps: b.Steps(), + Validate: true, + Name: "plan", + }).Build(path) +} + +// See GraphBuilder +func (b *PlanGraphBuilder) Steps() []GraphTransformer { + // Custom factory for creating providers. + providerFactory := func(name string, path []string) GraphNodeProvider { + return &NodeApplyableProvider{ + NameValue: name, + PathValue: path, + } + } + + concreteResource := func(a *NodeAbstractResource) dag.Vertex { + return &NodePlannableResource{ + NodeAbstractResource: a, + } + } + + concreteResourceOrphan := func(a *NodeAbstractResource) dag.Vertex { + return &NodePlannableResourceOrphan{ + NodeAbstractResource: a, + } + } + + steps := []GraphTransformer{ + // Creates all the resources represented in the config + &ConfigTransformer{ + Concrete: concreteResource, + Module: b.Module, + }, + + // Add the outputs + &OutputTransformer{Module: b.Module}, + + // Add orphan resources + &OrphanResourceTransformer{ + Concrete: concreteResourceOrphan, + State: b.State, + Module: b.Module, + }, + + // Attach the configuration to any resources + &AttachResourceConfigTransformer{Module: b.Module}, + + // Attach the state + &AttachStateTransformer{State: b.State}, + + // Connect so that the references are ready for targeting. We'll + // have to connect again later for providers and so on. + &ReferenceTransformer{}, + + // Target + &TargetsTransformer{Targets: b.Targets}, + + // Create all the providers + &MissingProviderTransformer{Providers: b.Providers, Factory: providerFactory}, + &ProviderTransformer{}, + &DisableProviderTransformer{}, + &ParentProviderTransformer{}, + &AttachProviderConfigTransformer{Module: b.Module}, + + // Add root variables + &RootVariableTransformer{Module: b.Module}, + + // Add module variables + &ModuleVariableTransformer{Module: b.Module}, + + // Connect references again to connect the providers, module variables, + // etc. This is idempotent. + &ReferenceTransformer{}, + + // Single root + &RootTransformer{}, + } + + if !b.DisableReduce { + // Perform the transitive reduction to make our graph a bit + // more sane if possible (it usually is possible). + steps = append(steps, &TransitiveReductionTransformer{}) + } + + return steps +} diff --git a/terraform/graph_builder_plan_test.go b/terraform/graph_builder_plan_test.go new file mode 100644 index 0000000000..235fea745b --- /dev/null +++ b/terraform/graph_builder_plan_test.go @@ -0,0 +1,52 @@ +package terraform + +import ( + "reflect" + "strings" + "testing" +) + +func TestPlanGraphBuilder_impl(t *testing.T) { + var _ GraphBuilder = new(PlanGraphBuilder) +} + +func TestPlanGraphBuilder(t *testing.T) { + b := &PlanGraphBuilder{ + Module: testModule(t, "graph-builder-plan-basic"), + Providers: []string{"aws", "openstack"}, + DisableReduce: true, + } + + g, err := b.Build(RootModulePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(g.Path, RootModulePath) { + t.Fatalf("bad: %#v", g.Path) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testPlanGraphBuilderStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +const testPlanGraphBuilderStr = ` +aws_instance.web + aws_security_group.firewall + provider.aws + var.foo +aws_load_balancer.weblb + aws_instance.web + provider.aws +aws_security_group.firewall + provider.aws +openstack_floating_ip.random + provider.openstack +provider.aws + openstack_floating_ip.random +provider.openstack +var.foo +` diff --git a/terraform/graph_config_node_module_test.go b/terraform/graph_config_node_module_test.go index 1b5430ddfd..6ba015a45c 100644 --- a/terraform/graph_config_node_module_test.go +++ b/terraform/graph_config_node_module_test.go @@ -26,7 +26,7 @@ func TestGraphNodeConfigModuleExpand(t *testing.T) { g, err := node.Expand(&BasicGraphBuilder{ Steps: []GraphTransformer{ - &ConfigTransformer{Module: mod}, + &ConfigTransformerOld{Module: mod}, }, }) if err != nil { @@ -51,7 +51,7 @@ func TestGraphNodeConfigModuleExpandFlatten(t *testing.T) { g, err := node.Expand(&BasicGraphBuilder{ Steps: []GraphTransformer{ - &ConfigTransformer{Module: mod}, + &ConfigTransformerOld{Module: mod}, }, }) if err != nil { diff --git a/terraform/graph_config_node_resource.go b/terraform/graph_config_node_resource.go index e3decb4525..db4e6626ce 100644 --- a/terraform/graph_config_node_resource.go +++ b/terraform/graph_config_node_resource.go @@ -156,7 +156,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) steps := make([]GraphTransformer, 0, 5) // Expand counts. - steps = append(steps, &ResourceCountTransformer{ + steps = append(steps, &ResourceCountTransformerOld{ Resource: n.Resource, Destroy: n.Destroy, Targets: n.Targets, diff --git a/terraform/node_output.go b/terraform/node_output.go index c10c6e4f85..3e60016184 100644 --- a/terraform/node_output.go +++ b/terraform/node_output.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "strings" "github.com/hashicorp/terraform/config" ) @@ -38,7 +39,12 @@ func (n *NodeApplyableOutput) References() []string { var result []string result = append(result, ReferencesFromConfig(n.Config.RawConfig)...) for _, v := range result { - result = append(result, v+".destroy") + split := strings.Split(v, "/") + for i, s := range split { + split[i] = s + ".destroy" + } + + result = append(result, strings.Join(split, "/")) } return result diff --git a/terraform/node_resource_abstract.go b/terraform/node_resource_abstract.go index 9ba303a6ed..f502547d7f 100644 --- a/terraform/node_resource_abstract.go +++ b/terraform/node_resource_abstract.go @@ -1,6 +1,8 @@ package terraform import ( + "fmt" + "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/dag" ) @@ -43,11 +45,43 @@ func (n *NodeAbstractResource) Path() []string { // GraphNodeReferenceable func (n *NodeAbstractResource) ReferenceableName() []string { - if n.Config == nil { + // We always are referenceable as "type.name" as long as + // we have a config or address. Determine what that value is. + var id string + if n.Config != nil { + id = n.Config.Id() + } else if n.Addr != nil { + addrCopy := n.Addr.Copy() + addrCopy.Index = -1 + id = addrCopy.String() + } else { + // No way to determine our type.name, just return return nil } - return []string{n.Config.Id()} + var result []string + + // Always include our own ID. This is primarily for backwards + // compatibility with states that didn't yet support the more + // specific dep string. + result = append(result, id) + + // We represent all multi-access + result = append(result, fmt.Sprintf("%s.*", id)) + + // We represent either a specific number, or all numbers + suffix := "N" + if n.Addr != nil { + idx := n.Addr.Index + if idx == -1 { + idx = 0 + } + + suffix = fmt.Sprintf("%d", idx) + } + result = append(result, fmt.Sprintf("%s.%s", id, suffix)) + + return result } // GraphNodeReferencer diff --git a/terraform/node_resource_plan.go b/terraform/node_resource_plan.go new file mode 100644 index 0000000000..c9e53e02c6 --- /dev/null +++ b/terraform/node_resource_plan.go @@ -0,0 +1,105 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// NodePlannableResource represents a resource that is "plannable": +// it is ready to be planned in order to create a diff. +type NodePlannableResource struct { + *NodeAbstractResource + + // Set by GraphNodeTargetable and used during DynamicExpand to + // forward targets downwards. + targets []ResourceAddress +} + +// GraphNodeTargetable +func (n *NodePlannableResource) SetTargets(targets []ResourceAddress) { + n.targets = targets +} + +// GraphNodeEvalable +func (n *NodePlannableResource) EvalTree() EvalNode { + return &EvalSequence{ + Nodes: []EvalNode{ + // The EvalTree for a plannable resource primarily involves + // interpolating the count since it can contain variables + // we only just received access to. + // + // With the interpolated count, we can then DynamicExpand + // into the proper number of instances. + &EvalInterpolate{Config: n.Config.RawCount}, + + &EvalCountFixZeroOneBoundary{Resource: n.Config}, + }, + } +} + +// 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 { + return nil, err + } + + // The concrete resource factory we'll use + concreteResource := func(a *NodeAbstractResource) dag.Vertex { + // Add the config and state since we don't do that via transforms + a.Config = n.Config + + return &NodePlannableResourceInstance{ + NodeAbstractResource: a, + } + } + + // The concrete resource factory we'll use for oprhans + concreteResourceOrphan := func(a *NodeAbstractResource) dag.Vertex { + // Add the config and state since we don't do that via transforms + a.Config = n.Config + + return &NodePlannableResourceOrphan{ + NodeAbstractResource: a, + } + } + + // Start creating the steps + steps := []GraphTransformer{ + // Expand the count. + &ResourceCountTransformer{ + Concrete: concreteResource, + Count: count, + Addr: n.ResourceAddr(), + }, + + // Add the count orphans + &OrphanResourceCountTransformer{ + Concrete: concreteResourceOrphan, + Count: count, + Addr: n.ResourceAddr(), + State: state, + }, + + // Attach the state + &AttachStateTransformer{State: state}, + + // Targeting + &TargetsTransformer{ParsedTargets: n.targets}, + + // Connect references so ordering is correct + &ReferenceTransformer{}, + + // Make sure there is a single root + &RootTransformer{}, + } + + // Build the graph + b := &BasicGraphBuilder{Steps: steps, Validate: true} + return b.Build(ctx.Path()) +} diff --git a/terraform/node_resource_plan_instance.go b/terraform/node_resource_plan_instance.go new file mode 100644 index 0000000000..9dafd22bad --- /dev/null +++ b/terraform/node_resource_plan_instance.go @@ -0,0 +1,197 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// NodePlannableResourceInstance represents a _single_ resource +// instance that is plannable. This means this represents a single +// count index, for example. +type NodePlannableResourceInstance struct { + *NodeAbstractResource +} + +// GraphNodeEvalable +func (n *NodePlannableResourceInstance) EvalTree() EvalNode { + addr := n.NodeAbstractResource.Addr + + // stateId is the ID to put into the state + stateId := addr.stateId() + if addr.Index > -1 { + stateId = fmt.Sprintf("%s.%d", stateId, addr.Index) + } + + // Build the instance info. More of this will be populated during eval + info := &InstanceInfo{ + Id: stateId, + Type: addr.Type, + ModulePath: normalizeModulePath(addr.Path), + } + + // Build the resource for eval + resource := &Resource{ + Name: addr.Name, + Type: addr.Type, + CountIndex: addr.Index, + } + if resource.CountIndex < 0 { + resource.CountIndex = 0 + } + + // Determine the dependencies for the state. We use some older + // code for this that we've used for a long time. + var stateDeps []string + { + oldN := &graphNodeExpandedResource{Resource: n.Config} + stateDeps = oldN.StateDependencies() + } + + // Eval info is different depending on what kind of resource this is + switch n.Config.Mode { + case config.ManagedResourceMode: + return n.evalTreeManagedResource( + stateId, info, resource, stateDeps, + ) + case config.DataResourceMode: + return n.evalTreeDataResource( + stateId, info, resource, stateDeps) + default: + panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode)) + } +} + +func (n *NodePlannableResourceInstance) evalTreeDataResource( + stateId string, info *InstanceInfo, + resource *Resource, stateDeps []string) EvalNode { + var provider ResourceProvider + var config *ResourceConfig + var diff *InstanceDiff + var state *InstanceState + + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalReadState{ + Name: stateId, + Output: &state, + }, + + // We need to re-interpolate the config here because some + // of the attributes may have become computed during + // earlier planning, due to other resources having + // "requires new resource" diffs. + &EvalInterpolate{ + Config: n.Config.RawConfig.Copy(), + Resource: resource, + Output: &config, + }, + + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + computed := config.ComputedKeys != nil && len(config.ComputedKeys) > 0 + + // If the configuration is complete and we + // already have a state then we don't need to + // do any further work during apply, because we + // already populated the state during refresh. + if !computed && state != nil { + return true, EvalEarlyExitError{} + } + + return true, nil + }, + Then: EvalNoop{}, + }, + + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + + &EvalReadDataDiff{ + Info: info, + Config: &config, + Provider: &provider, + Output: &diff, + OutputState: &state, + }, + + &EvalWriteState{ + Name: stateId, + ResourceType: n.Config.Type, + Provider: n.Config.Provider, + Dependencies: stateDeps, + State: &state, + }, + + &EvalWriteDiff{ + Name: stateId, + Diff: &diff, + }, + }, + } +} + +func (n *NodePlannableResourceInstance) evalTreeManagedResource( + stateId string, info *InstanceInfo, + resource *Resource, stateDeps []string) EvalNode { + // Declare a bunch of variables that are used for state during + // evaluation. Most of this are written to by-address below. + var provider ResourceProvider + var diff *InstanceDiff + var state *InstanceState + var resourceConfig *ResourceConfig + + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{ + Config: n.Config.RawConfig.Copy(), + Resource: resource, + Output: &resourceConfig, + }, + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + // Re-run validation to catch any errors we missed, e.g. type + // mismatches on computed values. + &EvalValidateResource{ + Provider: &provider, + Config: &resourceConfig, + ResourceName: n.Config.Name, + ResourceType: n.Config.Type, + ResourceMode: n.Config.Mode, + IgnoreWarnings: true, + }, + &EvalReadState{ + Name: stateId, + Output: &state, + }, + &EvalDiff{ + Info: info, + Config: &resourceConfig, + Resource: n.Config, + Provider: &provider, + State: &state, + OutputDiff: &diff, + OutputState: &state, + }, + &EvalCheckPreventDestroy{ + Resource: n.Config, + Diff: &diff, + }, + &EvalWriteState{ + Name: stateId, + ResourceType: n.Config.Type, + Provider: n.Config.Provider, + Dependencies: stateDeps, + State: &state, + }, + &EvalWriteDiff{ + Name: stateId, + Diff: &diff, + }, + }, + } +} diff --git a/terraform/node_resource_plan_orphan.go b/terraform/node_resource_plan_orphan.go new file mode 100644 index 0000000000..90641ca0f1 --- /dev/null +++ b/terraform/node_resource_plan_orphan.go @@ -0,0 +1,61 @@ +package terraform + +import ( + "fmt" +) + +// NodePlannableResourceOrphan represents a resource that is "applyable": +// it is ready to be applied and is represented by a diff. +type NodePlannableResourceOrphan struct { + *NodeAbstractResource +} + +func (n *NodePlannableResourceOrphan) Name() string { + return n.NodeAbstractResource.Name() + " (orphan)" +} + +// GraphNodeEvalable +func (n *NodePlannableResourceOrphan) EvalTree() EvalNode { + addr := n.NodeAbstractResource.Addr + + // stateId is the ID to put into the state + stateId := addr.stateId() + if addr.Index > -1 { + stateId = fmt.Sprintf("%s.%d", stateId, addr.Index) + } + + // Build the instance info. More of this will be populated during eval + info := &InstanceInfo{ + Id: stateId, + Type: addr.Type, + ModulePath: normalizeModulePath(addr.Path), + } + + // Declare a bunch of variables that are used for state during + // evaluation. Most of this are written to by-address below. + var diff *InstanceDiff + var state *InstanceState + + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalReadState{ + Name: stateId, + Output: &state, + }, + &EvalDiffDestroy{ + Info: info, + State: &state, + Output: &diff, + }, + &EvalCheckPreventDestroy{ + Resource: n.Config, + ResourceId: stateId, + Diff: &diff, + }, + &EvalWriteDiff{ + Name: stateId, + Diff: &diff, + }, + }, + } +} diff --git a/terraform/resource_address.go b/terraform/resource_address.go index 06e943f9e3..2dc2058f55 100644 --- a/terraform/resource_address.go +++ b/terraform/resource_address.go @@ -29,6 +29,10 @@ type ResourceAddress struct { // Copy returns a copy of this ResourceAddress func (r *ResourceAddress) Copy() *ResourceAddress { + if r == nil { + return nil + } + n := &ResourceAddress{ Path: make([]string, 0, len(r.Path)), Index: r.Index, diff --git a/terraform/shadow_resource_provider.go b/terraform/shadow_resource_provider.go index 72bb49cf53..6084db615d 100644 --- a/terraform/shadow_resource_provider.go +++ b/terraform/shadow_resource_provider.go @@ -475,7 +475,7 @@ func (p *shadowResourceProviderShadow) ValidateResource(t string, c *ResourceCon p.ErrorLock.Lock() defer p.ErrorLock.Unlock() p.Error = multierror.Append(p.Error, fmt.Errorf( - "Unknown 'ValidateResource' shadow value: %#v", raw)) + "Unknown 'ValidateResource' shadow value for %q: %#v", key, raw)) return nil, nil } @@ -567,7 +567,7 @@ func (p *shadowResourceProviderShadow) Diff( p.ErrorLock.Lock() defer p.ErrorLock.Unlock() p.Error = multierror.Append(p.Error, fmt.Errorf( - "Unknown 'diff' shadow value: %#v", raw)) + "Unknown 'diff' shadow value for %q: %#v", key, raw)) return nil, nil } diff --git a/terraform/test-fixtures/apply-unknown-interpolate/child/main.tf b/terraform/test-fixtures/apply-unknown-interpolate/child/main.tf index 6a2f859307..1caedabc45 100644 --- a/terraform/test-fixtures/apply-unknown-interpolate/child/main.tf +++ b/terraform/test-fixtures/apply-unknown-interpolate/child/main.tf @@ -1 +1,5 @@ variable "value" {} + +resource "aws_instance" "bar" { + foo = "${var.value}" +} diff --git a/terraform/test-fixtures/graph-builder-plan-basic/main.tf b/terraform/test-fixtures/graph-builder-plan-basic/main.tf new file mode 100644 index 0000000000..a40802cc98 --- /dev/null +++ b/terraform/test-fixtures/graph-builder-plan-basic/main.tf @@ -0,0 +1,24 @@ +variable "foo" { + default = "bar" + description = "bar" +} + +provider "aws" { + foo = "${openstack_floating_ip.random.value}" +} + +resource "openstack_floating_ip" "random" {} + +resource "aws_security_group" "firewall" {} + +resource "aws_instance" "web" { + ami = "${var.foo}" + security_groups = [ + "foo", + "${aws_security_group.firewall.foo}" + ] +} + +resource "aws_load_balancer" "weblb" { + members = "${aws_instance.web.id_list}" +} diff --git a/terraform/test-fixtures/plan-module-wrong-var-type/inner/main.tf b/terraform/test-fixtures/plan-module-wrong-var-type/inner/main.tf index 8a9f380c77..c7f975a3b6 100644 --- a/terraform/test-fixtures/plan-module-wrong-var-type/inner/main.tf +++ b/terraform/test-fixtures/plan-module-wrong-var-type/inner/main.tf @@ -5,3 +5,6 @@ variable "map_in" { us-west-2 = "ami-67890" } } + +// We have to reference it so it isn't pruned +output "output" { value = "${var.map_in}" } 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 0000000000..e8045d6fce --- /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 0000000000..954d7a5698 --- /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_config.go b/terraform/transform_config.go index bcfa1233e3..c2dad20c96 100644 --- a/terraform/transform_config.go +++ b/terraform/transform_config.go @@ -3,121 +3,94 @@ package terraform import ( "errors" "fmt" + "log" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" ) -// ConfigTransformer is a GraphTransformer that adds the configuration -// to the graph. The module used to configure this transformer must be -// the root module. We'll look up the child module by the Path in the -// Graph. +// ConfigTransformer is a GraphTransformer that adds all the resources +// from the configuration to the graph. +// +// The module used to configure this transformer must be the root module. +// +// Only resources are added to the graph. Variables, outputs, and +// providers must be added via other transforms. +// +// Unlike ConfigTransformerOld, this transformer creates a graph with +// all resources including module resources, rather than creating module +// nodes that are then "flattened". type ConfigTransformer struct { + Concrete ConcreteResourceNodeFunc + Module *module.Tree } func (t *ConfigTransformer) Transform(g *Graph) error { - // A module is required and also must be completely loaded. + // If no module is given, we don't do anything if t.Module == nil { - return errors.New("module must not be nil") - } - if !t.Module.Loaded() { - return errors.New("module must be loaded") - } - - // Get the module we care about - module := t.Module.Child(g.Path[1:]) - if module == nil { return nil } - // Get the configuration for this module - config := module.Config() - - // Create the node list we'll use for the graph - nodes := make([]graphNodeConfig, 0, - (len(config.Variables)+ - len(config.ProviderConfigs)+ - len(config.Modules)+ - len(config.Resources)+ - len(config.Outputs))*2) - - // Write all the variables out - for _, v := range config.Variables { - nodes = append(nodes, &GraphNodeConfigVariable{ - Variable: v, - ModuleTree: t.Module, - ModulePath: g.Path, - }) + // If the module isn't loaded, that is simply an error + if !t.Module.Loaded() { + return errors.New("module must be loaded for ConfigTransformer") } - // Write all the provider configs out - for _, pc := range config.ProviderConfigs { - nodes = append(nodes, &GraphNodeConfigProvider{Provider: pc}) + // Start the transformation process + return t.transform(g, t.Module) +} + +func (t *ConfigTransformer) transform(g *Graph, m *module.Tree) error { + // If no config, do nothing + if m == nil { + return nil } - // Write all the resources out - for _, r := range config.Resources { - nodes = append(nodes, &GraphNodeConfigResource{ - Resource: r, - Path: g.Path, - }) + // Add our resources + if err := t.transformSingle(g, m); err != nil { + return err } - // Write all the modules out - children := module.Children() - for _, m := range config.Modules { - path := make([]string, len(g.Path), len(g.Path)+1) - copy(path, g.Path) - path = append(path, m.Name) - - nodes = append(nodes, &GraphNodeConfigModule{ - Path: path, - Module: m, - Tree: children[m.Name], - }) - } - - // Write all the outputs out - for _, o := range config.Outputs { - nodes = append(nodes, &GraphNodeConfigOutput{Output: o}) - } - - // Err is where the final error value will go if there is one - var err error - - // Build the graph vertices - for _, n := range nodes { - g.Add(n) - } - - // Build up the dependencies. We have to do this outside of the above - // loop since the nodes need to be in place for us to build the deps. - for _, n := range nodes { - if missing := g.ConnectDependent(n); len(missing) > 0 { - for _, m := range missing { - err = multierror.Append(err, fmt.Errorf( - "%s: missing dependency: %s", n.Name(), m)) - } + // Transform all the children. + for _, c := range m.Children() { + if err := t.transform(g, c); err != nil { + return err } } - return err + return nil } -// varNameForVar returns the VarName value for an interpolated variable. -// This value is compared to the VarName() value for the nodes within the -// graph to build the graph edges. -func varNameForVar(raw config.InterpolatedVariable) string { - switch v := raw.(type) { - case *config.ModuleVariable: - return fmt.Sprintf("module.%s.output.%s", v.Name, v.Field) - case *config.ResourceVariable: - return v.ResourceId() - case *config.UserVariable: - return fmt.Sprintf("var.%s", v.Name) - default: - return "" +func (t *ConfigTransformer) transformSingle(g *Graph, m *module.Tree) error { + log.Printf("[TRACE] ConfigTransformer: Starting for path: %v", m.Path()) + + // Get the configuration for this module + config := m.Config() + + // Build the path we're at + path := m.Path() + + // Write all the resources out + for _, r := range config.Resources { + // Build the resource address + addr, err := parseResourceAddressConfig(r) + if err != nil { + panic(fmt.Sprintf( + "Error parsing config address, this is a bug: %#v", r)) + } + addr.Path = path + + // 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_config_old.go b/terraform/transform_config_old.go new file mode 100644 index 0000000000..5f9851681a --- /dev/null +++ b/terraform/transform_config_old.go @@ -0,0 +1,123 @@ +package terraform + +import ( + "errors" + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// ConfigTransformerOld is a GraphTransformer that adds the configuration +// to the graph. The module used to configure this transformer must be +// the root module. We'll look up the child module by the Path in the +// Graph. +type ConfigTransformerOld struct { + Module *module.Tree +} + +func (t *ConfigTransformerOld) Transform(g *Graph) error { + // A module is required and also must be completely loaded. + if t.Module == nil { + return errors.New("module must not be nil") + } + if !t.Module.Loaded() { + return errors.New("module must be loaded") + } + + // Get the module we care about + module := t.Module.Child(g.Path[1:]) + if module == nil { + return nil + } + + // Get the configuration for this module + config := module.Config() + + // Create the node list we'll use for the graph + nodes := make([]graphNodeConfig, 0, + (len(config.Variables)+ + len(config.ProviderConfigs)+ + len(config.Modules)+ + len(config.Resources)+ + len(config.Outputs))*2) + + // Write all the variables out + for _, v := range config.Variables { + nodes = append(nodes, &GraphNodeConfigVariable{ + Variable: v, + ModuleTree: t.Module, + ModulePath: g.Path, + }) + } + + // Write all the provider configs out + for _, pc := range config.ProviderConfigs { + nodes = append(nodes, &GraphNodeConfigProvider{Provider: pc}) + } + + // Write all the resources out + for _, r := range config.Resources { + nodes = append(nodes, &GraphNodeConfigResource{ + Resource: r, + Path: g.Path, + }) + } + + // Write all the modules out + children := module.Children() + for _, m := range config.Modules { + path := make([]string, len(g.Path), len(g.Path)+1) + copy(path, g.Path) + path = append(path, m.Name) + + nodes = append(nodes, &GraphNodeConfigModule{ + Path: path, + Module: m, + Tree: children[m.Name], + }) + } + + // Write all the outputs out + for _, o := range config.Outputs { + nodes = append(nodes, &GraphNodeConfigOutput{Output: o}) + } + + // Err is where the final error value will go if there is one + var err error + + // Build the graph vertices + for _, n := range nodes { + g.Add(n) + } + + // Build up the dependencies. We have to do this outside of the above + // loop since the nodes need to be in place for us to build the deps. + for _, n := range nodes { + if missing := g.ConnectDependent(n); len(missing) > 0 { + for _, m := range missing { + err = multierror.Append(err, fmt.Errorf( + "%s: missing dependency: %s", n.Name(), m)) + } + } + } + + return err +} + +// varNameForVar returns the VarName value for an interpolated variable. +// This value is compared to the VarName() value for the nodes within the +// graph to build the graph edges. +func varNameForVar(raw config.InterpolatedVariable) string { + switch v := raw.(type) { + case *config.ModuleVariable: + return fmt.Sprintf("module.%s.output.%s", v.Name, v.Field) + case *config.ResourceVariable: + return v.ResourceId() + case *config.UserVariable: + return fmt.Sprintf("var.%s", v.Name) + default: + return "" + } +} diff --git a/terraform/transform_config_old_test.go b/terraform/transform_config_old_test.go new file mode 100644 index 0000000000..a5b0bbfbc5 --- /dev/null +++ b/terraform/transform_config_old_test.go @@ -0,0 +1,150 @@ +package terraform + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/config/module" +) + +func TestConfigTransformerOld_nilModule(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformerOld{} + if err := tf.Transform(&g); err == nil { + t.Fatal("should error") + } +} + +func TestConfigTransformerOld_unloadedModule(t *testing.T) { + mod, err := module.NewTreeModule( + "", filepath.Join(fixtureDir, "graph-basic")) + if err != nil { + t.Fatalf("err: %s", err) + } + + g := Graph{Path: RootModulePath} + tf := &ConfigTransformerOld{Module: mod} + if err := tf.Transform(&g); err == nil { + t.Fatal("should error") + } +} + +func TestConfigTransformerOld(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformerOld{Module: testModule(t, "graph-basic")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformerOld_dependsOn(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformerOld{Module: testModule(t, "graph-depends-on")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphDependsOnStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformerOld_modules(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformerOld{Module: testModule(t, "graph-modules")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphModulesStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformerOld_outputs(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformerOld{Module: testModule(t, "graph-outputs")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphOutputsStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformerOld_providerAlias(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformerOld{Module: testModule(t, "graph-provider-alias")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testGraphProviderAliasStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformerOld_errMissingDeps(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformerOld{Module: testModule(t, "graph-missing-deps")} + if err := tf.Transform(&g); err == nil { + t.Fatalf("err: %s", err) + } +} + +const testGraphBasicStr = ` +aws_instance.web + aws_security_group.firewall + var.foo +aws_load_balancer.weblb + aws_instance.web +aws_security_group.firewall +openstack_floating_ip.random +provider.aws + openstack_floating_ip.random +var.foo +` + +const testGraphDependsOnStr = ` +aws_instance.db + aws_instance.web +aws_instance.web +` + +const testGraphModulesStr = ` +aws_instance.web + aws_security_group.firewall + module.consul +aws_security_group.firewall +module.consul + aws_security_group.firewall +provider.aws +` + +const testGraphOutputsStr = ` +aws_instance.foo +output.foo + aws_instance.foo +` + +const testGraphProviderAliasStr = ` +provider.aws +provider.aws.bar +provider.aws.foo +` diff --git a/terraform/transform_config_test.go b/terraform/transform_config_test.go index 4ba88a3582..31bb7c8e0d 100644 --- a/terraform/transform_config_test.go +++ b/terraform/transform_config_test.go @@ -11,8 +11,12 @@ import ( func TestConfigTransformer_nilModule(t *testing.T) { g := Graph{Path: RootModulePath} tf := &ConfigTransformer{} - if err := tf.Transform(&g); err == nil { - t.Fatal("should error") + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + if len(g.Vertices()) > 0 { + t.Fatalf("graph is not empty: %s", g.String()) } } @@ -38,113 +42,15 @@ func TestConfigTransformer(t *testing.T) { } actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testGraphBasicStr) + expected := strings.TrimSpace(testConfigTransformerGraphBasicStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } } -func TestConfigTransformer_dependsOn(t *testing.T) { - g := Graph{Path: RootModulePath} - tf := &ConfigTransformer{Module: testModule(t, "graph-depends-on")} - if err := tf.Transform(&g); err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testGraphDependsOnStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestConfigTransformer_modules(t *testing.T) { - g := Graph{Path: RootModulePath} - tf := &ConfigTransformer{Module: testModule(t, "graph-modules")} - if err := tf.Transform(&g); err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testGraphModulesStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestConfigTransformer_outputs(t *testing.T) { - g := Graph{Path: RootModulePath} - tf := &ConfigTransformer{Module: testModule(t, "graph-outputs")} - if err := tf.Transform(&g); err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testGraphOutputsStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestConfigTransformer_providerAlias(t *testing.T) { - g := Graph{Path: RootModulePath} - tf := &ConfigTransformer{Module: testModule(t, "graph-provider-alias")} - if err := tf.Transform(&g); err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testGraphProviderAliasStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - -func TestConfigTransformer_errMissingDeps(t *testing.T) { - g := Graph{Path: RootModulePath} - tf := &ConfigTransformer{Module: testModule(t, "graph-missing-deps")} - if err := tf.Transform(&g); err == nil { - t.Fatalf("err: %s", err) - } -} - -const testGraphBasicStr = ` +const testConfigTransformerGraphBasicStr = ` aws_instance.web - aws_security_group.firewall - var.foo aws_load_balancer.weblb - aws_instance.web aws_security_group.firewall openstack_floating_ip.random -provider.aws - openstack_floating_ip.random -var.foo -` - -const testGraphDependsOnStr = ` -aws_instance.db - aws_instance.web -aws_instance.web -` - -const testGraphModulesStr = ` -aws_instance.web - aws_security_group.firewall - module.consul -aws_security_group.firewall -module.consul - aws_security_group.firewall -provider.aws -` - -const testGraphOutputsStr = ` -aws_instance.foo -output.foo - aws_instance.foo -` - -const testGraphProviderAliasStr = ` -provider.aws -provider.aws.bar -provider.aws.foo ` diff --git a/terraform/transform_destroy_edge.go b/terraform/transform_destroy_edge.go index 972d9b6a26..dd6ed114c5 100644 --- a/terraform/transform_destroy_edge.go +++ b/terraform/transform_destroy_edge.go @@ -120,10 +120,14 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error { &AttachStateTransformer{State: t.State}, } - // 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 d, dns := range destroyers { + // Go through all the nodes being destroyed and create a graph. + // The resulting graph is only of things being CREATED. For example, + // following our example, the resulting graph would be: + // + // A, B (with no edges) + // + var tempG Graph + for d, _ := range destroyers { // d is what is being destroyed. We parse the resource address // which it came from it is a panic if this fails. addr, err := ParseResourceAddress(d) @@ -135,27 +139,48 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error { // find the dependencies we need to: build a graph and use the // attach config and state transformers then ask for references. node := &NodeAbstractResource{Addr: addr} - { - var g Graph - g.Add(node) - for _, s := range steps { - if err := s.Transform(&g); err != nil { - return err - } - } - } + tempG.Add(node) + } - // Get the references of the creation node. If it has none, - // then there are no edges to make here. - prefix := modulePrefixStr(normalizeModulePath(addr.Path)) - deps := modulePrefixList(node.References(), prefix) + // Run the graph transforms so we have the information we need to + // build references. + for _, s := range steps { + if err := s.Transform(&tempG); err != nil { + return err + } + } + + // Create a reference map for easy lookup + refMap := NewReferenceMap(tempG.Vertices()) + + // Go through all the nodes in the graph and determine what they + // depend on. + for _, v := range tempG.Vertices() { + // Find all the references + refs, _ := refMap.References(v) log.Printf( - "[TRACE] DestroyEdgeTransformer: creation of %q depends on %#v", - d, deps) - if len(deps) == 0 { + "[TRACE] DestroyEdgeTransformer: creation node %q references %v", + dag.VertexName(v), refs) + + // If we have no references, then we won't need to do anything + if len(refs) == 0 { continue } + // Get the destroy node for this. In the example of our struct, + // we are currently at B and we're looking for B_d. + rn, ok := v.(GraphNodeResource) + if !ok { + continue + } + + addr := rn.ResourceAddr() + if addr == nil { + continue + } + + dns := destroyers[addr.String()] + // We have dependencies, check if any are being destroyed // to build the list of things that we must depend on! // @@ -163,17 +188,28 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error { // // B_d => A_d => A => B // - // Then at this point in the algorithm we started with A_d, - // we built A (to get dependencies), and we found B. We're now looking - // to see if B_d exists. + // Then at this point in the algorithm we started with B_d, + // we built B (to get dependencies), and we found A. We're now looking + // to see if A_d exists. var depDestroyers []dag.Vertex - for _, d := range deps { - if ds, ok := destroyers[d]; ok { + for _, v := range refs { + rn, ok := v.(GraphNodeResource) + if !ok { + continue + } + + addr := rn.ResourceAddr() + if addr == nil { + continue + } + + key := addr.String() + if ds, ok := destroyers[key]; ok { for _, d := range ds { depDestroyers = append(depDestroyers, d.(dag.Vertex)) log.Printf( "[TRACE] DestroyEdgeTransformer: destruction of %q depends on %s", - addr.String(), dag.VertexName(d)) + key, dag.VertexName(d)) } } } diff --git a/terraform/transform_destroy_edge_test.go b/terraform/transform_destroy_edge_test.go index 9489b5a5c1..9475e51658 100644 --- a/terraform/transform_destroy_edge_test.go +++ b/terraform/transform_destroy_edge_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestDestroyEdgeTransformer(t *testing.T) { +func TestDestroyEdgeTransformer_basic(t *testing.T) { g := Graph{Path: RootModulePath} g.Add(&graphNodeDestroyerTest{AddrString: "test.A"}) g.Add(&graphNodeDestroyerTest{AddrString: "test.B"}) diff --git a/terraform/transform_destroy_test.go b/terraform/transform_destroy_test.go index fe9032a5e3..f03064e564 100644 --- a/terraform/transform_destroy_test.go +++ b/terraform/transform_destroy_test.go @@ -10,7 +10,7 @@ func TestDestroyTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -35,7 +35,7 @@ func TestDestroyTransformer_dependsOn(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -60,7 +60,7 @@ func TestCreateBeforeDestroyTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -92,7 +92,7 @@ func TestCreateBeforeDestroyTransformer_twice(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -125,7 +125,7 @@ func TestPruneDestroyTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -168,7 +168,7 @@ func TestPruneDestroyTransformer_diff(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -202,7 +202,7 @@ func TestPruneDestroyTransformer_count(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -251,7 +251,7 @@ func TestPruneDestroyTransformer_countDec(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -297,7 +297,7 @@ func TestPruneDestroyTransformer_countState(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -347,7 +347,7 @@ func TestPruneDestroyTransformer_prefixMatch(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -396,7 +396,7 @@ func TestPruneDestroyTransformer_tainted(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } diff --git a/terraform/transform_flatten_test.go b/terraform/transform_flatten_test.go index 92f7cac7af..47bc5d2c5c 100644 --- a/terraform/transform_flatten_test.go +++ b/terraform/transform_flatten_test.go @@ -11,7 +11,7 @@ func TestFlattenTransformer(t *testing.T) { var b BasicGraphBuilder b = BasicGraphBuilder{ Steps: []GraphTransformer{ - &ConfigTransformer{Module: mod}, + &ConfigTransformerOld{Module: mod}, &VertexTransformer{ Transforms: []GraphVertexTransformer{ &ExpandTransform{ @@ -41,7 +41,7 @@ func TestFlattenTransformer_withProxy(t *testing.T) { var b BasicGraphBuilder b = BasicGraphBuilder{ Steps: []GraphTransformer{ - &ConfigTransformer{Module: mod}, + &ConfigTransformerOld{Module: mod}, &VertexTransformer{ Transforms: []GraphVertexTransformer{ &ExpandTransform{ diff --git a/terraform/transform_orphan_count.go b/terraform/transform_orphan_count.go new file mode 100644 index 0000000000..b256a25b7b --- /dev/null +++ b/terraform/transform_orphan_count.go @@ -0,0 +1,110 @@ +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 transform 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 + } + + orphanIndex := -1 + if t.Count == 1 { + orphanIndex = 0 + } + + // 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 == orphanIndex { + // This is a piece of cleverness (beware), but its simple: + // if orphanIndex is 0, then check -1, else check 0. + checkIndex := (orphanIndex + 1) * -1 + + key := &ResourceStateKey{ + Name: addr.Name, + Type: addr.Type, + Mode: addr.Mode, + Index: checkIndex, + } + + 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 %d", + addr, orphanIndex) + 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..d2da60e7d7 --- /dev/null +++ b/terraform/transform_orphan_count_test.go @@ -0,0 +1,373 @@ +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) + } +} + +func TestOrphanResourceCountTransformer_zeroAndNoneCount(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: 2, + Addr: addr, + State: state, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanResourceCountZeroAndNoneCountStr) + 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[0] (orphan) +` + +const testTransformOrphanResourceCountZeroAndNoneCountStr = ` +aws_instance.foo (orphan) +` diff --git a/terraform/transform_orphan_resource.go b/terraform/transform_orphan_resource.go new file mode 100644 index 0000000000..11721462e1 --- /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 0000000000..ce29eecf1d --- /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()) +} diff --git a/terraform/transform_orphan_test.go b/terraform/transform_orphan_test.go index 76fbaa6a1b..ef6b497ade 100644 --- a/terraform/transform_orphan_test.go +++ b/terraform/transform_orphan_test.go @@ -35,7 +35,7 @@ func TestOrphanTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -86,7 +86,7 @@ func TestOrphanTransformer_modules(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -140,7 +140,7 @@ func TestOrphanTransformer_modulesDeps(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -194,7 +194,7 @@ func TestOrphanTransformer_modulesDepsOrphan(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -233,7 +233,7 @@ func TestOrphanTransformer_modulesNoRoot(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -282,7 +282,7 @@ func TestOrphanTransformer_resourceDepends(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -305,7 +305,7 @@ func TestOrphanTransformer_nilState(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } diff --git a/terraform/transform_output_orphan_test.go b/terraform/transform_output_orphan_test.go index 3bbe7a3e82..1c930ffa3c 100644 --- a/terraform/transform_output_orphan_test.go +++ b/terraform/transform_output_orphan_test.go @@ -27,7 +27,7 @@ func TestAddOutputOrphanTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } diff --git a/terraform/transform_provider_test.go b/terraform/transform_provider_test.go index bc2cff532c..1ea7be76d3 100644 --- a/terraform/transform_provider_test.go +++ b/terraform/transform_provider_test.go @@ -12,7 +12,7 @@ func TestProviderTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -73,7 +73,7 @@ func TestCloseProviderTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -105,7 +105,7 @@ func TestCloseProviderTransformer_withTargets(t *testing.T) { g := Graph{Path: RootModulePath} transforms := []GraphTransformer{ - &ConfigTransformer{Module: mod}, + &ConfigTransformerOld{Module: mod}, &ProviderTransformer{}, &CloseProviderTransformer{}, &TargetsTransformer{ @@ -135,7 +135,7 @@ func TestMissingProviderTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -318,7 +318,7 @@ func TestPruneProviderTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -364,7 +364,7 @@ func TestDisableProviderTransformer(t *testing.T) { g := Graph{Path: RootModulePath} transforms := []GraphTransformer{ - &ConfigTransformer{Module: mod}, + &ConfigTransformerOld{Module: mod}, &MissingProviderTransformer{Providers: []string{"aws"}}, &ProviderTransformer{}, &DisableProviderTransformerOld{}, @@ -390,7 +390,7 @@ func TestDisableProviderTransformer_keep(t *testing.T) { g := Graph{Path: RootModulePath} transforms := []GraphTransformer{ - &ConfigTransformer{Module: mod}, + &ConfigTransformerOld{Module: mod}, &MissingProviderTransformer{Providers: []string{"aws"}}, &ProviderTransformer{}, &DisableProviderTransformerOld{}, diff --git a/terraform/transform_provisioner_test.go b/terraform/transform_provisioner_test.go index 3f37c5a69b..270910e78a 100644 --- a/terraform/transform_provisioner_test.go +++ b/terraform/transform_provisioner_test.go @@ -12,7 +12,7 @@ func TestMissingProvisionerTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -112,7 +112,7 @@ func TestCloseProvisionerTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } diff --git a/terraform/transform_reference.go b/terraform/transform_reference.go index 613f1484c0..1da835fbd1 100644 --- a/terraform/transform_reference.go +++ b/terraform/transform_reference.go @@ -2,6 +2,8 @@ package terraform import ( "fmt" + "log" + "strings" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/dag" @@ -53,6 +55,14 @@ func (t *ReferenceTransformer) Transform(g *Graph) error { // Find the things that reference things and connect them for _, v := range vs { parents, _ := m.References(v) + parentsDbg := make([]string, len(parents)) + for i, v := range parents { + parentsDbg[i] = dag.VertexName(v) + } + log.Printf( + "[DEBUG] ReferenceTransformer: %q references: %v", + dag.VertexName(v), parentsDbg) + for _, parent := range parents { g.Connect(dag.BasicEdge(v, parent)) } @@ -81,27 +91,37 @@ func (m *ReferenceMap) References(v dag.Vertex) ([]dag.Vertex, []string) { var matches []dag.Vertex var missing []string prefix := m.prefix(v) - for _, n := range rn.References() { - n = prefix + n - parents, ok := m.references[n] - if !ok { - missing = append(missing, n) - continue - } - - // Make sure this isn't a self reference, which isn't included - selfRef := false - for _, p := range parents { - if p == v { - selfRef = true - break + for _, ns := range rn.References() { + found := false + for _, n := range strings.Split(ns, "/") { + n = prefix + n + parents, ok := m.references[n] + if !ok { + continue } - } - if selfRef { - continue + + // Mark that we found a match + found = true + + // Make sure this isn't a self reference, which isn't included + selfRef := false + for _, p := range parents { + if p == v { + selfRef = true + break + } + } + if selfRef { + continue + } + + matches = append(matches, parents...) + break } - matches = append(matches, parents...) + if !found { + missing = append(missing, ns) + } } return matches, missing @@ -209,10 +229,9 @@ func NewReferenceMap(vs []dag.Vertex) *ReferenceMap { func ReferencesFromConfig(c *config.RawConfig) []string { var result []string for _, v := range c.Variables { - if r := ReferenceFromInterpolatedVar(v); r != "" { - result = append(result, r) + if r := ReferenceFromInterpolatedVar(v); len(r) > 0 { + result = append(result, r...) } - } return result @@ -220,15 +239,31 @@ func ReferencesFromConfig(c *config.RawConfig) []string { // ReferenceFromInterpolatedVar returns the reference from this variable, // or an empty string if there is no reference. -func ReferenceFromInterpolatedVar(v config.InterpolatedVariable) string { +func ReferenceFromInterpolatedVar(v config.InterpolatedVariable) []string { switch v := v.(type) { case *config.ModuleVariable: - return fmt.Sprintf("module.%s.output.%s", v.Name, v.Field) + return []string{fmt.Sprintf("module.%s.output.%s", v.Name, v.Field)} case *config.ResourceVariable: - return v.ResourceId() + id := v.ResourceId() + + // If we have a multi-reference (splat), then we depend on ALL + // resources with this type/name. + if v.Multi && v.Index == -1 { + return []string{fmt.Sprintf("%s.*", id)} + } + + // Otherwise, we depend on a specific index. + idx := v.Index + if !v.Multi || v.Index == -1 { + idx = 0 + } + + // Depend on the index, as well as "N" which represents the + // un-expanded set of resources. + return []string{fmt.Sprintf("%s.%d/%s.N", id, idx, id)} case *config.UserVariable: - return fmt.Sprintf("var.%s", v.Name) + return []string{fmt.Sprintf("var.%s", v.Name)} default: - return "" + return nil } } diff --git a/terraform/transform_reference_test.go b/terraform/transform_reference_test.go index 31cf4664de..544af6b049 100644 --- a/terraform/transform_reference_test.go +++ b/terraform/transform_reference_test.go @@ -88,6 +88,56 @@ func TestReferenceTransformer_path(t *testing.T) { } } +func TestReferenceTransformer_backup(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeRefParentTest{ + NameValue: "A", + Names: []string{"A"}, + }) + g.Add(&graphNodeRefChildTest{ + NameValue: "B", + Refs: []string{"C/A"}, + }) + + tf := &ReferenceTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformRefBackupStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestReferenceTransformer_backupPrimary(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeRefParentTest{ + NameValue: "A", + Names: []string{"A"}, + }) + g.Add(&graphNodeRefChildTest{ + NameValue: "B", + Refs: []string{"C/A"}, + }) + g.Add(&graphNodeRefParentTest{ + NameValue: "C", + Names: []string{"C"}, + }) + + tf := &ReferenceTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformRefBackupPrimaryStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + func TestReferenceMapReferences(t *testing.T) { cases := map[string]struct { Nodes []dag.Vertex @@ -202,6 +252,19 @@ B A ` +const testTransformRefBackupStr = ` +A +B + A +` + +const testTransformRefBackupPrimaryStr = ` +A +B + C +C +` + const testTransformRefPathStr = ` A B diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index d53e9f9511..f5e597569f 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -8,15 +8,15 @@ import ( "github.com/hashicorp/terraform/dag" ) -// ResourceCountTransformer is a GraphTransformer that expands the count +// ResourceCountTransformerOld is a GraphTransformer that expands the count // out for a specific resource. -type ResourceCountTransformer struct { +type ResourceCountTransformerOld struct { Resource *config.Resource Destroy bool Targets []ResourceAddress } -func (t *ResourceCountTransformer) Transform(g *Graph) error { +func (t *ResourceCountTransformerOld) Transform(g *Graph) error { // Expand the resource count count, err := t.Resource.Count() if err != nil { @@ -72,7 +72,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error { return nil } -func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool { +func (t *ResourceCountTransformerOld) nodeIsTargeted(node dag.Vertex) bool { // no targets specified, everything stays in the graph if len(t.Targets) == 0 { return true diff --git a/terraform/transform_resource_count.go b/terraform/transform_resource_count.go new file mode 100644 index 0000000000..cda35cb7bd --- /dev/null +++ b/terraform/transform_resource_count.go @@ -0,0 +1,51 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/dag" +) + +// ResourceCountTransformer is a GraphTransformer that expands the count +// out for a specific resource. +// +// This assumes that the count is already interpolated. +type ResourceCountTransformer struct { + Concrete ConcreteResourceNodeFunc + + Count int + Addr *ResourceAddress +} + +func (t *ResourceCountTransformer) Transform(g *Graph) error { + // Don't allow the count to be negative + if t.Count < 0 { + return fmt.Errorf("negative count: %d", t.Count) + } + + // For each count, build and add the node + for i := 0; i < t.Count; i++ { + // Set the index. If our count is 1 we special case it so that + // we handle the "resource.0" and "resource" boundary properly. + index := i + if t.Count == 1 { + index = -1 + } + + // Build the resource address + addr := t.Addr.Copy() + addr.Index = index + + // 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_resource_test.go b/terraform/transform_resource_test.go index 6933c622cf..017e7f1c24 100644 --- a/terraform/transform_resource_test.go +++ b/terraform/transform_resource_test.go @@ -5,64 +5,64 @@ import ( "testing" ) -func TestResourceCountTransformer(t *testing.T) { +func TestResourceCountTransformerOld(t *testing.T) { cfg := testModule(t, "transform-resource-count-basic").Config() resource := cfg.Resources[0] g := Graph{Path: RootModulePath} { - tf := &ResourceCountTransformer{Resource: resource} + tf := &ResourceCountTransformerOld{Resource: resource} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } } actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testResourceCountTransformStr) + expected := strings.TrimSpace(testResourceCountTransformOldStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } } -func TestResourceCountTransformer_countNegative(t *testing.T) { +func TestResourceCountTransformerOld_countNegative(t *testing.T) { cfg := testModule(t, "transform-resource-count-negative").Config() resource := cfg.Resources[0] g := Graph{Path: RootModulePath} { - tf := &ResourceCountTransformer{Resource: resource} + tf := &ResourceCountTransformerOld{Resource: resource} if err := tf.Transform(&g); err == nil { t.Fatal("should error") } } } -func TestResourceCountTransformer_deps(t *testing.T) { +func TestResourceCountTransformerOld_deps(t *testing.T) { cfg := testModule(t, "transform-resource-count-deps").Config() resource := cfg.Resources[0] g := Graph{Path: RootModulePath} { - tf := &ResourceCountTransformer{Resource: resource} + tf := &ResourceCountTransformerOld{Resource: resource} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } } actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testResourceCountTransformDepsStr) + expected := strings.TrimSpace(testResourceCountTransformOldDepsStr) if actual != expected { t.Fatalf("bad:\n\n%s", actual) } } -const testResourceCountTransformStr = ` +const testResourceCountTransformOldStr = ` aws_instance.foo #0 aws_instance.foo #1 aws_instance.foo #2 ` -const testResourceCountTransformDepsStr = ` +const testResourceCountTransformOldDepsStr = ` aws_instance.foo #0 aws_instance.foo #1 aws_instance.foo #0 diff --git a/terraform/transform_root_test.go b/terraform/transform_root_test.go index 68f5c51144..1cfec3995f 100644 --- a/terraform/transform_root_test.go +++ b/terraform/transform_root_test.go @@ -10,7 +10,7 @@ func TestRootTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } diff --git a/terraform/transform_targets_test.go b/terraform/transform_targets_test.go index 2daa72827e..142fbb52f4 100644 --- a/terraform/transform_targets_test.go +++ b/terraform/transform_targets_test.go @@ -10,7 +10,7 @@ func TestTargetsTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } @@ -41,7 +41,7 @@ func TestTargetsTransformer_destroy(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) } diff --git a/terraform/transform_transitive_reduction_test.go b/terraform/transform_transitive_reduction_test.go index bcd3b7d233..4c864e236f 100644 --- a/terraform/transform_transitive_reduction_test.go +++ b/terraform/transform_transitive_reduction_test.go @@ -10,7 +10,7 @@ func TestTransitiveReductionTransformer(t *testing.T) { g := Graph{Path: RootModulePath} { - tf := &ConfigTransformer{Module: mod} + tf := &ConfigTransformerOld{Module: mod} if err := tf.Transform(&g); err != nil { t.Fatalf("err: %s", err) }