Merge pull request #9973 from hashicorp/f-new-plan

terraform: new plan graph
This commit is contained in:
Mitchell Hashimoto 2016-11-09 08:15:40 -08:00 committed by GitHub
commit 66ccc19d94
47 changed files with 2224 additions and 326 deletions

View File

@ -488,28 +488,79 @@ func (c *Context) Plan() (*Plan, error) {
c.diffLock.Unlock()
// Used throughout below
X_newApply := experiment.Enabled(experiment.X_newApply)
X_newDestroy := experiment.Enabled(experiment.X_newDestroy)
newGraphEnabled := (c.destroy && X_newDestroy) || (!c.destroy && X_newApply)
// Build the graph. We have a branch here since for the pure-destroy
// plan (c.destroy) we use a much simpler graph builder that simply
// walks the state and reverses edges.
var graph *Graph
var err error
if c.destroy && X_newDestroy {
graph, err = (&DestroyPlanGraphBuilder{
Module: c.module,
State: c.state,
Targets: c.targets,
}).Build(RootModulePath)
} else {
graph, err = c.Graph(&ContextGraphOpts{Validate: true})
// Build the original graph. This is before the new graph builders
// coming in 0.8. We do this for shadow graphing.
oldGraph, err := c.Graph(&ContextGraphOpts{Validate: true})
if err != nil && newGraphEnabled {
// If we had an error graphing but we're using the new graph,
// just set it to nil and let it go. There are some features that
// may work with the new graph that don't with the old.
oldGraph = nil
err = nil
}
if err != nil {
return nil, err
}
// Build the new graph. We do this no matter wht so we can shadow it.
var newGraph *Graph
err = nil
if c.destroy {
newGraph, err = (&DestroyPlanGraphBuilder{
Module: c.module,
State: c.state,
Targets: c.targets,
}).Build(RootModulePath)
} else {
newGraph, err = (&PlanGraphBuilder{
Module: c.module,
State: c.state,
Providers: c.components.ResourceProviders(),
Targets: c.targets,
}).Build(RootModulePath)
}
if err != nil && !newGraphEnabled {
// If we had an error graphing but we're not using this graph, just
// set it to nil and record it as a shadow error.
c.shadowErr = multierror.Append(c.shadowErr, fmt.Errorf(
"Error building new graph: %s", err))
newGraph = nil
err = nil
}
if err != nil {
return nil, err
}
// Determine what is the real and what is the shadow. The logic here
// is straightforward though the if statements are not:
//
// * If the new graph, shadow with experiment in both because the
// experiment has less nodes so the original can't shadow.
// * If not the new graph, shadow with the experiment
//
real := oldGraph
shadow := newGraph
if newGraphEnabled {
log.Printf("[WARN] terraform: real graph is experiment, shadow is experiment")
real = shadow
} else {
log.Printf("[WARN] terraform: real graph is original, shadow is experiment")
}
// Special case here: if we're using destroy don't shadow it because
// the new destroy graph behaves a bit differently on purpose by not
// setting the module destroy flag.
if c.destroy && !newGraphEnabled {
shadow = nil
}
// Do the walk
walker, err := c.walk(graph, graph, operation)
walker, err := c.walk(real, shadow, operation)
if err != nil {
return nil, err
}
@ -528,7 +579,7 @@ func (c *Context) Plan() (*Plan, error) {
// We don't do the reverification during the new destroy plan because
// it will use a different apply process.
if !(c.destroy && X_newDestroy) {
if !newGraphEnabled {
// Now that we have a diff, we can build the exact graph that Apply will use
// and catch any possible cycles during the Plan phase.
if _, err := c.Graph(&ContextGraphOpts{Validate: true}); err != nil {
@ -814,7 +865,12 @@ func (c *Context) walk(
//
// This must be done BEFORE appending shadowWalkErr since the
// shadowWalkErr may include expected errors.
if c.shadowErr != nil && contextFailOnShadowError {
//
// We only do this if we don't have a real error. In the case of
// a real error, we can't guarantee what nodes were and weren't
// traversed in parallel scenarios so we can't guarantee no
// shadow errors.
if c.shadowErr != nil && contextFailOnShadowError && realErr == nil {
panic(multierror.Prefix(c.shadowErr, "shadow graph:"))
}

View File

@ -3488,6 +3488,8 @@ func TestContext2Apply_destroyOrder(t *testing.T) {
t.Fatalf("err: %s", err)
}
t.Logf("State 1: %s", state)
// Next, plan and apply config-less to force a destroy with "apply"
h.Active = true
ctx = testContext2(t, &ContextOpts{
@ -3697,8 +3699,10 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) {
})
// First plan and apply a create operation
if _, err := ctx.Plan(); err != nil {
if p, err := ctx.Plan(); err != nil {
t.Fatalf("plan err: %s", err)
} else {
t.Logf("Step 1 plan: %s", p)
}
state, err = ctx.Apply()
@ -3732,6 +3736,8 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) {
t.Fatalf("destroy plan err: %s", err)
}
t.Logf("Step 2 plan: %s", plan)
var buf bytes.Buffer
if err := WritePlan(plan, &buf); err != nil {
t.Fatalf("plan write err: %s", err)
@ -3755,6 +3761,8 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) {
if err != nil {
t.Fatalf("destroy apply err: %s", err)
}
t.Logf("Step 2 state: %s", state)
}
//Test that things were destroyed
@ -3765,7 +3773,7 @@ module.child:
<no state>
`)
if actual != expected {
t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual)
t.Fatalf("expected:\n\n%s\n\nactual:\n\n%s", expected, actual)
}
}
@ -5120,8 +5128,10 @@ func TestContext2Apply_targetedModuleDep(t *testing.T) {
Targets: []string{"aws_instance.foo"},
})
if _, err := ctx.Plan(); err != nil {
if p, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
} else {
t.Logf("Diff: %s", p)
}
state, err := ctx.Apply()

View File

@ -11,7 +11,7 @@ import (
"testing"
)
func TestContext2Plan(t *testing.T) {
func TestContext2Plan_basic(t *testing.T) {
m := testModule(t, "plan-good")
p := testProvider("aws")
p.DiffFn = testDiffFn
@ -626,7 +626,7 @@ func TestContext2Plan_moduleVar(t *testing.T) {
}
}
func TestContext2Plan_moduleVarWrongType(t *testing.T) {
func TestContext2Plan_moduleVarWrongTypeBasic(t *testing.T) {
m := testModule(t, "plan-module-wrong-var-type")
p := testProvider("aws")
p.DiffFn = testDiffFn

View File

@ -32,6 +32,30 @@ type Diff struct {
Modules []*ModuleDiff
}
// Prune cleans out unused structures in the diff without affecting
// the behavior of the diff at all.
//
// This is not safe to call concurrently. This is safe to call on a
// nil Diff.
func (d *Diff) Prune() {
if d == nil {
return
}
// Prune all empty modules
newModules := make([]*ModuleDiff, 0, len(d.Modules))
for _, m := range d.Modules {
// If the module isn't empty, we keep it
if !m.Empty() {
newModules = append(newModules, m)
}
}
if len(newModules) == 0 {
newModules = nil
}
d.Modules = newModules
}
// AddModule adds the module with the given path to the diff.
//
// This should be the preferred method to add module diffs since it
@ -212,6 +236,10 @@ func (d *ModuleDiff) ChangeType() DiffChangeType {
// Empty returns true if the diff has no changes within this module.
func (d *ModuleDiff) Empty() bool {
if d.Destroy {
return false
}
if len(d.Resources) == 0 {
return true
}

View File

@ -103,6 +103,53 @@ func TestDiffEqual(t *testing.T) {
}
}
func TestDiffPrune(t *testing.T) {
cases := map[string]struct {
D1, D2 *Diff
}{
"nil": {
nil,
nil,
},
"empty": {
new(Diff),
new(Diff),
},
"empty module": {
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}},
},
},
&Diff{},
},
"destroy module": {
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
},
},
&Diff{
Modules: []*ModuleDiff{
&ModuleDiff{Path: []string{"root", "foo"}, Destroy: true},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
tc.D1.Prune()
if !tc.D1.Equal(tc.D2) {
t.Fatalf("bad:\n\n%#v\n\n%#v", tc.D1, tc.D2)
}
})
}
}
func TestModuleDiff_ChangeType(t *testing.T) {
cases := []struct {
Diff *ModuleDiff

View File

@ -129,7 +129,7 @@ func (b *BuiltinGraphBuilder) Build(path []string) (*Graph, error) {
func (b *BuiltinGraphBuilder) Steps(path []string) []GraphTransformer {
steps := []GraphTransformer{
// Create all our resources from the configuration and state
&ConfigTransformer{Module: b.Root},
&ConfigTransformerOld{Module: b.Root},
&OrphanTransformer{
State: b.State,
Module: b.Root,

View File

@ -38,7 +38,7 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
steps := []GraphTransformer{
// Create all our resources from the configuration and state
&ConfigTransformer{Module: mod},
&ConfigTransformerOld{Module: mod},
// Add the import steps
&ImportStateTransformer{Targets: b.ImportTargets},

View File

@ -0,0 +1,125 @@
package terraform
import (
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
)
// PlanGraphBuilder implements GraphBuilder and is responsible for building
// a graph for planning (creating a Terraform Diff).
//
// The primary difference between this graph and others:
//
// * Based on the config since it represents the target state
//
// * Ignores lifecycle options since no lifecycle events occur here. This
// simplifies the graph significantly since complex transforms such as
// create-before-destroy can be completely ignored.
//
type PlanGraphBuilder struct {
// Module is the root module for the graph to build.
Module *module.Tree
// State is the current state
State *State
// Providers is the list of providers supported.
Providers []string
// Targets are resources to target
Targets []string
// DisableReduce, if true, will not reduce the graph. Great for testing.
DisableReduce bool
}
// See GraphBuilder
func (b *PlanGraphBuilder) Build(path []string) (*Graph, error) {
return (&BasicGraphBuilder{
Steps: b.Steps(),
Validate: true,
Name: "plan",
}).Build(path)
}
// See GraphBuilder
func (b *PlanGraphBuilder) Steps() []GraphTransformer {
// Custom factory for creating providers.
providerFactory := func(name string, path []string) GraphNodeProvider {
return &NodeApplyableProvider{
NameValue: name,
PathValue: path,
}
}
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
return &NodePlannableResource{
NodeAbstractResource: a,
}
}
concreteResourceOrphan := func(a *NodeAbstractResource) dag.Vertex {
return &NodePlannableResourceOrphan{
NodeAbstractResource: a,
}
}
steps := []GraphTransformer{
// Creates all the resources represented in the config
&ConfigTransformer{
Concrete: concreteResource,
Module: b.Module,
},
// Add the outputs
&OutputTransformer{Module: b.Module},
// Add orphan resources
&OrphanResourceTransformer{
Concrete: concreteResourceOrphan,
State: b.State,
Module: b.Module,
},
// Attach the configuration to any resources
&AttachResourceConfigTransformer{Module: b.Module},
// Attach the state
&AttachStateTransformer{State: b.State},
// Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on.
&ReferenceTransformer{},
// Target
&TargetsTransformer{Targets: b.Targets},
// Create all the providers
&MissingProviderTransformer{Providers: b.Providers, Factory: providerFactory},
&ProviderTransformer{},
&DisableProviderTransformer{},
&ParentProviderTransformer{},
&AttachProviderConfigTransformer{Module: b.Module},
// Add root variables
&RootVariableTransformer{Module: b.Module},
// Add module variables
&ModuleVariableTransformer{Module: b.Module},
// Connect references again to connect the providers, module variables,
// etc. This is idempotent.
&ReferenceTransformer{},
// Single root
&RootTransformer{},
}
if !b.DisableReduce {
// Perform the transitive reduction to make our graph a bit
// more sane if possible (it usually is possible).
steps = append(steps, &TransitiveReductionTransformer{})
}
return steps
}

View File

@ -0,0 +1,52 @@
package terraform
import (
"reflect"
"strings"
"testing"
)
func TestPlanGraphBuilder_impl(t *testing.T) {
var _ GraphBuilder = new(PlanGraphBuilder)
}
func TestPlanGraphBuilder(t *testing.T) {
b := &PlanGraphBuilder{
Module: testModule(t, "graph-builder-plan-basic"),
Providers: []string{"aws", "openstack"},
DisableReduce: true,
}
g, err := b.Build(RootModulePath)
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(g.Path, RootModulePath) {
t.Fatalf("bad: %#v", g.Path)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testPlanGraphBuilderStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}
const testPlanGraphBuilderStr = `
aws_instance.web
aws_security_group.firewall
provider.aws
var.foo
aws_load_balancer.weblb
aws_instance.web
provider.aws
aws_security_group.firewall
provider.aws
openstack_floating_ip.random
provider.openstack
provider.aws
openstack_floating_ip.random
provider.openstack
var.foo
`

View File

@ -26,7 +26,7 @@ func TestGraphNodeConfigModuleExpand(t *testing.T) {
g, err := node.Expand(&BasicGraphBuilder{
Steps: []GraphTransformer{
&ConfigTransformer{Module: mod},
&ConfigTransformerOld{Module: mod},
},
})
if err != nil {
@ -51,7 +51,7 @@ func TestGraphNodeConfigModuleExpandFlatten(t *testing.T) {
g, err := node.Expand(&BasicGraphBuilder{
Steps: []GraphTransformer{
&ConfigTransformer{Module: mod},
&ConfigTransformerOld{Module: mod},
},
})
if err != nil {

View File

@ -156,7 +156,7 @@ func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error)
steps := make([]GraphTransformer, 0, 5)
// Expand counts.
steps = append(steps, &ResourceCountTransformer{
steps = append(steps, &ResourceCountTransformerOld{
Resource: n.Resource,
Destroy: n.Destroy,
Targets: n.Targets,

View File

@ -2,6 +2,7 @@ package terraform
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/config"
)
@ -38,7 +39,12 @@ func (n *NodeApplyableOutput) References() []string {
var result []string
result = append(result, ReferencesFromConfig(n.Config.RawConfig)...)
for _, v := range result {
result = append(result, v+".destroy")
split := strings.Split(v, "/")
for i, s := range split {
split[i] = s + ".destroy"
}
result = append(result, strings.Join(split, "/"))
}
return result

View File

@ -1,6 +1,8 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
)
@ -43,11 +45,43 @@ func (n *NodeAbstractResource) Path() []string {
// GraphNodeReferenceable
func (n *NodeAbstractResource) ReferenceableName() []string {
if n.Config == nil {
// We always are referenceable as "type.name" as long as
// we have a config or address. Determine what that value is.
var id string
if n.Config != nil {
id = n.Config.Id()
} else if n.Addr != nil {
addrCopy := n.Addr.Copy()
addrCopy.Index = -1
id = addrCopy.String()
} else {
// No way to determine our type.name, just return
return nil
}
return []string{n.Config.Id()}
var result []string
// Always include our own ID. This is primarily for backwards
// compatibility with states that didn't yet support the more
// specific dep string.
result = append(result, id)
// We represent all multi-access
result = append(result, fmt.Sprintf("%s.*", id))
// We represent either a specific number, or all numbers
suffix := "N"
if n.Addr != nil {
idx := n.Addr.Index
if idx == -1 {
idx = 0
}
suffix = fmt.Sprintf("%d", idx)
}
result = append(result, fmt.Sprintf("%s.%s", id, suffix))
return result
}
// GraphNodeReferencer

View File

@ -0,0 +1,105 @@
package terraform
import (
"github.com/hashicorp/terraform/dag"
)
// NodePlannableResource represents a resource that is "plannable":
// it is ready to be planned in order to create a diff.
type NodePlannableResource struct {
*NodeAbstractResource
// Set by GraphNodeTargetable and used during DynamicExpand to
// forward targets downwards.
targets []ResourceAddress
}
// GraphNodeTargetable
func (n *NodePlannableResource) SetTargets(targets []ResourceAddress) {
n.targets = targets
}
// GraphNodeEvalable
func (n *NodePlannableResource) EvalTree() EvalNode {
return &EvalSequence{
Nodes: []EvalNode{
// The EvalTree for a plannable resource primarily involves
// interpolating the count since it can contain variables
// we only just received access to.
//
// With the interpolated count, we can then DynamicExpand
// into the proper number of instances.
&EvalInterpolate{Config: n.Config.RawCount},
&EvalCountFixZeroOneBoundary{Resource: n.Config},
},
}
}
// GraphNodeDynamicExpandable
func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
// Grab the state which we read
state, lock := ctx.State()
lock.RLock()
defer lock.RUnlock()
// Expand the resource count which must be available by now from EvalTree
count, err := n.Config.Count()
if err != nil {
return nil, err
}
// The concrete resource factory we'll use
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
// Add the config and state since we don't do that via transforms
a.Config = n.Config
return &NodePlannableResourceInstance{
NodeAbstractResource: a,
}
}
// The concrete resource factory we'll use for oprhans
concreteResourceOrphan := func(a *NodeAbstractResource) dag.Vertex {
// Add the config and state since we don't do that via transforms
a.Config = n.Config
return &NodePlannableResourceOrphan{
NodeAbstractResource: a,
}
}
// Start creating the steps
steps := []GraphTransformer{
// Expand the count.
&ResourceCountTransformer{
Concrete: concreteResource,
Count: count,
Addr: n.ResourceAddr(),
},
// Add the count orphans
&OrphanResourceCountTransformer{
Concrete: concreteResourceOrphan,
Count: count,
Addr: n.ResourceAddr(),
State: state,
},
// Attach the state
&AttachStateTransformer{State: state},
// Targeting
&TargetsTransformer{ParsedTargets: n.targets},
// Connect references so ordering is correct
&ReferenceTransformer{},
// Make sure there is a single root
&RootTransformer{},
}
// Build the graph
b := &BasicGraphBuilder{Steps: steps, Validate: true}
return b.Build(ctx.Path())
}

View File

@ -0,0 +1,197 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/config"
)
// NodePlannableResourceInstance represents a _single_ resource
// instance that is plannable. This means this represents a single
// count index, for example.
type NodePlannableResourceInstance struct {
*NodeAbstractResource
}
// GraphNodeEvalable
func (n *NodePlannableResourceInstance) EvalTree() EvalNode {
addr := n.NodeAbstractResource.Addr
// stateId is the ID to put into the state
stateId := addr.stateId()
if addr.Index > -1 {
stateId = fmt.Sprintf("%s.%d", stateId, addr.Index)
}
// Build the instance info. More of this will be populated during eval
info := &InstanceInfo{
Id: stateId,
Type: addr.Type,
ModulePath: normalizeModulePath(addr.Path),
}
// Build the resource for eval
resource := &Resource{
Name: addr.Name,
Type: addr.Type,
CountIndex: addr.Index,
}
if resource.CountIndex < 0 {
resource.CountIndex = 0
}
// Determine the dependencies for the state. We use some older
// code for this that we've used for a long time.
var stateDeps []string
{
oldN := &graphNodeExpandedResource{Resource: n.Config}
stateDeps = oldN.StateDependencies()
}
// Eval info is different depending on what kind of resource this is
switch n.Config.Mode {
case config.ManagedResourceMode:
return n.evalTreeManagedResource(
stateId, info, resource, stateDeps,
)
case config.DataResourceMode:
return n.evalTreeDataResource(
stateId, info, resource, stateDeps)
default:
panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode))
}
}
func (n *NodePlannableResourceInstance) evalTreeDataResource(
stateId string, info *InstanceInfo,
resource *Resource, stateDeps []string) EvalNode {
var provider ResourceProvider
var config *ResourceConfig
var diff *InstanceDiff
var state *InstanceState
return &EvalSequence{
Nodes: []EvalNode{
&EvalReadState{
Name: stateId,
Output: &state,
},
// We need to re-interpolate the config here because some
// of the attributes may have become computed during
// earlier planning, due to other resources having
// "requires new resource" diffs.
&EvalInterpolate{
Config: n.Config.RawConfig.Copy(),
Resource: resource,
Output: &config,
},
&EvalIf{
If: func(ctx EvalContext) (bool, error) {
computed := config.ComputedKeys != nil && len(config.ComputedKeys) > 0
// If the configuration is complete and we
// already have a state then we don't need to
// do any further work during apply, because we
// already populated the state during refresh.
if !computed && state != nil {
return true, EvalEarlyExitError{}
}
return true, nil
},
Then: EvalNoop{},
},
&EvalGetProvider{
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadDataDiff{
Info: info,
Config: &config,
Provider: &provider,
Output: &diff,
OutputState: &state,
},
&EvalWriteState{
Name: stateId,
ResourceType: n.Config.Type,
Provider: n.Config.Provider,
Dependencies: stateDeps,
State: &state,
},
&EvalWriteDiff{
Name: stateId,
Diff: &diff,
},
},
}
}
func (n *NodePlannableResourceInstance) evalTreeManagedResource(
stateId string, info *InstanceInfo,
resource *Resource, stateDeps []string) EvalNode {
// Declare a bunch of variables that are used for state during
// evaluation. Most of this are written to by-address below.
var provider ResourceProvider
var diff *InstanceDiff
var state *InstanceState
var resourceConfig *ResourceConfig
return &EvalSequence{
Nodes: []EvalNode{
&EvalInterpolate{
Config: n.Config.RawConfig.Copy(),
Resource: resource,
Output: &resourceConfig,
},
&EvalGetProvider{
Name: n.ProvidedBy()[0],
Output: &provider,
},
// Re-run validation to catch any errors we missed, e.g. type
// mismatches on computed values.
&EvalValidateResource{
Provider: &provider,
Config: &resourceConfig,
ResourceName: n.Config.Name,
ResourceType: n.Config.Type,
ResourceMode: n.Config.Mode,
IgnoreWarnings: true,
},
&EvalReadState{
Name: stateId,
Output: &state,
},
&EvalDiff{
Info: info,
Config: &resourceConfig,
Resource: n.Config,
Provider: &provider,
State: &state,
OutputDiff: &diff,
OutputState: &state,
},
&EvalCheckPreventDestroy{
Resource: n.Config,
Diff: &diff,
},
&EvalWriteState{
Name: stateId,
ResourceType: n.Config.Type,
Provider: n.Config.Provider,
Dependencies: stateDeps,
State: &state,
},
&EvalWriteDiff{
Name: stateId,
Diff: &diff,
},
},
}
}

View File

@ -0,0 +1,61 @@
package terraform
import (
"fmt"
)
// NodePlannableResourceOrphan represents a resource that is "applyable":
// it is ready to be applied and is represented by a diff.
type NodePlannableResourceOrphan struct {
*NodeAbstractResource
}
func (n *NodePlannableResourceOrphan) Name() string {
return n.NodeAbstractResource.Name() + " (orphan)"
}
// GraphNodeEvalable
func (n *NodePlannableResourceOrphan) EvalTree() EvalNode {
addr := n.NodeAbstractResource.Addr
// stateId is the ID to put into the state
stateId := addr.stateId()
if addr.Index > -1 {
stateId = fmt.Sprintf("%s.%d", stateId, addr.Index)
}
// Build the instance info. More of this will be populated during eval
info := &InstanceInfo{
Id: stateId,
Type: addr.Type,
ModulePath: normalizeModulePath(addr.Path),
}
// Declare a bunch of variables that are used for state during
// evaluation. Most of this are written to by-address below.
var diff *InstanceDiff
var state *InstanceState
return &EvalSequence{
Nodes: []EvalNode{
&EvalReadState{
Name: stateId,
Output: &state,
},
&EvalDiffDestroy{
Info: info,
State: &state,
Output: &diff,
},
&EvalCheckPreventDestroy{
Resource: n.Config,
ResourceId: stateId,
Diff: &diff,
},
&EvalWriteDiff{
Name: stateId,
Diff: &diff,
},
},
}
}

View File

@ -29,6 +29,10 @@ type ResourceAddress struct {
// Copy returns a copy of this ResourceAddress
func (r *ResourceAddress) Copy() *ResourceAddress {
if r == nil {
return nil
}
n := &ResourceAddress{
Path: make([]string, 0, len(r.Path)),
Index: r.Index,

View File

@ -475,7 +475,7 @@ func (p *shadowResourceProviderShadow) ValidateResource(t string, c *ResourceCon
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ValidateResource' shadow value: %#v", raw))
"Unknown 'ValidateResource' shadow value for %q: %#v", key, raw))
return nil, nil
}
@ -567,7 +567,7 @@ func (p *shadowResourceProviderShadow) Diff(
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'diff' shadow value: %#v", raw))
"Unknown 'diff' shadow value for %q: %#v", key, raw))
return nil, nil
}

View File

@ -1 +1,5 @@
variable "value" {}
resource "aws_instance" "bar" {
foo = "${var.value}"
}

View File

@ -0,0 +1,24 @@
variable "foo" {
default = "bar"
description = "bar"
}
provider "aws" {
foo = "${openstack_floating_ip.random.value}"
}
resource "openstack_floating_ip" "random" {}
resource "aws_security_group" "firewall" {}
resource "aws_instance" "web" {
ami = "${var.foo}"
security_groups = [
"foo",
"${aws_security_group.firewall.foo}"
]
}
resource "aws_load_balancer" "weblb" {
members = "${aws_instance.web.id_list}"
}

View File

@ -5,3 +5,6 @@ variable "map_in" {
us-west-2 = "ami-67890"
}
}
// We have to reference it so it isn't pruned
output "output" { value = "${var.map_in}" }

View File

@ -0,0 +1 @@
# Purposefully empty

View File

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

View File

@ -3,121 +3,94 @@ package terraform
import (
"errors"
"fmt"
"log"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
)
// ConfigTransformer is a GraphTransformer that adds the configuration
// to the graph. The module used to configure this transformer must be
// the root module. We'll look up the child module by the Path in the
// Graph.
// ConfigTransformer is a GraphTransformer that adds all the resources
// from the configuration to the graph.
//
// The module used to configure this transformer must be the root module.
//
// Only resources are added to the graph. Variables, outputs, and
// providers must be added via other transforms.
//
// Unlike ConfigTransformerOld, this transformer creates a graph with
// all resources including module resources, rather than creating module
// nodes that are then "flattened".
type ConfigTransformer struct {
Concrete ConcreteResourceNodeFunc
Module *module.Tree
}
func (t *ConfigTransformer) Transform(g *Graph) error {
// A module is required and also must be completely loaded.
// If no module is given, we don't do anything
if t.Module == nil {
return errors.New("module must not be nil")
}
if !t.Module.Loaded() {
return errors.New("module must be loaded")
}
// Get the module we care about
module := t.Module.Child(g.Path[1:])
if module == nil {
return nil
}
// Get the configuration for this module
config := module.Config()
// Create the node list we'll use for the graph
nodes := make([]graphNodeConfig, 0,
(len(config.Variables)+
len(config.ProviderConfigs)+
len(config.Modules)+
len(config.Resources)+
len(config.Outputs))*2)
// Write all the variables out
for _, v := range config.Variables {
nodes = append(nodes, &GraphNodeConfigVariable{
Variable: v,
ModuleTree: t.Module,
ModulePath: g.Path,
})
// If the module isn't loaded, that is simply an error
if !t.Module.Loaded() {
return errors.New("module must be loaded for ConfigTransformer")
}
// Write all the provider configs out
for _, pc := range config.ProviderConfigs {
nodes = append(nodes, &GraphNodeConfigProvider{Provider: pc})
// Start the transformation process
return t.transform(g, t.Module)
}
// Write all the resources out
for _, r := range config.Resources {
nodes = append(nodes, &GraphNodeConfigResource{
Resource: r,
Path: g.Path,
})
}
// Write all the modules out
children := module.Children()
for _, m := range config.Modules {
path := make([]string, len(g.Path), len(g.Path)+1)
copy(path, g.Path)
path = append(path, m.Name)
nodes = append(nodes, &GraphNodeConfigModule{
Path: path,
Module: m,
Tree: children[m.Name],
})
}
// Write all the outputs out
for _, o := range config.Outputs {
nodes = append(nodes, &GraphNodeConfigOutput{Output: o})
}
// Err is where the final error value will go if there is one
var err error
// Build the graph vertices
for _, n := range nodes {
g.Add(n)
}
// Build up the dependencies. We have to do this outside of the above
// loop since the nodes need to be in place for us to build the deps.
for _, n := range nodes {
if missing := g.ConnectDependent(n); len(missing) > 0 {
for _, m := range missing {
err = multierror.Append(err, fmt.Errorf(
"%s: missing dependency: %s", n.Name(), m))
}
}
func (t *ConfigTransformer) transform(g *Graph, m *module.Tree) error {
// If no config, do nothing
if m == nil {
return nil
}
// Add our resources
if err := t.transformSingle(g, m); err != nil {
return err
}
// varNameForVar returns the VarName value for an interpolated variable.
// This value is compared to the VarName() value for the nodes within the
// graph to build the graph edges.
func varNameForVar(raw config.InterpolatedVariable) string {
switch v := raw.(type) {
case *config.ModuleVariable:
return fmt.Sprintf("module.%s.output.%s", v.Name, v.Field)
case *config.ResourceVariable:
return v.ResourceId()
case *config.UserVariable:
return fmt.Sprintf("var.%s", v.Name)
default:
return ""
// Transform all the children.
for _, c := range m.Children() {
if err := t.transform(g, c); err != nil {
return err
}
}
return nil
}
func (t *ConfigTransformer) transformSingle(g *Graph, m *module.Tree) error {
log.Printf("[TRACE] ConfigTransformer: Starting for path: %v", m.Path())
// Get the configuration for this module
config := m.Config()
// Build the path we're at
path := m.Path()
// Write all the resources out
for _, r := range config.Resources {
// Build the resource address
addr, err := parseResourceAddressConfig(r)
if err != nil {
panic(fmt.Sprintf(
"Error parsing config address, this is a bug: %#v", r))
}
addr.Path = path
// Build the abstract node and the concrete one
abstract := &NodeAbstractResource{Addr: addr}
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
// Add it to the graph
g.Add(node)
}
return nil
}

View File

@ -0,0 +1,123 @@
package terraform
import (
"errors"
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
)
// ConfigTransformerOld is a GraphTransformer that adds the configuration
// to the graph. The module used to configure this transformer must be
// the root module. We'll look up the child module by the Path in the
// Graph.
type ConfigTransformerOld struct {
Module *module.Tree
}
func (t *ConfigTransformerOld) Transform(g *Graph) error {
// A module is required and also must be completely loaded.
if t.Module == nil {
return errors.New("module must not be nil")
}
if !t.Module.Loaded() {
return errors.New("module must be loaded")
}
// Get the module we care about
module := t.Module.Child(g.Path[1:])
if module == nil {
return nil
}
// Get the configuration for this module
config := module.Config()
// Create the node list we'll use for the graph
nodes := make([]graphNodeConfig, 0,
(len(config.Variables)+
len(config.ProviderConfigs)+
len(config.Modules)+
len(config.Resources)+
len(config.Outputs))*2)
// Write all the variables out
for _, v := range config.Variables {
nodes = append(nodes, &GraphNodeConfigVariable{
Variable: v,
ModuleTree: t.Module,
ModulePath: g.Path,
})
}
// Write all the provider configs out
for _, pc := range config.ProviderConfigs {
nodes = append(nodes, &GraphNodeConfigProvider{Provider: pc})
}
// Write all the resources out
for _, r := range config.Resources {
nodes = append(nodes, &GraphNodeConfigResource{
Resource: r,
Path: g.Path,
})
}
// Write all the modules out
children := module.Children()
for _, m := range config.Modules {
path := make([]string, len(g.Path), len(g.Path)+1)
copy(path, g.Path)
path = append(path, m.Name)
nodes = append(nodes, &GraphNodeConfigModule{
Path: path,
Module: m,
Tree: children[m.Name],
})
}
// Write all the outputs out
for _, o := range config.Outputs {
nodes = append(nodes, &GraphNodeConfigOutput{Output: o})
}
// Err is where the final error value will go if there is one
var err error
// Build the graph vertices
for _, n := range nodes {
g.Add(n)
}
// Build up the dependencies. We have to do this outside of the above
// loop since the nodes need to be in place for us to build the deps.
for _, n := range nodes {
if missing := g.ConnectDependent(n); len(missing) > 0 {
for _, m := range missing {
err = multierror.Append(err, fmt.Errorf(
"%s: missing dependency: %s", n.Name(), m))
}
}
}
return err
}
// varNameForVar returns the VarName value for an interpolated variable.
// This value is compared to the VarName() value for the nodes within the
// graph to build the graph edges.
func varNameForVar(raw config.InterpolatedVariable) string {
switch v := raw.(type) {
case *config.ModuleVariable:
return fmt.Sprintf("module.%s.output.%s", v.Name, v.Field)
case *config.ResourceVariable:
return v.ResourceId()
case *config.UserVariable:
return fmt.Sprintf("var.%s", v.Name)
default:
return ""
}
}

View File

@ -0,0 +1,150 @@
package terraform
import (
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/terraform/config/module"
)
func TestConfigTransformerOld_nilModule(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformerOld{}
if err := tf.Transform(&g); err == nil {
t.Fatal("should error")
}
}
func TestConfigTransformerOld_unloadedModule(t *testing.T) {
mod, err := module.NewTreeModule(
"", filepath.Join(fixtureDir, "graph-basic"))
if err != nil {
t.Fatalf("err: %s", err)
}
g := Graph{Path: RootModulePath}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err == nil {
t.Fatal("should error")
}
}
func TestConfigTransformerOld(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformerOld{Module: testModule(t, "graph-basic")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformerOld_dependsOn(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformerOld{Module: testModule(t, "graph-depends-on")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphDependsOnStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformerOld_modules(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformerOld{Module: testModule(t, "graph-modules")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphModulesStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformerOld_outputs(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformerOld{Module: testModule(t, "graph-outputs")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphOutputsStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformerOld_providerAlias(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformerOld{Module: testModule(t, "graph-provider-alias")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphProviderAliasStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformerOld_errMissingDeps(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformerOld{Module: testModule(t, "graph-missing-deps")}
if err := tf.Transform(&g); err == nil {
t.Fatalf("err: %s", err)
}
}
const testGraphBasicStr = `
aws_instance.web
aws_security_group.firewall
var.foo
aws_load_balancer.weblb
aws_instance.web
aws_security_group.firewall
openstack_floating_ip.random
provider.aws
openstack_floating_ip.random
var.foo
`
const testGraphDependsOnStr = `
aws_instance.db
aws_instance.web
aws_instance.web
`
const testGraphModulesStr = `
aws_instance.web
aws_security_group.firewall
module.consul
aws_security_group.firewall
module.consul
aws_security_group.firewall
provider.aws
`
const testGraphOutputsStr = `
aws_instance.foo
output.foo
aws_instance.foo
`
const testGraphProviderAliasStr = `
provider.aws
provider.aws.bar
provider.aws.foo
`

View File

@ -11,8 +11,12 @@ import (
func TestConfigTransformer_nilModule(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformer{}
if err := tf.Transform(&g); err == nil {
t.Fatal("should error")
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
if len(g.Vertices()) > 0 {
t.Fatalf("graph is not empty: %s", g.String())
}
}
@ -38,113 +42,15 @@ func TestConfigTransformer(t *testing.T) {
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphBasicStr)
expected := strings.TrimSpace(testConfigTransformerGraphBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformer_dependsOn(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformer{Module: testModule(t, "graph-depends-on")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphDependsOnStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformer_modules(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformer{Module: testModule(t, "graph-modules")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphModulesStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformer_outputs(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformer{Module: testModule(t, "graph-outputs")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphOutputsStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformer_providerAlias(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformer{Module: testModule(t, "graph-provider-alias")}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphProviderAliasStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestConfigTransformer_errMissingDeps(t *testing.T) {
g := Graph{Path: RootModulePath}
tf := &ConfigTransformer{Module: testModule(t, "graph-missing-deps")}
if err := tf.Transform(&g); err == nil {
t.Fatalf("err: %s", err)
}
}
const testGraphBasicStr = `
const testConfigTransformerGraphBasicStr = `
aws_instance.web
aws_security_group.firewall
var.foo
aws_load_balancer.weblb
aws_instance.web
aws_security_group.firewall
openstack_floating_ip.random
provider.aws
openstack_floating_ip.random
var.foo
`
const testGraphDependsOnStr = `
aws_instance.db
aws_instance.web
aws_instance.web
`
const testGraphModulesStr = `
aws_instance.web
aws_security_group.firewall
module.consul
aws_security_group.firewall
module.consul
aws_security_group.firewall
provider.aws
`
const testGraphOutputsStr = `
aws_instance.foo
output.foo
aws_instance.foo
`
const testGraphProviderAliasStr = `
provider.aws
provider.aws.bar
provider.aws.foo
`

View File

@ -120,10 +120,14 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
&AttachStateTransformer{State: t.State},
}
// Go through the all destroyers and find what they're destroying.
// Use this to find the dependencies, look up if any of them are being
// destroyed, and to make the proper edge.
for d, dns := range destroyers {
// Go through all the nodes being destroyed and create a graph.
// The resulting graph is only of things being CREATED. For example,
// following our example, the resulting graph would be:
//
// A, B (with no edges)
//
var tempG Graph
for d, _ := range destroyers {
// d is what is being destroyed. We parse the resource address
// which it came from it is a panic if this fails.
addr, err := ParseResourceAddress(d)
@ -135,27 +139,48 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
// find the dependencies we need to: build a graph and use the
// attach config and state transformers then ask for references.
node := &NodeAbstractResource{Addr: addr}
{
var g Graph
g.Add(node)
tempG.Add(node)
}
// Run the graph transforms so we have the information we need to
// build references.
for _, s := range steps {
if err := s.Transform(&g); err != nil {
if err := s.Transform(&tempG); 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)
// Create a reference map for easy lookup
refMap := NewReferenceMap(tempG.Vertices())
// Go through all the nodes in the graph and determine what they
// depend on.
for _, v := range tempG.Vertices() {
// Find all the references
refs, _ := refMap.References(v)
log.Printf(
"[TRACE] DestroyEdgeTransformer: creation of %q depends on %#v",
d, deps)
if len(deps) == 0 {
"[TRACE] DestroyEdgeTransformer: creation node %q references %v",
dag.VertexName(v), refs)
// If we have no references, then we won't need to do anything
if len(refs) == 0 {
continue
}
// Get the destroy node for this. In the example of our struct,
// we are currently at B and we're looking for B_d.
rn, ok := v.(GraphNodeResource)
if !ok {
continue
}
addr := rn.ResourceAddr()
if addr == nil {
continue
}
dns := destroyers[addr.String()]
// We have dependencies, check if any are being destroyed
// to build the list of things that we must depend on!
//
@ -163,17 +188,28 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
//
// B_d => A_d => A => B
//
// Then at this point in the algorithm we started with A_d,
// we built A (to get dependencies), and we found B. We're now looking
// to see if B_d exists.
// Then at this point in the algorithm we started with B_d,
// we built B (to get dependencies), and we found A. We're now looking
// to see if A_d exists.
var depDestroyers []dag.Vertex
for _, d := range deps {
if ds, ok := destroyers[d]; ok {
for _, v := range refs {
rn, ok := v.(GraphNodeResource)
if !ok {
continue
}
addr := rn.ResourceAddr()
if addr == nil {
continue
}
key := addr.String()
if ds, ok := destroyers[key]; ok {
for _, d := range ds {
depDestroyers = append(depDestroyers, d.(dag.Vertex))
log.Printf(
"[TRACE] DestroyEdgeTransformer: destruction of %q depends on %s",
addr.String(), dag.VertexName(d))
key, dag.VertexName(d))
}
}
}

View File

@ -5,7 +5,7 @@ import (
"testing"
)
func TestDestroyEdgeTransformer(t *testing.T) {
func TestDestroyEdgeTransformer_basic(t *testing.T) {
g := Graph{Path: RootModulePath}
g.Add(&graphNodeDestroyerTest{AddrString: "test.A"})
g.Add(&graphNodeDestroyerTest{AddrString: "test.B"})

View File

@ -10,7 +10,7 @@ func TestDestroyTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -35,7 +35,7 @@ func TestDestroyTransformer_dependsOn(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -60,7 +60,7 @@ func TestCreateBeforeDestroyTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -92,7 +92,7 @@ func TestCreateBeforeDestroyTransformer_twice(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -125,7 +125,7 @@ func TestPruneDestroyTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -168,7 +168,7 @@ func TestPruneDestroyTransformer_diff(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -202,7 +202,7 @@ func TestPruneDestroyTransformer_count(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -251,7 +251,7 @@ func TestPruneDestroyTransformer_countDec(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -297,7 +297,7 @@ func TestPruneDestroyTransformer_countState(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -347,7 +347,7 @@ func TestPruneDestroyTransformer_prefixMatch(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -396,7 +396,7 @@ func TestPruneDestroyTransformer_tainted(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -11,7 +11,7 @@ func TestFlattenTransformer(t *testing.T) {
var b BasicGraphBuilder
b = BasicGraphBuilder{
Steps: []GraphTransformer{
&ConfigTransformer{Module: mod},
&ConfigTransformerOld{Module: mod},
&VertexTransformer{
Transforms: []GraphVertexTransformer{
&ExpandTransform{
@ -41,7 +41,7 @@ func TestFlattenTransformer_withProxy(t *testing.T) {
var b BasicGraphBuilder
b = BasicGraphBuilder{
Steps: []GraphTransformer{
&ConfigTransformer{Module: mod},
&ConfigTransformerOld{Module: mod},
&VertexTransformer{
Transforms: []GraphVertexTransformer{
&ExpandTransform{

View File

@ -0,0 +1,110 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/dag"
)
// OrphanResourceCountTransformer is a GraphTransformer that adds orphans
// for an expanded count to the graph. The determination of this depends
// on the count argument given.
//
// Orphans are found by comparing the count to what is found in the state.
// This transform assumes that if an element in the state is within the count
// bounds given, that it is not an orphan.
type OrphanResourceCountTransformer struct {
Concrete ConcreteResourceNodeFunc
Count int // Actual count of the resource
Addr *ResourceAddress // Addr of the resource to look for orphans
State *State // Full global state
}
func (t *OrphanResourceCountTransformer) Transform(g *Graph) error {
log.Printf("[TRACE] OrphanResourceCount: Starting...")
// Grab the module in the state just for this resource address
ms := t.State.ModuleByPath(normalizeModulePath(t.Addr.Path))
if ms == nil {
// If no state, there can't be orphans
return nil
}
orphanIndex := -1
if t.Count == 1 {
orphanIndex = 0
}
// Go through the orphans and add them all to the state
for key, _ := range ms.Resources {
// Build the address
addr, err := parseResourceAddressInternal(key)
if err != nil {
return err
}
addr.Path = ms.Path[1:]
// Copy the address for comparison. If we aren't looking at
// the same resource, then just ignore it.
addrCopy := addr.Copy()
addrCopy.Index = -1
if !addrCopy.Equals(t.Addr) {
continue
}
log.Printf("[TRACE] OrphanResourceCount: Checking: %s", addr)
idx := addr.Index
// If we have zero and the index here is 0 or 1, then we
// change the index to a high number so that we treat it as
// an orphan.
if t.Count <= 0 && idx <= 0 {
idx = t.Count + 1
}
// If we have a count greater than 0 and we're at the zero index,
// we do a special case check to see if our state also has a
// -1 index value. If so, this is an orphan because our rules are
// that if both a -1 and 0 are in the state, the 0 is destroyed.
if t.Count > 0 && idx == orphanIndex {
// This is a piece of cleverness (beware), but its simple:
// if orphanIndex is 0, then check -1, else check 0.
checkIndex := (orphanIndex + 1) * -1
key := &ResourceStateKey{
Name: addr.Name,
Type: addr.Type,
Mode: addr.Mode,
Index: checkIndex,
}
if _, ok := ms.Resources[key.String()]; ok {
// We have a -1 index, too. Make an arbitrarily high
// index so that we always mark this as an orphan.
log.Printf(
"[WARN] OrphanResourceCount: %q both -1 and 0 index found, orphaning %d",
addr, orphanIndex)
idx = t.Count + 1
}
}
// If the index is within the count bounds, it is not an orphan
if idx < t.Count {
continue
}
// Build the abstract node and the concrete one
abstract := &NodeAbstractResource{Addr: addr}
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
// Add it to the graph
g.Add(node)
}
return nil
}

View File

@ -0,0 +1,373 @@
package terraform
import (
"strings"
"testing"
)
func TestOrphanResourceCountTransformer(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 1,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_zero(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 0,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountZeroStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_oneNoIndex(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 1,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountOneNoIndexStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_oneIndex(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 1,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountOneIndexStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_zeroAndNone(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 1,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountZeroAndNoneStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceCountTransformer_zeroAndNoneCount(t *testing.T) {
addr, err := parseResourceAddressInternal("aws_instance.foo")
if err != nil {
t.Fatalf("err: %s", err)
}
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &OrphanResourceCountTransformer{
Concrete: testOrphanResourceConcreteFunc,
Count: 2,
Addr: addr,
State: state,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountZeroAndNoneCountStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
const testTransformOrphanResourceCountBasicStr = `
aws_instance.foo[2] (orphan)
`
const testTransformOrphanResourceCountZeroStr = `
aws_instance.foo (orphan)
aws_instance.foo[2] (orphan)
`
const testTransformOrphanResourceCountOneNoIndexStr = `
aws_instance.foo[2] (orphan)
`
const testTransformOrphanResourceCountOneIndexStr = `
aws_instance.foo[1] (orphan)
`
const testTransformOrphanResourceCountZeroAndNoneStr = `
aws_instance.foo[0] (orphan)
`
const testTransformOrphanResourceCountZeroAndNoneCountStr = `
aws_instance.foo (orphan)
`

View File

@ -0,0 +1,74 @@
package terraform
import (
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/dag"
)
// OrphanResourceTransformer is a GraphTransformer that adds resource
// orphans to the graph. A resource orphan is a resource that is
// represented in the state but not in the configuration.
//
// This only adds orphans that have no representation at all in the
// configuration.
type OrphanResourceTransformer struct {
Concrete ConcreteResourceNodeFunc
// State is the global state. We require the global state to
// properly find module orphans at our path.
State *State
// Module is the root module. We'll look up the proper configuration
// using the graph path.
Module *module.Tree
}
func (t *OrphanResourceTransformer) Transform(g *Graph) error {
if t.State == nil {
// If the entire state is nil, there can't be any orphans
return nil
}
// Go through the modules and for each module transform in order
// to add the orphan.
for _, ms := range t.State.Modules {
if err := t.transform(g, ms); err != nil {
return err
}
}
return nil
}
func (t *OrphanResourceTransformer) transform(g *Graph, ms *ModuleState) error {
// Get the configuration for this path. The configuration might be
// nil if the module was removed from the configuration. This is okay,
// this just means that every resource is an orphan.
var c *config.Config
if m := t.Module.Child(ms.Path[1:]); m != nil {
c = m.Config()
}
// Go through the orphans and add them all to the state
for _, key := range ms.Orphans(c) {
// Build the abstract resource
addr, err := parseResourceAddressInternal(key)
if err != nil {
return err
}
addr.Path = ms.Path[1:]
// Build the abstract node and the concrete one
abstract := &NodeAbstractResource{Addr: addr}
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
// Add it to the graph
g.Add(node)
}
return nil
}

View File

@ -0,0 +1,246 @@
package terraform
import (
"fmt"
"strings"
"testing"
"github.com/hashicorp/terraform/dag"
)
func TestOrphanResourceTransformer(t *testing.T) {
mod := testModule(t, "transform-orphan-basic")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
// The orphan
"aws_instance.db": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
tf := &OrphanResourceTransformer{
Concrete: testOrphanResourceConcreteFunc,
State: state, Module: mod,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceBasicStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceTransformer_countGood(t *testing.T) {
mod := testModule(t, "transform-orphan-count")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
tf := &OrphanResourceTransformer{
Concrete: testOrphanResourceConcreteFunc,
State: state, Module: mod,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceTransformer_countBad(t *testing.T) {
mod := testModule(t, "transform-orphan-count-empty")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
tf := &OrphanResourceTransformer{
Concrete: testOrphanResourceConcreteFunc,
State: state, Module: mod,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceCountBadStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestOrphanResourceTransformer_modules(t *testing.T) {
mod := testModule(t, "transform-orphan-modules")
state := &State{
Modules: []*ModuleState{
&ModuleState{
Path: RootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
&ModuleState{
Path: []string{"root", "child"},
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
}
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
{
tf := &OrphanResourceTransformer{
Concrete: testOrphanResourceConcreteFunc,
State: state, Module: mod,
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformOrphanResourceModulesStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
const testTransformOrphanResourceBasicStr = `
aws_instance.db (orphan)
aws_instance.web
`
const testTransformOrphanResourceCountStr = `
aws_instance.foo
`
const testTransformOrphanResourceCountBadStr = `
aws_instance.foo[0] (orphan)
aws_instance.foo[1] (orphan)
`
const testTransformOrphanResourceModulesStr = `
aws_instance.foo
module.child.aws_instance.web (orphan)
`
func testOrphanResourceConcreteFunc(a *NodeAbstractResource) dag.Vertex {
return &testOrphanResourceConcrete{a}
}
type testOrphanResourceConcrete struct {
*NodeAbstractResource
}
func (n *testOrphanResourceConcrete) Name() string {
return fmt.Sprintf("%s (orphan)", n.NodeAbstractResource.Name())
}

View File

@ -35,7 +35,7 @@ func TestOrphanTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -86,7 +86,7 @@ func TestOrphanTransformer_modules(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -140,7 +140,7 @@ func TestOrphanTransformer_modulesDeps(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -194,7 +194,7 @@ func TestOrphanTransformer_modulesDepsOrphan(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -233,7 +233,7 @@ func TestOrphanTransformer_modulesNoRoot(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -282,7 +282,7 @@ func TestOrphanTransformer_resourceDepends(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -305,7 +305,7 @@ func TestOrphanTransformer_nilState(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -27,7 +27,7 @@ func TestAddOutputOrphanTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -12,7 +12,7 @@ func TestProviderTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -73,7 +73,7 @@ func TestCloseProviderTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -105,7 +105,7 @@ func TestCloseProviderTransformer_withTargets(t *testing.T) {
g := Graph{Path: RootModulePath}
transforms := []GraphTransformer{
&ConfigTransformer{Module: mod},
&ConfigTransformerOld{Module: mod},
&ProviderTransformer{},
&CloseProviderTransformer{},
&TargetsTransformer{
@ -135,7 +135,7 @@ func TestMissingProviderTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -318,7 +318,7 @@ func TestPruneProviderTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -364,7 +364,7 @@ func TestDisableProviderTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
transforms := []GraphTransformer{
&ConfigTransformer{Module: mod},
&ConfigTransformerOld{Module: mod},
&MissingProviderTransformer{Providers: []string{"aws"}},
&ProviderTransformer{},
&DisableProviderTransformerOld{},
@ -390,7 +390,7 @@ func TestDisableProviderTransformer_keep(t *testing.T) {
g := Graph{Path: RootModulePath}
transforms := []GraphTransformer{
&ConfigTransformer{Module: mod},
&ConfigTransformerOld{Module: mod},
&MissingProviderTransformer{Providers: []string{"aws"}},
&ProviderTransformer{},
&DisableProviderTransformerOld{},

View File

@ -12,7 +12,7 @@ func TestMissingProvisionerTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -112,7 +112,7 @@ func TestCloseProvisionerTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -2,6 +2,8 @@ package terraform
import (
"fmt"
"log"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
@ -53,6 +55,14 @@ func (t *ReferenceTransformer) Transform(g *Graph) error {
// Find the things that reference things and connect them
for _, v := range vs {
parents, _ := m.References(v)
parentsDbg := make([]string, len(parents))
for i, v := range parents {
parentsDbg[i] = dag.VertexName(v)
}
log.Printf(
"[DEBUG] ReferenceTransformer: %q references: %v",
dag.VertexName(v), parentsDbg)
for _, parent := range parents {
g.Connect(dag.BasicEdge(v, parent))
}
@ -81,14 +91,18 @@ func (m *ReferenceMap) References(v dag.Vertex) ([]dag.Vertex, []string) {
var matches []dag.Vertex
var missing []string
prefix := m.prefix(v)
for _, n := range rn.References() {
for _, ns := range rn.References() {
found := false
for _, n := range strings.Split(ns, "/") {
n = prefix + n
parents, ok := m.references[n]
if !ok {
missing = append(missing, n)
continue
}
// Mark that we found a match
found = true
// Make sure this isn't a self reference, which isn't included
selfRef := false
for _, p := range parents {
@ -102,6 +116,12 @@ func (m *ReferenceMap) References(v dag.Vertex) ([]dag.Vertex, []string) {
}
matches = append(matches, parents...)
break
}
if !found {
missing = append(missing, ns)
}
}
return matches, missing
@ -209,10 +229,9 @@ func NewReferenceMap(vs []dag.Vertex) *ReferenceMap {
func ReferencesFromConfig(c *config.RawConfig) []string {
var result []string
for _, v := range c.Variables {
if r := ReferenceFromInterpolatedVar(v); r != "" {
result = append(result, r)
if r := ReferenceFromInterpolatedVar(v); len(r) > 0 {
result = append(result, r...)
}
}
return result
@ -220,15 +239,31 @@ func ReferencesFromConfig(c *config.RawConfig) []string {
// ReferenceFromInterpolatedVar returns the reference from this variable,
// or an empty string if there is no reference.
func ReferenceFromInterpolatedVar(v config.InterpolatedVariable) string {
func ReferenceFromInterpolatedVar(v config.InterpolatedVariable) []string {
switch v := v.(type) {
case *config.ModuleVariable:
return fmt.Sprintf("module.%s.output.%s", v.Name, v.Field)
return []string{fmt.Sprintf("module.%s.output.%s", v.Name, v.Field)}
case *config.ResourceVariable:
return v.ResourceId()
id := v.ResourceId()
// If we have a multi-reference (splat), then we depend on ALL
// resources with this type/name.
if v.Multi && v.Index == -1 {
return []string{fmt.Sprintf("%s.*", id)}
}
// Otherwise, we depend on a specific index.
idx := v.Index
if !v.Multi || v.Index == -1 {
idx = 0
}
// Depend on the index, as well as "N" which represents the
// un-expanded set of resources.
return []string{fmt.Sprintf("%s.%d/%s.N", id, idx, id)}
case *config.UserVariable:
return fmt.Sprintf("var.%s", v.Name)
return []string{fmt.Sprintf("var.%s", v.Name)}
default:
return ""
return nil
}
}

View File

@ -88,6 +88,56 @@ func TestReferenceTransformer_path(t *testing.T) {
}
}
func TestReferenceTransformer_backup(t *testing.T) {
g := Graph{Path: RootModulePath}
g.Add(&graphNodeRefParentTest{
NameValue: "A",
Names: []string{"A"},
})
g.Add(&graphNodeRefChildTest{
NameValue: "B",
Refs: []string{"C/A"},
})
tf := &ReferenceTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformRefBackupStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestReferenceTransformer_backupPrimary(t *testing.T) {
g := Graph{Path: RootModulePath}
g.Add(&graphNodeRefParentTest{
NameValue: "A",
Names: []string{"A"},
})
g.Add(&graphNodeRefChildTest{
NameValue: "B",
Refs: []string{"C/A"},
})
g.Add(&graphNodeRefParentTest{
NameValue: "C",
Names: []string{"C"},
})
tf := &ReferenceTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformRefBackupPrimaryStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestReferenceMapReferences(t *testing.T) {
cases := map[string]struct {
Nodes []dag.Vertex
@ -202,6 +252,19 @@ B
A
`
const testTransformRefBackupStr = `
A
B
A
`
const testTransformRefBackupPrimaryStr = `
A
B
C
C
`
const testTransformRefPathStr = `
A
B

View File

@ -8,15 +8,15 @@ import (
"github.com/hashicorp/terraform/dag"
)
// ResourceCountTransformer is a GraphTransformer that expands the count
// ResourceCountTransformerOld is a GraphTransformer that expands the count
// out for a specific resource.
type ResourceCountTransformer struct {
type ResourceCountTransformerOld struct {
Resource *config.Resource
Destroy bool
Targets []ResourceAddress
}
func (t *ResourceCountTransformer) Transform(g *Graph) error {
func (t *ResourceCountTransformerOld) Transform(g *Graph) error {
// Expand the resource count
count, err := t.Resource.Count()
if err != nil {
@ -72,7 +72,7 @@ func (t *ResourceCountTransformer) Transform(g *Graph) error {
return nil
}
func (t *ResourceCountTransformer) nodeIsTargeted(node dag.Vertex) bool {
func (t *ResourceCountTransformerOld) nodeIsTargeted(node dag.Vertex) bool {
// no targets specified, everything stays in the graph
if len(t.Targets) == 0 {
return true

View File

@ -0,0 +1,51 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/dag"
)
// ResourceCountTransformer is a GraphTransformer that expands the count
// out for a specific resource.
//
// This assumes that the count is already interpolated.
type ResourceCountTransformer struct {
Concrete ConcreteResourceNodeFunc
Count int
Addr *ResourceAddress
}
func (t *ResourceCountTransformer) Transform(g *Graph) error {
// Don't allow the count to be negative
if t.Count < 0 {
return fmt.Errorf("negative count: %d", t.Count)
}
// For each count, build and add the node
for i := 0; i < t.Count; i++ {
// Set the index. If our count is 1 we special case it so that
// we handle the "resource.0" and "resource" boundary properly.
index := i
if t.Count == 1 {
index = -1
}
// Build the resource address
addr := t.Addr.Copy()
addr.Index = index
// Build the abstract node and the concrete one
abstract := &NodeAbstractResource{Addr: addr}
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
// Add it to the graph
g.Add(node)
}
return nil
}

View File

@ -5,64 +5,64 @@ import (
"testing"
)
func TestResourceCountTransformer(t *testing.T) {
func TestResourceCountTransformerOld(t *testing.T) {
cfg := testModule(t, "transform-resource-count-basic").Config()
resource := cfg.Resources[0]
g := Graph{Path: RootModulePath}
{
tf := &ResourceCountTransformer{Resource: resource}
tf := &ResourceCountTransformerOld{Resource: resource}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testResourceCountTransformStr)
expected := strings.TrimSpace(testResourceCountTransformOldStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
func TestResourceCountTransformer_countNegative(t *testing.T) {
func TestResourceCountTransformerOld_countNegative(t *testing.T) {
cfg := testModule(t, "transform-resource-count-negative").Config()
resource := cfg.Resources[0]
g := Graph{Path: RootModulePath}
{
tf := &ResourceCountTransformer{Resource: resource}
tf := &ResourceCountTransformerOld{Resource: resource}
if err := tf.Transform(&g); err == nil {
t.Fatal("should error")
}
}
}
func TestResourceCountTransformer_deps(t *testing.T) {
func TestResourceCountTransformerOld_deps(t *testing.T) {
cfg := testModule(t, "transform-resource-count-deps").Config()
resource := cfg.Resources[0]
g := Graph{Path: RootModulePath}
{
tf := &ResourceCountTransformer{Resource: resource}
tf := &ResourceCountTransformerOld{Resource: resource}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testResourceCountTransformDepsStr)
expected := strings.TrimSpace(testResourceCountTransformOldDepsStr)
if actual != expected {
t.Fatalf("bad:\n\n%s", actual)
}
}
const testResourceCountTransformStr = `
const testResourceCountTransformOldStr = `
aws_instance.foo #0
aws_instance.foo #1
aws_instance.foo #2
`
const testResourceCountTransformDepsStr = `
const testResourceCountTransformOldDepsStr = `
aws_instance.foo #0
aws_instance.foo #1
aws_instance.foo #0

View File

@ -10,7 +10,7 @@ func TestRootTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -10,7 +10,7 @@ func TestTargetsTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
@ -41,7 +41,7 @@ func TestTargetsTransformer_destroy(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -10,7 +10,7 @@ func TestTransitiveReductionTransformer(t *testing.T) {
g := Graph{Path: RootModulePath}
{
tf := &ConfigTransformer{Module: mod}
tf := &ConfigTransformerOld{Module: mod}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}