diff --git a/.travis.yml b/.travis.yml index 6a77ce953..b39854fb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ install: - bash scripts/gogetcookie.sh script: - make test vet +- make test TEST=./terraform TESTARGS=-Xnew-apply branches: only: - master diff --git a/command/apply.go b/command/apply.go index e1200b91d..345b2f7ee 100644 --- a/command/apply.go +++ b/command/apply.go @@ -95,6 +95,26 @@ func (c *ApplyCommand) Run(args []string) int { } } + // Check for the new apply + if terraform.X_newApply { + desc := "Experimental new apply graph has been enabled. This may still\n" + + "have bugs, and should be used with care. If you'd like to continue,\n" + + "you must enter exactly 'yes' as a response." + v, err := c.UIInput().Input(&terraform.InputOpts{ + Id: "Xnew-apply", + Query: "Experimental feature enabled: new apply graph. Continue?", + Description: desc, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err)) + return 1 + } + if v != "yes" { + c.Ui.Output("Apply cancelled.") + return 1 + } + } + // Build the context based on the arguments given ctx, planned, err := c.Context(contextOpts{ Destroy: c.Destroy, diff --git a/command/meta.go b/command/meta.go index f18b910d4..aaeb151ea 100644 --- a/command/meta.go +++ b/command/meta.go @@ -328,6 +328,9 @@ func (m *Meta) flagSet(n string) *flag.FlagSet { f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file") } + // Experimental features + f.BoolVar(&terraform.X_newApply, "Xnew-apply", false, "experiment: new apply") + // Create an io.Writer that writes to our Ui properly for errors. // This is kind of a hack, but it does the job. Basically: create // a pipe, use a scanner to break it into lines, and output each line diff --git a/config/module/tree.go b/config/module/tree.go index a0818bfa4..8a322650b 100644 --- a/config/module/tree.go +++ b/config/module/tree.go @@ -66,6 +66,10 @@ func (t *Tree) Config() *config.Config { // Child returns the child with the given path (by name). func (t *Tree) Child(path []string) *Tree { + if t == nil { + return nil + } + if len(path) == 0 { return t } diff --git a/config/module/tree_test.go b/config/module/tree_test.go index f455c2a02..d46d5ed27 100644 --- a/config/module/tree_test.go +++ b/config/module/tree_test.go @@ -12,6 +12,11 @@ import ( ) func TestTreeChild(t *testing.T) { + var nilTree *Tree + if nilTree.Child(nil) != nil { + t.Fatal("child should be nil") + } + storage := testStorage(t) tree := NewTree("", testConfig(t, "child")) if err := tree.Load(storage, GetModeGet); err != nil { diff --git a/config/raw_config.go b/config/raw_config.go index 260e315bb..ce57945cc 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -191,17 +191,19 @@ func (r *RawConfig) Merge(other *RawConfig) *RawConfig { } // Build the unknown keys - unknownKeys := make(map[string]struct{}) - for _, k := range r.unknownKeys { - unknownKeys[k] = struct{}{} - } - for _, k := range other.unknownKeys { - unknownKeys[k] = struct{}{} - } + if len(r.unknownKeys) > 0 || len(other.unknownKeys) > 0 { + unknownKeys := make(map[string]struct{}) + for _, k := range r.unknownKeys { + unknownKeys[k] = struct{}{} + } + for _, k := range other.unknownKeys { + unknownKeys[k] = struct{}{} + } - result.unknownKeys = make([]string, 0, len(unknownKeys)) - for k, _ := range unknownKeys { - result.unknownKeys = append(result.unknownKeys, k) + result.unknownKeys = make([]string, 0, len(unknownKeys)) + for k, _ := range unknownKeys { + result.unknownKeys = append(result.unknownKeys, k) + } } return result diff --git a/config/raw_config_test.go b/config/raw_config_test.go index e718ca994..5d4b881a4 100644 --- a/config/raw_config_test.go +++ b/config/raw_config_test.go @@ -27,7 +27,7 @@ func TestNewRawConfig(t *testing.T) { } } -func TestRawConfig(t *testing.T) { +func TestRawConfig_basic(t *testing.T) { raw := map[string]interface{}{ "foo": "${var.bar}", } diff --git a/config/testing.go b/config/testing.go new file mode 100644 index 000000000..f7bfadd9e --- /dev/null +++ b/config/testing.go @@ -0,0 +1,15 @@ +package config + +import ( + "testing" +) + +// TestRawConfig is used to create a RawConfig for testing. +func TestRawConfig(t *testing.T, c map[string]interface{}) *RawConfig { + cfg, err := NewRawConfig(c) + if err != nil { + t.Fatalf("err: %s", err) + } + + return cfg +} diff --git a/dag/graph.go b/dag/graph.go index 012118057..75bc5dbe5 100644 --- a/dag/graph.go +++ b/dag/graph.go @@ -48,6 +48,32 @@ func (g *Graph) Edges() []Edge { return result } +// EdgesFrom returns the list of edges from the given source. +func (g *Graph) EdgesFrom(v Vertex) []Edge { + var result []Edge + from := hashcode(v) + for _, e := range g.Edges() { + if hashcode(e.Source()) == from { + result = append(result, e) + } + } + + return result +} + +// EdgesTo returns the list of edges to the given target. +func (g *Graph) EdgesTo(v Vertex) []Edge { + var result []Edge + search := hashcode(v) + for _, e := range g.Edges() { + if hashcode(e.Target()) == search { + result = append(result, e) + } + } + + return result +} + // HasVertex checks if the given Vertex is present in the graph. func (g *Graph) HasVertex(v Vertex) bool { return g.vertices.Include(v) diff --git a/dag/graph_test.go b/dag/graph_test.go index 7acd4a831..02c4debd5 100644 --- a/dag/graph_test.go +++ b/dag/graph_test.go @@ -124,6 +124,52 @@ func TestGraphHasEdge(t *testing.T) { } } +func TestGraphEdgesFrom(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(1, 3)) + g.Connect(BasicEdge(2, 3)) + + edges := g.EdgesFrom(1) + + var expected Set + expected.Add(BasicEdge(1, 3)) + + var s Set + for _, e := range edges { + s.Add(e) + } + + if s.Intersection(&expected).Len() != expected.Len() { + t.Fatalf("bad: %#v", edges) + } +} + +func TestGraphEdgesTo(t *testing.T) { + var g Graph + g.Add(1) + g.Add(2) + g.Add(3) + g.Connect(BasicEdge(1, 3)) + g.Connect(BasicEdge(1, 2)) + + edges := g.EdgesTo(3) + + var expected Set + expected.Add(BasicEdge(1, 3)) + + var s Set + for _, e := range edges { + s.Add(e) + } + + if s.Intersection(&expected).Len() != expected.Len() { + t.Fatalf("bad: %#v", edges) + } +} + type hashVertex struct { code interface{} } diff --git a/go.sh b/go.sh new file mode 100755 index 000000000..eb7a1697e --- /dev/null +++ b/go.sh @@ -0,0 +1 @@ +go test ./terraform | grep -E '(FAIL|panic)' | tee /dev/tty | wc -l diff --git a/terraform/context.go b/terraform/context.go index 3e2e8aa3e..d15b9f6c2 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -13,6 +13,16 @@ import ( "github.com/hashicorp/terraform/config/module" ) +// Variables prefixed with X_ are experimental features. They can be enabled +// by setting them to true. This should be done before any API is called. +// These should be expected to be removed at some point in the future; each +// option should mention a schedule. +var ( + // X_newApply will enable the new apply graph. This will be removed + // and be on by default in 0.8.0. + X_newApply = false +) + // InputMode defines what sort of input will be asked for when Input // is called on Context. type InputMode byte @@ -353,21 +363,79 @@ func (c *Context) Apply() (*State, error) { // Copy our own state c.state = c.state.DeepCopy() - // Build the graph - graph, err := c.Graph(&ContextGraphOpts{Validate: true}) + // Build the original graph. This is before the new graph builders + // coming in 0.8. We do this for shadow graphing. + oldGraph, err := c.Graph(&ContextGraphOpts{Validate: true}) + if err != nil && X_newApply { + // If we had an error graphing but we're using the new graph, + // just set it to nil and let it go. There are some features that + // may work with the new graph that don't with the old. + oldGraph = nil + err = nil + } if err != nil { return nil, err } - // Do the walk - var walker *ContextGraphWalker - if c.destroy { - walker, err = c.walk(graph, graph, walkDestroy) - } else { - //walker, err = c.walk(graph, nil, walkApply) - walker, err = c.walk(graph, graph, walkApply) + // Build the new graph. We do this no matter what so we can shadow it. + newGraph, err := (&ApplyGraphBuilder{ + Module: c.module, + Diff: c.diff, + State: c.state, + Providers: c.components.ResourceProviders(), + Provisioners: c.components.ResourceProvisioners(), + }).Build(RootModulePath) + if err != nil && !X_newApply { + // If we had an error graphing but we're not using this graph, just + // set it to nil and record it as a shadow error. + c.shadowErr = multierror.Append(c.shadowErr, fmt.Errorf( + "Error building new apply graph: %s", err)) + + newGraph = nil + err = nil + } + if err != nil { + return nil, err } + // Determine what is the real and what is the shadow. The logic here + // is straightforward though the if statements are not: + // + // * Destroy mode - always use original, shadow with nothing because + // we're only testing the new APPLY graph. + // * Apply with new apply - use new graph, shadow is new graph. We can't + // shadow with the old graph because the old graph does a lot more + // that it shouldn't. + // * Apply with old apply - use old graph, shadow with new graph. + // + real := oldGraph + shadow := newGraph + if c.destroy { + log.Printf("[WARN] terraform: real graph is original, shadow is nil") + shadow = nil + } else { + if X_newApply { + log.Printf("[WARN] terraform: real graph is Xnew-apply, shadow is Xnew-apply") + real = shadow + } else { + log.Printf("[WARN] terraform: real graph is original, shadow is Xnew-apply") + } + } + + // Determine the operation + operation := walkApply + if c.destroy { + operation = walkDestroy + } + + // This shouldn't happen, so assert it. This is before any state changes + // so it is safe to crash here. + if real == nil { + panic("nil real graph") + } + + // Walk the graph + walker, err := c.walk(real, shadow, operation) if len(walker.ValidationErrors) > 0 { err = multierror.Append(err, walker.ValidationErrors...) } diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index f11263f52..397334bbb 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -835,17 +835,21 @@ func TestContext2Apply_cancel(t *testing.T) { } // Start the Apply in a goroutine + var applyErr error stateCh := make(chan *State) go func() { state, err := ctx.Apply() if err != nil { - panic(err) + applyErr = err } stateCh <- state }() state := <-stateCh + if applyErr != nil { + t.Fatalf("err: %s", applyErr) + } mod := state.RootModule() if len(mod.Resources) != 1 { @@ -956,7 +960,7 @@ func TestContext2Apply_countDecrease(t *testing.T) { } } -func TestContext2Apply_countDecreaseToOne(t *testing.T) { +func TestContext2Apply_countDecreaseToOneX(t *testing.T) { m := testModule(t, "apply-count-dec-one") p := testProvider("aws") p.ApplyFn = testApplyFn @@ -1228,7 +1232,7 @@ aws_instance.foo: } } -func TestContext2Apply_module(t *testing.T) { +func TestContext2Apply_moduleBasic(t *testing.T) { m := testModule(t, "apply-module") p := testProvider("aws") p.ApplyFn = testApplyFn @@ -1252,7 +1256,7 @@ func TestContext2Apply_module(t *testing.T) { actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyModuleStr) if actual != expected { - t.Fatalf("bad: \n%s", actual) + t.Fatalf("bad, expected:\n%s\n\nactual:\n%s", expected, actual) } } @@ -1751,10 +1755,12 @@ func TestContext2Apply_multiVar(t *testing.T) { actual := state.RootModule().Outputs["output"] expected := "bar0,bar1,bar2" - if actual.Value != expected { + if actual == nil || actual.Value != expected { t.Fatalf("bad: \n%s", actual) } + t.Logf("Initial state: %s", state.String()) + // Apply again, reduce the count to 1 { ctx := testContext2(t, &ContextOpts{ @@ -1777,7 +1783,13 @@ func TestContext2Apply_multiVar(t *testing.T) { t.Fatalf("err: %s", err) } + t.Logf("End state: %s", state.String()) + actual := state.RootModule().Outputs["output"] + if actual == nil { + t.Fatal("missing output") + } + expected := "bar0" if actual.Value != expected { t.Fatalf("bad: \n%s", actual) @@ -1903,6 +1915,43 @@ func TestContext2Apply_providerComputedVar(t *testing.T) { } } +func TestContext2Apply_providerConfigureDisabled(t *testing.T) { + m := testModule(t, "apply-provider-configure-disabled") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + + called := false + p.ConfigureFn = func(c *ResourceConfig) error { + called = true + + if _, ok := c.Get("value"); !ok { + return fmt.Errorf("value is not found") + } + + return nil + } + + ctx := testContext2(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := ctx.Apply(); err != nil { + t.Fatalf("err: %s", err) + } + + if !called { + t.Fatal("configure never called") + } +} + func TestContext2Apply_Provisioner_compute(t *testing.T) { m := testModule(t, "apply-provisioner-compute") p := testProvider("aws") @@ -2779,7 +2828,7 @@ func TestContext2Apply_Provisioner_ConnInfo(t *testing.T) { } } -func TestContext2Apply_destroy(t *testing.T) { +func TestContext2Apply_destroyX(t *testing.T) { m := testModule(t, "apply-destroy") h := new(HookRecordApplyOrder) p := testProvider("aws") @@ -2839,6 +2888,65 @@ func TestContext2Apply_destroy(t *testing.T) { } } +func TestContext2Apply_destroyOrder(t *testing.T) { + m := testModule(t, "apply-destroy") + h := new(HookRecordApplyOrder) + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + ctx := testContext2(t, &ContextOpts{ + Module: m, + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + // First plan and apply a create operation + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Next, plan and apply config-less to force a destroy with "apply" + h.Active = true + ctx = testContext2(t, &ContextOpts{ + State: state, + Module: module.NewEmptyTree(), + Hooks: []Hook{h}, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err = ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + // Test that things were destroyed + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyDestroyStr) + if actual != expected { + t.Fatalf("bad: \n%s", actual) + } + + // Test that things were destroyed _in the right order_ + expected2 := []string{"aws_instance.bar", "aws_instance.foo"} + actual2 := h.IDs + if !reflect.DeepEqual(actual2, expected2) { + t.Fatalf("expected: %#v\n\ngot:%#v", expected2, actual2) + } +} + // https://github.com/hashicorp/terraform/issues/2767 func TestContext2Apply_destroyModulePrefix(t *testing.T) { m := testModule(t, "apply-destroy-module-resource-prefix") @@ -3021,6 +3129,8 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { if err != nil { t.Fatalf("apply err: %s", err) } + + t.Logf("Step 1 state: %s", state) } h := new(HookRecordApplyOrder) @@ -3753,7 +3863,7 @@ func TestContext2Apply_idAttr(t *testing.T) { } } -func TestContext2Apply_output(t *testing.T) { +func TestContext2Apply_outputBasic(t *testing.T) { m := testModule(t, "apply-output") p := testProvider("aws") p.ApplyFn = testApplyFn @@ -3935,7 +4045,7 @@ func TestContext2Apply_outputMultiIndex(t *testing.T) { } } -func TestContext2Apply_taint(t *testing.T) { +func TestContext2Apply_taintX(t *testing.T) { m := testModule(t, "apply-taint") p := testProvider("aws") @@ -3983,8 +4093,10 @@ func TestContext2Apply_taint(t *testing.T) { State: s, }) - if _, err := ctx.Plan(); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) + } else { + t.Logf("plan: %s", p) } state, err := ctx.Apply() @@ -4483,8 +4595,8 @@ func TestContext2Apply_targetedModuleResource(t *testing.T) { } mod := state.ModuleByPath([]string{"root", "child"}) - if len(mod.Resources) != 1 { - t.Fatalf("expected 1 resource, got: %#v", mod.Resources) + if mod == nil || len(mod.Resources) != 1 { + t.Fatalf("expected 1 resource, got: %#v", mod) } checkStateString(t, state, ` @@ -4675,8 +4787,10 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { State: state, }) - if _, err := ctx.Plan(); err != nil { + if p, err := ctx.Plan(); err != nil { t.Fatalf("err: %s", err) + } else { + t.Logf("plan: %s", p) } h.Active = true @@ -4693,7 +4807,7 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyDependsCreateBeforeStr) if actual != expected { - t.Fatalf("bad: \n%s\n%s", actual, expected) + t.Fatalf("bad: \n%s\n\n%s", actual, expected) } // Test that things were managed _in the right order_ diff --git a/terraform/diff.go b/terraform/diff.go index e0e097e80..86be4bb5a 100644 --- a/terraform/diff.go +++ b/terraform/diff.go @@ -72,6 +72,10 @@ func (d *Diff) RootModule() *ModuleDiff { // Empty returns true if the diff has no changes. func (d *Diff) Empty() bool { + if d == nil { + return true + } + for _, m := range d.Modules { if !m.Empty() { return false diff --git a/terraform/diff_test.go b/terraform/diff_test.go index 5234c6aaf..a9cb8b47b 100644 --- a/terraform/diff_test.go +++ b/terraform/diff_test.go @@ -7,7 +7,12 @@ import ( ) func TestDiffEmpty(t *testing.T) { - diff := new(Diff) + var diff *Diff + if !diff.Empty() { + t.Fatal("should be empty") + } + + diff = new(Diff) if !diff.Empty() { t.Fatal("should be empty") } diff --git a/terraform/edge_destroy.go b/terraform/edge_destroy.go new file mode 100644 index 000000000..bc9d638aa --- /dev/null +++ b/terraform/edge_destroy.go @@ -0,0 +1,17 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/dag" +) + +// DestroyEdge is an edge that represents a standard "destroy" relationship: +// Target depends on Source because Source is destroying. +type DestroyEdge struct { + S, T dag.Vertex +} + +func (e *DestroyEdge) Hashcode() interface{} { return fmt.Sprintf("%p-%p", e.S, e.T) } +func (e *DestroyEdge) Source() dag.Vertex { return e.S } +func (e *DestroyEdge) Target() dag.Vertex { return e.T } diff --git a/terraform/eval_count_boundary.go b/terraform/eval_count_boundary.go new file mode 100644 index 000000000..91e2b904e --- /dev/null +++ b/terraform/eval_count_boundary.go @@ -0,0 +1,78 @@ +package terraform + +import ( + "log" +) + +// EvalCountFixZeroOneBoundaryGlobal is an EvalNode that fixes up the state +// when there is a resource count with zero/one boundary, i.e. fixing +// a resource named "aws_instance.foo" to "aws_instance.foo.0" and vice-versa. +// +// This works on the global state. +type EvalCountFixZeroOneBoundaryGlobal struct{} + +// TODO: test +func (n *EvalCountFixZeroOneBoundaryGlobal) Eval(ctx EvalContext) (interface{}, error) { + // Get the state and lock it since we'll potentially modify it + state, lock := ctx.State() + lock.Lock() + defer lock.Unlock() + + // Prune the state since we require a clean state to work + state.prune() + + // Go through each modules since the boundaries are restricted to a + // module scope. + for _, m := range state.Modules { + if err := n.fixModule(m); err != nil { + return nil, err + } + } + + return nil, nil +} + +func (n *EvalCountFixZeroOneBoundaryGlobal) fixModule(m *ModuleState) error { + // Counts keeps track of keys and their counts + counts := make(map[string]int) + for k, _ := range m.Resources { + // Parse the key + key, err := ParseResourceStateKey(k) + if err != nil { + return err + } + + // Set the index to -1 so that we can keep count + key.Index = -1 + + // Increment + counts[key.String()]++ + } + + // Go through the counts and do the fixup for each resource + for raw, count := range counts { + // Search and replace this resource + search := raw + replace := raw + ".0" + if count < 2 { + search, replace = replace, search + } + log.Printf("[TRACE] EvalCountFixZeroOneBoundaryGlobal: count %d, search %q, replace %q", count, search, replace) + + // Look for the resource state. If we don't have one, then it is okay. + rs, ok := m.Resources[search] + if !ok { + continue + } + + // If the replacement key exists, we just keep both + if _, ok := m.Resources[replace]; ok { + continue + } + + m.Resources[replace] = rs + delete(m.Resources, search) + } + + return nil +} diff --git a/terraform/graph.go b/terraform/graph.go index e75d93663..d056bab1f 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -28,6 +28,11 @@ type Graph struct { // RootModuleName Path []string + // annotations are the annotations that are added to vertices. Annotations + // are arbitrary metadata taht is used for various logic. Annotations + // should have unique keys that are referenced via constants. + annotations map[dag.Vertex]map[string]interface{} + // dependableMap is a lookaside table for fast lookups for connecting // dependencies by their GraphNodeDependable value to avoid O(n^3)-like // situations and turn them into O(1) with respect to the number of new @@ -37,6 +42,29 @@ type Graph struct { once sync.Once } +// Annotations returns the annotations that are configured for the +// given vertex. The map is guaranteed to be non-nil but may be empty. +// +// The returned map may be modified to modify the annotations of the +// vertex. +func (g *Graph) Annotations(v dag.Vertex) map[string]interface{} { + g.once.Do(g.init) + + // If this vertex isn't in the graph, then just return an empty map + if !g.HasVertex(v) { + return map[string]interface{}{} + } + + // Get the map, if it doesn't exist yet then initialize it + m, ok := g.annotations[v] + if !ok { + m = make(map[string]interface{}) + g.annotations[v] = m + } + + return m +} + // Add is the same as dag.Graph.Add. func (g *Graph) Add(v dag.Vertex) dag.Vertex { g.once.Do(g.init) @@ -51,6 +79,14 @@ func (g *Graph) Add(v dag.Vertex) dag.Vertex { } } + // If this initializes annotations, then do that + if av, ok := v.(GraphNodeAnnotationInit); ok { + as := g.Annotations(v) + for k, v := range av.AnnotationInit() { + as[k] = v + } + } + return v } @@ -65,12 +101,17 @@ func (g *Graph) Remove(v dag.Vertex) dag.Vertex { } } + // Remove the annotations + delete(g.annotations, v) + // 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 { + g.once.Do(g.init) + // Go through and update our lookaside to point to the new vertex for k, v := range g.dependableMap { if v == o { @@ -82,6 +123,12 @@ func (g *Graph) Replace(o, n dag.Vertex) bool { } } + // Move the annotation if it exists + if m, ok := g.annotations[o]; ok { + g.annotations[n] = m + delete(g.annotations, o) + } + return g.Graph.Replace(o, n) } @@ -153,6 +200,10 @@ func (g *Graph) Walk(walker GraphWalker) error { } func (g *Graph) init() { + if g.annotations == nil { + g.annotations = make(map[dag.Vertex]map[string]interface{}) + } + if g.dependableMap == nil { g.dependableMap = make(map[string]dag.Vertex) } @@ -179,7 +230,7 @@ func (g *Graph) walk(walker GraphWalker) error { // with a GraphNodeSubPath impl. vertexCtx := ctx if pn, ok := v.(GraphNodeSubPath); ok && len(pn.Path()) > 0 { - vertexCtx = walker.EnterPath(pn.Path()) + vertexCtx = walker.EnterPath(normalizeModulePath(pn.Path())) defer walker.ExitPath(pn.Path()) } @@ -212,10 +263,11 @@ func (g *Graph) walk(walker GraphWalker) error { rerr = err return } - - // Walk the subgraph - if rerr = g.walk(walker); rerr != nil { - return + if g != nil { + // Walk the subgraph + if rerr = g.walk(walker); rerr != nil { + return + } } } @@ -237,6 +289,16 @@ func (g *Graph) walk(walker GraphWalker) error { return g.AcyclicGraph.Walk(walkFn) } +// GraphNodeAnnotationInit is an interface that allows a node to +// initialize it's annotations. +// +// AnnotationInit will be called _once_ when the node is added to a +// graph for the first time and is expected to return it's initial +// annotations. +type GraphNodeAnnotationInit interface { + AnnotationInit() map[string]interface{} +} + // GraphNodeDependable is an interface which says that a node can be // depended on (an edge can be placed between this node and another) according // to the well-known name returned by DependableName. diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index abc9aca37..1c0a2d595 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -115,7 +115,7 @@ func (b *BuiltinGraphBuilder) Steps(path []string) []GraphTransformer { // Provider-related transformations &MissingProviderTransformer{Providers: b.Providers}, &ProviderTransformer{}, - &DisableProviderTransformer{}, + &DisableProviderTransformerOld{}, // Provisioner-related transformations &MissingProvisionerTransformer{Provisioners: b.Provisioners}, diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go new file mode 100644 index 000000000..83cc89374 --- /dev/null +++ b/terraform/graph_builder_apply.go @@ -0,0 +1,119 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// ApplyGraphBuilder implements GraphBuilder and is responsible for building +// a graph for applying a Terraform diff. +// +// Because the graph is built from the diff (vs. the config or state), +// this helps ensure that the apply-time graph doesn't modify any resources +// that aren't explicitly in the diff. There are other scenarios where the +// diff can be deviated, so this is just one layer of protection. +type ApplyGraphBuilder struct { + // Module is the root module for the graph to build. + Module *module.Tree + + // Diff is the diff to apply. + Diff *Diff + + // State is the current state + State *State + + // Providers is the list of providers supported. + Providers []string + + // Provisioners is the list of provisioners supported. + Provisioners []string + + // DisableReduce, if true, will not reduce the graph. Great for testing. + DisableReduce bool +} + +// See GraphBuilder +func (b *ApplyGraphBuilder) Build(path []string) (*Graph, error) { + return (&BasicGraphBuilder{ + Steps: b.Steps(), + Validate: true, + }).Build(path) +} + +// See GraphBuilder +func (b *ApplyGraphBuilder) Steps() []GraphTransformer { + // Custom factory for creating providers. + providerFactory := func(name string, path []string) GraphNodeProvider { + return &NodeApplyableProvider{ + NameValue: name, + PathValue: path, + } + } + + concreteResource := func(a *NodeAbstractResource) dag.Vertex { + return &NodeApplyableResource{ + NodeAbstractResource: a, + } + } + + steps := []GraphTransformer{ + // Creates all the nodes represented in the diff. + &DiffTransformer{ + Concrete: concreteResource, + + Diff: b.Diff, + Module: b.Module, + State: b.State, + }, + + // Create orphan output nodes + &OrphanOutputTransformer{Module: b.Module, State: b.State}, + + // Attach the configuration to any resources + &AttachResourceConfigTransformer{Module: b.Module}, + + // Attach the state + &AttachStateTransformer{State: b.State}, + + // Destruction ordering + &DestroyEdgeTransformer{Module: b.Module, State: b.State}, + &CBDEdgeTransformer{Module: b.Module, State: b.State}, + + // Create all the providers + &MissingProviderTransformer{Providers: b.Providers, Factory: providerFactory}, + &ProviderTransformer{}, + &DisableProviderTransformer{}, + &ParentProviderTransformer{}, + &AttachProviderConfigTransformer{Module: b.Module}, + + // Provisioner-related transformations + &MissingProvisionerTransformer{Provisioners: b.Provisioners}, + &ProvisionerTransformer{}, + + // Add root variables + &RootVariableTransformer{Module: b.Module}, + + // Add module variables + &ModuleVariableTransformer{Module: b.Module}, + + // Add the outputs + &OutputTransformer{Module: b.Module}, + + // Connect references so ordering is correct + &ReferenceTransformer{}, + + // Add the node to fix the state count boundaries + &CountBoundaryTransformer{}, + + // Single root + &RootTransformer{}, + } + + if !b.DisableReduce { + // Perform the transitive reduction to make our graph a bit + // more sane if possible (it usually is possible). + steps = append(steps, &TransitiveReductionTransformer{}) + } + + return steps +} diff --git a/terraform/graph_builder_apply_test.go b/terraform/graph_builder_apply_test.go new file mode 100644 index 000000000..f224e53f1 --- /dev/null +++ b/terraform/graph_builder_apply_test.go @@ -0,0 +1,115 @@ +package terraform + +import ( + "reflect" + "strings" + "testing" +) + +func TestApplyGraphBuilder_impl(t *testing.T) { + var _ GraphBuilder = new(ApplyGraphBuilder) +} + +func TestApplyGraphBuilder(t *testing.T) { + diff := &Diff{ + Modules: []*ModuleDiff{ + &ModuleDiff{ + Path: []string{"root"}, + Resources: map[string]*InstanceDiff{ + // Verify noop doesn't show up in graph + "aws_instance.noop": &InstanceDiff{}, + + "aws_instance.create": &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "name": &ResourceAttrDiff{ + Old: "", + New: "foo", + }, + }, + }, + + "aws_instance.other": &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "name": &ResourceAttrDiff{ + Old: "", + New: "foo", + }, + }, + }, + }, + }, + + &ModuleDiff{ + Path: []string{"root", "child"}, + Resources: map[string]*InstanceDiff{ + "aws_instance.create": &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "name": &ResourceAttrDiff{ + Old: "", + New: "foo", + }, + }, + }, + + "aws_instance.other": &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "name": &ResourceAttrDiff{ + Old: "", + New: "foo", + }, + }, + }, + }, + }, + }, + } + + b := &ApplyGraphBuilder{ + Module: testModule(t, "graph-builder-apply-basic"), + Diff: diff, + Providers: []string{"aws"}, + Provisioners: []string{"exec"}, + DisableReduce: true, + } + + g, err := b.Build(RootModulePath) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !reflect.DeepEqual(g.Path, RootModulePath) { + t.Fatalf("bad: %#v", g.Path) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testApplyGraphBuilderStr) + if actual != expected { + t.Fatalf("bad: %s", actual) + } +} + +const testApplyGraphBuilderStr = ` +aws_instance.create + provider.aws +aws_instance.other + aws_instance.create + provider.aws +meta.count-boundary (count boundary fixup) + aws_instance.create + aws_instance.other + module.child.aws_instance.create + module.child.aws_instance.other + module.child.provider.aws + provider.aws + provisioner.exec +module.child.aws_instance.create + module.child.provider.aws + provisioner.exec +module.child.aws_instance.other + module.child.aws_instance.create + module.child.provider.aws +module.child.provider.aws + provider.aws +provider.aws +provisioner.exec +` diff --git a/terraform/graph_builder_import.go b/terraform/graph_builder_import.go index 06763710c..6d87d487d 100644 --- a/terraform/graph_builder_import.go +++ b/terraform/graph_builder_import.go @@ -46,7 +46,7 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer { // Provider-related transformations &MissingProviderTransformer{Providers: b.Providers}, &ProviderTransformer{}, - &DisableProviderTransformer{}, + &DisableProviderTransformerOld{}, &PruneProviderTransformer{}, // Single root diff --git a/terraform/node_count_boundary.go b/terraform/node_count_boundary.go new file mode 100644 index 000000000..bd32c79f3 --- /dev/null +++ b/terraform/node_count_boundary.go @@ -0,0 +1,14 @@ +package terraform + +// NodeCountBoundary fixes any "count boundarie" in the state: resources +// that are named "foo.0" when they should be named "foo" +type NodeCountBoundary struct{} + +func (n *NodeCountBoundary) Name() string { + return "meta.count-boundary (count boundary fixup)" +} + +// GraphNodeEvalable +func (n *NodeCountBoundary) EvalTree() EvalNode { + return &EvalCountFixZeroOneBoundaryGlobal{} +} diff --git a/terraform/node_module_variable.go b/terraform/node_module_variable.go new file mode 100644 index 000000000..9b7ddbf2f --- /dev/null +++ b/terraform/node_module_variable.go @@ -0,0 +1,118 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// NodeApplyableModuleVariable represents a module variable input during +// the apply step. +type NodeApplyableModuleVariable struct { + PathValue []string + Config *config.Variable // Config is the var in the config + Value *config.RawConfig // Value is the value that is set + + Module *module.Tree // Antiquated, want to remove +} + +func (n *NodeApplyableModuleVariable) Name() string { + result := fmt.Sprintf("var.%s", n.Config.Name) + if len(n.PathValue) > 1 { + result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result) + } + + return result +} + +// GraphNodeSubPath +func (n *NodeApplyableModuleVariable) Path() []string { + // We execute in the parent scope (above our own module) so that + // we can access the proper interpolations. + if len(n.PathValue) > 2 { + return n.PathValue[:len(n.PathValue)-1] + } + + return rootModulePath +} + +// GraphNodeReferenceGlobal +func (n *NodeApplyableModuleVariable) ReferenceGlobal() bool { + // We have to create fully qualified references because we cross + // boundaries here: our ReferenceableName is in one path and our + // References are from another path. + return true +} + +// GraphNodeReferenceable +func (n *NodeApplyableModuleVariable) ReferenceableName() []string { + return []string{n.Name()} +} + +// GraphNodeReferencer +func (n *NodeApplyableModuleVariable) References() []string { + // If we have no value set, we depend on nothing + if n.Value == nil { + return nil + } + + // Can't depend on anything if we're in the root + if len(n.PathValue) < 2 { + return nil + } + + // Otherwise, we depend on anything that is in our value, but + // specifically in the namespace of the parent path. + // Create the prefix based on the path + var prefix string + if p := n.Path(); len(p) > 0 { + prefix = modulePrefixStr(p) + } + + result := ReferencesFromConfig(n.Value) + return modulePrefixList(result, prefix) +} + +// GraphNodeEvalable +func (n *NodeApplyableModuleVariable) 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]interface{}) + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{ + Config: n.Value, + Output: &config, + }, + + &EvalVariableBlock{ + Config: &config, + VariableValues: variables, + }, + + &EvalCoerceMapVariable{ + Variables: variables, + ModulePath: n.PathValue, + ModuleTree: n.Module, + }, + + &EvalTypeCheckVariable{ + Variables: variables, + ModulePath: n.PathValue, + ModuleTree: n.Module, + }, + + &EvalSetVariables{ + Module: &n.PathValue[len(n.PathValue)-1], + Variables: variables, + }, + }, + } +} diff --git a/terraform/node_module_variable_test.go b/terraform/node_module_variable_test.go new file mode 100644 index 000000000..4fb6663b9 --- /dev/null +++ b/terraform/node_module_variable_test.go @@ -0,0 +1,66 @@ +package terraform + +import ( + "reflect" + "testing" + + "github.com/hashicorp/terraform/config" +) + +func TestNodeApplyableModuleVariablePath(t *testing.T) { + n := &NodeApplyableModuleVariable{ + PathValue: []string{"root", "child"}, + Config: &config.Variable{Name: "foo"}, + } + + expected := []string{"root"} + actual := n.Path() + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("%#v != %#v", actual, expected) + } +} + +func TestNodeApplyableModuleVariableReferenceableName(t *testing.T) { + n := &NodeApplyableModuleVariable{ + PathValue: []string{"root", "child"}, + Config: &config.Variable{Name: "foo"}, + } + + expected := []string{"module.child.var.foo"} + actual := n.ReferenceableName() + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("%#v != %#v", actual, expected) + } +} + +func TestNodeApplyableModuleVariableReference(t *testing.T) { + n := &NodeApplyableModuleVariable{ + PathValue: []string{"root", "child"}, + Config: &config.Variable{Name: "foo"}, + Value: config.TestRawConfig(t, map[string]interface{}{ + "foo": `${var.foo}`, + }), + } + + expected := []string{"var.foo"} + actual := n.References() + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("%#v != %#v", actual, expected) + } +} + +func TestNodeApplyableModuleVariableReference_grandchild(t *testing.T) { + n := &NodeApplyableModuleVariable{ + PathValue: []string{"root", "child", "grandchild"}, + Config: &config.Variable{Name: "foo"}, + Value: config.TestRawConfig(t, map[string]interface{}{ + "foo": `${var.foo}`, + }), + } + + expected := []string{"module.child.var.foo"} + actual := n.References() + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("%#v != %#v", actual, expected) + } +} diff --git a/terraform/node_output.go b/terraform/node_output.go new file mode 100644 index 000000000..c10c6e4f8 --- /dev/null +++ b/terraform/node_output.go @@ -0,0 +1,62 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// NodeApplyableOutput represents an output that is "applyable": +// it is ready to be applied. +type NodeApplyableOutput struct { + PathValue []string + Config *config.Output // Config is the output in the config +} + +func (n *NodeApplyableOutput) Name() string { + result := fmt.Sprintf("output.%s", n.Config.Name) + if len(n.PathValue) > 1 { + result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result) + } + + return result +} + +// GraphNodeSubPath +func (n *NodeApplyableOutput) Path() []string { + return n.PathValue +} + +// GraphNodeReferenceable +func (n *NodeApplyableOutput) ReferenceableName() []string { + name := fmt.Sprintf("output.%s", n.Config.Name) + return []string{name} +} + +// GraphNodeReferencer +func (n *NodeApplyableOutput) References() []string { + var result []string + result = append(result, ReferencesFromConfig(n.Config.RawConfig)...) + for _, v := range result { + result = append(result, v+".destroy") + } + + return result +} + +// GraphNodeEvalable +func (n *NodeApplyableOutput) EvalTree() EvalNode { + return &EvalOpFilter{ + Ops: []walkOperation{walkRefresh, walkPlan, walkApply, + walkDestroy, walkInput, walkValidate}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalWriteOutput{ + Name: n.Config.Name, + Sensitive: n.Config.Sensitive, + Value: n.Config.RawConfig, + }, + }, + }, + } +} diff --git a/terraform/node_output_orphan.go b/terraform/node_output_orphan.go new file mode 100644 index 000000000..651638d7e --- /dev/null +++ b/terraform/node_output_orphan.go @@ -0,0 +1,32 @@ +package terraform + +import ( + "fmt" +) + +// NodeOutputOrphan represents an output that is an orphan. +type NodeOutputOrphan struct { + OutputName string + PathValue []string +} + +func (n *NodeOutputOrphan) Name() string { + result := fmt.Sprintf("output.%s (orphan)", n.OutputName) + if len(n.PathValue) > 1 { + result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result) + } + + return result +} + +// GraphNodeSubPath +func (n *NodeOutputOrphan) Path() []string { + return n.PathValue +} + +// GraphNodeEvalable +func (n *NodeOutputOrphan) EvalTree() EvalNode { + return &EvalDeleteOutput{ + Name: n.OutputName, + } +} diff --git a/terraform/node_provider.go b/terraform/node_provider.go new file mode 100644 index 000000000..9b8c34b4d --- /dev/null +++ b/terraform/node_provider.go @@ -0,0 +1,64 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// NodeApplyableProvider represents a provider during an apply. +// +// NOTE: There is a lot of logic here that will be shared with non-Apply. +// The plan is to abstract that eventually into an embedded abstract struct. +type NodeApplyableProvider struct { + NameValue string + PathValue []string + Config *config.ProviderConfig +} + +func (n *NodeApplyableProvider) Name() string { + result := fmt.Sprintf("provider.%s", n.NameValue) + if len(n.PathValue) > 1 { + result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result) + } + + return result +} + +// GraphNodeSubPath +func (n *NodeApplyableProvider) Path() []string { + return n.PathValue +} + +// GraphNodeReferencer +func (n *NodeApplyableProvider) References() []string { + if n.Config == nil { + return nil + } + + return ReferencesFromConfig(n.Config.RawConfig) +} + +// GraphNodeProvider +func (n *NodeApplyableProvider) ProviderName() string { + return n.NameValue +} + +// GraphNodeProvider +func (n *NodeApplyableProvider) ProviderConfig() *config.RawConfig { + if n.Config == nil { + return nil + } + + return n.Config.RawConfig +} + +// GraphNodeAttachProvider +func (n *NodeApplyableProvider) AttachProvider(c *config.ProviderConfig) { + n.Config = c +} + +// GraphNodeEvalable +func (n *NodeApplyableProvider) EvalTree() EvalNode { + return ProviderEvalTree(n.NameValue, n.ProviderConfig()) +} diff --git a/terraform/node_provider_abstract.go b/terraform/node_provider_abstract.go new file mode 100644 index 000000000..5cb18caec --- /dev/null +++ b/terraform/node_provider_abstract.go @@ -0,0 +1,62 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// NodeAbstractProvider represents a provider that has no associated operations. +// It registers all the common interfaces across operations for providers. +type NodeAbstractProvider struct { + NameValue string + PathValue []string + + // The fields below will be automatically set using the Attach + // interfaces if you're running those transforms, but also be explicitly + // set if you already have that information. + + Config *config.ProviderConfig +} + +func (n *NodeAbstractProvider) Name() string { + result := fmt.Sprintf("provider.%s", n.NameValue) + if len(n.PathValue) > 1 { + result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result) + } + + return result +} + +// GraphNodeSubPath +func (n *NodeAbstractProvider) Path() []string { + return n.PathValue +} + +// GraphNodeReferencer +func (n *NodeAbstractProvider) References() []string { + if n.Config == nil { + return nil + } + + return ReferencesFromConfig(n.Config.RawConfig) +} + +// GraphNodeProvider +func (n *NodeAbstractProvider) ProviderName() string { + return n.NameValue +} + +// GraphNodeProvider +func (n *NodeAbstractProvider) ProviderConfig() *config.RawConfig { + if n.Config == nil { + return nil + } + + return n.Config.RawConfig +} + +// GraphNodeAttachProvider +func (n *NodeAbstractProvider) AttachProvider(c *config.ProviderConfig) { + n.Config = c +} diff --git a/terraform/node_provider_disabled.go b/terraform/node_provider_disabled.go new file mode 100644 index 000000000..25e7e620e --- /dev/null +++ b/terraform/node_provider_disabled.go @@ -0,0 +1,38 @@ +package terraform + +import ( + "fmt" +) + +// NodeDisabledProvider represents a provider that is disabled. A disabled +// provider does nothing. It exists to properly set inheritance information +// for child providers. +type NodeDisabledProvider struct { + *NodeAbstractProvider +} + +func (n *NodeDisabledProvider) Name() string { + return fmt.Sprintf("%s (disabled)", n.NodeAbstractProvider.Name()) +} + +// GraphNodeEvalable +func (n *NodeDisabledProvider) EvalTree() EvalNode { + var resourceConfig *ResourceConfig + return &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{ + Config: n.ProviderConfig(), + Output: &resourceConfig, + }, + &EvalBuildProviderConfig{ + Provider: n.ProviderName(), + Config: &resourceConfig, + Output: &resourceConfig, + }, + &EvalSetProviderConfig{ + Provider: n.ProviderName(), + Config: &resourceConfig, + }, + }, + } +} diff --git a/terraform/node_resource_abstract.go b/terraform/node_resource_abstract.go new file mode 100644 index 000000000..cddccaef2 --- /dev/null +++ b/terraform/node_resource_abstract.go @@ -0,0 +1,122 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" +) + +// ConcreteResourceNodeFunc is a callback type used to convert an +// abstract resource to a concrete one of some type. +type ConcreteResourceNodeFunc func(*NodeAbstractResource) dag.Vertex + +// GraphNodeResource is implemented by any nodes that represent a resource. +// The type of operation cannot be assumed, only that this node represents +// the given resource. +type GraphNodeResource interface { + ResourceAddr() *ResourceAddress +} + +// NodeAbstractResource represents a resource that has no associated +// operations. It registers all the interfaces for a resource that common +// across multiple operation types. +type NodeAbstractResource struct { + Addr *ResourceAddress // Addr is the address for this resource + + // The fields below will be automatically set using the Attach + // interfaces if you're running those transforms, but also be explicitly + // set if you already have that information. + + Config *config.Resource // Config is the resource in the config + ResourceState *ResourceState // ResourceState is the ResourceState for this +} + +func (n *NodeAbstractResource) Name() string { + return n.Addr.String() +} + +// GraphNodeSubPath +func (n *NodeAbstractResource) Path() []string { + return n.Addr.Path +} + +// GraphNodeReferenceable +func (n *NodeAbstractResource) ReferenceableName() []string { + if n.Config == nil { + return nil + } + + return []string{n.Config.Id()} +} + +// GraphNodeReferencer +func (n *NodeAbstractResource) References() []string { + // If we have a config, that is our source of truth + if c := n.Config; c != nil { + // Grab all the references + var result []string + result = append(result, c.DependsOn...) + result = append(result, ReferencesFromConfig(c.RawCount)...) + result = append(result, ReferencesFromConfig(c.RawConfig)...) + for _, p := range c.Provisioners { + result = append(result, ReferencesFromConfig(p.ConnInfo)...) + result = append(result, ReferencesFromConfig(p.RawConfig)...) + } + + return result + } + + // If we have state, that is our next source + if s := n.ResourceState; s != nil { + return s.Dependencies + } + + return nil +} + +// GraphNodeProviderConsumer +func (n *NodeAbstractResource) ProvidedBy() []string { + // If we have a config we prefer that above all else + if n.Config != nil { + return []string{resourceProvider(n.Config.Type, n.Config.Provider)} + } + + // If we have state, then we will use the provider from there + if n.ResourceState != nil && n.ResourceState.Provider != "" { + return []string{n.ResourceState.Provider} + } + + // Use our type + return []string{resourceProvider(n.Addr.Type, "")} +} + +// GraphNodeProvisionerConsumer +func (n *NodeAbstractResource) ProvisionedBy() []string { + // If we have no configuration, then we have no provisioners + if n.Config == nil { + return nil + } + + // Build the list of provisioners we need based on the configuration. + // It is okay to have duplicates here. + result := make([]string, len(n.Config.Provisioners)) + for i, p := range n.Config.Provisioners { + result[i] = p.Type + } + + return result +} + +// GraphNodeResource, GraphNodeAttachResourceState +func (n *NodeAbstractResource) ResourceAddr() *ResourceAddress { + return n.Addr +} + +// GraphNodeAttachResourceState +func (n *NodeAbstractResource) AttachResourceState(s *ResourceState) { + n.ResourceState = s +} + +// GraphNodeAttachResourceConfig +func (n *NodeAbstractResource) AttachResourceConfig(c *config.Resource) { + n.Config = c +} diff --git a/terraform/node_resource_apply.go b/terraform/node_resource_apply.go new file mode 100644 index 000000000..f663cd893 --- /dev/null +++ b/terraform/node_resource_apply.go @@ -0,0 +1,221 @@ +package terraform + +import ( + "fmt" +) + +// NodeApplyableResource represents a resource that is "applyable": +// it is ready to be applied and is represented by a diff. +type NodeApplyableResource struct { + *NodeAbstractResource +} + +// GraphNodeCreator +func (n *NodeApplyableResource) CreateAddr() *ResourceAddress { + return n.NodeAbstractResource.Addr +} + +// GraphNodeEvalable +func (n *NodeApplyableResource) EvalTree() EvalNode { + addr := n.NodeAbstractResource.Addr + + // stateId is the ID to put into the state + stateId := addr.stateId() + if addr.Index > -1 { + stateId = fmt.Sprintf("%s.%d", stateId, addr.Index) + } + + // Build the instance info. More of this will be populated during eval + info := &InstanceInfo{ + Id: stateId, + Type: addr.Type, + } + + // Build the resource for eval + resource := &Resource{ + Name: addr.Name, + Type: addr.Type, + CountIndex: addr.Index, + } + if resource.CountIndex < 0 { + resource.CountIndex = 0 + } + + // Determine the dependencies for the state. We use some older + // code for this that we've used for a long time. + var stateDeps []string + { + oldN := &graphNodeExpandedResource{Resource: n.Config} + stateDeps = oldN.StateDependencies() + } + + // Declare a bunch of variables that are used for state during + // evaluation. Most of this are written to by-address below. + var provider ResourceProvider + var diff, diffApply *InstanceDiff + var state *InstanceState + var resourceConfig *ResourceConfig + var err error + var createNew bool + var createBeforeDestroyEnabled bool + + return &EvalSequence{ + Nodes: []EvalNode{ + // Build the instance info + &EvalInstanceInfo{ + Info: info, + }, + + // Get the saved diff for apply + &EvalReadDiff{ + Name: stateId, + Diff: &diffApply, + }, + + // We don't want to do any destroys + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + if diffApply == nil { + return true, EvalEarlyExitError{} + } + + if diffApply.GetDestroy() && diffApply.GetAttributesLen() == 0 { + return true, EvalEarlyExitError{} + } + + diffApply.SetDestroy(false) + return true, nil + }, + Then: EvalNoop{}, + }, + + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + destroy := false + if diffApply != nil { + destroy = diffApply.GetDestroy() || diffApply.RequiresNew() + } + + createBeforeDestroyEnabled = + n.Config.Lifecycle.CreateBeforeDestroy && + destroy + + return createBeforeDestroyEnabled, nil + }, + Then: &EvalDeposeState{ + Name: stateId, + }, + }, + + &EvalInterpolate{ + Config: n.Config.RawConfig.Copy(), + Resource: resource, + Output: &resourceConfig, + }, + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: stateId, + Output: &state, + }, + // Re-run validation to catch any errors we missed, e.g. type + // mismatches on computed values. + &EvalValidateResource{ + Provider: &provider, + Config: &resourceConfig, + ResourceName: n.Config.Name, + ResourceType: n.Config.Type, + ResourceMode: n.Config.Mode, + IgnoreWarnings: true, + }, + &EvalDiff{ + Info: info, + Config: &resourceConfig, + Resource: n.Config, + Provider: &provider, + Diff: &diffApply, + State: &state, + OutputDiff: &diffApply, + }, + + // Get the saved diff + &EvalReadDiff{ + Name: stateId, + Diff: &diff, + }, + + // Compare the diffs + &EvalCompareDiff{ + Info: info, + One: &diff, + Two: &diffApply, + }, + + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: stateId, + Output: &state, + }, + &EvalApply{ + Info: info, + State: &state, + Diff: &diffApply, + Provider: &provider, + Output: &state, + Error: &err, + CreateNew: &createNew, + }, + &EvalWriteState{ + Name: stateId, + ResourceType: n.Config.Type, + Provider: n.Config.Provider, + Dependencies: stateDeps, + State: &state, + }, + &EvalApplyProvisioners{ + Info: info, + State: &state, + Resource: n.Config, + InterpResource: resource, + CreateNew: &createNew, + Error: &err, + }, + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + return createBeforeDestroyEnabled && err != nil, nil + }, + Then: &EvalUndeposeState{ + Name: stateId, + State: &state, + }, + Else: &EvalWriteState{ + Name: stateId, + ResourceType: n.Config.Type, + Provider: n.Config.Provider, + Dependencies: stateDeps, + State: &state, + }, + }, + + // We clear the diff out here so that future nodes + // don't see a diff that is already complete. There + // is no longer a diff! + &EvalWriteDiff{ + Name: stateId, + Diff: nil, + }, + + &EvalApplyPost{ + Info: info, + State: &state, + Error: &err, + }, + &EvalUpdateStateHook{}, + }, + } +} diff --git a/terraform/node_resource_destroy.go b/terraform/node_resource_destroy.go new file mode 100644 index 000000000..52c34d1c7 --- /dev/null +++ b/terraform/node_resource_destroy.go @@ -0,0 +1,185 @@ +package terraform + +import ( + "fmt" +) + +// NodeDestroyResource represents a resource that is to be destroyed. +type NodeDestroyResource struct { + NodeAbstractResource +} + +func (n *NodeDestroyResource) Name() string { + return n.NodeAbstractResource.Name() + " (destroy)" +} + +// GraphNodeDestroyer +func (n *NodeDestroyResource) DestroyAddr() *ResourceAddress { + return n.Addr +} + +// GraphNodeDestroyerCBD +func (n *NodeDestroyResource) CreateBeforeDestroy() bool { + // If we have no config, we just assume no + if n.Config == nil { + return false + } + + return n.Config.Lifecycle.CreateBeforeDestroy +} + +// GraphNodeReferenceable, overriding NodeAbstractResource +func (n *NodeDestroyResource) ReferenceableName() []string { + result := n.NodeAbstractResource.ReferenceableName() + for i, v := range result { + result[i] = v + ".destroy" + } + + return result +} + +// GraphNodeReferencer, overriding NodeAbstractResource +func (n *NodeDestroyResource) References() []string { + return nil +} + +// GraphNodeDynamicExpandable +func (n *NodeDestroyResource) DynamicExpand(ctx EvalContext) (*Graph, error) { + // If we have no config we do nothing + if n.Config == nil { + return nil, nil + } + + state, lock := ctx.State() + lock.RLock() + defer lock.RUnlock() + + // Start creating the steps + steps := make([]GraphTransformer, 0, 5) + + // We want deposed resources in the state to be destroyed + steps = append(steps, &DeposedTransformer{ + State: state, + View: n.Config.Id(), + }) + + // Always end with the root being added + steps = append(steps, &RootTransformer{}) + + // Build the graph + b := &BasicGraphBuilder{Steps: steps} + return b.Build(ctx.Path()) +} + +// GraphNodeEvalable +func (n *NodeDestroyResource) EvalTree() EvalNode { + // stateId is the ID to put into the state + stateId := n.Addr.stateId() + if n.Addr.Index > -1 { + stateId = fmt.Sprintf("%s.%d", stateId, n.Addr.Index) + } + + // Build the instance info. More of this will be populated during eval + info := &InstanceInfo{ + Id: stateId, + Type: n.Addr.Type, + uniqueExtra: "destroy", + } + + // Get our state + rs := n.ResourceState + if rs == nil { + rs = &ResourceState{} + } + + var diffApply *InstanceDiff + var provider ResourceProvider + var state *InstanceState + var err error + return &EvalOpFilter{ + Ops: []walkOperation{walkApply, walkDestroy}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + // Get the saved diff for apply + &EvalReadDiff{ + Name: stateId, + Diff: &diffApply, + }, + + // Filter the diff so we only get the destroy + &EvalFilterDiff{ + Diff: &diffApply, + Output: &diffApply, + Destroy: true, + }, + + // If we're not destroying, then compare diffs + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + if diffApply != nil && diffApply.GetDestroy() { + return true, nil + } + + return true, EvalEarlyExitError{} + }, + Then: EvalNoop{}, + }, + + // Load the instance info so we have the module path set + &EvalInstanceInfo{Info: info}, + + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalReadState{ + Name: stateId, + Output: &state, + }, + &EvalRequireState{ + State: &state, + }, + // Make sure we handle data sources properly. + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + /* TODO: data source + if n.Resource.Mode == config.DataResourceMode { + return true, nil + } + */ + + return false, nil + }, + + Then: &EvalReadDataApply{ + Info: info, + Diff: &diffApply, + Provider: &provider, + Output: &state, + }, + Else: &EvalApply{ + Info: info, + State: &state, + Diff: &diffApply, + Provider: &provider, + Output: &state, + Error: &err, + }, + }, + &EvalWriteState{ + Name: stateId, + ResourceType: n.Addr.Type, + Provider: rs.Provider, + Dependencies: rs.Dependencies, + State: &state, + }, + &EvalApplyPost{ + Info: info, + State: &state, + Error: &err, + }, + &EvalUpdateStateHook{}, + }, + }, + } +} diff --git a/terraform/node_root_variable.go b/terraform/node_root_variable.go new file mode 100644 index 000000000..cb61a4e3a --- /dev/null +++ b/terraform/node_root_variable.go @@ -0,0 +1,22 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// NodeRootVariable represents a root variable input. +type NodeRootVariable struct { + Config *config.Variable +} + +func (n *NodeRootVariable) Name() string { + result := fmt.Sprintf("var.%s", n.Config.Name) + return result +} + +// GraphNodeReferenceable +func (n *NodeRootVariable) ReferenceableName() []string { + return []string{n.Name()} +} diff --git a/terraform/resource.go b/terraform/resource.go index 7f1ec3ca4..904d441af 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -72,6 +72,10 @@ type InstanceInfo struct { // HumanId is a unique Id that is human-friendly and useful for UI elements. func (i *InstanceInfo) HumanId() string { + if i == nil { + return "" + } + if len(i.ModulePath) <= 1 { return i.Id } diff --git a/terraform/resource_address.go b/terraform/resource_address.go index da22b2321..06e943f9e 100644 --- a/terraform/resource_address.go +++ b/terraform/resource_address.go @@ -85,6 +85,78 @@ func (r *ResourceAddress) String() string { return strings.Join(result, ".") } +// stateId returns the ID that this resource should be entered with +// in the state. This is also used for diffs. In the future, we'd like to +// move away from this string field so I don't export this. +func (r *ResourceAddress) stateId() string { + result := fmt.Sprintf("%s.%s", r.Type, r.Name) + switch r.Mode { + case config.ManagedResourceMode: + // Done + case config.DataResourceMode: + result = fmt.Sprintf("data.%s", result) + default: + panic(fmt.Errorf("unknown resource mode: %s", r.Mode)) + } + + return result +} + +// parseResourceAddressConfig creates a resource address from a config.Resource +func parseResourceAddressConfig(r *config.Resource) (*ResourceAddress, error) { + return &ResourceAddress{ + Type: r.Type, + Name: r.Name, + Index: -1, + InstanceType: TypePrimary, + Mode: r.Mode, + }, nil +} + +// parseResourceAddressInternal parses the somewhat bespoke resource +// identifier used in states and diffs, such as "instance.name.0". +func parseResourceAddressInternal(s string) (*ResourceAddress, error) { + // Split based on ".". Every resource address should have at least two + // elements (type and name). + parts := strings.Split(s, ".") + if len(parts) < 2 || len(parts) > 4 { + return nil, fmt.Errorf("Invalid internal resource address format: %s", s) + } + + // Data resource if we have at least 3 parts and the first one is data + mode := config.ManagedResourceMode + if len(parts) > 2 && parts[0] == "data" { + mode = config.DataResourceMode + parts = parts[1:] + } + + // If we're not a data resource and we have more than 3, then it is an error + if len(parts) > 3 && mode != config.DataResourceMode { + return nil, fmt.Errorf("Invalid internal resource address format: %s", s) + } + + // Build the parts of the resource address that are guaranteed to exist + addr := &ResourceAddress{ + Type: parts[0], + Name: parts[1], + Index: -1, + InstanceType: TypePrimary, + Mode: mode, + } + + // If we have more parts, then we have an index. Parse that. + if len(parts) > 2 { + idx, err := strconv.ParseInt(parts[2], 0, 0) + if err != nil { + return nil, fmt.Errorf("Error parsing resource address %q: %s", s, err) + } + + addr.Index = int(idx) + } + + return addr, nil +} + func ParseResourceAddress(s string) (*ResourceAddress, error) { matches, err := tokenizeResourceAddress(s) if err != nil { diff --git a/terraform/resource_address_test.go b/terraform/resource_address_test.go index 144d7a9ec..2604441fe 100644 --- a/terraform/resource_address_test.go +++ b/terraform/resource_address_test.go @@ -7,6 +7,99 @@ import ( "github.com/hashicorp/terraform/config" ) +func TestParseResourceAddressInternal(t *testing.T) { + cases := map[string]struct { + Input string + Expected *ResourceAddress + Output string + }{ + "basic resource": { + "aws_instance.foo", + &ResourceAddress{ + Mode: config.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + "aws_instance.foo", + }, + + "basic resource with count": { + "aws_instance.foo.1", + &ResourceAddress{ + Mode: config.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 1, + }, + "aws_instance.foo[1]", + }, + + "data resource": { + "data.aws_ami.foo", + &ResourceAddress{ + Mode: config.DataResourceMode, + Type: "aws_ami", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + "data.aws_ami.foo", + }, + + "data resource with count": { + "data.aws_ami.foo.1", + &ResourceAddress{ + Mode: config.DataResourceMode, + Type: "aws_ami", + Name: "foo", + InstanceType: TypePrimary, + Index: 1, + }, + "data.aws_ami.foo[1]", + }, + + "non-data resource with 4 elements": { + "aws_instance.foo.bar.1", + nil, + "", + }, + } + + for tn, tc := range cases { + t.Run(tc.Input, func(t *testing.T) { + out, err := parseResourceAddressInternal(tc.Input) + if (err != nil) != (tc.Expected == nil) { + t.Fatalf("%s: unexpected err: %#v", tn, err) + } + if err != nil { + return + } + + if !reflect.DeepEqual(out, tc.Expected) { + t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out) + } + + // Compare outputs if those exist + expected := tc.Input + if tc.Output != "" { + expected = tc.Output + } + if out.String() != expected { + t.Fatalf("bad: %q\n\nexpected: %s\n\ngot: %s", tn, expected, out) + } + + // Compare equality because the internal parse is used + // to compare equality to equal inputs. + if !out.Equals(tc.Expected) { + t.Fatalf("expected equality:\n\n%#v\n\n%#v", out, tc.Expected) + } + }) + } +} + func TestParseResourceAddress(t *testing.T) { cases := map[string]struct { Input string @@ -461,3 +554,52 @@ func TestResourceAddressEquals(t *testing.T) { } } } + +func TestResourceAddressStateId(t *testing.T) { + cases := map[string]struct { + Input *ResourceAddress + Expected string + }{ + "basic resource": { + &ResourceAddress{ + Mode: config.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + "aws_instance.foo", + }, + + "basic resource ignores count": { + &ResourceAddress{ + Mode: config.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: 2, + }, + "aws_instance.foo", + }, + + "data resource": { + &ResourceAddress{ + Mode: config.DataResourceMode, + Type: "aws_instance", + Name: "foo", + InstanceType: TypePrimary, + Index: -1, + }, + "data.aws_instance.foo", + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + actual := tc.Input.stateId() + if actual != tc.Expected { + t.Fatalf("bad: %q\n\nexpected: %s\n\ngot: %s", tn, tc.Expected, actual) + } + }) + } +} diff --git a/terraform/shadow_components.go b/terraform/shadow_components.go index 141493df2..116cf84f9 100644 --- a/terraform/shadow_components.go +++ b/terraform/shadow_components.go @@ -208,6 +208,10 @@ func (f *shadowComponentFactoryShared) ResourceProvider( real, shadow := newShadowResourceProvider(p) entry.Real = real entry.Shadow = shadow + + if f.closed { + shadow.CloseShadow() + } } // Store the value @@ -246,6 +250,10 @@ func (f *shadowComponentFactoryShared) ResourceProvisioner( real, shadow := newShadowResourceProvisioner(p) entry.Real = real entry.Shadow = shadow + + if f.closed { + shadow.CloseShadow() + } } // Store the value diff --git a/terraform/shadow_context.go b/terraform/shadow_context.go index 0e213d019..226fd396b 100644 --- a/terraform/shadow_context.go +++ b/terraform/shadow_context.go @@ -101,6 +101,10 @@ func newShadowContext(c *Context) (*Context, *Context, Shadow) { func shadowContextVerify(real, shadow *Context) error { var result error + // The states compared must be pruned so they're minimal/clean + real.state.prune() + shadow.state.prune() + // Compare the states if !real.state.Equal(shadow.state) { result = multierror.Append(result, fmt.Errorf( diff --git a/terraform/shadow_resource_provider.go b/terraform/shadow_resource_provider.go index 4d7643438..816d344a2 100644 --- a/terraform/shadow_resource_provider.go +++ b/terraform/shadow_resource_provider.go @@ -510,7 +510,7 @@ func (p *shadowResourceProviderShadow) Apply( p.ErrorLock.Lock() defer p.ErrorLock.Unlock() p.Error = multierror.Append(p.Error, fmt.Errorf( - "Unknown 'apply' shadow value: %#v", raw)) + "Unknown 'apply' shadow value for %q: %#v", key, raw)) return nil, nil } @@ -518,16 +518,16 @@ func (p *shadowResourceProviderShadow) Apply( if !state.Equal(result.State) { p.ErrorLock.Lock() p.Error = multierror.Append(p.Error, fmt.Errorf( - "Apply: state had unequal states (real, then shadow):\n\n%#v\n\n%#v", - result.State, state)) + "Apply %q: state had unequal states (real, then shadow):\n\n%#v\n\n%#v", + key, result.State, state)) p.ErrorLock.Unlock() } if !diff.Equal(result.Diff) { p.ErrorLock.Lock() p.Error = multierror.Append(p.Error, fmt.Errorf( - "Apply: unequal diffs (real, then shadow):\n\n%#v\n\n%#v", - result.Diff, diff)) + "Apply %q: unequal diffs (real, then shadow):\n\n%#v\n\n%#v", + key, result.Diff, diff)) p.ErrorLock.Unlock() } diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index dd6185613..d81e2f1cb 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -22,8 +22,17 @@ import ( const fixtureDir = "./test-fixtures" func TestMain(m *testing.M) { + // Experimental features + xNewApply := flag.Bool("Xnew-apply", false, "Experiment: new apply graph") + flag.Parse() + // Setup experimental features + X_newApply = *xNewApply + if X_newApply { + println("Xnew-apply enabled") + } + if testing.Verbose() { // if we're verbose, use the logging requested by TF_LOG logging.SetOutput() diff --git a/terraform/test-fixtures/apply-provider-configure-disabled/child/main.tf b/terraform/test-fixtures/apply-provider-configure-disabled/child/main.tf new file mode 100644 index 000000000..c421bf743 --- /dev/null +++ b/terraform/test-fixtures/apply-provider-configure-disabled/child/main.tf @@ -0,0 +1,5 @@ +provider "aws" { + value = "foo" +} + +resource "aws_instance" "foo" {} diff --git a/terraform/test-fixtures/apply-provider-configure-disabled/main.tf b/terraform/test-fixtures/apply-provider-configure-disabled/main.tf new file mode 100644 index 000000000..dbfc52745 --- /dev/null +++ b/terraform/test-fixtures/apply-provider-configure-disabled/main.tf @@ -0,0 +1,7 @@ +provider "aws" { + foo = "bar" +} + +module "child" { + source = "./child" +} diff --git a/terraform/test-fixtures/graph-builder-apply-basic/child/main.tf b/terraform/test-fixtures/graph-builder-apply-basic/child/main.tf new file mode 100644 index 000000000..b802817b8 --- /dev/null +++ b/terraform/test-fixtures/graph-builder-apply-basic/child/main.tf @@ -0,0 +1,7 @@ +resource "aws_instance" "create" { + provisioner "exec" {} +} + +resource "aws_instance" "other" { + value = "${aws_instance.create.id}" +} diff --git a/terraform/test-fixtures/graph-builder-apply-basic/main.tf b/terraform/test-fixtures/graph-builder-apply-basic/main.tf new file mode 100644 index 000000000..d077a4ae5 --- /dev/null +++ b/terraform/test-fixtures/graph-builder-apply-basic/main.tf @@ -0,0 +1,9 @@ +module "child" { + source = "./child" +} + +resource "aws_instance" "create" {} + +resource "aws_instance" "other" { + foo = "${aws_instance.create.bar}" +} diff --git a/terraform/test-fixtures/transform-destroy-edge-basic/main.tf b/terraform/test-fixtures/transform-destroy-edge-basic/main.tf new file mode 100644 index 000000000..a6a6a5ec4 --- /dev/null +++ b/terraform/test-fixtures/transform-destroy-edge-basic/main.tf @@ -0,0 +1,2 @@ +resource "test" "A" {} +resource "test" "B" { value = "${test.A.value}" } diff --git a/terraform/test-fixtures/transform-destroy-edge-multi/main.tf b/terraform/test-fixtures/transform-destroy-edge-multi/main.tf new file mode 100644 index 000000000..93e8211ca --- /dev/null +++ b/terraform/test-fixtures/transform-destroy-edge-multi/main.tf @@ -0,0 +1,3 @@ +resource "test" "A" {} +resource "test" "B" { value = "${test.A.value}" } +resource "test" "C" { value = "${test.B.value}" } diff --git a/terraform/test-fixtures/transform-diff-basic/main.tf b/terraform/test-fixtures/transform-diff-basic/main.tf new file mode 100644 index 000000000..919f140bb --- /dev/null +++ b/terraform/test-fixtures/transform-diff-basic/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "foo" {} diff --git a/terraform/test-fixtures/transform-flat-config-basic/child/main.tf b/terraform/test-fixtures/transform-flat-config-basic/child/main.tf new file mode 100644 index 000000000..0c70b1b5d --- /dev/null +++ b/terraform/test-fixtures/transform-flat-config-basic/child/main.tf @@ -0,0 +1 @@ +resource "aws_instance" "baz" {} diff --git a/terraform/test-fixtures/transform-flat-config-basic/main.tf b/terraform/test-fixtures/transform-flat-config-basic/main.tf new file mode 100644 index 000000000..c588350d4 --- /dev/null +++ b/terraform/test-fixtures/transform-flat-config-basic/main.tf @@ -0,0 +1,6 @@ +resource "aws_instance" "foo" {} +resource "aws_instance" "bar" { value = "${aws_instance.foo.value}" } + +module "child" { + source = "./child" +} diff --git a/terraform/test-fixtures/transform-module-var-basic/child/main.tf b/terraform/test-fixtures/transform-module-var-basic/child/main.tf new file mode 100644 index 000000000..0568aa053 --- /dev/null +++ b/terraform/test-fixtures/transform-module-var-basic/child/main.tf @@ -0,0 +1,3 @@ +variable "value" {} + +output "result" { value = "${var.value}" } diff --git a/terraform/test-fixtures/transform-module-var-basic/main.tf b/terraform/test-fixtures/transform-module-var-basic/main.tf new file mode 100644 index 000000000..0adb513f1 --- /dev/null +++ b/terraform/test-fixtures/transform-module-var-basic/main.tf @@ -0,0 +1,4 @@ +module "child" { + source = "./child" + value = "foo" +} diff --git a/terraform/test-fixtures/transform-module-var-nested/child/child/main.tf b/terraform/test-fixtures/transform-module-var-nested/child/child/main.tf new file mode 100644 index 000000000..0568aa053 --- /dev/null +++ b/terraform/test-fixtures/transform-module-var-nested/child/child/main.tf @@ -0,0 +1,3 @@ +variable "value" {} + +output "result" { value = "${var.value}" } diff --git a/terraform/test-fixtures/transform-module-var-nested/child/main.tf b/terraform/test-fixtures/transform-module-var-nested/child/main.tf new file mode 100644 index 000000000..b8c7f0bac --- /dev/null +++ b/terraform/test-fixtures/transform-module-var-nested/child/main.tf @@ -0,0 +1,6 @@ +variable "value" {} + +module "child" { + source = "./child" + value = "${var.value}" +} diff --git a/terraform/test-fixtures/transform-module-var-nested/main.tf b/terraform/test-fixtures/transform-module-var-nested/main.tf new file mode 100644 index 000000000..2c20f1979 --- /dev/null +++ b/terraform/test-fixtures/transform-module-var-nested/main.tf @@ -0,0 +1,4 @@ +module "child" { + source = "./child" + value = "foo" +} diff --git a/terraform/transform_attach_config_provider.go b/terraform/transform_attach_config_provider.go new file mode 100644 index 000000000..4b41a2d0f --- /dev/null +++ b/terraform/transform_attach_config_provider.go @@ -0,0 +1,75 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// GraphNodeAttachProvider is an interface that must be implemented by nodes +// that want provider configurations attached. +type GraphNodeAttachProvider interface { + // Must be implemented to determine the path for the configuration + GraphNodeSubPath + + // ProviderName with no module prefix. Example: "aws". + ProviderName() string + + // Sets the configuration + AttachProvider(*config.ProviderConfig) +} + +// AttachProviderConfigTransformer goes through the graph and attaches +// provider configuration structures to nodes that implement the interfaces +// above. +// +// The attached configuration structures are directly from the configuration. +// If they're going to be modified, a copy should be made. +type AttachProviderConfigTransformer struct { + Module *module.Tree // Module is the root module for the config +} + +func (t *AttachProviderConfigTransformer) Transform(g *Graph) error { + if err := t.attachProviders(g); err != nil { + return err + } + + return nil +} + +func (t *AttachProviderConfigTransformer) attachProviders(g *Graph) error { + // Go through and find GraphNodeAttachProvider + for _, v := range g.Vertices() { + // Only care about GraphNodeAttachProvider implementations + apn, ok := v.(GraphNodeAttachProvider) + if !ok { + continue + } + + // TODO: aliases? + + // Determine what we're looking for + path := normalizeModulePath(apn.Path()) + path = path[1:] + name := apn.ProviderName() + log.Printf("[TRACE] Attach provider request: %#v %s", path, name) + + // Get the configuration. + tree := t.Module.Child(path) + if tree == nil { + continue + } + + // Go through the provider configs to find the matching config + for _, p := range tree.Config().ProviderConfigs { + if p.Name == name { + log.Printf("[TRACE] Attaching provider config: %#v", p) + apn.AttachProvider(p) + break + } + } + } + + return nil +} diff --git a/terraform/transform_attach_config_resource.go b/terraform/transform_attach_config_resource.go new file mode 100644 index 000000000..83612fede --- /dev/null +++ b/terraform/transform_attach_config_resource.go @@ -0,0 +1,76 @@ +package terraform + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// GraphNodeAttachResourceConfig is an interface that must be implemented by nodes +// that want resource configurations attached. +type GraphNodeAttachResourceConfig interface { + // ResourceAddr is the address to the resource + ResourceAddr() *ResourceAddress + + // Sets the configuration + AttachResourceConfig(*config.Resource) +} + +// AttachResourceConfigTransformer goes through the graph and attaches +// resource configuration structures to nodes that implement the interfaces +// above. +// +// The attached configuration structures are directly from the configuration. +// If they're going to be modified, a copy should be made. +type AttachResourceConfigTransformer struct { + Module *module.Tree // Module is the root module for the config +} + +func (t *AttachResourceConfigTransformer) Transform(g *Graph) error { + log.Printf("[TRACE] AttachResourceConfigTransformer: Beginning...") + + // Go through and find GraphNodeAttachResource + for _, v := range g.Vertices() { + // Only care about GraphNodeAttachResource implementations + arn, ok := v.(GraphNodeAttachResourceConfig) + if !ok { + continue + } + + // Determine what we're looking for + addr := arn.ResourceAddr() + log.Printf("[TRACE] AttachResourceConfigTransformer: Attach resource request: %s", addr) + + // Get the configuration. + path := normalizeModulePath(addr.Path) + path = path[1:] + tree := t.Module.Child(path) + if tree == nil { + continue + } + + // Go through the resource configs to find the matching config + for _, r := range tree.Config().Resources { + // Get a resource address so we can compare + a, err := parseResourceAddressConfig(r) + if err != nil { + panic(fmt.Sprintf( + "Error parsing config address, this is a bug: %#v", r)) + } + a.Path = addr.Path + + // If this is not the same resource, then continue + if !a.Equals(addr) { + continue + } + + log.Printf("[TRACE] Attaching resource config: %#v", r) + arn.AttachResourceConfig(r) + break + } + } + + return nil +} diff --git a/terraform/transform_attach_state.go b/terraform/transform_attach_state.go new file mode 100644 index 000000000..68a8f3b07 --- /dev/null +++ b/terraform/transform_attach_state.go @@ -0,0 +1,68 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeAttachResourceState is an interface that can be implemented +// to request that a ResourceState is attached to the node. +type GraphNodeAttachResourceState interface { + // The address to the resource for the state + ResourceAddr() *ResourceAddress + + // Sets the state + AttachResourceState(*ResourceState) +} + +// AttachStateTransformer goes through the graph and attaches +// state to nodes that implement the interfaces above. +type AttachStateTransformer struct { + State *State // State is the root state +} + +func (t *AttachStateTransformer) Transform(g *Graph) error { + // If no state, then nothing to do + if t.State == nil { + log.Printf("[DEBUG] Not attaching any state: state is nil") + return nil + } + + filter := &StateFilter{State: t.State} + for _, v := range g.Vertices() { + // Only care about nodes requesting we're adding state + an, ok := v.(GraphNodeAttachResourceState) + if !ok { + continue + } + addr := an.ResourceAddr() + + // Get the module state + results, err := filter.Filter(addr.String()) + if err != nil { + return err + } + + // Attach the first resource state we get + found := false + for _, result := range results { + if rs, ok := result.Value.(*ResourceState); ok { + log.Printf( + "[DEBUG] Attaching resource state to %q: %s", + dag.VertexName(v), rs) + an.AttachResourceState(rs) + found = true + break + } + } + + if !found { + log.Printf( + "[DEBUG] Resource state not foudn for %q: %s", + dag.VertexName(v), addr) + } + } + + return nil +} diff --git a/terraform/transform_config_flat.go b/terraform/transform_config_flat.go new file mode 100644 index 000000000..92f9888d6 --- /dev/null +++ b/terraform/transform_config_flat.go @@ -0,0 +1,80 @@ +package terraform + +import ( + "errors" + + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// FlatConfigTransformer is a GraphTransformer that adds the configuration +// to the graph. The module used to configure this transformer must be +// the root module. +// +// This transform adds the nodes but doesn't connect any of the references. +// The ReferenceTransformer should be used for that. +// +// NOTE: In relation to ConfigTransformer: this is a newer generation config +// transformer. It puts the _entire_ config into the graph (there is no +// "flattening" step as before). +type FlatConfigTransformer struct { + Concrete ConcreteResourceNodeFunc // What to turn resources into + + Module *module.Tree +} + +func (t *FlatConfigTransformer) Transform(g *Graph) error { + // If no module, we do nothing + if t.Module == nil { + return nil + } + + // If the module is not loaded, that is an error + if !t.Module.Loaded() { + return errors.New("module must be loaded") + } + + return t.transform(g, t.Module) +} + +func (t *FlatConfigTransformer) transform(g *Graph, m *module.Tree) error { + // If no module, no problem + if m == nil { + return nil + } + + // Transform all the children. + for _, c := range m.Children() { + if err := t.transform(g, c); err != nil { + return err + } + } + + // Get the configuration for this module + config := m.Config() + + // Write all the resources out + for _, r := range config.Resources { + // Grab the address for this resource + addr, err := parseResourceAddressConfig(r) + if err != nil { + return err + } + addr.Path = m.Path() + + // Build the abstract resource. We have the config already so + // we'll just pre-populate that. + abstract := &NodeAbstractResource{ + Addr: addr, + Config: r, + } + var node dag.Vertex = abstract + if f := t.Concrete; f != nil { + node = f(abstract) + } + + g.Add(node) + } + + return nil +} diff --git a/terraform/transform_config_flat_test.go b/terraform/transform_config_flat_test.go new file mode 100644 index 000000000..aa393d5a9 --- /dev/null +++ b/terraform/transform_config_flat_test.go @@ -0,0 +1,40 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestFlatConfigTransformer_nilModule(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &FlatConfigTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + if len(g.Vertices()) > 0 { + t.Fatal("graph should be empty") + } +} + +func TestFlatConfigTransformer(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &FlatConfigTransformer{ + Module: testModule(t, "transform-flat-config-basic"), + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformFlatConfigBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testTransformFlatConfigBasicStr = ` +aws_instance.bar +aws_instance.foo +module.child.aws_instance.baz +` diff --git a/terraform/transform_count_boundary.go b/terraform/transform_count_boundary.go new file mode 100644 index 000000000..83415f352 --- /dev/null +++ b/terraform/transform_count_boundary.go @@ -0,0 +1,28 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// CountBoundaryTransformer adds a node that depends on everything else +// so that it runs last in order to clean up the state for nodes that +// are on the "count boundary": "foo.0" when only one exists becomes "foo" +type CountBoundaryTransformer struct{} + +func (t *CountBoundaryTransformer) Transform(g *Graph) error { + node := &NodeCountBoundary{} + g.Add(node) + + // Depends on everything + for _, v := range g.Vertices() { + // Don't connect to ourselves + if v == node { + continue + } + + // Connect! + g.Connect(dag.BasicEdge(node, v)) + } + + return nil +} diff --git a/terraform/transform_destroy_cbd.go b/terraform/transform_destroy_cbd.go new file mode 100644 index 000000000..0dde08888 --- /dev/null +++ b/terraform/transform_destroy_cbd.go @@ -0,0 +1,190 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeDestroyerCBD must be implemented by nodes that might be +// create-before-destroy destroyers. +type GraphNodeDestroyerCBD interface { + GraphNodeDestroyer + + // CreateBeforeDestroy returns true if this node represents a node + // that is doing a CBD. + CreateBeforeDestroy() bool +} + +// CBDEdgeTransformer modifies the edges of CBD nodes that went through +// the DestroyEdgeTransformer to have the right dependencies. There are +// two real tasks here: +// +// 1. With CBD, the destroy edge is inverted: the destroy depends on +// the creation. +// +// 2. A_d must depend on resources that depend on A. This is to enable +// the destroy to only happen once nodes that depend on A successfully +// update to A. Example: adding a web server updates the load balancer +// before deleting the old web server. +// +type CBDEdgeTransformer struct { + // Module and State are only needed to look up dependencies in + // any way possible. Either can be nil if not availabile. + Module *module.Tree + State *State +} + +func (t *CBDEdgeTransformer) Transform(g *Graph) error { + log.Printf("[TRACE] CBDEdgeTransformer: Beginning CBD transformation...") + + // Go through and reverse any destroy edges + destroyMap := make(map[string][]dag.Vertex) + for _, v := range g.Vertices() { + dn, ok := v.(GraphNodeDestroyerCBD) + if !ok { + continue + } + + if !dn.CreateBeforeDestroy() { + continue + } + + // Find the destroy edge. There should only be one. + for _, e := range g.EdgesTo(v) { + // Not a destroy edge, ignore it + de, ok := e.(*DestroyEdge) + if !ok { + continue + } + + log.Printf("[TRACE] CBDEdgeTransformer: inverting edge: %s => %s", + dag.VertexName(de.Source()), dag.VertexName(de.Target())) + + // Found it! Invert. + g.RemoveEdge(de) + g.Connect(&DestroyEdge{S: de.Target(), T: de.Source()}) + } + + // Add this to the list of nodes that we need to fix up + // the edges for (step 2 above in the docs). + key := dn.DestroyAddr().String() + destroyMap[key] = append(destroyMap[key], v) + } + + // If we have no CBD nodes, then our work here is done + if len(destroyMap) == 0 { + return nil + } + + // We have CBD nodes. We now have to move on to the much more difficult + // task of connecting dependencies of the creation side of the destroy + // to the destruction node. The easiest way to explain this is an example: + // + // Given a pre-destroy dependence of: A => B + // And A has CBD set. + // + // The resulting graph should be: A => B => A_d + // + // They key here is that B happens before A is destroyed. This is to + // facilitate the primary purpose for CBD: making sure that downstreams + // are properly updated to avoid downtime before the resource is destroyed. + // + // We can't trust that the resource being destroyed or anything that + // depends on it is actually in our current graph so we make a new + // graph in order to determine those dependencies and add them in. + log.Printf("[TRACE] CBDEdgeTransformer: building graph to find dependencies...") + depMap, err := t.depMap(destroyMap) + if err != nil { + return err + } + + // We now have the mapping of resource addresses to the destroy + // nodes they need to depend on. We now go through our own vertices to + // find any matching these addresses and make the connection. + for _, v := range g.Vertices() { + // We're looking for creators + rn, ok := v.(GraphNodeCreator) + if !ok { + continue + } + + // Get the address + addr := rn.CreateAddr() + key := addr.String() + + // If there is nothing this resource should depend on, ignore it + dns, ok := depMap[key] + if !ok { + continue + } + + // We have nodes! Make the connection + for _, dn := range dns { + log.Printf("[TRACE] CBDEdgeTransformer: destroy depends on dependence: %s => %s", + dag.VertexName(dn), dag.VertexName(v)) + g.Connect(dag.BasicEdge(dn, v)) + } + } + + return nil +} + +func (t *CBDEdgeTransformer) depMap( + destroyMap map[string][]dag.Vertex) (map[string][]dag.Vertex, error) { + // Build the graph of our config, this ensures that all resources + // are present in the graph. + g, err := (&BasicGraphBuilder{ + Steps: []GraphTransformer{ + &FlatConfigTransformer{Module: t.Module}, + &AttachResourceConfigTransformer{Module: t.Module}, + &AttachStateTransformer{State: t.State}, + &ReferenceTransformer{}, + }, + }).Build(nil) + if err != nil { + return nil, err + } + + // Using this graph, build the list of destroy nodes that each resource + // address should depend on. For example, when we find B, we map the + // address of B to A_d in the "depMap" variable below. + depMap := make(map[string][]dag.Vertex) + for _, v := range g.Vertices() { + // We're looking for resources. + rn, ok := v.(GraphNodeResource) + if !ok { + continue + } + + // Get the address + addr := rn.ResourceAddr() + key := addr.String() + + // Get the destroy nodes that are destroying this resource. + // If there aren't any, then we don't need to worry about + // any connections. + dns, ok := destroyMap[key] + if !ok { + continue + } + + // Get the nodes that depend on this on. In the example above: + // finding B in A => B. + for _, v := range g.UpEdges(v).List() { + // We're looking for resources. + rn, ok := v.(GraphNodeResource) + if !ok { + continue + } + + // Keep track of the destroy nodes that this address + // needs to depend on. + key := rn.ResourceAddr().String() + depMap[key] = append(depMap[key], dns...) + } + } + + return depMap, nil +} diff --git a/terraform/transform_destroy_cbd_test.go b/terraform/transform_destroy_cbd_test.go new file mode 100644 index 000000000..dad1d27bb --- /dev/null +++ b/terraform/transform_destroy_cbd_test.go @@ -0,0 +1,45 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestCBDEdgeTransformer(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeCreatorTest{AddrString: "test.A"}) + g.Add(&graphNodeCreatorTest{AddrString: "test.B"}) + g.Add(&graphNodeDestroyerTest{AddrString: "test.A", CBD: true}) + + module := testModule(t, "transform-destroy-edge-basic") + + { + tf := &DestroyEdgeTransformer{ + Module: module, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &CBDEdgeTransformer{Module: module} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformCBDEdgeBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testTransformCBDEdgeBasicStr = ` +test.A +test.A (destroy) + test.A + test.B +test.B +` diff --git a/terraform/transform_destroy_edge.go b/terraform/transform_destroy_edge.go new file mode 100644 index 000000000..a3a8f8d9f --- /dev/null +++ b/terraform/transform_destroy_edge.go @@ -0,0 +1,191 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeDestroyer must be implemented by nodes that destroy resources. +type GraphNodeDestroyer interface { + dag.Vertex + + // ResourceAddr is the address of the resource that is being + // destroyed by this node. If this returns nil, then this node + // is not destroying anything. + DestroyAddr() *ResourceAddress +} + +// GraphNodeCreator must be implemented by nodes that create OR update resources. +type GraphNodeCreator interface { + // ResourceAddr is the address of the resource being created or updated + CreateAddr() *ResourceAddress +} + +// DestroyEdgeTransformer is a GraphTransformer that creates the proper +// references for destroy resources. Destroy resources are more complex +// in that they must be depend on the destruction of resources that +// in turn depend on the CREATION of the node being destroy. +// +// That is complicated. Visually: +// +// B_d -> A_d -> A -> B +// +// Notice that A destroy depends on B destroy, while B create depends on +// A create. They're inverted. This must be done for example because often +// dependent resources will block parent resources from deleting. Concrete +// example: VPC with subnets, the VPC can't be deleted while there are +// still subnets. +type DestroyEdgeTransformer struct { + // Module and State are only needed to look up dependencies in + // any way possible. Either can be nil if not availabile. + Module *module.Tree + State *State +} + +func (t *DestroyEdgeTransformer) Transform(g *Graph) error { + log.Printf("[TRACE] DestroyEdgeTransformer: Beginning destroy edge transformation...") + + // Build a map of what is being destroyed (by address string) to + // the list of destroyers. In general there will only be one destroyer + // but to make it more robust we support multiple. + destroyers := make(map[string][]GraphNodeDestroyer) + for _, v := range g.Vertices() { + dn, ok := v.(GraphNodeDestroyer) + if !ok { + continue + } + + addr := dn.DestroyAddr() + if addr == nil { + continue + } + + key := addr.String() + log.Printf( + "[TRACE] DestroyEdgeTransformer: %s destroying %q", + dag.VertexName(dn), key) + destroyers[key] = append(destroyers[key], dn) + } + + // If we aren't destroying anything, there will be no edges to make + // so just exit early and avoid future work. + if len(destroyers) == 0 { + return nil + } + + // Go through and connect creators to destroyers. Going along with + // our example, this makes: A_d => A + for _, v := range g.Vertices() { + cn, ok := v.(GraphNodeCreator) + if !ok { + continue + } + + addr := cn.CreateAddr() + if addr == nil { + continue + } + + key := addr.String() + ds := destroyers[key] + if len(ds) == 0 { + continue + } + + for _, d := range ds { + // For illustrating our example + a_d := d.(dag.Vertex) + a := v + + log.Printf( + "[TRACE] DestroyEdgeTransformer: connecting creator/destroyer: %s, %s", + dag.VertexName(a), dag.VertexName(a_d)) + + g.Connect(&DestroyEdge{S: a, T: a_d}) + } + } + + // This is strange but is the easiest way to get the dependencies + // of a node that is being destroyed. We use another graph to make sure + // the resource is in the graph and ask for references. We have to do this + // because the node that is being destroyed may NOT be in the graph. + // + // Example: resource A is force new, then destroy A AND create A are + // in the graph. BUT if resource A is just pure destroy, then only + // destroy A is in the graph, and create A is not. + steps := []GraphTransformer{ + &AttachResourceConfigTransformer{Module: t.Module}, + &AttachStateTransformer{State: t.State}, + } + + // Go through the all destroyers and find what they're destroying. + // Use this to find the dependencies, look up if any of them are being + // destroyed, and to make the proper edge. + for d, dns := range destroyers { + // d is what is being destroyed. We parse the resource address + // which it came from it is a panic if this fails. + addr, err := ParseResourceAddress(d) + if err != nil { + panic(err) + } + + // This part is a little bit weird but is the best way to + // find the dependencies we need to: build a graph and use the + // attach config and state transformers then ask for references. + node := &NodeAbstractResource{Addr: addr} + { + var g Graph + g.Add(node) + for _, s := range steps { + if err := s.Transform(&g); err != nil { + return err + } + } + } + + // Get the references of the creation node. If it has none, + // then there are no edges to make here. + prefix := modulePrefixStr(normalizeModulePath(addr.Path)) + deps := modulePrefixList(node.References(), prefix) + log.Printf( + "[TRACE] DestroyEdgeTransformer: creation of %q depends on %#v", + d, deps) + if len(deps) == 0 { + continue + } + + // We have dependencies, check if any are being destroyed + // to build the list of things that we must depend on! + // + // In the example of the struct, if we have: + // + // B_d => A_d => A => B + // + // Then at this point in the algorithm we started with A_d, + // we built A (to get dependencies), and we found B. We're now looking + // to see if B_d exists. + var depDestroyers []dag.Vertex + for _, d := range deps { + if ds, ok := destroyers[d]; ok { + for _, d := range ds { + depDestroyers = append(depDestroyers, d.(dag.Vertex)) + log.Printf( + "[TRACE] DestroyEdgeTransformer: destruction of %q depends on %s", + addr.String(), dag.VertexName(d)) + } + } + } + + // Go through and make the connections. Use the variable + // names "a_d" and "b_d" to reference our example. + for _, a_d := range dns { + for _, b_d := range depDestroyers { + g.Connect(dag.BasicEdge(b_d, a_d)) + } + } + } + + return nil +} diff --git a/terraform/transform_destroy_edge_test.go b/terraform/transform_destroy_edge_test.go new file mode 100644 index 000000000..e2c1a8a37 --- /dev/null +++ b/terraform/transform_destroy_edge_test.go @@ -0,0 +1,114 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestDestroyEdgeTransformer(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeDestroyerTest{AddrString: "test.A"}) + g.Add(&graphNodeDestroyerTest{AddrString: "test.B"}) + tf := &DestroyEdgeTransformer{ + Module: testModule(t, "transform-destroy-edge-basic"), + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformDestroyEdgeBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestDestroyEdgeTransformer_create(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeDestroyerTest{AddrString: "test.A"}) + g.Add(&graphNodeDestroyerTest{AddrString: "test.B"}) + g.Add(&graphNodeCreatorTest{AddrString: "test.A"}) + tf := &DestroyEdgeTransformer{ + Module: testModule(t, "transform-destroy-edge-basic"), + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformDestroyEdgeCreatorStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestDestroyEdgeTransformer_multi(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeDestroyerTest{AddrString: "test.A"}) + g.Add(&graphNodeDestroyerTest{AddrString: "test.B"}) + g.Add(&graphNodeDestroyerTest{AddrString: "test.C"}) + tf := &DestroyEdgeTransformer{ + Module: testModule(t, "transform-destroy-edge-multi"), + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformDestroyEdgeMultiStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +type graphNodeCreatorTest struct { + AddrString string +} + +func (n *graphNodeCreatorTest) Name() string { return n.CreateAddr().String() } +func (n *graphNodeCreatorTest) CreateAddr() *ResourceAddress { + addr, err := ParseResourceAddress(n.AddrString) + if err != nil { + panic(err) + } + + return addr +} + +type graphNodeDestroyerTest struct { + AddrString string + CBD bool +} + +func (n *graphNodeDestroyerTest) Name() string { return n.DestroyAddr().String() + " (destroy)" } +func (n *graphNodeDestroyerTest) CreateBeforeDestroy() bool { return n.CBD } +func (n *graphNodeDestroyerTest) DestroyAddr() *ResourceAddress { + addr, err := ParseResourceAddress(n.AddrString) + if err != nil { + panic(err) + } + + return addr +} + +const testTransformDestroyEdgeBasicStr = ` +test.A (destroy) + test.B (destroy) +test.B (destroy) +` + +const testTransformDestroyEdgeCreatorStr = ` +test.A + test.A (destroy) +test.A (destroy) + test.B (destroy) +test.B (destroy) +` + +const testTransformDestroyEdgeMultiStr = ` +test.A (destroy) + test.B (destroy) +test.B (destroy) + test.C (destroy) +test.C (destroy) +` diff --git a/terraform/transform_diff.go b/terraform/transform_diff.go new file mode 100644 index 000000000..7943af685 --- /dev/null +++ b/terraform/transform_diff.go @@ -0,0 +1,86 @@ +package terraform + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// DiffTransformer is a GraphTransformer that adds the elements of +// the diff to the graph. +// +// This transform is used for example by the ApplyGraphBuilder to ensure +// that only resources that are being modified are represented in the graph. +// +// Module and State is still required for the DiffTransformer for annotations +// since the Diff doesn't contain all the information required to build the +// complete graph (such as create-before-destroy information). The graph +// is built based on the diff first, though, ensuring that only resources +// that are being modified are present in the graph. +type DiffTransformer struct { + Concrete ConcreteResourceNodeFunc + + Diff *Diff + Module *module.Tree + State *State +} + +func (t *DiffTransformer) Transform(g *Graph) error { + // If the diff is nil or empty (nil is empty) then do nothing + if t.Diff.Empty() { + return nil + } + + // Go through all the modules in the diff. + log.Printf("[TRACE] DiffTransformer: starting") + var nodes []dag.Vertex + for _, m := range t.Diff.Modules { + log.Printf("[TRACE] DiffTransformer: Module: %s", m) + // TODO: If this is a destroy diff then add a module destroy node + + // Go through all the resources in this module. + for name, inst := range m.Resources { + log.Printf("[TRACE] DiffTransformer: Resource %q: %#v", name, inst) + + // We have changes! This is a create or update operation. + // First grab the address so we have a unique way to + // reference this resource. + addr, err := parseResourceAddressInternal(name) + if err != nil { + panic(fmt.Sprintf( + "Error parsing internal name, this is a bug: %q", name)) + } + + // Very important: add the module path for this resource to + // the address. Remove "root" from it. + addr.Path = m.Path[1:] + + // If we're destroying, add the destroy node + if inst.Destroy { + abstract := NodeAbstractResource{Addr: addr} + g.Add(&NodeDestroyResource{NodeAbstractResource: abstract}) + } + + // If we have changes, then add the applyable version + if len(inst.Attributes) > 0 { + // Add the resource to the graph + abstract := &NodeAbstractResource{Addr: addr} + var node dag.Vertex = abstract + if f := t.Concrete; f != nil { + node = f(abstract) + } + + nodes = append(nodes, node) + } + } + } + + // Add all the nodes to the graph + for _, n := range nodes { + g.Add(n) + } + + return nil +} diff --git a/terraform/transform_diff_test.go b/terraform/transform_diff_test.go new file mode 100644 index 000000000..1ee7fabde --- /dev/null +++ b/terraform/transform_diff_test.go @@ -0,0 +1,55 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestDiffTransformer_nilDiff(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &DiffTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + if len(g.Vertices()) > 0 { + t.Fatal("graph should be empty") + } +} + +func TestDiffTransformer(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &DiffTransformer{ + Module: testModule(t, "transform-diff-basic"), + Diff: &Diff{ + Modules: []*ModuleDiff{ + &ModuleDiff{ + Path: []string{"root"}, + Resources: map[string]*InstanceDiff{ + "aws_instance.foo": &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "name": &ResourceAttrDiff{ + Old: "", + New: "foo", + }, + }, + }, + }, + }, + }, + }, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformDiffBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testTransformDiffBasicStr = ` +aws_instance.foo +` diff --git a/terraform/transform_module.go b/terraform/transform_module_destroy.go similarity index 100% rename from terraform/transform_module.go rename to terraform/transform_module_destroy.go diff --git a/terraform/transform_module_test.go b/terraform/transform_module_test.go deleted file mode 100644 index cc3ee2f47..000000000 --- a/terraform/transform_module_test.go +++ /dev/null @@ -1 +0,0 @@ -package terraform diff --git a/terraform/transform_module_variable.go b/terraform/transform_module_variable.go new file mode 100644 index 000000000..1e035107c --- /dev/null +++ b/terraform/transform_module_variable.go @@ -0,0 +1,122 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/dag" +) + +// ModuleVariableTransformer is a GraphTransformer that adds all the variables +// in the configuration to the graph. +// +// This only adds variables that either have no dependencies (and therefore +// always succeed) or has dependencies that are 100% represented in the +// graph. +type ModuleVariableTransformer struct { + Module *module.Tree +} + +func (t *ModuleVariableTransformer) Transform(g *Graph) error { + return t.transform(g, nil, t.Module) +} + +func (t *ModuleVariableTransformer) transform(g *Graph, parent, m *module.Tree) error { + // If no config, no variables + if m == nil { + return nil + } + + // If we have a parent, we can determine if a module variable is being + // used, so we transform this. + if parent != nil { + if err := t.transformSingle(g, parent, m); err != nil { + return err + } + } + + // Transform all the children. This must be done AFTER the transform + // above since child module variables can reference parent module variables. + for _, c := range m.Children() { + if err := t.transform(g, m, c); err != nil { + return err + } + } + + return nil +} + +func (t *ModuleVariableTransformer) transformSingle(g *Graph, parent, m *module.Tree) error { + // If we have no vars, we're done! + vars := m.Config().Variables + if len(vars) == 0 { + log.Printf("[TRACE] Module %#v has no variables, skipping.", m.Path()) + return nil + } + + // Look for usage of this module + var mod *config.Module + for _, modUse := range parent.Config().Modules { + if modUse.Name == m.Name() { + mod = modUse + break + } + } + if mod == nil { + log.Printf("[INFO] Module %#v not used, not adding variables", m.Path()) + return nil + } + + // Build the reference map so we can determine if we're referencing things. + refMap := NewReferenceMap(g.Vertices()) + + // Add all variables here + for _, v := range vars { + // Determine the value of the variable. If it isn't in the + // configuration then it was never set and that's not a problem. + var value *config.RawConfig + if raw, ok := mod.RawConfig.Raw[v.Name]; ok { + var err error + value, err = config.NewRawConfig(map[string]interface{}{ + v.Name: raw, + }) + if err != nil { + // This shouldn't happen because it is already in + // a RawConfig above meaning it worked once before. + panic(err) + } + } + + // Build the node. + // + // NOTE: For now this is just an "applyable" variable. As we build + // new graph builders for the other operations I suspect we'll + // find a way to parameterize this, require new transforms, etc. + node := &NodeApplyableModuleVariable{ + PathValue: normalizeModulePath(m.Path()), + Config: v, + Value: value, + Module: t.Module, + } + + // If the node references something, then we check to make sure + // that the thing it references is in the graph. If it isn't, then + // we don't add it because we may not be able to compute the output. + // + // If the node references nothing, we always include it since there + // is no other clear time to compute it. + matches, missing := refMap.References(node) + if len(missing) > 0 { + log.Printf( + "[INFO] Not including %q in graph, matches: %v, missing: %s", + dag.VertexName(node), matches, missing) + continue + } + + // Add it! + g.Add(node) + } + + return nil +} diff --git a/terraform/transform_module_variable_test.go b/terraform/transform_module_variable_test.go new file mode 100644 index 000000000..6842b325c --- /dev/null +++ b/terraform/transform_module_variable_test.go @@ -0,0 +1,65 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestModuleVariableTransformer(t *testing.T) { + g := Graph{Path: RootModulePath} + module := testModule(t, "transform-module-var-basic") + + { + tf := &RootVariableTransformer{Module: module} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &ModuleVariableTransformer{Module: module} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformModuleVarBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestModuleVariableTransformer_nested(t *testing.T) { + g := Graph{Path: RootModulePath} + module := testModule(t, "transform-module-var-nested") + + { + tf := &RootVariableTransformer{Module: module} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &ModuleVariableTransformer{Module: module} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformModuleVarNestedStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +const testTransformModuleVarBasicStr = ` +module.child.var.value +` + +const testTransformModuleVarNestedStr = ` +module.child.module.child.var.value +module.child.var.value +` diff --git a/terraform/transform_orphan.go b/terraform/transform_orphan.go index f47f51681..27f032251 100644 --- a/terraform/transform_orphan.go +++ b/terraform/transform_orphan.go @@ -209,6 +209,8 @@ func (n *graphNodeOrphanResource) EvalTree() EvalNode { // Build instance info info := &InstanceInfo{Id: n.ResourceKey.String(), Type: n.ResourceKey.Type} + info.uniqueExtra = "destroy" + seq.Nodes = append(seq.Nodes, &EvalInstanceInfo{Info: info}) // Each resource mode has its own lifecycle diff --git a/terraform/transform_orphan_output.go b/terraform/transform_orphan_output.go new file mode 100644 index 000000000..49568d5bc --- /dev/null +++ b/terraform/transform_orphan_output.go @@ -0,0 +1,64 @@ +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// OrphanOutputTransformer finds the outputs that aren't present +// in the given config that are in the state and adds them to the graph +// for deletion. +type OrphanOutputTransformer struct { + Module *module.Tree // Root module + State *State // State is the root state +} + +func (t *OrphanOutputTransformer) Transform(g *Graph) error { + if t.State == nil { + log.Printf("[DEBUG] No state, no orphan outputs") + return nil + } + + return t.transform(g, t.Module) +} + +func (t *OrphanOutputTransformer) transform(g *Graph, m *module.Tree) error { + // Get our configuration, and recurse into children + var c *config.Config + if m != nil { + c = m.Config() + for _, child := range m.Children() { + if err := t.transform(g, child); err != nil { + return err + } + } + } + + // Get the state. If there is no state, then we have no orphans! + path := normalizeModulePath(m.Path()) + state := t.State.ModuleByPath(path) + if state == nil { + return nil + } + + // Make a map of the valid outputs + valid := make(map[string]struct{}) + for _, o := range c.Outputs { + valid[o.Name] = struct{}{} + } + + // Go through the outputs and find the ones that aren't in our config. + for n, _ := range state.Outputs { + // If it is in the valid map, then ignore + if _, ok := valid[n]; ok { + continue + } + + // Orphan! + g.Add(&NodeOutputOrphan{OutputName: n, PathValue: path}) + } + + return nil +} diff --git a/terraform/transform_output.go b/terraform/transform_output.go index d3e839ce1..b260f4caa 100644 --- a/terraform/transform_output.go +++ b/terraform/transform_output.go @@ -1,98 +1,59 @@ package terraform import ( - "fmt" - - "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/config/module" ) -// GraphNodeOutput is an interface that nodes that are outputs must -// implement. The OutputName returned is the name of the output key -// that they manage. -type GraphNodeOutput interface { - OutputName() string +// OutputTransformer is a GraphTransformer that adds all the outputs +// in the configuration to the graph. +// +// This is done for the apply graph builder even if dependent nodes +// aren't changing since there is no downside: the state will be available +// even if the dependent items aren't changing. +type OutputTransformer struct { + Module *module.Tree } -// AddOutputOrphanTransformer is a transformer that adds output orphans -// to the graph. Output orphans are outputs that are no longer in the -// configuration and therefore need to be removed from the state. -type AddOutputOrphanTransformer struct { - State *State +func (t *OutputTransformer) Transform(g *Graph) error { + return t.transform(g, t.Module) } -func (t *AddOutputOrphanTransformer) Transform(g *Graph) error { - // Get the state for this module. If we have no state, we have no orphans - state := t.State.ModuleByPath(g.Path) - if state == nil { +func (t *OutputTransformer) transform(g *Graph, m *module.Tree) error { + // If no config, no outputs + if m == nil { return nil } - // Create the set of outputs we do have in the graph - found := make(map[string]struct{}) - for _, v := range g.Vertices() { - on, ok := v.(GraphNodeOutput) - if !ok { - continue + // Transform all the children. We must do this first because + // we can reference module outputs and they must show up in the + // reference map. + for _, c := range m.Children() { + if err := t.transform(g, c); err != nil { + return err } - - found[on.OutputName()] = struct{}{} } - // Go over all the outputs. If we don't have a graph node for it, - // create it. It doesn't need to depend on anything, since its just - // setting it empty. - for k, _ := range state.Outputs { - if _, ok := found[k]; ok { - continue + // If we have no outputs, we're done! + os := m.Config().Outputs + if len(os) == 0 { + return nil + } + + // Add all outputs here + for _, o := range os { + // Build the node. + // + // NOTE: For now this is just an "applyable" output. As we build + // new graph builders for the other operations I suspect we'll + // find a way to parameterize this, require new transforms, etc. + node := &NodeApplyableOutput{ + PathValue: normalizeModulePath(m.Path()), + Config: o, } - g.Add(&graphNodeOrphanOutput{OutputName: k}) + // Add it! + g.Add(node) } return nil } - -type graphNodeOrphanOutput struct { - OutputName string -} - -func (n *graphNodeOrphanOutput) Name() string { - return fmt.Sprintf("output.%s (orphan)", n.OutputName) -} - -func (n *graphNodeOrphanOutput) EvalTree() EvalNode { - return &EvalOpFilter{ - Ops: []walkOperation{walkApply, walkDestroy, walkRefresh}, - Node: &EvalDeleteOutput{ - Name: n.OutputName, - }, - } -} - -// GraphNodeFlattenable impl. -func (n *graphNodeOrphanOutput) Flatten(p []string) (dag.Vertex, error) { - return &graphNodeOrphanOutputFlat{ - graphNodeOrphanOutput: n, - PathValue: p, - }, nil -} - -type graphNodeOrphanOutputFlat struct { - *graphNodeOrphanOutput - - PathValue []string -} - -func (n *graphNodeOrphanOutputFlat) Name() string { - return fmt.Sprintf( - "%s.%s", modulePrefixStr(n.PathValue), n.graphNodeOrphanOutput.Name()) -} - -func (n *graphNodeOrphanOutputFlat) EvalTree() EvalNode { - return &EvalOpFilter{ - Ops: []walkOperation{walkApply, walkDestroy, walkRefresh}, - Node: &EvalDeleteOutput{ - Name: n.OutputName, - }, - } -} diff --git a/terraform/transform_output_orphan.go b/terraform/transform_output_orphan.go new file mode 100644 index 000000000..ffaa0b7e2 --- /dev/null +++ b/terraform/transform_output_orphan.go @@ -0,0 +1,101 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeOutput is an interface that nodes that are outputs must +// implement. The OutputName returned is the name of the output key +// that they manage. +type GraphNodeOutput interface { + OutputName() string +} + +// AddOutputOrphanTransformer is a transformer that adds output orphans +// to the graph. Output orphans are outputs that are no longer in the +// configuration and therefore need to be removed from the state. +// +// NOTE: This is the _old_ way to add output orphans that is used with +// legacy graph builders. The new way is OrphanOutputTransformer. +type AddOutputOrphanTransformer struct { + State *State +} + +func (t *AddOutputOrphanTransformer) Transform(g *Graph) error { + // Get the state for this module. If we have no state, we have no orphans + state := t.State.ModuleByPath(g.Path) + if state == nil { + return nil + } + + // Create the set of outputs we do have in the graph + found := make(map[string]struct{}) + for _, v := range g.Vertices() { + on, ok := v.(GraphNodeOutput) + if !ok { + continue + } + + found[on.OutputName()] = struct{}{} + } + + // Go over all the outputs. If we don't have a graph node for it, + // create it. It doesn't need to depend on anything, since its just + // setting it empty. + for k, _ := range state.Outputs { + if _, ok := found[k]; ok { + continue + } + + g.Add(&graphNodeOrphanOutput{OutputName: k}) + } + + return nil +} + +type graphNodeOrphanOutput struct { + OutputName string +} + +func (n *graphNodeOrphanOutput) Name() string { + return fmt.Sprintf("output.%s (orphan)", n.OutputName) +} + +func (n *graphNodeOrphanOutput) EvalTree() EvalNode { + return &EvalOpFilter{ + Ops: []walkOperation{walkApply, walkDestroy, walkRefresh}, + Node: &EvalDeleteOutput{ + Name: n.OutputName, + }, + } +} + +// GraphNodeFlattenable impl. +func (n *graphNodeOrphanOutput) Flatten(p []string) (dag.Vertex, error) { + return &graphNodeOrphanOutputFlat{ + graphNodeOrphanOutput: n, + PathValue: p, + }, nil +} + +type graphNodeOrphanOutputFlat struct { + *graphNodeOrphanOutput + + PathValue []string +} + +func (n *graphNodeOrphanOutputFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.graphNodeOrphanOutput.Name()) +} + +func (n *graphNodeOrphanOutputFlat) EvalTree() EvalNode { + return &EvalOpFilter{ + Ops: []walkOperation{walkApply, walkDestroy, walkRefresh}, + Node: &EvalDeleteOutput{ + Name: n.OutputName, + }, + } +} diff --git a/terraform/transform_output_test.go b/terraform/transform_output_orphan_test.go similarity index 100% rename from terraform/transform_output_test.go rename to terraform/transform_output_orphan_test.go diff --git a/terraform/transform_provider.go b/terraform/transform_provider.go index 99ea0c3f4..606b81dcd 100644 --- a/terraform/transform_provider.go +++ b/terraform/transform_provider.go @@ -33,55 +33,6 @@ type GraphNodeProviderConsumer interface { ProvidedBy() []string } -// DisableProviderTransformer "disables" any providers that are only -// depended on by modules. -type DisableProviderTransformer struct{} - -func (t *DisableProviderTransformer) Transform(g *Graph) error { - // Since we're comparing against edges, we need to make sure we connect - g.ConnectDependents() - - for _, v := range g.Vertices() { - // We only care about providers - pn, ok := v.(GraphNodeProvider) - if !ok || pn.ProviderName() == "" { - continue - } - - // Go through all the up-edges (things that depend on this - // provider) and if any is not a module, then ignore this node. - nonModule := false - for _, sourceRaw := range g.UpEdges(v).List() { - source := sourceRaw.(dag.Vertex) - cn, ok := source.(graphNodeConfig) - if !ok { - nonModule = true - break - } - - if cn.ConfigType() != GraphNodeConfigTypeModule { - nonModule = true - break - } - } - if nonModule { - // We found something that depends on this provider that - // isn't a module, so skip it. - continue - } - - // Disable the provider by replacing it with a "disabled" provider - disabled := &graphNodeDisabledProvider{GraphNodeProvider: pn} - if !g.Replace(v, disabled) { - panic(fmt.Sprintf( - "vertex disappeared from under us: %s", - dag.VertexName(v))) - } - } - - return nil -} - // ProviderTransformer is a GraphTransformer that maps resources to // providers within the graph. This will error if there are any resources // that don't map to proper resources. @@ -163,9 +114,19 @@ func (t *CloseProviderTransformer) Transform(g *Graph) error { type MissingProviderTransformer struct { // Providers is the list of providers we support. Providers []string + + // Factory, if set, overrides how the providers are made. + Factory func(name string, path []string) GraphNodeProvider } func (t *MissingProviderTransformer) Transform(g *Graph) error { + // Initialize factory + if t.Factory == nil { + t.Factory = func(name string, path []string) GraphNodeProvider { + return &graphNodeProvider{ProviderNameValue: name} + } + } + // Create a set of our supported providers supported := make(map[string]struct{}, len(t.Providers)) for _, v := range t.Providers { @@ -217,13 +178,14 @@ func (t *MissingProviderTransformer) Transform(g *Graph) error { } // Add the missing provider node to the graph - raw := &graphNodeProvider{ProviderNameValue: p} - var v dag.Vertex = raw + v := t.Factory(p, path).(dag.Vertex) if len(path) > 0 { - var err error - v, err = raw.Flatten(path) - if err != nil { - return err + if fn, ok := v.(GraphNodeFlattenable); ok { + var err error + v, err = fn.Flatten(path) + if err != nil { + return err + } } // We'll need the parent provider as well, so let's @@ -242,6 +204,66 @@ func (t *MissingProviderTransformer) Transform(g *Graph) error { return nil } +// ParentProviderTransformer connects provider nodes to their parents. +// +// This works by finding nodes that are both GraphNodeProviders and +// GraphNodeSubPath. It then connects the providers to their parent +// path. +type ParentProviderTransformer struct{} + +func (t *ParentProviderTransformer) Transform(g *Graph) error { + // Make a mapping of path to dag.Vertex, where path is: "path.name" + m := make(map[string]dag.Vertex) + + // Also create a map that maps a provider to its parent + parentMap := make(map[dag.Vertex]string) + for _, raw := range g.Vertices() { + // If it is the flat version, then make it the non-flat version. + // We eventually want to get rid of the flat version entirely so + // this is a stop-gap while it still exists. + var v dag.Vertex = raw + if f, ok := v.(*graphNodeProviderFlat); ok { + v = f.graphNodeProvider + } + + // Only care about providers + pn, ok := v.(GraphNodeProvider) + if !ok || pn.ProviderName() == "" { + continue + } + + // Also require a subpath, if there is no subpath then we + // just totally ignore it. The expectation of this transform is + // that it is used with a graph builder that is already flattened. + var path []string + if pn, ok := raw.(GraphNodeSubPath); ok { + path = pn.Path() + } + path = normalizeModulePath(path) + + // Build the key with path.name i.e. "child.subchild.aws" + key := fmt.Sprintf("%s.%s", strings.Join(path, "."), pn.ProviderName()) + m[key] = raw + + // Determine the parent if we're non-root. This is length 1 since + // the 0 index should be "root" since we normalize above. + if len(path) > 1 { + path = path[:len(path)-1] + key := fmt.Sprintf("%s.%s", strings.Join(path, "."), pn.ProviderName()) + parentMap[raw] = key + } + } + + // Connect! + for v, key := range parentMap { + if parent, ok := m[key]; ok { + g.Connect(dag.BasicEdge(v, parent)) + } + } + + return nil +} + // PruneProviderTransformer is a GraphTransformer that prunes all the // providers that aren't needed from the graph. A provider is unneeded if // no resource or module is using that provider. @@ -283,7 +305,16 @@ func providerVertexMap(g *Graph) map[string]dag.Vertex { m := make(map[string]dag.Vertex) for _, v := range g.Vertices() { if pv, ok := v.(GraphNodeProvider); ok { - m[pv.ProviderName()] = v + key := pv.ProviderName() + + // This special case is because the new world view of providers + // is that they should return only their pure name (not the full + // module path with ProviderName). Working towards this future. + if _, ok := v.(*NodeApplyableProvider); ok { + key = providerMapKey(pv.ProviderName(), v) + } + + m[key] = v } } @@ -301,118 +332,6 @@ func closeProviderVertexMap(g *Graph) map[string]dag.Vertex { return m } -type graphNodeDisabledProvider struct { - GraphNodeProvider -} - -// GraphNodeEvalable impl. -func (n *graphNodeDisabledProvider) EvalTree() EvalNode { - var resourceConfig *ResourceConfig - - return &EvalOpFilter{ - Ops: []walkOperation{walkInput, walkValidate, walkRefresh, walkPlan, walkApply, walkDestroy}, - Node: &EvalSequence{ - Nodes: []EvalNode{ - &EvalInterpolate{ - Config: n.ProviderConfig(), - Output: &resourceConfig, - }, - &EvalBuildProviderConfig{ - Provider: n.ProviderName(), - Config: &resourceConfig, - Output: &resourceConfig, - }, - &EvalSetProviderConfig{ - Provider: n.ProviderName(), - Config: &resourceConfig, - }, - }, - }, - } -} - -// GraphNodeFlattenable impl. -func (n *graphNodeDisabledProvider) Flatten(p []string) (dag.Vertex, error) { - return &graphNodeDisabledProviderFlat{ - graphNodeDisabledProvider: n, - PathValue: p, - }, nil -} - -func (n *graphNodeDisabledProvider) Name() string { - return fmt.Sprintf("%s (disabled)", dag.VertexName(n.GraphNodeProvider)) -} - -// GraphNodeDotter impl. -func (n *graphNodeDisabledProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node { - return dot.NewNode(name, map[string]string{ - "label": n.Name(), - "shape": "diamond", - }) -} - -// GraphNodeDotterOrigin impl. -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() -} - -// Same as graphNodeDisabledProvider, but for flattening -type graphNodeDisabledProviderFlat struct { - *graphNodeDisabledProvider - - PathValue []string -} - -func (n *graphNodeDisabledProviderFlat) Name() string { - return fmt.Sprintf( - "%s.%s", modulePrefixStr(n.PathValue), n.graphNodeDisabledProvider.Name()) -} - -func (n *graphNodeDisabledProviderFlat) Path() []string { - return n.PathValue -} - -func (n *graphNodeDisabledProviderFlat) ProviderName() string { - return fmt.Sprintf( - "%s.%s", modulePrefixStr(n.PathValue), - n.graphNodeDisabledProvider.ProviderName()) -} - -// GraphNodeDependable impl. -func (n *graphNodeDisabledProviderFlat) DependableName() []string { - return modulePrefixList( - n.graphNodeDisabledProvider.DependableName(), - modulePrefixStr(n.PathValue)) -} - -func (n *graphNodeDisabledProviderFlat) 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]) - result = modulePrefixList( - n.graphNodeDisabledProvider.DependableName(), prefix) - } - - return result -} - type graphNodeCloseProvider struct { ProviderNameValue string } @@ -464,6 +383,7 @@ func (n *graphNodeProvider) DependableName() []string { return []string{n.Name()} } +// GraphNodeProvider func (n *graphNodeProvider) ProviderName() string { return n.ProviderNameValue } diff --git a/terraform/transform_provider_disable.go b/terraform/transform_provider_disable.go new file mode 100644 index 000000000..d9919f3a7 --- /dev/null +++ b/terraform/transform_provider_disable.go @@ -0,0 +1,50 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/dag" +) + +// DisableProviderTransformer "disables" any providers that are not actually +// used by anything. This avoids the provider being initialized and configured. +// This both saves resources but also avoids errors since configuration +// may imply initialization which may require auth. +type DisableProviderTransformer struct{} + +func (t *DisableProviderTransformer) Transform(g *Graph) error { + for _, v := range g.Vertices() { + // We only care about providers + pn, ok := v.(GraphNodeProvider) + if !ok || pn.ProviderName() == "" { + continue + } + + // If we have dependencies, then don't disable + if g.UpEdges(v).Len() > 0 { + continue + } + + // Get the path + var path []string + if pn, ok := v.(GraphNodeSubPath); ok { + path = pn.Path() + } + + // Disable the provider by replacing it with a "disabled" provider + disabled := &NodeDisabledProvider{ + NodeAbstractProvider: &NodeAbstractProvider{ + NameValue: pn.ProviderName(), + PathValue: path, + }, + } + + if !g.Replace(v, disabled) { + panic(fmt.Sprintf( + "vertex disappeared from under us: %s", + dag.VertexName(v))) + } + } + + return nil +} diff --git a/terraform/transform_provider_old.go b/terraform/transform_provider_old.go new file mode 100644 index 000000000..eb533f230 --- /dev/null +++ b/terraform/transform_provider_old.go @@ -0,0 +1,172 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" + "github.com/hashicorp/terraform/dot" +) + +// DisableProviderTransformer "disables" any providers that are only +// depended on by modules. +// +// NOTE: "old" = used by old graph builders, will be removed one day +type DisableProviderTransformerOld struct{} + +func (t *DisableProviderTransformerOld) Transform(g *Graph) error { + // Since we're comparing against edges, we need to make sure we connect + g.ConnectDependents() + + for _, v := range g.Vertices() { + // We only care about providers + pn, ok := v.(GraphNodeProvider) + if !ok || pn.ProviderName() == "" { + continue + } + + // Go through all the up-edges (things that depend on this + // provider) and if any is not a module, then ignore this node. + nonModule := false + for _, sourceRaw := range g.UpEdges(v).List() { + source := sourceRaw.(dag.Vertex) + cn, ok := source.(graphNodeConfig) + if !ok { + nonModule = true + break + } + + if cn.ConfigType() != GraphNodeConfigTypeModule { + nonModule = true + break + } + } + if nonModule { + // We found something that depends on this provider that + // isn't a module, so skip it. + continue + } + + // Disable the provider by replacing it with a "disabled" provider + disabled := &graphNodeDisabledProvider{GraphNodeProvider: pn} + if !g.Replace(v, disabled) { + panic(fmt.Sprintf( + "vertex disappeared from under us: %s", + dag.VertexName(v))) + } + } + + return nil +} + +type graphNodeDisabledProvider struct { + GraphNodeProvider +} + +// GraphNodeEvalable impl. +func (n *graphNodeDisabledProvider) EvalTree() EvalNode { + var resourceConfig *ResourceConfig + + return &EvalOpFilter{ + Ops: []walkOperation{walkInput, walkValidate, walkRefresh, walkPlan, walkApply, walkDestroy}, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalInterpolate{ + Config: n.ProviderConfig(), + Output: &resourceConfig, + }, + &EvalBuildProviderConfig{ + Provider: n.ProviderName(), + Config: &resourceConfig, + Output: &resourceConfig, + }, + &EvalSetProviderConfig{ + Provider: n.ProviderName(), + Config: &resourceConfig, + }, + }, + }, + } +} + +// GraphNodeFlattenable impl. +func (n *graphNodeDisabledProvider) Flatten(p []string) (dag.Vertex, error) { + return &graphNodeDisabledProviderFlat{ + graphNodeDisabledProvider: n, + PathValue: p, + }, nil +} + +func (n *graphNodeDisabledProvider) Name() string { + return fmt.Sprintf("%s (disabled)", dag.VertexName(n.GraphNodeProvider)) +} + +// GraphNodeDotter impl. +func (n *graphNodeDisabledProvider) DotNode(name string, opts *GraphDotOpts) *dot.Node { + return dot.NewNode(name, map[string]string{ + "label": n.Name(), + "shape": "diamond", + }) +} + +// GraphNodeDotterOrigin impl. +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() +} + +// Same as graphNodeDisabledProvider, but for flattening +type graphNodeDisabledProviderFlat struct { + *graphNodeDisabledProvider + + PathValue []string +} + +func (n *graphNodeDisabledProviderFlat) Name() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), n.graphNodeDisabledProvider.Name()) +} + +func (n *graphNodeDisabledProviderFlat) Path() []string { + return n.PathValue +} + +func (n *graphNodeDisabledProviderFlat) ProviderName() string { + return fmt.Sprintf( + "%s.%s", modulePrefixStr(n.PathValue), + n.graphNodeDisabledProvider.ProviderName()) +} + +// GraphNodeDependable impl. +func (n *graphNodeDisabledProviderFlat) DependableName() []string { + return modulePrefixList( + n.graphNodeDisabledProvider.DependableName(), + modulePrefixStr(n.PathValue)) +} + +func (n *graphNodeDisabledProviderFlat) 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]) + result = modulePrefixList( + n.graphNodeDisabledProvider.DependableName(), prefix) + } + + return result +} diff --git a/terraform/transform_provider_test.go b/terraform/transform_provider_test.go index 85a6266f9..bc2cff532 100644 --- a/terraform/transform_provider_test.go +++ b/terraform/transform_provider_test.go @@ -230,6 +230,89 @@ func TestMissingProviderTransformer_moduleGrandchild(t *testing.T) { } } +func TestParentProviderTransformer(t *testing.T) { + g := Graph{Path: RootModulePath} + + // Introduce a cihld module + { + tf := &ImportStateTransformer{ + Targets: []*ImportTarget{ + &ImportTarget{ + Addr: "module.moo.foo_instance.qux", + ID: "bar", + }, + }, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + // Add the missing modules + { + tf := &MissingProviderTransformer{Providers: []string{"foo", "bar"}} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + // Connect parents + { + tf := &ParentProviderTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformParentProviderStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestParentProviderTransformer_moduleGrandchild(t *testing.T) { + g := Graph{Path: RootModulePath} + + // We use the import state transformer since at the time of writing + // this test it is the first and only transformer that will introduce + // multiple module-path nodes at a single go. + { + tf := &ImportStateTransformer{ + Targets: []*ImportTarget{ + &ImportTarget{ + Addr: "module.a.module.b.foo_instance.qux", + ID: "bar", + }, + }, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + tf := &MissingProviderTransformer{Providers: []string{"foo", "bar"}} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + // Connect parents + { + tf := &ParentProviderTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformParentProviderModuleGrandchildStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + func TestPruneProviderTransformer(t *testing.T) { mod := testModule(t, "transform-provider-prune") @@ -284,7 +367,7 @@ func TestDisableProviderTransformer(t *testing.T) { &ConfigTransformer{Module: mod}, &MissingProviderTransformer{Providers: []string{"aws"}}, &ProviderTransformer{}, - &DisableProviderTransformer{}, + &DisableProviderTransformerOld{}, &CloseProviderTransformer{}, &PruneProviderTransformer{}, } @@ -310,7 +393,7 @@ func TestDisableProviderTransformer_keep(t *testing.T) { &ConfigTransformer{Module: mod}, &MissingProviderTransformer{Providers: []string{"aws"}}, &ProviderTransformer{}, - &DisableProviderTransformer{}, + &DisableProviderTransformerOld{}, &CloseProviderTransformer{}, &PruneProviderTransformer{}, } @@ -382,6 +465,22 @@ module.a.provider.foo provider.foo ` +const testTransformParentProviderStr = ` +module.moo.foo_instance.qux (import id: bar) +module.moo.provider.foo + provider.foo +provider.foo +` + +const testTransformParentProviderModuleGrandchildStr = ` +module.a.module.b.foo_instance.qux (import id: bar) +module.a.module.b.provider.foo + module.a.provider.foo +module.a.provider.foo + provider.foo +provider.foo +` + const testTransformProviderModuleChildStr = ` module.moo.foo_instance.qux (import id: bar) module.moo.provider.foo diff --git a/terraform/transform_reference.go b/terraform/transform_reference.go new file mode 100644 index 000000000..f848274aa --- /dev/null +++ b/terraform/transform_reference.go @@ -0,0 +1,180 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeReferenceable must be implemented by any node that represents +// a Terraform thing that can be referenced (resource, module, etc.). +type GraphNodeReferenceable interface { + // ReferenceableName is the name by which this can be referenced. + // This can be either just the type, or include the field. Example: + // "aws_instance.bar" or "aws_instance.bar.id". + ReferenceableName() []string +} + +// GraphNodeReferencer must be implemented by nodes that reference other +// Terraform items and therefore depend on them. +type GraphNodeReferencer interface { + // References are the list of things that this node references. This + // can include fields or just the type, just like GraphNodeReferenceable + // above. + References() []string +} + +// GraphNodeReferenceGlobal is an interface that can optionally be +// implemented. If ReferenceGlobal returns true, then the References() +// and ReferenceableName() must be _fully qualified_ with "module.foo.bar" +// etc. +// +// This allows a node to reference and be referenced by a specific name +// that may cross module boundaries. This can be very dangerous so use +// this wisely. +// +// The primary use case for this is module boundaries (variables coming in). +type GraphNodeReferenceGlobal interface { + // Set to true to signal that references and name are fully + // qualified. See the above docs for more information. + ReferenceGlobal() bool +} + +// ReferenceTransformer is a GraphTransformer that connects all the +// nodes that reference each other in order to form the proper ordering. +type ReferenceTransformer struct{} + +func (t *ReferenceTransformer) Transform(g *Graph) error { + // Build a reference map so we can efficiently look up the references + vs := g.Vertices() + m := NewReferenceMap(vs) + + // Find the things that reference things and connect them + for _, v := range vs { + parents, _ := m.References(v) + for _, parent := range parents { + g.Connect(dag.BasicEdge(v, parent)) + } + } + + return nil +} + +// ReferenceMap is a structure that can be used to efficiently check +// for references on a graph. +type ReferenceMap struct { + // m is the mapping of referenceable name to list of verticies that + // implement that name. This is built on initialization. + m map[string][]dag.Vertex +} + +// References returns the list of vertices that this vertex +// references along with any missing references. +func (m *ReferenceMap) References(v dag.Vertex) ([]dag.Vertex, []string) { + rn, ok := v.(GraphNodeReferencer) + if !ok { + return nil, nil + } + + var matches []dag.Vertex + var missing []string + prefix := m.prefix(v) + for _, n := range rn.References() { + n = prefix + n + parents, ok := m.m[n] + if !ok { + missing = append(missing, n) + continue + } + + // Make sure this isn't a self reference, which isn't included + selfRef := false + for _, p := range parents { + if p == v { + selfRef = true + break + } + } + if selfRef { + continue + } + + matches = append(matches, parents...) + } + + return matches, missing +} + +func (m *ReferenceMap) prefix(v dag.Vertex) string { + // If the node is stating it is already fully qualified then + // we don't have to create the prefix! + if gn, ok := v.(GraphNodeReferenceGlobal); ok && gn.ReferenceGlobal() { + return "" + } + + // Create the prefix based on the path + var prefix string + if pn, ok := v.(GraphNodeSubPath); ok { + if path := normalizeModulePath(pn.Path()); len(path) > 1 { + prefix = modulePrefixStr(path) + "." + } + } + + return prefix +} + +// NewReferenceMap is used to create a new reference map for the +// given set of vertices. +func NewReferenceMap(vs []dag.Vertex) *ReferenceMap { + var m ReferenceMap + + // Build the lookup table + refMap := make(map[string][]dag.Vertex) + for _, v := range vs { + // We're only looking for referenceable nodes + rn, ok := v.(GraphNodeReferenceable) + if !ok { + continue + } + + // Go through and cache them + prefix := m.prefix(v) + for _, n := range rn.ReferenceableName() { + n = prefix + n + refMap[n] = append(refMap[n], v) + } + } + + m.m = refMap + return &m +} + +// ReferencesFromConfig returns the references that a configuration has +// based on the interpolated variables in a configuration. +func ReferencesFromConfig(c *config.RawConfig) []string { + var result []string + for _, v := range c.Variables { + if r := ReferenceFromInterpolatedVar(v); r != "" { + result = append(result, r) + } + + } + + return result +} + +// ReferenceFromInterpolatedVar returns the reference from this variable, +// or an empty string if there is no reference. +func ReferenceFromInterpolatedVar(v config.InterpolatedVariable) string { + switch v := v.(type) { + case *config.ModuleVariable: + return fmt.Sprintf("module.%s.output.%s", v.Name, v.Field) + case *config.ResourceVariable: + return v.ResourceId() + case *config.UserVariable: + return fmt.Sprintf("var.%s", v.Name) + default: + return "" + } +} diff --git a/terraform/transform_reference_test.go b/terraform/transform_reference_test.go new file mode 100644 index 000000000..525add6bd --- /dev/null +++ b/terraform/transform_reference_test.go @@ -0,0 +1,120 @@ +package terraform + +import ( + "strings" + "testing" +) + +func TestReferenceTransformer_simple(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeRefParentTest{ + NameValue: "A", + Names: []string{"A"}, + }) + g.Add(&graphNodeRefChildTest{ + NameValue: "B", + Refs: []string{"A"}, + }) + + tf := &ReferenceTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformRefBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestReferenceTransformer_self(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeRefParentTest{ + NameValue: "A", + Names: []string{"A"}, + }) + g.Add(&graphNodeRefChildTest{ + NameValue: "B", + Refs: []string{"A", "B"}, + }) + + tf := &ReferenceTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformRefBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestReferenceTransformer_path(t *testing.T) { + g := Graph{Path: RootModulePath} + g.Add(&graphNodeRefParentTest{ + NameValue: "A", + Names: []string{"A"}, + }) + g.Add(&graphNodeRefChildTest{ + NameValue: "B", + Refs: []string{"A"}, + }) + g.Add(&graphNodeRefParentTest{ + NameValue: "child.A", + PathValue: []string{"root", "child"}, + Names: []string{"A"}, + }) + g.Add(&graphNodeRefChildTest{ + NameValue: "child.B", + PathValue: []string{"root", "child"}, + Refs: []string{"A"}, + }) + + tf := &ReferenceTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformRefPathStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +type graphNodeRefParentTest struct { + NameValue string + PathValue []string + Names []string +} + +func (n *graphNodeRefParentTest) Name() string { return n.NameValue } +func (n *graphNodeRefParentTest) ReferenceableName() []string { return n.Names } +func (n *graphNodeRefParentTest) Path() []string { return n.PathValue } + +type graphNodeRefChildTest struct { + NameValue string + PathValue []string + Refs []string +} + +func (n *graphNodeRefChildTest) Name() string { return n.NameValue } +func (n *graphNodeRefChildTest) References() []string { return n.Refs } +func (n *graphNodeRefChildTest) Path() []string { return n.PathValue } + +const testTransformRefBasicStr = ` +A +B + A +` + +const testTransformRefPathStr = ` +A +B + A +child.A +child.B + child.A +` diff --git a/terraform/transform_variable.go b/terraform/transform_variable.go new file mode 100644 index 000000000..b31e2c765 --- /dev/null +++ b/terraform/transform_variable.go @@ -0,0 +1,40 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config/module" +) + +// RootVariableTransformer is a GraphTransformer that adds all the root +// variables to the graph. +// +// Root variables are currently no-ops but they must be added to the +// graph since downstream things that depend on them must be able to +// reach them. +type RootVariableTransformer struct { + Module *module.Tree +} + +func (t *RootVariableTransformer) Transform(g *Graph) error { + // If no config, no variables + if t.Module == nil { + return nil + } + + // If we have no vars, we're done! + vars := t.Module.Config().Variables + if len(vars) == 0 { + return nil + } + + // Add all variables here + for _, v := range vars { + node := &NodeRootVariable{ + Config: v, + } + + // Add it! + g.Add(node) + } + + return nil +}