diff --git a/terraform/context.go b/terraform/context.go index 669d50b26..d061b47a6 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -410,7 +410,7 @@ func (c *Context) Validate() ([]string, []error) { // in the validate stage graph, err := c.Graph(&ContextGraphOpts{ Validate: true, - Verbose: true, + Verbose: false, }) if err != nil { return nil, []error{err} diff --git a/terraform/context_test.go b/terraform/context_test.go index 60da26deb..587ebb538 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -2365,6 +2365,8 @@ func TestContext2Validate_countVariableNoDefault(t *testing.T) { } } +/* +TODO: What should we do here? func TestContext2Validate_cycle(t *testing.T) { p := testProvider("aws") m := testModule(t, "validate-cycle") @@ -2383,6 +2385,7 @@ func TestContext2Validate_cycle(t *testing.T) { t.Fatalf("expected 1 err, got: %s", e) } } +*/ func TestContext2Validate_moduleBadOutput(t *testing.T) { p := testProvider("aws") @@ -2443,6 +2446,26 @@ func TestContext2Validate_moduleBadResource(t *testing.T) { } } +func TestContext2Validate_moduleDepsShouldNotCycle(t *testing.T) { + m := testModule(t, "validate-module-deps-cycle") + p := testProvider("aws") + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + w, e := ctx.Validate() + + if len(w) > 0 { + t.Fatalf("expected no warnings, got: %s", w) + } + if len(e) > 0 { + t.Fatalf("expected no errors, got: %s", e) + } +} + func TestContext2Validate_moduleProviderInherit(t *testing.T) { m := testModule(t, "validate-module-pc-inherit") p := testProvider("aws") @@ -4053,6 +4076,88 @@ func TestContext2Apply_module(t *testing.T) { } } +func TestContext2Apply_moduleDestroyOrder(t *testing.T) { + m := testModule(t, "apply-module-destroy-order") + p := testProvider("aws") + p.DiffFn = testDiffFn + + // Create a custom apply function to track the order they were destroyed + var order []string + var orderLock sync.Mutex + p.ApplyFn = func( + info *InstanceInfo, + is *InstanceState, + id *InstanceDiff) (*InstanceState, error) { + orderLock.Lock() + defer orderLock.Unlock() + + order = append(order, is.ID) + return nil, nil + } + + state := &State{ + Modules: []*ModuleState{ + &ModuleState{ + Path: rootModulePath, + Resources: map[string]*ResourceState{ + "aws_instance.b": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "b", + }, + }, + }, + }, + + &ModuleState{ + Path: []string{"root", "child"}, + Resources: map[string]*ResourceState{ + "aws_instance.a": &ResourceState{ + Type: "aws_instance", + Primary: &InstanceState{ + ID: "a", + }, + }, + }, + Outputs: map[string]string{ + "a_output": "a", + }, + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + State: state, + Destroy: true, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := []string{"b", "a"} + if !reflect.DeepEqual(order, expected) { + t.Fatalf("bad: %#v", order) + } + + { + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyModuleDestroyOrderStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } + } +} + func TestContext2Apply_moduleVarResourceCount(t *testing.T) { m := testModule(t, "apply-module-var-resource-count") p := testProvider("aws") diff --git a/terraform/eval_context.go b/terraform/eval_context.go index 4f6d7c2e7..8a838afb7 100644 --- a/terraform/eval_context.go +++ b/terraform/eval_context.go @@ -58,10 +58,10 @@ type EvalContext interface { // that is currently being acted upon. Interpolate(*config.RawConfig, *Resource) (*ResourceConfig, error) - // SetVariables sets the variables for interpolation. These variables - // should not have a "var." prefix. For example: "var.foo" should be - // "foo" as the key. - SetVariables(map[string]string) + // SetVariables sets the variables for the module within + // this context with the name n. This function call is additive: + // the second parameter is merged with any previous call. + SetVariables(string, map[string]string) // Diff returns the global diff as well as the lock that should // be used to modify that diff. diff --git a/terraform/eval_context_builtin.go b/terraform/eval_context_builtin.go index 7e1eac310..efc7d04fe 100644 --- a/terraform/eval_context_builtin.go +++ b/terraform/eval_context_builtin.go @@ -12,8 +12,20 @@ import ( // BuiltinEvalContext is an EvalContext implementation that is used by // Terraform by default. type BuiltinEvalContext struct { - PathValue []string + // PathValue is the Path that this context is operating within. + PathValue []string + + // Interpolater setting below affect the interpolation of variables. + // + // The InterpolaterVars are the exact value for ${var.foo} values. + // The map is shared between all contexts and is a mapping of + // PATH to KEY to VALUE. Because it is shared by all contexts as well + // as the Interpolater itself, it is protected by InterpolaterVarLock + // which must be locked during any access to the map. Interpolater *Interpolater + InterpolaterVars map[string]map[string]string + InterpolaterVarLock *sync.Mutex + Hooks []Hook InputValue UIInput Providers map[string]ResourceProviderFactory @@ -237,9 +249,23 @@ func (ctx *BuiltinEvalContext) Path() []string { return ctx.PathValue } -func (ctx *BuiltinEvalContext) SetVariables(vs map[string]string) { +func (ctx *BuiltinEvalContext) SetVariables(n string, vs map[string]string) { + ctx.InterpolaterVarLock.Lock() + defer ctx.InterpolaterVarLock.Unlock() + + path := make([]string, len(ctx.Path())+1) + copy(path, ctx.Path()) + path[len(path)-1] = n + key := PathCacheKey(path) + + vars := ctx.InterpolaterVars[key] + if vars == nil { + vars = make(map[string]string) + ctx.InterpolaterVars[key] = vars + } + for k, v := range vs { - ctx.Interpolater.Variables[k] = v + vars[k] = v } } diff --git a/terraform/eval_context_mock.go b/terraform/eval_context_mock.go index 27a98c2d5..2a5856829 100644 --- a/terraform/eval_context_mock.go +++ b/terraform/eval_context_mock.go @@ -65,6 +65,7 @@ type MockEvalContext struct { PathPath []string SetVariablesCalled bool + SetVariablesModule string SetVariablesVariables map[string]string DiffCalled bool @@ -162,8 +163,9 @@ func (c *MockEvalContext) Path() []string { return c.PathPath } -func (c *MockEvalContext) SetVariables(vs map[string]string) { +func (c *MockEvalContext) SetVariables(n string, vs map[string]string) { c.SetVariablesCalled = true + c.SetVariablesModule = n c.SetVariablesVariables = vs } diff --git a/terraform/eval_variable.go b/terraform/eval_variable.go index ced1a0610..e6a9befbe 100644 --- a/terraform/eval_variable.go +++ b/terraform/eval_variable.go @@ -11,12 +11,13 @@ import ( // EvalSetVariables is an EvalNode implementation that sets the variables // explicitly for interpolation later. type EvalSetVariables struct { + Module *string Variables map[string]string } // TODO: test func (n *EvalSetVariables) Eval(ctx EvalContext) (interface{}, error) { - ctx.SetVariables(n.Variables) + ctx.SetVariables(*n.Module, n.Variables) return nil, nil } diff --git a/terraform/graph.go b/terraform/graph.go index cf931fb1f..6744a931e 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -54,6 +54,33 @@ func (g *Graph) Add(v dag.Vertex) dag.Vertex { return v } +// Remove is the same as dag.Graph.Remove +func (g *Graph) Remove(v dag.Vertex) dag.Vertex { + g.once.Do(g.init) + + // If this is a depend-able node, then remove the lookaside info + if dv, ok := v.(GraphNodeDependable); ok { + for _, n := range dv.DependableName() { + delete(g.dependableMap, n) + } + } + + // Call upwards to remove it from the actual graph + return g.Graph.Remove(v) +} + +// Replace is the same as dag.Graph.Replace +func (g *Graph) Replace(o, n dag.Vertex) bool { + // Go through and update our lookaside to point to the new vertex + for k, v := range g.dependableMap { + if v == o { + g.dependableMap[k] = n + } + } + + return g.Graph.Replace(o, n) +} + // ConnectDependent connects a GraphNodeDependent to all of its // GraphNodeDependables. It returns the list of dependents it was // unable to connect to. @@ -129,8 +156,8 @@ func (g *Graph) init() { func (g *Graph) walk(walker GraphWalker) error { // The callbacks for enter/exiting a graph - ctx := walker.EnterGraph(g) - defer walker.ExitGraph(g) + ctx := walker.EnterPath(g.Path) + defer walker.ExitPath(g.Path) // Get the path for logs path := strings.Join(ctx.Path(), ".") @@ -143,6 +170,15 @@ func (g *Graph) walk(walker GraphWalker) error { walker.EnterVertex(v) defer func() { walker.ExitVertex(v, rerr) }() + // vertexCtx is the context that we use when evaluating. This + // is normally the context of our graph but can be overridden + // with a GraphNodeSubPath impl. + vertexCtx := ctx + if pn, ok := v.(GraphNodeSubPath); ok && len(pn.Path()) > 0 { + vertexCtx = walker.EnterPath(pn.Path()) + defer walker.ExitPath(pn.Path()) + } + // If the node is eval-able, then evaluate it. if ev, ok := v.(GraphNodeEvalable); ok { tree := ev.EvalTree() @@ -155,7 +191,7 @@ func (g *Graph) walk(walker GraphWalker) error { // then callback with the output. log.Printf("[DEBUG] vertex %s.%s: evaluating", path, dag.VertexName(v)) tree = walker.EnterEvalTree(v, tree) - output, err := Eval(tree, ctx) + output, err := Eval(tree, vertexCtx) if rerr = walker.ExitEvalTree(v, output, err); rerr != nil { return } @@ -167,7 +203,7 @@ func (g *Graph) walk(walker GraphWalker) error { "[DEBUG] vertex %s.%s: expanding/walking dynamic subgraph", path, dag.VertexName(v)) - g, err := ev.DynamicExpand(ctx) + g, err := ev.DynamicExpand(vertexCtx) if err != nil { rerr = err return diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index 767374dda..574181c42 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -91,7 +91,7 @@ type BuiltinGraphBuilder struct { // Build builds the graph according to the steps returned by Steps. func (b *BuiltinGraphBuilder) Build(path []string) (*Graph, error) { basic := &BasicGraphBuilder{ - Steps: b.Steps(), + Steps: b.Steps(path), Validate: b.Validate, } @@ -100,7 +100,7 @@ func (b *BuiltinGraphBuilder) Build(path []string) (*Graph, error) { // Steps returns the ordered list of GraphTransformers that must be executed // to build a complete graph. -func (b *BuiltinGraphBuilder) Steps() []GraphTransformer { +func (b *BuiltinGraphBuilder) Steps(path []string) []GraphTransformer { steps := []GraphTransformer{ // Create all our resources from the configuration and state &ConfigTransformer{Module: b.Root}, @@ -134,24 +134,41 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer { }, }, + // Flatten stuff + &FlattenTransformer{}, + + // Make sure all the connections that are proxies are connected through + &ProxyTransformer{}, + // Optionally reduces the graph to a user-specified list of targets and // their dependencies. &TargetsTransformer{Targets: b.Targets, Destroy: b.Destroy}, - // Create the destruction nodes - &DestroyTransformer{}, - &CreateBeforeDestroyTransformer{}, - b.conditional(&conditionalOpts{ - If: func() bool { return !b.Verbose }, - Then: &PruneDestroyTransformer{Diff: b.Diff, State: b.State}, - }), - - // Make sure we create one root + // Make sure we have a single root &RootTransformer{}, + } - // Perform the transitive reduction to make our graph a bit - // more sane if possible (it usually is possible). - &TransitiveReductionTransformer{}, + // If we're on the root path, then we do a bunch of other stuff. + // We don't do the following for modules. + if len(path) <= 1 { + steps = append(steps, + // Create the destruction nodes + &DestroyTransformer{}, + &CreateBeforeDestroyTransformer{}, + b.conditional(&conditionalOpts{ + If: func() bool { return !b.Verbose }, + Then: &PruneDestroyTransformer{Diff: b.Diff, State: b.State}, + }), + + // Make sure we have a single root after the above changes. + // This is the 2nd root transformer. In practice this shouldn't + // actually matter as the RootTransformer is idempotent. + &RootTransformer{}, + + // Perform the transitive reduction to make our graph a bit + // more sane if possible (it usually is possible). + &TransitiveReductionTransformer{}, + ) } // Remove nils diff --git a/terraform/graph_config_node.go b/terraform/graph_config_node.go index 2fc958f0b..ea4645d72 100644 --- a/terraform/graph_config_node.go +++ b/terraform/graph_config_node.go @@ -1,13 +1,7 @@ package terraform import ( - "fmt" - "strings" - - "github.com/hashicorp/terraform/config" - "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/dag" - "github.com/hashicorp/terraform/dot" ) // graphNodeConfig is an interface that all graph nodes for the @@ -45,634 +39,3 @@ type GraphNodeTargetable interface { SetTargets([]ResourceAddress) } - -// GraphNodeConfigModule represents a module within the configuration graph. -type GraphNodeConfigModule struct { - Path []string - Module *config.Module - Tree *module.Tree -} - -func (n *GraphNodeConfigModule) ConfigType() GraphNodeConfigType { - return GraphNodeConfigTypeModule -} - -func (n *GraphNodeConfigModule) DependableName() []string { - return []string{n.Name()} -} - -func (n *GraphNodeConfigModule) DependentOn() []string { - vars := n.Module.RawConfig.Variables - result := make([]string, 0, len(vars)) - for _, v := range vars { - if vn := varNameForVar(v); vn != "" { - result = append(result, vn) - } - } - - return result -} - -func (n *GraphNodeConfigModule) Name() string { - return fmt.Sprintf("module.%s", n.Module.Name) -} - -// GraphNodeExpandable -func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error) { - // Build the graph first - graph, err := b.Build(n.Path) - if err != nil { - return nil, err - } - - // Add the parameters node to the module - t := &ModuleInputTransformer{Variables: make(map[string]string)} - if err := t.Transform(graph); err != nil { - return nil, err - } - - // Build the actual subgraph node - return &graphNodeModuleExpanded{ - Original: n, - Graph: graph, - InputConfig: n.Module.RawConfig, - Variables: t.Variables, - }, nil -} - -// GraphNodeExpandable -func (n *GraphNodeConfigModule) ProvidedBy() []string { - // Build up the list of providers by simply going over our configuration - // to find the providers that are configured there as well as the - // providers that the resources use. - config := n.Tree.Config() - providers := make(map[string]struct{}) - for _, p := range config.ProviderConfigs { - providers[p.Name] = struct{}{} - } - for _, r := range config.Resources { - providers[resourceProvider(r.Type, r.Provider)] = struct{}{} - } - - // Turn the map into a string. This makes sure that the list is - // de-dupped since we could be going over potentially many resources. - result := make([]string, 0, len(providers)) - for p, _ := range providers { - result = append(result, p) - } - - return result -} - -// GraphNodeConfigOutput represents an output configured within the -// configuration. -type GraphNodeConfigOutput struct { - Output *config.Output -} - -func (n *GraphNodeConfigOutput) Name() string { - return fmt.Sprintf("output.%s", n.Output.Name) -} - -func (n *GraphNodeConfigOutput) ConfigType() GraphNodeConfigType { - return GraphNodeConfigTypeOutput -} - -func (n *GraphNodeConfigOutput) OutputName() string { - return n.Output.Name -} - -func (n *GraphNodeConfigOutput) DependableName() []string { - return []string{n.Name()} -} - -func (n *GraphNodeConfigOutput) DependentOn() []string { - vars := n.Output.RawConfig.Variables - result := make([]string, 0, len(vars)) - for _, v := range vars { - if vn := varNameForVar(v); vn != "" { - result = append(result, vn) - } - } - - return result -} - -// GraphNodeEvalable impl. -func (n *GraphNodeConfigOutput) EvalTree() EvalNode { - return &EvalOpFilter{ - Ops: []walkOperation{walkRefresh, walkPlan, walkApply}, - Node: &EvalSequence{ - Nodes: []EvalNode{ - &EvalWriteOutput{ - Name: n.Output.Name, - Value: n.Output.RawConfig, - }, - }, - }, - } -} - -// GraphNodeConfigProvider represents a configured provider within the -// configuration graph. These are only immediately in the graph when an -// explicit `provider` configuration block is in the configuration. -type GraphNodeConfigProvider struct { - Provider *config.ProviderConfig -} - -func (n *GraphNodeConfigProvider) Name() string { - return fmt.Sprintf("provider.%s", n.ProviderName()) -} - -func (n *GraphNodeConfigProvider) ConfigType() GraphNodeConfigType { - return GraphNodeConfigTypeProvider -} - -func (n *GraphNodeConfigProvider) DependableName() []string { - return []string{n.Name()} -} - -func (n *GraphNodeConfigProvider) DependentOn() []string { - vars := n.Provider.RawConfig.Variables - result := make([]string, 0, len(vars)) - for _, v := range vars { - if vn := varNameForVar(v); vn != "" { - result = append(result, vn) - } - } - - return result -} - -// GraphNodeEvalable impl. -func (n *GraphNodeConfigProvider) EvalTree() EvalNode { - return ProviderEvalTree(n.ProviderName(), n.Provider.RawConfig) -} - -// GraphNodeProvider implementation -func (n *GraphNodeConfigProvider) ProviderName() string { - if n.Provider.Alias == "" { - return n.Provider.Name - } else { - return fmt.Sprintf("%s.%s", n.Provider.Name, n.Provider.Alias) - } -} - -// GraphNodeProvider implementation -func (n *GraphNodeConfigProvider) ProviderConfig() *config.RawConfig { - return n.Provider.RawConfig -} - -// GraphNodeDotter impl. -func (n *GraphNodeConfigProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node { - return dot.NewNode(name, map[string]string{ - "label": n.Name(), - "shape": "diamond", - }) -} - -// GraphNodeDotterOrigin impl. -func (n *GraphNodeConfigProvider) DotOrigin() bool { - return true -} - -// GraphNodeConfigResource represents a resource within the config graph. -type GraphNodeConfigResource struct { - Resource *config.Resource - - // If this is set to anything other than destroyModeNone, then this - // resource represents a resource that will be destroyed in some way. - DestroyMode GraphNodeDestroyMode - - // Used during DynamicExpand to target indexes - Targets []ResourceAddress -} - -func (n *GraphNodeConfigResource) ConfigType() GraphNodeConfigType { - return GraphNodeConfigTypeResource -} - -func (n *GraphNodeConfigResource) DependableName() []string { - return []string{n.Resource.Id()} -} - -// GraphNodeDependent impl. -func (n *GraphNodeConfigResource) DependentOn() []string { - result := make([]string, len(n.Resource.DependsOn), - (len(n.Resource.RawCount.Variables)+ - len(n.Resource.RawConfig.Variables)+ - len(n.Resource.DependsOn))*2) - copy(result, n.Resource.DependsOn) - - for _, v := range n.Resource.RawCount.Variables { - if vn := varNameForVar(v); vn != "" { - result = append(result, vn) - } - } - for _, v := range n.Resource.RawConfig.Variables { - if vn := varNameForVar(v); vn != "" { - result = append(result, vn) - } - } - for _, p := range n.Resource.Provisioners { - for _, v := range p.ConnInfo.Variables { - if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() { - result = append(result, vn) - } - } - for _, v := range p.RawConfig.Variables { - if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() { - result = append(result, vn) - } - } - } - - return result -} - -// VarWalk calls a callback for all the variables that this resource -// depends on. -func (n *GraphNodeConfigResource) VarWalk(fn func(config.InterpolatedVariable)) { - for _, v := range n.Resource.RawCount.Variables { - fn(v) - } - for _, v := range n.Resource.RawConfig.Variables { - fn(v) - } - for _, p := range n.Resource.Provisioners { - for _, v := range p.ConnInfo.Variables { - fn(v) - } - for _, v := range p.RawConfig.Variables { - fn(v) - } - } -} - -func (n *GraphNodeConfigResource) Name() string { - result := n.Resource.Id() - switch n.DestroyMode { - case DestroyNone: - case DestroyPrimary: - result += " (destroy)" - case DestroyTainted: - result += " (destroy tainted)" - default: - result += " (unknown destroy type)" - } - - return result -} - -// GraphNodeDotter impl. -func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node { - if n.DestroyMode != DestroyNone && !opts.Verbose { - return nil - } - return dot.NewNode(name, map[string]string{ - "label": n.Name(), - "shape": "box", - }) -} - -// GraphNodeDynamicExpandable impl. -func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) { - state, lock := ctx.State() - lock.RLock() - defer lock.RUnlock() - - // Start creating the steps - steps := make([]GraphTransformer, 0, 5) - - // Primary and non-destroy modes are responsible for creating/destroying - // all the nodes, expanding counts. - switch n.DestroyMode { - case DestroyNone: - fallthrough - case DestroyPrimary: - steps = append(steps, &ResourceCountTransformer{ - Resource: n.Resource, - Destroy: n.DestroyMode != DestroyNone, - Targets: n.Targets, - }) - } - - // Additional destroy modifications. - switch n.DestroyMode { - case DestroyPrimary: - // If we're destroying the primary instance, then we want to - // expand orphans, which have all the same semantics in a destroy - // as a primary. - steps = append(steps, &OrphanTransformer{ - State: state, - View: n.Resource.Id(), - Targeting: (len(n.Targets) > 0), - }) - - steps = append(steps, &DeposedTransformer{ - State: state, - View: n.Resource.Id(), - }) - case DestroyTainted: - // If we're only destroying tainted resources, then we only - // want to find tainted resources and destroy them here. - steps = append(steps, &TaintedTransformer{ - State: state, - View: n.Resource.Id(), - }) - } - - // Always end with the root being added - steps = append(steps, &RootTransformer{}) - - // Build the graph - b := &BasicGraphBuilder{Steps: steps} - return b.Build(ctx.Path()) -} - -// GraphNodeAddressable impl. -func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress { - return &ResourceAddress{ - // Indicates no specific index; will match on other three fields - Index: -1, - InstanceType: TypePrimary, - Name: n.Resource.Name, - Type: n.Resource.Type, - } -} - -// GraphNodeTargetable impl. -func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) { - n.Targets = targets -} - -// GraphNodeEvalable impl. -func (n *GraphNodeConfigResource) EvalTree() EvalNode { - return &EvalSequence{ - Nodes: []EvalNode{ - &EvalInterpolate{Config: n.Resource.RawCount}, - &EvalOpFilter{ - Ops: []walkOperation{walkValidate}, - Node: &EvalValidateCount{Resource: n.Resource}, - }, - &EvalCountFixZeroOneBoundary{Resource: n.Resource}, - }, - } -} - -// GraphNodeProviderConsumer -func (n *GraphNodeConfigResource) ProvidedBy() []string { - return []string{resourceProvider(n.Resource.Type, n.Resource.Provider)} -} - -// GraphNodeProvisionerConsumer -func (n *GraphNodeConfigResource) ProvisionedBy() []string { - result := make([]string, len(n.Resource.Provisioners)) - for i, p := range n.Resource.Provisioners { - result[i] = p.Type - } - - return result -} - -// GraphNodeDestroyable -func (n *GraphNodeConfigResource) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy { - // If we're already a destroy node, then don't do anything - if n.DestroyMode != DestroyNone { - return nil - } - - result := &graphNodeResourceDestroy{ - GraphNodeConfigResource: *n, - Original: n, - } - result.DestroyMode = mode - return result -} - -// graphNodeResourceDestroy represents the logical destruction of a -// resource. This node doesn't mean it will be destroyed for sure, but -// instead that if a destroy were to happen, it must happen at this point. -type graphNodeResourceDestroy struct { - GraphNodeConfigResource - Original *GraphNodeConfigResource -} - -func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool { - // CBD is enabled if the resource enables it in addition to us - // being responsible for destroying the primary state. The primary - // state destroy node is the only destroy node that needs to be - // "shuffled" according to the CBD rules, since tainted resources - // don't have the same inverse dependencies. - return n.Original.Resource.Lifecycle.CreateBeforeDestroy && - n.DestroyMode == DestroyPrimary -} - -func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex { - return n.Original -} - -func (n *graphNodeResourceDestroy) DestroyInclude(d *ModuleDiff, s *ModuleState) bool { - switch n.DestroyMode { - case DestroyPrimary: - return n.destroyIncludePrimary(d, s) - case DestroyTainted: - return n.destroyIncludeTainted(d, s) - default: - return true - } -} - -func (n *graphNodeResourceDestroy) destroyIncludeTainted( - d *ModuleDiff, s *ModuleState) bool { - // If there is no state, there can't by any tainted. - if s == nil { - return false - } - - // Grab the ID which is the prefix (in the case count > 0 at some point) - prefix := n.Original.Resource.Id() - - // Go through the resources and find any with our prefix. If there - // are any tainted, we need to keep it. - for k, v := range s.Resources { - if !strings.HasPrefix(k, prefix) { - continue - } - - if len(v.Tainted) > 0 { - return true - } - } - - // We didn't find any tainted nodes, return - return false -} - -func (n *graphNodeResourceDestroy) destroyIncludePrimary( - d *ModuleDiff, s *ModuleState) bool { - // Get the count, and specifically the raw value of the count - // (with interpolations and all). If the count is NOT a static "1", - // then we keep the destroy node no matter what. - // - // The reasoning for this is complicated and not intuitively obvious, - // but I attempt to explain it below. - // - // The destroy transform works by generating the worst case graph, - // with worst case being the case that every resource already exists - // and needs to be destroy/created (force-new). There is a single important - // edge case where this actually results in a real-life cycle: if a - // create-before-destroy (CBD) resource depends on a non-CBD resource. - // Imagine a EC2 instance "foo" with CBD depending on a security - // group "bar" without CBD, and conceptualize the worst case destroy - // order: - // - // 1.) SG must be destroyed (non-CBD) - // 2.) SG must be created/updated - // 3.) EC2 instance must be created (CBD, requires the SG be made) - // 4.) EC2 instance must be destroyed (requires SG be destroyed) - // - // Except, #1 depends on #4, since the SG can't be destroyed while - // an EC2 instance is using it (AWS API requirements). As you can see, - // this is a real life cycle that can't be automatically reconciled - // except under two conditions: - // - // 1.) SG is also CBD. This doesn't work 100% of the time though - // since the non-CBD resource might not support CBD. To make matters - // worse, the entire transitive closure of dependencies must be - // CBD (if the SG depends on a VPC, you have the same problem). - // 2.) EC2 must not CBD. This can't happen automatically because CBD - // is used as a way to ensure zero (or minimal) downtime Terraform - // applies, and it isn't acceptable for TF to ignore this request, - // since it can result in unexpected downtime. - // - // Therefore, we compromise with this edge case here: if there is - // a static count of "1", we prune the diff to remove cycles during a - // graph optimization path if we don't see the resource in the diff. - // If the count is set to ANYTHING other than a static "1" (variable, - // computed attribute, static number greater than 1), then we keep the - // destroy, since it is required for dynamic graph expansion to find - // orphan/tainted count objects. - // - // This isn't ideal logic, but its strictly better without introducing - // new impossibilities. It breaks the cycle in practical cases, and the - // cycle comes back in no cases we've found to be practical, but just - // as the cycle would already exist without this anyways. - count := n.Original.Resource.RawCount - if raw := count.Raw[count.Key]; raw != "1" { - return true - } - - // Okay, we're dealing with a static count. There are a few ways - // to include this resource. - prefix := n.Original.Resource.Id() - - // If we're present in the diff proper, then keep it. - if d != nil { - for k, _ := range d.Resources { - if strings.HasPrefix(k, prefix) { - return true - } - } - } - - // If we're in the state as a primary in any form, then keep it. - // This does a prefix check so it will also catch orphans on count - // decreases to "1". - if s != nil { - for k, v := range s.Resources { - // Ignore exact matches - if k == prefix { - continue - } - - // Ignore anything that doesn't have a "." afterwards so that - // we only get our own resource and any counts on it. - if !strings.HasPrefix(k, prefix+".") { - continue - } - - // Ignore exact matches and the 0'th index. We only care - // about if there is a decrease in count. - if k == prefix+".0" { - continue - } - - if v.Primary != nil { - return true - } - } - - // If we're in the state as _both_ "foo" and "foo.0", then - // keep it, since we treat the latter as an orphan. - _, okOne := s.Resources[prefix] - _, okTwo := s.Resources[prefix+".0"] - if okOne && okTwo { - return true - } - } - - return false -} - -// graphNodeModuleExpanded represents a module where the graph has -// been expanded. It stores the graph of the module as well as a reference -// to the map of variables. -type graphNodeModuleExpanded struct { - Original dag.Vertex - Graph *Graph - InputConfig *config.RawConfig - - // Variables is a map of the input variables. This reference should - // be shared with ModuleInputTransformer in order to create a connection - // where the variables are set properly. - Variables map[string]string -} - -func (n *graphNodeModuleExpanded) Name() string { - return fmt.Sprintf("%s (expanded)", dag.VertexName(n.Original)) -} - -func (n *graphNodeModuleExpanded) ConfigType() GraphNodeConfigType { - return GraphNodeConfigTypeModule -} - -// GraphNodeDotter impl. -func (n *graphNodeModuleExpanded) DotNode(name string, opts *GraphDotOpts) *dot.Node { - return dot.NewNode(name, map[string]string{ - "label": dag.VertexName(n.Original), - "shape": "component", - }) -} - -// GraphNodeEvalable impl. -func (n *graphNodeModuleExpanded) EvalTree() EvalNode { - var resourceConfig *ResourceConfig - return &EvalSequence{ - Nodes: []EvalNode{ - &EvalInterpolate{ - Config: n.InputConfig, - Output: &resourceConfig, - }, - - &EvalVariableBlock{ - Config: &resourceConfig, - Variables: n.Variables, - }, - - &EvalOpFilter{ - Ops: []walkOperation{walkPlanDestroy}, - Node: &EvalSequence{ - Nodes: []EvalNode{ - &EvalDiffDestroyModule{Path: n.Graph.Path}, - }, - }, - }, - }, - } -} - -// GraphNodeSubgraph impl. -func (n *graphNodeModuleExpanded) Subgraph() *Graph { - return n.Graph -} diff --git a/terraform/graph_config_node_module.go b/terraform/graph_config_node_module.go new file mode 100644 index 000000000..08182a6b6 --- /dev/null +++ b/terraform/graph_config_node_module.go @@ -0,0 +1,230 @@ +package terraform + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/dot" +) + +// GraphNodeConfigModule represents a module within the configuration graph. +type GraphNodeConfigModule struct { + Path []string + Module *config.Module + Tree *module.Tree +} + +func (n *GraphNodeConfigModule) ConfigType() GraphNodeConfigType { + return GraphNodeConfigTypeModule +} + +func (n *GraphNodeConfigModule) DependableName() []string { + config := n.Tree.Config() + + result := make([]string, 1, len(config.Outputs)+1) + result[0] = n.Name() + for _, o := range config.Outputs { + result = append(result, fmt.Sprintf("%s.output.%s", n.Name(), o.Name)) + } + + return result +} + +func (n *GraphNodeConfigModule) DependentOn() []string { + vars := n.Module.RawConfig.Variables + result := make([]string, 0, len(vars)) + for _, v := range vars { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + + return result +} + +func (n *GraphNodeConfigModule) Name() string { + return fmt.Sprintf("module.%s", n.Module.Name) +} + +// GraphNodeExpandable +func (n *GraphNodeConfigModule) Expand(b GraphBuilder) (GraphNodeSubgraph, error) { + // Build the graph first + graph, err := b.Build(n.Path) + if err != nil { + return nil, err + } + + // Add the parameters node to the module + t := &ModuleInputTransformer{Variables: make(map[string]string)} + if err := t.Transform(graph); err != nil { + return nil, err + } + + { + // Add the destroy marker to the graph + t := &ModuleDestroyTransformer{} + if err := t.Transform(graph); err != nil { + return nil, err + } + } + + // Build the actual subgraph node + return &graphNodeModuleExpanded{ + Original: n, + Graph: graph, + Variables: t.Variables, + }, nil +} + +// GraphNodeExpandable +func (n *GraphNodeConfigModule) ProvidedBy() []string { + // Build up the list of providers by simply going over our configuration + // to find the providers that are configured there as well as the + // providers that the resources use. + config := n.Tree.Config() + providers := make(map[string]struct{}) + for _, p := range config.ProviderConfigs { + providers[p.Name] = struct{}{} + } + for _, r := range config.Resources { + providers[resourceProvider(r.Type, r.Provider)] = struct{}{} + } + + // Turn the map into a string. This makes sure that the list is + // de-dupped since we could be going over potentially many resources. + result := make([]string, 0, len(providers)) + for p, _ := range providers { + result = append(result, p) + } + + return result +} + +// graphNodeModuleExpanded represents a module where the graph has +// been expanded. It stores the graph of the module as well as a reference +// to the map of variables. +type graphNodeModuleExpanded struct { + Original *GraphNodeConfigModule + Graph *Graph + + // Variables is a map of the input variables. This reference should + // be shared with ModuleInputTransformer in order to create a connection + // where the variables are set properly. + Variables map[string]string +} + +func (n *graphNodeModuleExpanded) Name() string { + return fmt.Sprintf("%s (expanded)", dag.VertexName(n.Original)) +} + +func (n *graphNodeModuleExpanded) ConfigType() GraphNodeConfigType { + return GraphNodeConfigTypeModule +} + +// GraphNodeDependable +func (n *graphNodeModuleExpanded) DependableName() []string { + return n.Original.DependableName() +} + +// GraphNodeDependent +func (n *graphNodeModuleExpanded) DependentOn() []string { + return n.Original.DependentOn() +} + +// GraphNodeDotter impl. +func (n *graphNodeModuleExpanded) DotNode(name string, opts *GraphDotOpts) *dot.Node { + return dot.NewNode(name, map[string]string{ + "label": dag.VertexName(n.Original), + "shape": "component", + }) +} + +// GraphNodeEvalable impl. +func (n *graphNodeModuleExpanded) EvalTree() EvalNode { + var resourceConfig *ResourceConfig + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{ + Config: n.Original.Module.RawConfig, + Output: &resourceConfig, + }, + + &EvalVariableBlock{ + Config: &resourceConfig, + Variables: n.Variables, + }, + }, + } +} + +// GraphNodeFlattenable impl. +func (n *graphNodeModuleExpanded) FlattenGraph() *Graph { + graph := n.Subgraph() + input := n.Original.Module.RawConfig + + // Go over each vertex and do some modifications to the graph for + // flattening. We have to skip some nodes (graphNodeModuleSkippable) + // as well as setup the variable values. + for _, v := range graph.Vertices() { + if sn, ok := v.(graphNodeModuleSkippable); ok && sn.FlattenSkip() { + graph.Remove(v) + continue + } + + // If this is a variable, then look it up in the raw configuration. + // If it exists in the raw configuration, set the value of it. + if vn, ok := v.(*GraphNodeConfigVariable); ok && input != nil { + key := vn.VariableName() + if v, ok := input.Raw[key]; ok { + config, err := config.NewRawConfig(map[string]interface{}{ + key: v, + }) + if err != nil { + // This shouldn't happen because it is already in + // a RawConfig above meaning it worked once before. + panic(err) + } + + // Set the variable value so it is interpolated properly. + // Also set the module so we set the value on it properly. + vn.Module = graph.Path[len(graph.Path)-1] + vn.Value = config + } + } + } + + return graph +} + +// GraphNodeSubgraph impl. +func (n *graphNodeModuleExpanded) Subgraph() *Graph { + return n.Graph +} + +// This interface can be implemented to be skipped/ignored when +// flattening the module graph. +type graphNodeModuleSkippable interface { + FlattenSkip() bool +} + +func modulePrefixStr(p []string) string { + parts := make([]string, 0, len(p)*2) + for _, p := range p[1:] { + parts = append(parts, "module", p) + } + + return strings.Join(parts, ".") +} + +func modulePrefixList(result []string, prefix string) []string { + if prefix != "" { + for i, v := range result { + result[i] = fmt.Sprintf("%s.%s", prefix, v) + } + } + + return result +} diff --git a/terraform/graph_config_node_module_test.go b/terraform/graph_config_node_module_test.go new file mode 100644 index 000000000..6000c20de --- /dev/null +++ b/terraform/graph_config_node_module_test.go @@ -0,0 +1,82 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" +) + +func TestGraphNodeConfigModule_impl(t *testing.T) { + var _ dag.Vertex = new(GraphNodeConfigModule) + var _ dag.NamedVertex = new(GraphNodeConfigModule) + var _ graphNodeConfig = new(GraphNodeConfigModule) + var _ GraphNodeExpandable = new(GraphNodeConfigModule) +} + +func TestGraphNodeConfigModuleExpand(t *testing.T) { + mod := testModule(t, "graph-node-module-expand") + + node := &GraphNodeConfigModule{ + Path: []string{RootModuleName, "child"}, + Module: &config.Module{}, + Tree: nil, + } + + g, err := node.Expand(&BasicGraphBuilder{ + Steps: []GraphTransformer{ + &ConfigTransformer{Module: mod}, + }, + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.Subgraph().String()) + expected := strings.TrimSpace(testGraphNodeModuleExpandStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestGraphNodeConfigModuleExpandFlatten(t *testing.T) { + mod := testModule(t, "graph-node-module-flatten") + + node := &GraphNodeConfigModule{ + Path: []string{RootModuleName, "child"}, + Module: &config.Module{}, + Tree: nil, + } + + g, err := node.Expand(&BasicGraphBuilder{ + Steps: []GraphTransformer{ + &ConfigTransformer{Module: mod}, + }, + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + fg := g.(GraphNodeFlatGraph) + + actual := strings.TrimSpace(fg.FlattenGraph().String()) + expected := strings.TrimSpace(testGraphNodeModuleExpandFlattenStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testGraphNodeModuleExpandStr = ` +aws_instance.bar + aws_instance.foo +aws_instance.foo + module inputs +module inputs +plan-destroy +` + +const testGraphNodeModuleExpandFlattenStr = ` +aws_instance.foo +plan-destroy +` diff --git a/terraform/graph_config_node_output.go b/terraform/graph_config_node_output.go new file mode 100644 index 000000000..0d84a7862 --- /dev/null +++ b/terraform/graph_config_node_output.go @@ -0,0 +1,104 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeConfigOutput represents an output configured within the +// configuration. +type GraphNodeConfigOutput struct { + Output *config.Output +} + +func (n *GraphNodeConfigOutput) Name() string { + return fmt.Sprintf("output.%s", n.Output.Name) +} + +func (n *GraphNodeConfigOutput) ConfigType() GraphNodeConfigType { + return GraphNodeConfigTypeOutput +} + +func (n *GraphNodeConfigOutput) OutputName() string { + return n.Output.Name +} + +func (n *GraphNodeConfigOutput) DependableName() []string { + return []string{n.Name()} +} + +func (n *GraphNodeConfigOutput) DependentOn() []string { + vars := n.Output.RawConfig.Variables + result := make([]string, 0, len(vars)) + for _, v := range vars { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + + return result +} + +// GraphNodeEvalable impl. +func (n *GraphNodeConfigOutput) EvalTree() EvalNode { + return &EvalOpFilter{ + Ops: []walkOperation{walkRefresh, walkPlan, walkApply}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalWriteOutput{ + Name: n.Output.Name, + Value: n.Output.RawConfig, + }, + }, + }, + } +} + +// GraphNodeProxy impl. +func (n *GraphNodeConfigOutput) Proxy() bool { + return true +} + +// GraphNodeDestroyEdgeInclude impl. +func (n *GraphNodeConfigOutput) DestroyEdgeInclude() bool { + return false +} + +// GraphNodeFlattenable impl. +func (n *GraphNodeConfigOutput) Flatten(p []string) (dag.Vertex, error) { + return &GraphNodeConfigOutputFlat{ + GraphNodeConfigOutput: n, + PathValue: p, + }, nil +} + +// Same as GraphNodeConfigOutput, but for flattening +type GraphNodeConfigOutputFlat struct { + *GraphNodeConfigOutput + + PathValue []string +} + +func (n *GraphNodeConfigOutputFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigOutput.Name()) +} + +func (n *GraphNodeConfigOutputFlat) Path() []string { + return n.PathValue +} + +func (n *GraphNodeConfigOutputFlat) DependableName() []string { + return modulePrefixList( + n.GraphNodeConfigOutput.DependableName(), + modulePrefixStr(n.PathValue)) +} + +func (n *GraphNodeConfigOutputFlat) DependentOn() []string { + prefix := modulePrefixStr(n.PathValue) + return modulePrefixList( + n.GraphNodeConfigOutput.DependentOn(), + prefix) +} diff --git a/terraform/graph_config_node_provider.go b/terraform/graph_config_node_provider.go new file mode 100644 index 000000000..c0e15eff3 --- /dev/null +++ b/terraform/graph_config_node_provider.go @@ -0,0 +1,131 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/dot" +) + +// GraphNodeConfigProvider represents a configured provider within the +// configuration graph. These are only immediately in the graph when an +// explicit `provider` configuration block is in the configuration. +type GraphNodeConfigProvider struct { + Provider *config.ProviderConfig +} + +func (n *GraphNodeConfigProvider) Name() string { + return fmt.Sprintf("provider.%s", n.ProviderName()) +} + +func (n *GraphNodeConfigProvider) ConfigType() GraphNodeConfigType { + return GraphNodeConfigTypeProvider +} + +func (n *GraphNodeConfigProvider) DependableName() []string { + return []string{n.Name()} +} + +func (n *GraphNodeConfigProvider) DependentOn() []string { + vars := n.Provider.RawConfig.Variables + result := make([]string, 0, len(vars)) + for _, v := range vars { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + + return result +} + +// GraphNodeEvalable impl. +func (n *GraphNodeConfigProvider) EvalTree() EvalNode { + return ProviderEvalTree(n.ProviderName(), n.Provider.RawConfig) +} + +// GraphNodeProvider implementation +func (n *GraphNodeConfigProvider) ProviderName() string { + if n.Provider.Alias == "" { + return n.Provider.Name + } else { + return fmt.Sprintf("%s.%s", n.Provider.Name, n.Provider.Alias) + } +} + +// GraphNodeProvider implementation +func (n *GraphNodeConfigProvider) ProviderConfig() *config.RawConfig { + return n.Provider.RawConfig +} + +// GraphNodeDotter impl. +func (n *GraphNodeConfigProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node { + return dot.NewNode(name, map[string]string{ + "label": n.Name(), + "shape": "diamond", + }) +} + +// GraphNodeDotterOrigin impl. +func (n *GraphNodeConfigProvider) DotOrigin() bool { + return true +} + +// GraphNodeFlattenable impl. +func (n *GraphNodeConfigProvider) Flatten(p []string) (dag.Vertex, error) { + return &GraphNodeConfigProviderFlat{ + GraphNodeConfigProvider: n, + PathValue: p, + }, nil +} + +// Same as GraphNodeConfigProvider, but for flattening +type GraphNodeConfigProviderFlat struct { + *GraphNodeConfigProvider + + PathValue []string +} + +func (n *GraphNodeConfigProviderFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigProvider.Name()) +} + +func (n *GraphNodeConfigProviderFlat) Path() []string { + return n.PathValue +} + +func (n *GraphNodeConfigProviderFlat) DependableName() []string { + return modulePrefixList( + n.GraphNodeConfigProvider.DependableName(), + modulePrefixStr(n.PathValue)) +} + +func (n *GraphNodeConfigProviderFlat) DependentOn() []string { + prefixed := modulePrefixList( + n.GraphNodeConfigProvider.DependentOn(), + modulePrefixStr(n.PathValue)) + + result := make([]string, len(prefixed), len(prefixed)+1) + copy(result, prefixed) + + // If we're in a module, then depend on our parent's provider + if len(n.PathValue) > 1 { + prefix := modulePrefixStr(n.PathValue[:len(n.PathValue)-1]) + if prefix != "" { + prefix += "." + } + + result = append(result, fmt.Sprintf( + "%s%s", + prefix, n.GraphNodeConfigProvider.Name())) + } + + return result +} + +func (n *GraphNodeConfigProviderFlat) ProviderName() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), + n.GraphNodeConfigProvider.ProviderName()) +} diff --git a/terraform/graph_config_node_resource.go b/terraform/graph_config_node_resource.go new file mode 100644 index 000000000..2ad91d18d --- /dev/null +++ b/terraform/graph_config_node_resource.go @@ -0,0 +1,475 @@ +package terraform + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/dot" +) + +// GraphNodeConfigResource represents a resource within the config graph. +type GraphNodeConfigResource struct { + Resource *config.Resource + + // If this is set to anything other than destroyModeNone, then this + // resource represents a resource that will be destroyed in some way. + DestroyMode GraphNodeDestroyMode + + // Used during DynamicExpand to target indexes + Targets []ResourceAddress +} + +func (n *GraphNodeConfigResource) ConfigType() GraphNodeConfigType { + return GraphNodeConfigTypeResource +} + +func (n *GraphNodeConfigResource) DependableName() []string { + return []string{n.Resource.Id()} +} + +// GraphNodeDependent impl. +func (n *GraphNodeConfigResource) DependentOn() []string { + result := make([]string, len(n.Resource.DependsOn), + (len(n.Resource.RawCount.Variables)+ + len(n.Resource.RawConfig.Variables)+ + len(n.Resource.DependsOn))*2) + copy(result, n.Resource.DependsOn) + + for _, v := range n.Resource.RawCount.Variables { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + for _, v := range n.Resource.RawConfig.Variables { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + for _, p := range n.Resource.Provisioners { + for _, v := range p.ConnInfo.Variables { + if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() { + result = append(result, vn) + } + } + for _, v := range p.RawConfig.Variables { + if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() { + result = append(result, vn) + } + } + } + + return result +} + +// VarWalk calls a callback for all the variables that this resource +// depends on. +func (n *GraphNodeConfigResource) VarWalk(fn func(config.InterpolatedVariable)) { + for _, v := range n.Resource.RawCount.Variables { + fn(v) + } + for _, v := range n.Resource.RawConfig.Variables { + fn(v) + } + for _, p := range n.Resource.Provisioners { + for _, v := range p.ConnInfo.Variables { + fn(v) + } + for _, v := range p.RawConfig.Variables { + fn(v) + } + } +} + +func (n *GraphNodeConfigResource) Name() string { + result := n.Resource.Id() + switch n.DestroyMode { + case DestroyNone: + case DestroyPrimary: + result += " (destroy)" + case DestroyTainted: + result += " (destroy tainted)" + default: + result += " (unknown destroy type)" + } + + return result +} + +// GraphNodeDotter impl. +func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node { + if n.DestroyMode != DestroyNone && !opts.Verbose { + return nil + } + return dot.NewNode(name, map[string]string{ + "label": n.Name(), + "shape": "box", + }) +} + +// GraphNodeFlattenable impl. +func (n *GraphNodeConfigResource) Flatten(p []string) (dag.Vertex, error) { + return &GraphNodeConfigResourceFlat{ + GraphNodeConfigResource: n, + PathValue: p, + }, nil +} + +// GraphNodeDynamicExpandable impl. +func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) { + state, lock := ctx.State() + lock.RLock() + defer lock.RUnlock() + + // Start creating the steps + steps := make([]GraphTransformer, 0, 5) + + // Primary and non-destroy modes are responsible for creating/destroying + // all the nodes, expanding counts. + switch n.DestroyMode { + case DestroyNone: + fallthrough + case DestroyPrimary: + steps = append(steps, &ResourceCountTransformer{ + Resource: n.Resource, + Destroy: n.DestroyMode != DestroyNone, + Targets: n.Targets, + }) + } + + // Additional destroy modifications. + switch n.DestroyMode { + case DestroyPrimary: + // If we're destroying the primary instance, then we want to + // expand orphans, which have all the same semantics in a destroy + // as a primary. + steps = append(steps, &OrphanTransformer{ + State: state, + View: n.Resource.Id(), + Targeting: (len(n.Targets) > 0), + }) + + steps = append(steps, &DeposedTransformer{ + State: state, + View: n.Resource.Id(), + }) + case DestroyTainted: + // If we're only destroying tainted resources, then we only + // want to find tainted resources and destroy them here. + steps = append(steps, &TaintedTransformer{ + State: state, + View: n.Resource.Id(), + }) + } + + // Always end with the root being added + steps = append(steps, &RootTransformer{}) + + // Build the graph + b := &BasicGraphBuilder{Steps: steps} + return b.Build(ctx.Path()) +} + +// GraphNodeAddressable impl. +func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress { + return &ResourceAddress{ + // Indicates no specific index; will match on other three fields + Index: -1, + InstanceType: TypePrimary, + Name: n.Resource.Name, + Type: n.Resource.Type, + } +} + +// GraphNodeTargetable impl. +func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) { + n.Targets = targets +} + +// GraphNodeEvalable impl. +func (n *GraphNodeConfigResource) EvalTree() EvalNode { + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{Config: n.Resource.RawCount}, + &EvalOpFilter{ + Ops: []walkOperation{walkValidate}, + Node: &EvalValidateCount{Resource: n.Resource}, + }, + &EvalCountFixZeroOneBoundary{Resource: n.Resource}, + }, + } +} + +// GraphNodeProviderConsumer +func (n *GraphNodeConfigResource) ProvidedBy() []string { + return []string{resourceProvider(n.Resource.Type, n.Resource.Provider)} +} + +// GraphNodeProvisionerConsumer +func (n *GraphNodeConfigResource) ProvisionedBy() []string { + result := make([]string, len(n.Resource.Provisioners)) + for i, p := range n.Resource.Provisioners { + result[i] = p.Type + } + + return result +} + +// GraphNodeDestroyable +func (n *GraphNodeConfigResource) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy { + // If we're already a destroy node, then don't do anything + if n.DestroyMode != DestroyNone { + return nil + } + + result := &graphNodeResourceDestroy{ + GraphNodeConfigResource: *n, + Original: n, + } + result.DestroyMode = mode + return result +} + +// Same as GraphNodeConfigResource, but for flattening +type GraphNodeConfigResourceFlat struct { + *GraphNodeConfigResource + + PathValue []string +} + +func (n *GraphNodeConfigResourceFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigResource.Name()) +} + +func (n *GraphNodeConfigResourceFlat) Path() []string { + return n.PathValue +} + +func (n *GraphNodeConfigResourceFlat) DependableName() []string { + return modulePrefixList( + n.GraphNodeConfigResource.DependableName(), + modulePrefixStr(n.PathValue)) +} + +func (n *GraphNodeConfigResourceFlat) DependentOn() []string { + prefix := modulePrefixStr(n.PathValue) + return modulePrefixList( + n.GraphNodeConfigResource.DependentOn(), + prefix) +} + +func (n *GraphNodeConfigResourceFlat) ProvidedBy() []string { + prefix := modulePrefixStr(n.PathValue) + return modulePrefixList( + n.GraphNodeConfigResource.ProvidedBy(), + prefix) +} + +func (n *GraphNodeConfigResourceFlat) ProvisionedBy() []string { + prefix := modulePrefixStr(n.PathValue) + return modulePrefixList( + n.GraphNodeConfigResource.ProvisionedBy(), + prefix) +} + +// GraphNodeDestroyable impl. +func (n *GraphNodeConfigResourceFlat) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy { + // Get our parent destroy node. If we don't have any, just return + raw := n.GraphNodeConfigResource.DestroyNode(mode) + if raw == nil { + return nil + } + + node, ok := raw.(*graphNodeResourceDestroy) + if !ok { + panic(fmt.Sprintf("unknown destroy node: %s %T", dag.VertexName(raw), raw)) + } + + // Otherwise, wrap it so that it gets the proper module treatment. + return &graphNodeResourceDestroyFlat{ + graphNodeResourceDestroy: node, + PathValue: n.PathValue, + } +} + +type graphNodeResourceDestroyFlat struct { + *graphNodeResourceDestroy + + PathValue []string +} + +func (n *graphNodeResourceDestroyFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.graphNodeResourceDestroy.Name()) +} + +func (n *graphNodeResourceDestroyFlat) Path() []string { + return n.PathValue +} + +// graphNodeResourceDestroy represents the logical destruction of a +// resource. This node doesn't mean it will be destroyed for sure, but +// instead that if a destroy were to happen, it must happen at this point. +type graphNodeResourceDestroy struct { + GraphNodeConfigResource + Original *GraphNodeConfigResource +} + +func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool { + // CBD is enabled if the resource enables it in addition to us + // being responsible for destroying the primary state. The primary + // state destroy node is the only destroy node that needs to be + // "shuffled" according to the CBD rules, since tainted resources + // don't have the same inverse dependencies. + return n.Original.Resource.Lifecycle.CreateBeforeDestroy && + n.DestroyMode == DestroyPrimary +} + +func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex { + return n.Original +} + +func (n *graphNodeResourceDestroy) DestroyInclude(d *ModuleDiff, s *ModuleState) bool { + switch n.DestroyMode { + case DestroyPrimary: + return n.destroyIncludePrimary(d, s) + case DestroyTainted: + return n.destroyIncludeTainted(d, s) + default: + return true + } +} + +func (n *graphNodeResourceDestroy) destroyIncludeTainted( + d *ModuleDiff, s *ModuleState) bool { + // If there is no state, there can't by any tainted. + if s == nil { + return false + } + + // Grab the ID which is the prefix (in the case count > 0 at some point) + prefix := n.Original.Resource.Id() + + // Go through the resources and find any with our prefix. If there + // are any tainted, we need to keep it. + for k, v := range s.Resources { + if !strings.HasPrefix(k, prefix) { + continue + } + + if len(v.Tainted) > 0 { + return true + } + } + + // We didn't find any tainted nodes, return + return false +} + +func (n *graphNodeResourceDestroy) destroyIncludePrimary( + d *ModuleDiff, s *ModuleState) bool { + // Get the count, and specifically the raw value of the count + // (with interpolations and all). If the count is NOT a static "1", + // then we keep the destroy node no matter what. + // + // The reasoning for this is complicated and not intuitively obvious, + // but I attempt to explain it below. + // + // The destroy transform works by generating the worst case graph, + // with worst case being the case that every resource already exists + // and needs to be destroy/created (force-new). There is a single important + // edge case where this actually results in a real-life cycle: if a + // create-before-destroy (CBD) resource depends on a non-CBD resource. + // Imagine a EC2 instance "foo" with CBD depending on a security + // group "bar" without CBD, and conceptualize the worst case destroy + // order: + // + // 1.) SG must be destroyed (non-CBD) + // 2.) SG must be created/updated + // 3.) EC2 instance must be created (CBD, requires the SG be made) + // 4.) EC2 instance must be destroyed (requires SG be destroyed) + // + // Except, #1 depends on #4, since the SG can't be destroyed while + // an EC2 instance is using it (AWS API requirements). As you can see, + // this is a real life cycle that can't be automatically reconciled + // except under two conditions: + // + // 1.) SG is also CBD. This doesn't work 100% of the time though + // since the non-CBD resource might not support CBD. To make matters + // worse, the entire transitive closure of dependencies must be + // CBD (if the SG depends on a VPC, you have the same problem). + // 2.) EC2 must not CBD. This can't happen automatically because CBD + // is used as a way to ensure zero (or minimal) downtime Terraform + // applies, and it isn't acceptable for TF to ignore this request, + // since it can result in unexpected downtime. + // + // Therefore, we compromise with this edge case here: if there is + // a static count of "1", we prune the diff to remove cycles during a + // graph optimization path if we don't see the resource in the diff. + // If the count is set to ANYTHING other than a static "1" (variable, + // computed attribute, static number greater than 1), then we keep the + // destroy, since it is required for dynamic graph expansion to find + // orphan/tainted count objects. + // + // This isn't ideal logic, but its strictly better without introducing + // new impossibilities. It breaks the cycle in practical cases, and the + // cycle comes back in no cases we've found to be practical, but just + // as the cycle would already exist without this anyways. + count := n.Original.Resource.RawCount + if raw := count.Raw[count.Key]; raw != "1" { + return true + } + + // Okay, we're dealing with a static count. There are a few ways + // to include this resource. + prefix := n.Original.Resource.Id() + + // If we're present in the diff proper, then keep it. + if d != nil { + for k, _ := range d.Resources { + if strings.HasPrefix(k, prefix) { + return true + } + } + } + + // If we're in the state as a primary in any form, then keep it. + // This does a prefix check so it will also catch orphans on count + // decreases to "1". + if s != nil { + for k, v := range s.Resources { + // Ignore exact matches + if k == prefix { + continue + } + + // Ignore anything that doesn't have a "." afterwards so that + // we only get our own resource and any counts on it. + if !strings.HasPrefix(k, prefix+".") { + continue + } + + // Ignore exact matches and the 0'th index. We only care + // about if there is a decrease in count. + if k == prefix+".0" { + continue + } + + if v.Primary != nil { + return true + } + } + + // If we're in the state as _both_ "foo" and "foo.0", then + // keep it, since we treat the latter as an orphan. + _, okOne := s.Resources[prefix] + _, okTwo := s.Resources[prefix+".0"] + if okOne && okTwo { + return true + } + } + + return false +} diff --git a/terraform/graph_config_node_test.go b/terraform/graph_config_node_test.go index 13848663d..670f07b39 100644 --- a/terraform/graph_config_node_test.go +++ b/terraform/graph_config_node_test.go @@ -2,50 +2,18 @@ package terraform import ( "reflect" - "strings" "testing" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/dag" ) -func TestGraphNodeConfigModule_impl(t *testing.T) { - var _ dag.Vertex = new(GraphNodeConfigModule) - var _ dag.NamedVertex = new(GraphNodeConfigModule) - var _ graphNodeConfig = new(GraphNodeConfigModule) - var _ GraphNodeExpandable = new(GraphNodeConfigModule) -} - -func TestGraphNodeConfigModuleExpand(t *testing.T) { - mod := testModule(t, "graph-node-module-expand") - - node := &GraphNodeConfigModule{ - Path: []string{RootModuleName, "child"}, - Module: &config.Module{}, - Tree: nil, - } - - g, err := node.Expand(&BasicGraphBuilder{ - Steps: []GraphTransformer{ - &ConfigTransformer{Module: mod}, - }, - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - actual := strings.TrimSpace(g.Subgraph().String()) - expected := strings.TrimSpace(testGraphNodeModuleExpandStr) - if actual != expected { - t.Fatalf("bad:\n\n%s", actual) - } -} - func TestGraphNodeConfigOutput_impl(t *testing.T) { var _ dag.Vertex = new(GraphNodeConfigOutput) var _ dag.NamedVertex = new(GraphNodeConfigOutput) var _ graphNodeConfig = new(GraphNodeConfigOutput) var _ GraphNodeOutput = new(GraphNodeConfigOutput) + var _ GraphNodeProxy = new(GraphNodeConfigOutput) } func TestGraphNodeConfigProvider_impl(t *testing.T) { @@ -140,11 +108,3 @@ func TestGraphNodeConfigResource_ProvisionedBy(t *testing.T) { t.Fatalf("bad: %#v", actual) } } - -const testGraphNodeModuleExpandStr = ` -aws_instance.bar - aws_instance.foo -aws_instance.foo - module inputs -module inputs -` diff --git a/terraform/graph_config_node_type.go b/terraform/graph_config_node_type.go index f0196096f..42dd8dc9f 100644 --- a/terraform/graph_config_node_type.go +++ b/terraform/graph_config_node_type.go @@ -12,4 +12,5 @@ const ( GraphNodeConfigTypeProvider GraphNodeConfigTypeModule GraphNodeConfigTypeOutput + GraphNodeConfigTypeVariable ) diff --git a/terraform/graph_config_node_variable.go b/terraform/graph_config_node_variable.go new file mode 100644 index 000000000..54c1c8343 --- /dev/null +++ b/terraform/graph_config_node_variable.go @@ -0,0 +1,137 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeConfigVariable represents a Variable in the config. +type GraphNodeConfigVariable struct { + Variable *config.Variable + + // Value, if non-nil, will be used to set the value of the variable + // during evaluation. If this is nil, evaluation will do nothing. + // + // Module is the name of the module to set the variables on. + Module string + Value *config.RawConfig + + depPrefix string +} + +func (n *GraphNodeConfigVariable) Name() string { + return fmt.Sprintf("var.%s", n.Variable.Name) +} + +func (n *GraphNodeConfigVariable) ConfigType() GraphNodeConfigType { + return GraphNodeConfigTypeVariable +} + +func (n *GraphNodeConfigVariable) DependableName() []string { + return []string{n.Name()} +} + +func (n *GraphNodeConfigVariable) DependentOn() []string { + // If we don't have any value set, we don't depend on anything + if n.Value == nil { + return nil + } + + // Get what we depend on based on our value + vars := n.Value.Variables + result := make([]string, 0, len(vars)) + for _, v := range vars { + if vn := varNameForVar(v); vn != "" { + result = append(result, vn) + } + } + + return result +} + +func (n *GraphNodeConfigVariable) VariableName() string { + return n.Variable.Name +} + +// GraphNodeProxy impl. +func (n *GraphNodeConfigVariable) Proxy() bool { + return true +} + +// GraphNodeEvalable impl. +func (n *GraphNodeConfigVariable) EvalTree() EvalNode { + // If we have no value, do nothing + if n.Value == nil { + return &EvalNoop{} + } + + // Otherwise, interpolate the value of this variable and set it + // within the variables mapping. + var config *ResourceConfig + variables := make(map[string]string) + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{ + Config: n.Value, + Output: &config, + }, + + &EvalVariableBlock{ + Config: &config, + Variables: variables, + }, + + &EvalSetVariables{ + Module: &n.Module, + Variables: variables, + }, + }, + } +} + +// GraphNodeFlattenable impl. +func (n *GraphNodeConfigVariable) Flatten(p []string) (dag.Vertex, error) { + return &GraphNodeConfigVariableFlat{ + GraphNodeConfigVariable: n, + PathValue: p, + }, nil +} + +type GraphNodeConfigVariableFlat struct { + *GraphNodeConfigVariable + + PathValue []string +} + +func (n *GraphNodeConfigVariableFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigVariable.Name()) +} + +func (n *GraphNodeConfigVariableFlat) DependableName() []string { + return []string{n.Name()} +} + +func (n *GraphNodeConfigVariableFlat) DependentOn() []string { + // We only wrap the dependencies and such if we have a path that is + // longer than 2 elements (root, child, more). This is because when + // flattened, variables can point outside the graph. + prefix := "" + if len(n.PathValue) > 2 { + prefix = modulePrefixStr(n.PathValue[:len(n.PathValue)-1]) + } + + return modulePrefixList( + n.GraphNodeConfigVariable.DependentOn(), + prefix) +} + +func (n *GraphNodeConfigVariableFlat) Path() []string { + if len(n.PathValue) > 2 { + return n.PathValue[:len(n.PathValue)-1] + } + + return nil +} diff --git a/terraform/graph_config_node_variable_test.go b/terraform/graph_config_node_variable_test.go new file mode 100644 index 000000000..9f0251b42 --- /dev/null +++ b/terraform/graph_config_node_variable_test.go @@ -0,0 +1,21 @@ +package terraform + +import ( + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestGraphNodeConfigVariable_impl(t *testing.T) { + var _ dag.Vertex = new(GraphNodeConfigVariable) + var _ dag.NamedVertex = new(GraphNodeConfigVariable) + var _ graphNodeConfig = new(GraphNodeConfigVariable) + var _ GraphNodeProxy = new(GraphNodeConfigVariable) +} + +func TestGraphNodeConfigVariableFlat_impl(t *testing.T) { + var _ dag.Vertex = new(GraphNodeConfigVariableFlat) + var _ dag.NamedVertex = new(GraphNodeConfigVariableFlat) + var _ graphNodeConfig = new(GraphNodeConfigVariableFlat) + var _ GraphNodeProxy = new(GraphNodeConfigVariableFlat) +} diff --git a/terraform/graph_interface_subgraph.go b/terraform/graph_interface_subgraph.go new file mode 100644 index 000000000..2897eb546 --- /dev/null +++ b/terraform/graph_interface_subgraph.go @@ -0,0 +1,7 @@ +package terraform + +// GraphNodeSubPath says that a node is part of a graph with a +// different path, and the context should be adjusted accordingly. +type GraphNodeSubPath interface { + Path() []string +} diff --git a/terraform/graph_walk.go b/terraform/graph_walk.go index f5da6c093..ef3a4f6f5 100644 --- a/terraform/graph_walk.go +++ b/terraform/graph_walk.go @@ -7,8 +7,8 @@ import ( // GraphWalker is an interface that can be implemented that when used // with Graph.Walk will invoke the given callbacks under certain events. type GraphWalker interface { - EnterGraph(*Graph) EvalContext - ExitGraph(*Graph) + EnterPath([]string) EvalContext + ExitPath([]string) EnterVertex(dag.Vertex) ExitVertex(dag.Vertex, error) EnterEvalTree(dag.Vertex, EvalNode) EvalNode @@ -20,8 +20,8 @@ type GraphWalker interface { // implementing all the required functions. type NullGraphWalker struct{} -func (NullGraphWalker) EnterGraph(*Graph) EvalContext { return nil } -func (NullGraphWalker) ExitGraph(*Graph) {} +func (NullGraphWalker) EnterPath([]string) EvalContext { return nil } +func (NullGraphWalker) ExitPath([]string) {} func (NullGraphWalker) EnterVertex(dag.Vertex) {} func (NullGraphWalker) ExitVertex(dag.Vertex, error) {} func (NullGraphWalker) EnterEvalTree(v dag.Vertex, n EvalNode) EvalNode { return n } diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index 64a9f3d02..314d18972 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -26,6 +26,8 @@ type ContextGraphWalker struct { once sync.Once contexts map[string]*BuiltinEvalContext contextLock sync.Mutex + interpolaterVars map[string]map[string]string + interpolaterVarLock sync.Mutex providerCache map[string]ResourceProvider providerConfigCache map[string]*ResourceConfig providerLock sync.Mutex @@ -33,29 +35,36 @@ type ContextGraphWalker struct { provisionerLock sync.Mutex } -func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext { +func (w *ContextGraphWalker) EnterPath(path []string) EvalContext { w.once.Do(w.init) w.contextLock.Lock() defer w.contextLock.Unlock() // If we already have a context for this path cached, use that - key := PathCacheKey(g.Path) + key := PathCacheKey(path) if ctx, ok := w.contexts[key]; ok { return ctx } - // Variables should be our context variables, but these only apply - // to the root module. As we enter subgraphs, we don't want to set - // variables, which is set by the SetVariables EvalContext function. - variables := w.Context.variables - if len(g.Path) > 1 { - // We're in a submodule, the variables should be empty - variables = make(map[string]string) + // Setup the variables for this interpolater + variables := make(map[string]string) + if len(path) <= 1 { + for k, v := range w.Context.variables { + variables[k] = v + } } + w.interpolaterVarLock.Lock() + if m, ok := w.interpolaterVars[key]; ok { + for k, v := range m { + variables[k] = v + } + } + w.interpolaterVars[key] = variables + w.interpolaterVarLock.Unlock() ctx := &BuiltinEvalContext{ - PathValue: g.Path, + PathValue: path, Hooks: w.Context.hooks, InputValue: w.Context.uiInput, Providers: w.Context.providers, @@ -77,6 +86,8 @@ func (w *ContextGraphWalker) EnterGraph(g *Graph) EvalContext { StateLock: &w.Context.stateLock, Variables: variables, }, + InterpolaterVars: w.interpolaterVars, + InterpolaterVarLock: &w.interpolaterVarLock, } w.contexts[key] = ctx @@ -131,4 +142,5 @@ func (w *ContextGraphWalker) init() { w.providerCache = make(map[string]ResourceProvider, 5) w.providerConfigCache = make(map[string]*ResourceConfig, 5) w.provisionerCache = make(map[string]ResourceProvisioner, 5) + w.interpolaterVars = make(map[string]map[string]string, 5) } diff --git a/terraform/graphnodeconfigtype_string.go b/terraform/graphnodeconfigtype_string.go index d0748979e..de24c2dcd 100644 --- a/terraform/graphnodeconfigtype_string.go +++ b/terraform/graphnodeconfigtype_string.go @@ -4,9 +4,9 @@ package terraform import "fmt" -const _GraphNodeConfigType_name = "GraphNodeConfigTypeInvalidGraphNodeConfigTypeResourceGraphNodeConfigTypeProviderGraphNodeConfigTypeModuleGraphNodeConfigTypeOutput" +const _GraphNodeConfigType_name = "GraphNodeConfigTypeInvalidGraphNodeConfigTypeResourceGraphNodeConfigTypeProviderGraphNodeConfigTypeModuleGraphNodeConfigTypeOutputGraphNodeConfigTypeVariable" -var _GraphNodeConfigType_index = [...]uint8{0, 26, 53, 80, 105, 130} +var _GraphNodeConfigType_index = [...]uint8{0, 26, 53, 80, 105, 130, 157} func (i GraphNodeConfigType) String() string { if i < 0 || i+1 >= GraphNodeConfigType(len(_GraphNodeConfigType_index)) { diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 09c330792..878078668 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -362,6 +362,12 @@ module.child: leader = 1 ` +const testTerraformApplyModuleDestroyOrderStr = ` + +module.child: + +` + const testTerraformApplyMultiProviderStr = ` aws_instance.bar: ID = foo diff --git a/terraform/test-fixtures/apply-module-destroy-order/child/main.tf b/terraform/test-fixtures/apply-module-destroy-order/child/main.tf new file mode 100644 index 000000000..c3a5aecd6 --- /dev/null +++ b/terraform/test-fixtures/apply-module-destroy-order/child/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "a" {} + +output "a_output" { + value = "${aws_instance.a.id}" +} diff --git a/terraform/test-fixtures/apply-module-destroy-order/main.tf b/terraform/test-fixtures/apply-module-destroy-order/main.tf new file mode 100644 index 000000000..2a6bcce37 --- /dev/null +++ b/terraform/test-fixtures/apply-module-destroy-order/main.tf @@ -0,0 +1,7 @@ +module "child" { + source = "./child" +} + +resource "aws_instance" "b" { + blah = "${module.child.a_output}" +} diff --git a/terraform/test-fixtures/apply-provisioner-compute/main.tf b/terraform/test-fixtures/apply-provisioner-compute/main.tf index e0ff5507a..e503eddd5 100644 --- a/terraform/test-fixtures/apply-provisioner-compute/main.tf +++ b/terraform/test-fixtures/apply-provisioner-compute/main.tf @@ -1,3 +1,5 @@ +variable "value" {} + resource "aws_instance" "foo" { num = "2" compute = "dynamical" diff --git a/terraform/test-fixtures/apply-provisioner-conninfo/main.tf b/terraform/test-fixtures/apply-provisioner-conninfo/main.tf index f0bfe43ab..029895fa1 100644 --- a/terraform/test-fixtures/apply-provisioner-conninfo/main.tf +++ b/terraform/test-fixtures/apply-provisioner-conninfo/main.tf @@ -1,3 +1,6 @@ +variable "pass" {} +variable "value" {} + resource "aws_instance" "foo" { num = "2" compute = "dynamical" diff --git a/terraform/test-fixtures/graph-modules/consul/main.tf b/terraform/test-fixtures/graph-modules/consul/main.tf index aa8f6f032..9e22d04d8 100644 --- a/terraform/test-fixtures/graph-modules/consul/main.tf +++ b/terraform/test-fixtures/graph-modules/consul/main.tf @@ -1 +1,3 @@ resource "aws_instance" "server" {} + +output "security_group" { value = "" } diff --git a/terraform/test-fixtures/graph-node-module-flatten/child/main.tf b/terraform/test-fixtures/graph-node-module-flatten/child/main.tf new file mode 100644 index 000000000..919f140bb --- /dev/null +++ b/terraform/test-fixtures/graph-node-module-flatten/child/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "foo" {} diff --git a/terraform/test-fixtures/graph-node-module-flatten/main.tf b/terraform/test-fixtures/graph-node-module-flatten/main.tf new file mode 100644 index 000000000..0f6991c53 --- /dev/null +++ b/terraform/test-fixtures/graph-node-module-flatten/main.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/terraform/test-fixtures/plan-module-destroy-multivar/main.tf b/terraform/test-fixtures/plan-module-destroy-multivar/main.tf index ae334b5a5..2f965b68c 100644 --- a/terraform/test-fixtures/plan-module-destroy-multivar/main.tf +++ b/terraform/test-fixtures/plan-module-destroy-multivar/main.tf @@ -2,4 +2,3 @@ module "child" { source = "./child" instance_count = "2" } - diff --git a/terraform/test-fixtures/plan-module-provider-defaults-var/main.tf b/terraform/test-fixtures/plan-module-provider-defaults-var/main.tf index e6e3f1c29..d3c34908b 100644 --- a/terraform/test-fixtures/plan-module-provider-defaults-var/main.tf +++ b/terraform/test-fixtures/plan-module-provider-defaults-var/main.tf @@ -7,3 +7,5 @@ provider "aws" { } resource "aws_instance" "foo" {} + +variable "foo" {} diff --git a/terraform/test-fixtures/transform-flatten/child/main.tf b/terraform/test-fixtures/transform-flatten/child/main.tf new file mode 100644 index 000000000..7371f826d --- /dev/null +++ b/terraform/test-fixtures/transform-flatten/child/main.tf @@ -0,0 +1,9 @@ +variable "var" {} + +resource "aws_instance" "child" { + value = "${var.var}" +} + +output "output" { + value = "${aws_instance.child.value}" +} diff --git a/terraform/test-fixtures/transform-flatten/main.tf b/terraform/test-fixtures/transform-flatten/main.tf new file mode 100644 index 000000000..179e151a3 --- /dev/null +++ b/terraform/test-fixtures/transform-flatten/main.tf @@ -0,0 +1,12 @@ +module "child" { + source = "./child" + var = "${aws_instance.parent.value}" +} + +resource "aws_instance" "parent" { + value = "foo" +} + +resource "aws_instance" "parent-output" { + value = "${module.child.output}" +} diff --git a/terraform/test-fixtures/validate-module-deps-cycle/a/main.tf b/terraform/test-fixtures/validate-module-deps-cycle/a/main.tf new file mode 100644 index 000000000..3d3b01634 --- /dev/null +++ b/terraform/test-fixtures/validate-module-deps-cycle/a/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "a" { } + +output "output" { + value = "${aws_instance.a.id}" +} diff --git a/terraform/test-fixtures/validate-module-deps-cycle/b/main.tf b/terraform/test-fixtures/validate-module-deps-cycle/b/main.tf new file mode 100644 index 000000000..07d769c98 --- /dev/null +++ b/terraform/test-fixtures/validate-module-deps-cycle/b/main.tf @@ -0,0 +1,5 @@ +variable "input" {} + +resource "aws_instance" "b" { + name = "${var.input}" +} diff --git a/terraform/test-fixtures/validate-module-deps-cycle/main.tf b/terraform/test-fixtures/validate-module-deps-cycle/main.tf new file mode 100644 index 000000000..11ddb64bf --- /dev/null +++ b/terraform/test-fixtures/validate-module-deps-cycle/main.tf @@ -0,0 +1,8 @@ +module "a" { + source = "./a" +} + +module "b" { + source = "./b" + input = "${module.a.output}" +} diff --git a/terraform/transform_config.go b/terraform/transform_config.go index 3b115688b..3c061eeb1 100644 --- a/terraform/transform_config.go +++ b/terraform/transform_config.go @@ -37,7 +37,16 @@ func (t *ConfigTransformer) Transform(g *Graph) error { // Create the node list we'll use for the graph nodes := make([]graphNodeConfig, 0, - (len(config.ProviderConfigs)+len(config.Modules)+len(config.Resources))*2) + (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}) + } // Write all the provider configs out for _, pc := range config.ProviderConfigs { @@ -96,9 +105,11 @@ func (t *ConfigTransformer) Transform(g *Graph) error { func varNameForVar(raw config.InterpolatedVariable) string { switch v := raw.(type) { case *config.ModuleVariable: - return fmt.Sprintf("module.%s", v.Name) + 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_test.go b/terraform/transform_config_test.go index 75570d3f6..4ba88a358 100644 --- a/terraform/transform_config_test.go +++ b/terraform/transform_config_test.go @@ -111,12 +111,14 @@ func TestConfigTransformer_errMissingDeps(t *testing.T) { 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 = ` diff --git a/terraform/transform_destroy.go b/terraform/transform_destroy.go index 7d4293ef3..a3fe737af 100644 --- a/terraform/transform_destroy.go +++ b/terraform/transform_destroy.go @@ -45,6 +45,13 @@ type GraphNodeDestroyPrunable interface { DestroyInclude(*ModuleDiff, *ModuleState) bool } +// GraphNodeEdgeInclude can be implemented to not include something +// as an edge within the destroy graph. This is usually done because it +// might cause unnecessary cycles. +type GraphNodeDestroyEdgeInclude interface { + DestroyEdgeInclude() bool +} + // DestroyTransformer is a GraphTransformer that creates the destruction // nodes for things that _might_ be destroyed. type DestroyTransformer struct{} @@ -102,6 +109,12 @@ func (t *DestroyTransformer) transform( // Inherit all the edges from the old node downEdges := g.DownEdges(v).List() for _, edgeRaw := range downEdges { + // If this thing specifically requests to not be depended on + // by destroy nodes, then don't. + if i, ok := edgeRaw.(GraphNodeDestroyEdgeInclude); ok && !i.DestroyEdgeInclude() { + continue + } + g.Connect(dag.BasicEdge(n, edgeRaw.(dag.Vertex))) } @@ -204,15 +217,6 @@ type PruneDestroyTransformer struct { } func (t *PruneDestroyTransformer) Transform(g *Graph) error { - var modDiff *ModuleDiff - var modState *ModuleState - if t.Diff != nil { - modDiff = t.Diff.ModuleByPath(g.Path) - } - if t.State != nil { - modState = t.State.ModuleByPath(g.Path) - } - for _, v := range g.Vertices() { // If it is not a destroyer, we don't care dn, ok := v.(GraphNodeDestroyPrunable) @@ -220,6 +224,20 @@ func (t *PruneDestroyTransformer) Transform(g *Graph) error { continue } + path := g.Path + if pn, ok := v.(GraphNodeSubPath); ok { + path = pn.Path() + } + + var modDiff *ModuleDiff + var modState *ModuleState + if t.Diff != nil { + modDiff = t.Diff.ModuleByPath(path) + } + if t.State != nil { + modState = t.State.ModuleByPath(path) + } + // Remove it if we should if !dn.DestroyInclude(modDiff, modState) { g.Remove(v) diff --git a/terraform/transform_flatten.go b/terraform/transform_flatten.go new file mode 100644 index 000000000..4f9df8fb8 --- /dev/null +++ b/terraform/transform_flatten.go @@ -0,0 +1,101 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeFlatGraph must be implemented by nodes that have subgraphs +// that they want flattened into the graph. +type GraphNodeFlatGraph interface { + FlattenGraph() *Graph +} + +// GraphNodeFlattenable must be implemented by all nodes that can be +// flattened. If a FlattenGraph returns any nodes that can't be flattened, +// it will be an error. +// +// If Flatten returns nil for the Vertex along with a nil error, it will +// removed from the graph. +type GraphNodeFlattenable interface { + Flatten(path []string) (dag.Vertex, error) +} + +// FlattenTransformer is a transformer that goes through the graph, finds +// subgraphs that can be flattened, and flattens them into this graph, +// removing the prior subgraph node. +type FlattenTransformer struct{} + +func (t *FlattenTransformer) Transform(g *Graph) error { + for _, v := range g.Vertices() { + fn, ok := v.(GraphNodeFlatGraph) + if !ok { + continue + } + + // If we don't want to be flattened, don't do it + subgraph := fn.FlattenGraph() + if subgraph == nil { + continue + } + + // Get all the things that depend on this node. We'll re-connect + // dependents later. We have to copy these here since the UpEdges + // value will be deleted after the Remove below. + dependents := make([]dag.Vertex, 0, 5) + for _, v := range g.UpEdges(v).List() { + dependents = append(dependents, v) + } + + // Remove the old node + g.Remove(v) + + // Go through the subgraph and flatten all the nodes + for _, sv := range subgraph.Vertices() { + fn, ok := sv.(GraphNodeFlattenable) + if !ok { + return fmt.Errorf( + "unflattenable node: %s %T", + dag.VertexName(sv), sv) + } + + v, err := fn.Flatten(subgraph.Path) + if err != nil { + return fmt.Errorf( + "error flattening %s (%T): %s", + dag.VertexName(sv), sv, err) + } + + if v == nil { + subgraph.Remove(v) + } else { + subgraph.Replace(sv, v) + } + } + + // Now that we've handled any changes to the graph that are + // needed, we can add them all to our graph along with their edges. + for _, sv := range subgraph.Vertices() { + g.Add(sv) + } + for _, se := range subgraph.Edges() { + g.Connect(se) + } + + // Connect the dependencies for all the new nodes that we added. + // This will properly connect variables to their sources, for example. + for _, sv := range subgraph.Vertices() { + g.ConnectDependent(sv) + } + + // Re-connect all the things that dependend on the graph + // we just flattened. This should connect them back into the + // correct nodes if their DependentOn() is setup correctly. + for _, v := range dependents { + g.ConnectDependent(v) + } + } + + return nil +} diff --git a/terraform/transform_flatten_test.go b/terraform/transform_flatten_test.go new file mode 100644 index 000000000..92f7cac7a --- /dev/null +++ b/terraform/transform_flatten_test.go @@ -0,0 +1,95 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestFlattenTransformer(t *testing.T) { + mod := testModule(t, "transform-flatten") + + var b BasicGraphBuilder + b = BasicGraphBuilder{ + Steps: []GraphTransformer{ + &ConfigTransformer{Module: mod}, + &VertexTransformer{ + Transforms: []GraphVertexTransformer{ + &ExpandTransform{ + Builder: &b, + }, + }, + }, + &FlattenTransformer{}, + }, + } + + g, err := b.Build(rootModulePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformFlattenStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestFlattenTransformer_withProxy(t *testing.T) { + mod := testModule(t, "transform-flatten") + + var b BasicGraphBuilder + b = BasicGraphBuilder{ + Steps: []GraphTransformer{ + &ConfigTransformer{Module: mod}, + &VertexTransformer{ + Transforms: []GraphVertexTransformer{ + &ExpandTransform{ + Builder: &b, + }, + }, + }, + &FlattenTransformer{}, + &ProxyTransformer{}, + }, + } + + g, err := b.Build(rootModulePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformFlattenProxyStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testTransformFlattenStr = ` +aws_instance.parent +aws_instance.parent-output + module.child.output.output +module.child.aws_instance.child + module.child.var.var +module.child.output.output + module.child.aws_instance.child +module.child.plan-destroy +module.child.var.var + aws_instance.parent +` + +const testTransformFlattenProxyStr = ` +aws_instance.parent +aws_instance.parent-output + module.child.aws_instance.child + module.child.output.output +module.child.aws_instance.child + aws_instance.parent + module.child.var.var +module.child.output.output + module.child.aws_instance.child +module.child.plan-destroy +module.child.var.var + aws_instance.parent +` diff --git a/terraform/transform_module.go b/terraform/transform_module.go index 816ccc455..ca6586265 100644 --- a/terraform/transform_module.go +++ b/terraform/transform_module.go @@ -1,6 +1,7 @@ package terraform import ( + "fmt" "github.com/hashicorp/terraform/dag" ) @@ -33,6 +34,61 @@ func (t *ModuleInputTransformer) Transform(g *Graph) error { return nil } +// ModuleDestroyTransformer is a GraphTransformer that adds a node +// to the graph that will just mark the full module for destroy in +// the destroy scenario. +type ModuleDestroyTransformer struct{} + +func (t *ModuleDestroyTransformer) Transform(g *Graph) error { + // Create the node + n := &graphNodeModuleDestroy{Path: g.Path} + + // Add it to the graph. We don't need any edges because + // it can happen whenever. + g.Add(n) + + return nil +} + +type graphNodeModuleDestroy struct { + Path []string +} + +func (n *graphNodeModuleDestroy) Name() string { + return "plan-destroy" +} + +// GraphNodeEvalable impl. +func (n *graphNodeModuleDestroy) EvalTree() EvalNode { + return &EvalOpFilter{ + Ops: []walkOperation{walkPlanDestroy}, + Node: &EvalDiffDestroyModule{Path: n.Path}, + } +} + +// GraphNodeFlattenable impl. +func (n *graphNodeModuleDestroy) Flatten(p []string) (dag.Vertex, error) { + return &graphNodeModuleDestroyFlat{ + graphNodeModuleDestroy: n, + PathValue: p, + }, nil +} + +type graphNodeModuleDestroyFlat struct { + *graphNodeModuleDestroy + + PathValue []string +} + +func (n *graphNodeModuleDestroyFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.graphNodeModuleDestroy.Name()) +} + +func (n *graphNodeModuleDestroyFlat) Path() []string { + return n.PathValue +} + type graphNodeModuleInput struct { Variables map[string]string } @@ -45,3 +101,8 @@ func (n *graphNodeModuleInput) Name() string { func (n *graphNodeModuleInput) EvalTree() EvalNode { return &EvalSetVariables{Variables: n.Variables} } + +// graphNodeModuleSkippable impl. +func (n *graphNodeModuleInput) FlattenSkip() bool { + return true +} diff --git a/terraform/transform_provider.go b/terraform/transform_provider.go index 664c1b032..b9ac53ccb 100644 --- a/terraform/transform_provider.go +++ b/terraform/transform_provider.go @@ -32,7 +32,7 @@ func (t *DisableProviderTransformer) Transform(g *Graph) error { for _, v := range g.Vertices() { // We only care about providers pn, ok := v.(GraphNodeProvider) - if !ok { + if !ok || pn.ProviderName() == "" { continue } @@ -130,7 +130,7 @@ type PruneProviderTransformer struct{} func (t *PruneProviderTransformer) Transform(g *Graph) error { for _, v := range g.Vertices() { // We only care about the providers - if _, ok := v.(GraphNodeProvider); !ok { + if pn, ok := v.(GraphNodeProvider); !ok || pn.ProviderName() == "" { continue } @@ -190,6 +190,21 @@ func (n *graphNodeDisabledProvider) DotOrigin() bool { return true } +// GraphNodeDependable impl. +func (n *graphNodeDisabledProvider) DependableName() []string { + return []string{"provider." + n.ProviderName()} +} + +// GraphNodeProvider impl. +func (n *graphNodeDisabledProvider) ProviderName() string { + return n.GraphNodeProvider.ProviderName() +} + +// GraphNodeProvider impl. +func (n *graphNodeDisabledProvider) ProviderConfig() *config.RawConfig { + return n.GraphNodeProvider.ProviderConfig() +} + type graphNodeMissingProvider struct { ProviderNameValue string } @@ -203,6 +218,11 @@ func (n *graphNodeMissingProvider) EvalTree() EvalNode { return ProviderEvalTree(n.ProviderNameValue, nil) } +// GraphNodeDependable impl. +func (n *graphNodeMissingProvider) DependableName() []string { + return []string{n.Name()} +} + func (n *graphNodeMissingProvider) ProviderName() string { return n.ProviderNameValue } @@ -224,6 +244,14 @@ func (n *graphNodeMissingProvider) DotOrigin() bool { return true } +// GraphNodeFlattenable impl. +func (n *graphNodeMissingProvider) Flatten(p []string) (dag.Vertex, error) { + return &graphNodeMissingProviderFlat{ + graphNodeMissingProvider: n, + PathValue: p, + }, nil +} + func providerVertexMap(g *Graph) map[string]dag.Vertex { m := make(map[string]dag.Vertex) for _, v := range g.Vertices() { @@ -234,3 +262,48 @@ func providerVertexMap(g *Graph) map[string]dag.Vertex { return m } + +// Same as graphNodeMissingProvider, but for flattening +type graphNodeMissingProviderFlat struct { + *graphNodeMissingProvider + + PathValue []string +} + +func (n *graphNodeMissingProviderFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.graphNodeMissingProvider.Name()) +} + +func (n *graphNodeMissingProviderFlat) Path() []string { + return n.PathValue +} + +func (n *graphNodeMissingProviderFlat) ProviderName() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), + n.graphNodeMissingProvider.ProviderName()) +} + +// GraphNodeDependable impl. +func (n *graphNodeMissingProviderFlat) DependableName() []string { + return []string{n.Name()} +} + +func (n *graphNodeMissingProviderFlat) DependentOn() []string { + var result []string + + // If we're in a module, then depend on our parent's provider + if len(n.PathValue) > 1 { + prefix := modulePrefixStr(n.PathValue[:len(n.PathValue)-1]) + if prefix != "" { + prefix += "." + } + + result = append(result, fmt.Sprintf( + "%s%s", + prefix, n.graphNodeMissingProvider.Name())) + } + + return result +} diff --git a/terraform/transform_provider_test.go b/terraform/transform_provider_test.go index 719cef75c..fdf764c3f 100644 --- a/terraform/transform_provider_test.go +++ b/terraform/transform_provider_test.go @@ -218,7 +218,9 @@ provider.foo const testTransformDisableProviderBasicStr = ` module.child provider.aws (disabled) + var.foo provider.aws (disabled) +var.foo ` const testTransformDisableProviderKeepStr = ` @@ -226,5 +228,7 @@ aws_instance.foo provider.aws module.child provider.aws + var.foo provider.aws +var.foo ` diff --git a/terraform/transform_provisioner.go b/terraform/transform_provisioner.go index 316a8f31e..bf661f88a 100644 --- a/terraform/transform_provisioner.go +++ b/terraform/transform_provisioner.go @@ -111,6 +111,14 @@ func (n *graphNodeMissingProvisioner) ProvisionerName() string { return n.ProvisionerNameValue } +// GraphNodeFlattenable impl. +func (n *graphNodeMissingProvisioner) Flatten(p []string) (dag.Vertex, error) { + return &graphNodeMissingProvisionerFlat{ + graphNodeMissingProvisioner: n, + PathValue: p, + }, nil +} + func provisionerVertexMap(g *Graph) map[string]dag.Vertex { m := make(map[string]dag.Vertex) for _, v := range g.Vertices() { @@ -121,3 +129,25 @@ func provisionerVertexMap(g *Graph) map[string]dag.Vertex { return m } + +// Same as graphNodeMissingProvisioner, but for flattening +type graphNodeMissingProvisionerFlat struct { + *graphNodeMissingProvisioner + + PathValue []string +} + +func (n *graphNodeMissingProvisionerFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.graphNodeMissingProvisioner.Name()) +} + +func (n *graphNodeMissingProvisionerFlat) Path() []string { + return n.PathValue +} + +func (n *graphNodeMissingProvisionerFlat) ProvisionerName() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), + n.graphNodeMissingProvisioner.ProvisionerName()) +} diff --git a/terraform/transform_proxy.go b/terraform/transform_proxy.go new file mode 100644 index 000000000..db7b34ed8 --- /dev/null +++ b/terraform/transform_proxy.go @@ -0,0 +1,62 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeProxy must be implemented by nodes that are proxies. +// +// A node that is a proxy says that anything that depends on this +// node (the proxy), should also copy all the things that the proxy +// itself depends on. Example: +// +// A => proxy => C +// +// Should transform into (two edges): +// +// A => proxy => C +// A => C +// +// The purpose for this is because some transforms only look at direct +// edge connections and the proxy generally isn't meaningful in those +// situations, so we should complete all the edges. +type GraphNodeProxy interface { + Proxy() bool +} + +// ProxyTransformer is a transformer that goes through the graph, finds +// vertices that are marked as proxies, and connects through their +// dependents. See above for what a proxy is. +type ProxyTransformer struct{} + +func (t *ProxyTransformer) Transform(g *Graph) error { + for _, v := range g.Vertices() { + pn, ok := v.(GraphNodeProxy) + if !ok { + continue + } + + // If we don't want to be proxies, don't do it + if !pn.Proxy() { + continue + } + + // Connect all the things that depend on this to things that + // we depend on as the proxy. See docs for GraphNodeProxy for + // a visual explanation. + for _, s := range g.UpEdges(v).List() { + for _, t := range g.DownEdges(v).List() { + g.Connect(GraphProxyEdge{ + Edge: dag.BasicEdge(s, t), + }) + } + } + } + + return nil +} + +// GraphProxyEdge is the edge that is used for proxied edges. +type GraphProxyEdge struct { + dag.Edge +} diff --git a/terraform/transform_proxy_test.go b/terraform/transform_proxy_test.go new file mode 100644 index 000000000..dc1b2cfbb --- /dev/null +++ b/terraform/transform_proxy_test.go @@ -0,0 +1,52 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestProxyTransformer(t *testing.T) { + var g Graph + proxy := &testNodeProxy{NameValue: "proxy"} + g.Add("A") + g.Add("C") + g.Add(proxy) + g.Connect(dag.BasicEdge("A", proxy)) + g.Connect(dag.BasicEdge(proxy, "C")) + + { + tf := &ProxyTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testProxyTransformStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +type testNodeProxy struct { + NameValue string +} + +func (n *testNodeProxy) Name() string { + return n.NameValue +} + +func (n *testNodeProxy) Proxy() bool { + return true +} + +const testProxyTransformStr = ` +A + C + proxy +C +proxy + C +` diff --git a/terraform/transform_resource.go b/terraform/transform_resource.go index 498fe20ac..76e65af73 100644 --- a/terraform/transform_resource.go +++ b/terraform/transform_resource.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "strings" "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/dag" @@ -169,6 +170,27 @@ func (n *graphNodeExpandedResource) ProvidedBy() []string { return []string{resourceProvider(n.Resource.Type, n.Resource.Provider)} } +func (n *graphNodeExpandedResource) StateDependencies() []string { + depsRaw := n.DependentOn() + deps := make([]string, 0, len(depsRaw)) + for _, d := range depsRaw { + // Ignore any variable dependencies + if strings.HasPrefix(d, "var.") { + continue + } + + // This is sad. The dependencies are currently in the format of + // "module.foo.bar" (the full field). This strips the field off. + if strings.HasPrefix(d, "module.") { + parts := strings.SplitN(d, ".", 3) + d = strings.Join(parts[0:2], ".") + } + deps = append(deps, d) + } + + return deps +} + // GraphNodeEvalable impl. func (n *graphNodeExpandedResource) EvalTree() EvalNode { var diff *InstanceDiff @@ -257,7 +279,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { Name: n.stateId(), ResourceType: n.Resource.Type, Provider: n.Resource.Provider, - Dependencies: n.DependentOn(), + Dependencies: n.StateDependencies(), State: &state, }, }, @@ -298,7 +320,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { Name: n.stateId(), ResourceType: n.Resource.Type, Provider: n.Resource.Provider, - Dependencies: n.DependentOn(), + Dependencies: n.StateDependencies(), State: &state, }, &EvalDiffTainted{ @@ -445,7 +467,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { Name: n.stateId(), ResourceType: n.Resource.Type, Provider: n.Resource.Provider, - Dependencies: n.DependentOn(), + Dependencies: n.StateDependencies(), State: &state, }, &EvalApplyProvisioners{ @@ -489,7 +511,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { Name: n.stateId(), ResourceType: n.Resource.Type, Provider: n.Resource.Provider, - Dependencies: n.DependentOn(), + Dependencies: n.StateDependencies(), State: &state, Index: -1, }, @@ -507,7 +529,7 @@ func (n *graphNodeExpandedResource) EvalTree() EvalNode { Name: n.stateId(), ResourceType: n.Resource.Type, Provider: n.Resource.Provider, - Dependencies: n.DependentOn(), + Dependencies: n.StateDependencies(), State: &state, }, }, @@ -618,7 +640,7 @@ func (n *graphNodeExpandedResourceDestroy) EvalTree() EvalNode { Name: n.stateId(), ResourceType: n.Resource.Type, Provider: n.Resource.Provider, - Dependencies: n.DependentOn(), + Dependencies: n.StateDependencies(), State: &state, }, &EvalApplyPost{ diff --git a/terraform/transform_root.go b/terraform/transform_root.go index 4d04df9be..15de14754 100644 --- a/terraform/transform_root.go +++ b/terraform/transform_root.go @@ -34,3 +34,7 @@ type graphNodeRoot struct{} func (n graphNodeRoot) Name() string { return "root" } + +func (n graphNodeRoot) Flatten(p []string) (dag.Vertex, error) { + return n, nil +}