Merge pull request #9388 from hashicorp/f-apply-builder

terraform: new apply graph builder based on the "diff"
This commit is contained in:
Mitchell Hashimoto 2016-10-20 15:13:29 -07:00 committed by GitHub
commit 14cff93b67
83 changed files with 4248 additions and 294 deletions

View File

@ -10,6 +10,7 @@ install:
- bash scripts/gogetcookie.sh
script:
- make test vet
- make test TEST=./terraform TESTARGS=-Xnew-apply
branches:
only:
- master

View File

@ -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,

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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}",
}

15
config/testing.go Normal file
View File

@ -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
}

View File

@ -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)

View File

@ -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{}
}

1
go.sh Executable file
View File

@ -0,0 +1 @@
go test ./terraform | grep -E '(FAIL|panic)' | tee /dev/tty | wc -l

View File

@ -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...)
}

View File

@ -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_

View File

@ -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

View File

@ -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")
}

17
terraform/edge_destroy.go Normal file
View File

@ -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 }

View File

@ -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
}

View File

@ -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.

View File

@ -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},

View File

@ -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
}

View File

@ -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
`

View File

@ -46,7 +46,7 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
// Provider-related transformations
&MissingProviderTransformer{Providers: b.Providers},
&ProviderTransformer{},
&DisableProviderTransformer{},
&DisableProviderTransformerOld{},
&PruneProviderTransformer{},
// Single root

View File

@ -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{}
}

View File

@ -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,
},
},
}
}

View File

@ -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)
}
}

62
terraform/node_output.go Normal file
View File

@ -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,
},
},
},
}
}

View File

@ -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,
}
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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,
},
},
}
}

View File

@ -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
}

View File

@ -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{},
},
}
}

View File

@ -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{},
},
},
}
}

View File

@ -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()}
}

View File

@ -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 "<nil>"
}
if len(i.ModulePath) <= 1 {
return i.Id
}

View File

@ -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 {

View File

@ -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)
}
})
}
}

View File

@ -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

View File

@ -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(

View File

@ -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()
}

View File

@ -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()

View File

@ -0,0 +1,5 @@
provider "aws" {
value = "foo"
}
resource "aws_instance" "foo" {}

View File

@ -0,0 +1,7 @@
provider "aws" {
foo = "bar"
}
module "child" {
source = "./child"
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "create" {
provisioner "exec" {}
}
resource "aws_instance" "other" {
value = "${aws_instance.create.id}"
}

View File

@ -0,0 +1,9 @@
module "child" {
source = "./child"
}
resource "aws_instance" "create" {}
resource "aws_instance" "other" {
foo = "${aws_instance.create.bar}"
}

View File

@ -0,0 +1,2 @@
resource "test" "A" {}
resource "test" "B" { value = "${test.A.value}" }

View File

@ -0,0 +1,3 @@
resource "test" "A" {}
resource "test" "B" { value = "${test.A.value}" }
resource "test" "C" { value = "${test.B.value}" }

View File

@ -0,0 +1 @@
resource "aws_instance" "foo" {}

View File

@ -0,0 +1 @@
resource "aws_instance" "baz" {}

View File

@ -0,0 +1,6 @@
resource "aws_instance" "foo" {}
resource "aws_instance" "bar" { value = "${aws_instance.foo.value}" }
module "child" {
source = "./child"
}

View File

@ -0,0 +1,3 @@
variable "value" {}
output "result" { value = "${var.value}" }

View File

@ -0,0 +1,4 @@
module "child" {
source = "./child"
value = "foo"
}

View File

@ -0,0 +1,3 @@
variable "value" {}
output "result" { value = "${var.value}" }

View File

@ -0,0 +1,6 @@
variable "value" {}
module "child" {
source = "./child"
value = "${var.value}"
}

View File

@ -0,0 +1,4 @@
module "child" {
source = "./child"
value = "foo"
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
`

View File

@ -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
}

View File

@ -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
}

View File

@ -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
`

View File

@ -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
}

View File

@ -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)
`

View File

@ -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
}

View File

@ -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
`

View File

@ -1 +0,0 @@
package terraform

View File

@ -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
}

View File

@ -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
`

View File

@ -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

View File

@ -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
}

View File

@ -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,
},
}
}

View File

@ -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,
},
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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 ""
}
}

View File

@ -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
`

View File

@ -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
}