terraform: redo how flattening works

This commit is contained in:
Mitchell Hashimoto 2015-05-01 15:18:40 -07:00
parent f2e7f505d4
commit 86d07d3b5b
11 changed files with 681 additions and 597 deletions

View File

@ -134,6 +134,12 @@ func (b *BuiltinGraphBuilder) Steps() []GraphTransformer {
},
},
// Flatten stuff
&FlattenTransformer{},
// Make sure all the connections that are proxies are connected through
&ProxyTransformer{},
// Optionally reduces the graph to a user-specified list of targets and
// their dependencies.
&TargetsTransformer{Targets: b.Targets, Destroy: b.Destroy},

View File

@ -2,7 +2,6 @@ package terraform
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
@ -45,60 +44,6 @@ type GraphNodeTargetable interface {
SetTargets([]ResourceAddress)
}
// GraphNodeConfigOutput represents an output configured within the
// configuration.
type GraphNodeConfigOutput struct {
Output *config.Output
}
func (n *GraphNodeConfigOutput) Name() string {
return fmt.Sprintf("output.%s", n.Output.Name)
}
func (n *GraphNodeConfigOutput) ConfigType() GraphNodeConfigType {
return GraphNodeConfigTypeOutput
}
func (n *GraphNodeConfigOutput) OutputName() string {
return n.Output.Name
}
func (n *GraphNodeConfigOutput) DependableName() []string {
return []string{n.Name()}
}
func (n *GraphNodeConfigOutput) DependentOn() []string {
vars := n.Output.RawConfig.Variables
result := make([]string, 0, len(vars))
for _, v := range vars {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
return result
}
// GraphNodeEvalable impl.
func (n *GraphNodeConfigOutput) EvalTree() EvalNode {
return &EvalOpFilter{
Ops: []walkOperation{walkRefresh, walkPlan, walkApply},
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalWriteOutput{
Name: n.Output.Name,
Value: n.Output.RawConfig,
},
},
},
}
}
// GraphNodeProxy impl.
func (n *GraphNodeConfigOutput) Proxy() bool {
return true
}
// GraphNodeConfigProvider represents a configured provider within the
// configuration graph. These are only immediately in the graph when an
// explicit `provider` configuration block is in the configuration.
@ -161,382 +106,3 @@ func (n *GraphNodeConfigProvider) DotNode(name string, opts *GraphDotOpts) *dot.
func (n *GraphNodeConfigProvider) DotOrigin() bool {
return true
}
// GraphNodeConfigResource represents a resource within the config graph.
type GraphNodeConfigResource struct {
Resource *config.Resource
// If this is set to anything other than destroyModeNone, then this
// resource represents a resource that will be destroyed in some way.
DestroyMode GraphNodeDestroyMode
// Used during DynamicExpand to target indexes
Targets []ResourceAddress
}
func (n *GraphNodeConfigResource) ConfigType() GraphNodeConfigType {
return GraphNodeConfigTypeResource
}
func (n *GraphNodeConfigResource) DependableName() []string {
return []string{n.Resource.Id()}
}
// GraphNodeDependent impl.
func (n *GraphNodeConfigResource) DependentOn() []string {
result := make([]string, len(n.Resource.DependsOn),
(len(n.Resource.RawCount.Variables)+
len(n.Resource.RawConfig.Variables)+
len(n.Resource.DependsOn))*2)
copy(result, n.Resource.DependsOn)
for _, v := range n.Resource.RawCount.Variables {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
for _, v := range n.Resource.RawConfig.Variables {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
for _, p := range n.Resource.Provisioners {
for _, v := range p.ConnInfo.Variables {
if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() {
result = append(result, vn)
}
}
for _, v := range p.RawConfig.Variables {
if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() {
result = append(result, vn)
}
}
}
return result
}
// VarWalk calls a callback for all the variables that this resource
// depends on.
func (n *GraphNodeConfigResource) VarWalk(fn func(config.InterpolatedVariable)) {
for _, v := range n.Resource.RawCount.Variables {
fn(v)
}
for _, v := range n.Resource.RawConfig.Variables {
fn(v)
}
for _, p := range n.Resource.Provisioners {
for _, v := range p.ConnInfo.Variables {
fn(v)
}
for _, v := range p.RawConfig.Variables {
fn(v)
}
}
}
func (n *GraphNodeConfigResource) Name() string {
result := n.Resource.Id()
switch n.DestroyMode {
case DestroyNone:
case DestroyPrimary:
result += " (destroy)"
case DestroyTainted:
result += " (destroy tainted)"
default:
result += " (unknown destroy type)"
}
return result
}
// GraphNodeDotter impl.
func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node {
if n.DestroyMode != DestroyNone && !opts.Verbose {
return nil
}
return dot.NewNode(name, map[string]string{
"label": n.Name(),
"shape": "box",
})
}
// GraphNodeDynamicExpandable impl.
func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
state, lock := ctx.State()
lock.RLock()
defer lock.RUnlock()
// Start creating the steps
steps := make([]GraphTransformer, 0, 5)
// Primary and non-destroy modes are responsible for creating/destroying
// all the nodes, expanding counts.
switch n.DestroyMode {
case DestroyNone:
fallthrough
case DestroyPrimary:
steps = append(steps, &ResourceCountTransformer{
Resource: n.Resource,
Destroy: n.DestroyMode != DestroyNone,
Targets: n.Targets,
})
}
// Additional destroy modifications.
switch n.DestroyMode {
case DestroyPrimary:
// If we're destroying the primary instance, then we want to
// expand orphans, which have all the same semantics in a destroy
// as a primary.
steps = append(steps, &OrphanTransformer{
State: state,
View: n.Resource.Id(),
Targeting: (len(n.Targets) > 0),
})
steps = append(steps, &DeposedTransformer{
State: state,
View: n.Resource.Id(),
})
case DestroyTainted:
// If we're only destroying tainted resources, then we only
// want to find tainted resources and destroy them here.
steps = append(steps, &TaintedTransformer{
State: state,
View: n.Resource.Id(),
})
}
// Always end with the root being added
steps = append(steps, &RootTransformer{})
// Build the graph
b := &BasicGraphBuilder{Steps: steps}
return b.Build(ctx.Path())
}
// GraphNodeAddressable impl.
func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress {
return &ResourceAddress{
// Indicates no specific index; will match on other three fields
Index: -1,
InstanceType: TypePrimary,
Name: n.Resource.Name,
Type: n.Resource.Type,
}
}
// GraphNodeTargetable impl.
func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) {
n.Targets = targets
}
// GraphNodeEvalable impl.
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
return &EvalSequence{
Nodes: []EvalNode{
&EvalInterpolate{Config: n.Resource.RawCount},
&EvalOpFilter{
Ops: []walkOperation{walkValidate},
Node: &EvalValidateCount{Resource: n.Resource},
},
&EvalCountFixZeroOneBoundary{Resource: n.Resource},
},
}
}
// GraphNodeProviderConsumer
func (n *GraphNodeConfigResource) ProvidedBy() []string {
return []string{resourceProvider(n.Resource.Type, n.Resource.Provider)}
}
// GraphNodeProvisionerConsumer
func (n *GraphNodeConfigResource) ProvisionedBy() []string {
result := make([]string, len(n.Resource.Provisioners))
for i, p := range n.Resource.Provisioners {
result[i] = p.Type
}
return result
}
// GraphNodeDestroyable
func (n *GraphNodeConfigResource) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy {
// If we're already a destroy node, then don't do anything
if n.DestroyMode != DestroyNone {
return nil
}
result := &graphNodeResourceDestroy{
GraphNodeConfigResource: *n,
Original: n,
}
result.DestroyMode = mode
return result
}
// graphNodeResourceDestroy represents the logical destruction of a
// resource. This node doesn't mean it will be destroyed for sure, but
// instead that if a destroy were to happen, it must happen at this point.
type graphNodeResourceDestroy struct {
GraphNodeConfigResource
Original *GraphNodeConfigResource
}
func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool {
// CBD is enabled if the resource enables it in addition to us
// being responsible for destroying the primary state. The primary
// state destroy node is the only destroy node that needs to be
// "shuffled" according to the CBD rules, since tainted resources
// don't have the same inverse dependencies.
return n.Original.Resource.Lifecycle.CreateBeforeDestroy &&
n.DestroyMode == DestroyPrimary
}
func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex {
return n.Original
}
func (n *graphNodeResourceDestroy) DestroyInclude(d *ModuleDiff, s *ModuleState) bool {
switch n.DestroyMode {
case DestroyPrimary:
return n.destroyIncludePrimary(d, s)
case DestroyTainted:
return n.destroyIncludeTainted(d, s)
default:
return true
}
}
func (n *graphNodeResourceDestroy) destroyIncludeTainted(
d *ModuleDiff, s *ModuleState) bool {
// If there is no state, there can't by any tainted.
if s == nil {
return false
}
// Grab the ID which is the prefix (in the case count > 0 at some point)
prefix := n.Original.Resource.Id()
// Go through the resources and find any with our prefix. If there
// are any tainted, we need to keep it.
for k, v := range s.Resources {
if !strings.HasPrefix(k, prefix) {
continue
}
if len(v.Tainted) > 0 {
return true
}
}
// We didn't find any tainted nodes, return
return false
}
func (n *graphNodeResourceDestroy) destroyIncludePrimary(
d *ModuleDiff, s *ModuleState) bool {
// Get the count, and specifically the raw value of the count
// (with interpolations and all). If the count is NOT a static "1",
// then we keep the destroy node no matter what.
//
// The reasoning for this is complicated and not intuitively obvious,
// but I attempt to explain it below.
//
// The destroy transform works by generating the worst case graph,
// with worst case being the case that every resource already exists
// and needs to be destroy/created (force-new). There is a single important
// edge case where this actually results in a real-life cycle: if a
// create-before-destroy (CBD) resource depends on a non-CBD resource.
// Imagine a EC2 instance "foo" with CBD depending on a security
// group "bar" without CBD, and conceptualize the worst case destroy
// order:
//
// 1.) SG must be destroyed (non-CBD)
// 2.) SG must be created/updated
// 3.) EC2 instance must be created (CBD, requires the SG be made)
// 4.) EC2 instance must be destroyed (requires SG be destroyed)
//
// Except, #1 depends on #4, since the SG can't be destroyed while
// an EC2 instance is using it (AWS API requirements). As you can see,
// this is a real life cycle that can't be automatically reconciled
// except under two conditions:
//
// 1.) SG is also CBD. This doesn't work 100% of the time though
// since the non-CBD resource might not support CBD. To make matters
// worse, the entire transitive closure of dependencies must be
// CBD (if the SG depends on a VPC, you have the same problem).
// 2.) EC2 must not CBD. This can't happen automatically because CBD
// is used as a way to ensure zero (or minimal) downtime Terraform
// applies, and it isn't acceptable for TF to ignore this request,
// since it can result in unexpected downtime.
//
// Therefore, we compromise with this edge case here: if there is
// a static count of "1", we prune the diff to remove cycles during a
// graph optimization path if we don't see the resource in the diff.
// If the count is set to ANYTHING other than a static "1" (variable,
// computed attribute, static number greater than 1), then we keep the
// destroy, since it is required for dynamic graph expansion to find
// orphan/tainted count objects.
//
// This isn't ideal logic, but its strictly better without introducing
// new impossibilities. It breaks the cycle in practical cases, and the
// cycle comes back in no cases we've found to be practical, but just
// as the cycle would already exist without this anyways.
count := n.Original.Resource.RawCount
if raw := count.Raw[count.Key]; raw != "1" {
return true
}
// Okay, we're dealing with a static count. There are a few ways
// to include this resource.
prefix := n.Original.Resource.Id()
// If we're present in the diff proper, then keep it.
if d != nil {
for k, _ := range d.Resources {
if strings.HasPrefix(k, prefix) {
return true
}
}
}
// If we're in the state as a primary in any form, then keep it.
// This does a prefix check so it will also catch orphans on count
// decreases to "1".
if s != nil {
for k, v := range s.Resources {
// Ignore exact matches
if k == prefix {
continue
}
// Ignore anything that doesn't have a "." afterwards so that
// we only get our own resource and any counts on it.
if !strings.HasPrefix(k, prefix+".") {
continue
}
// Ignore exact matches and the 0'th index. We only care
// about if there is a decrease in count.
if k == prefix+".0" {
continue
}
if v.Primary != nil {
return true
}
}
// If we're in the state as _both_ "foo" and "foo.0", then
// keep it, since we treat the latter as an orphan.
_, okOne := s.Resources[prefix]
_, okTwo := s.Resources[prefix+".0"]
if okOne && okTwo {
return true
}
}
return false
}

View File

@ -166,22 +166,6 @@ func (n *graphNodeModuleExpanded) FlattenGraph() *Graph {
graph := n.Subgraph()
input := n.Original.Module.RawConfig
// Build the string that represents the path. We do this once here
// so that we only have to compute it once. The block below is in {}
// so that parts drops out of scope immediately.
var pathStr, parentPathStr string
{
parts := make([]string, 0, len(graph.Path)*2)
for _, p := range graph.Path[1:] {
parts = append(parts, "module", p)
}
pathStr = strings.Join(parts, ".")
if len(parts) > 2 {
parentPathStr = strings.Join(parts[0:len(parts)-2], ".")
}
}
// Go over each vertex in the graph and wrap the configuration
// items so that the dependencies properly map to the modules.
// See the docs for graphNodeModuleWrappable for more info.
@ -191,15 +175,6 @@ func (n *graphNodeModuleExpanded) FlattenGraph() *Graph {
continue
}
wn, ok := v.(graphNodeModuleWrappable)
if !ok {
panic("unwrappable node: " + dag.VertexName(v))
}
// Prefix for dependencies. We set this to blank for variables
// (see below).
depPrefix := pathStr
// If this is a variable, then look it up in the raw configuration.
// If it exists in the raw configuration, set the value of it.
if vn, ok := v.(GraphNodeVariable); ok && input != nil {
@ -217,20 +192,7 @@ func (n *graphNodeModuleExpanded) FlattenGraph() *Graph {
// Set the variable value so it is interpolated properly
vn.SetVariableValue(config)
}
// We set the dependency prefix to the parent path
// since variables can reference outside of our own graph
// to the parent graph.
depPrefix = parentPathStr
}
graph.Replace(v, &graphNodeModuleFlatWrap{
graphNodeModuleWrappable: wn,
PathValue: graph.Path,
NamePrefix: pathStr,
DependentOnPrefix: depPrefix,
})
}
return graph
@ -247,58 +209,57 @@ type graphNodeModuleSkippable interface {
FlattenSkip() bool
}
// This is the interface that all nodes within a module graph must
// implement to be wrapped for flattening.
type graphNodeModuleWrappable interface {
graphNodeConfig
}
/*
// graphNodeModuleFlatWrap wraps elements of the module graph
// when they are flattened so that the dependency information is
// correct.
type graphNodeModuleFlatWrap struct {
graphNodeModuleWrappable
Vertex dag.Vertex
PathValue []string
NamePrefix string
DependentOnPrefix string
}
func (n *graphNodeModuleFlatWrap) Name() string {
return fmt.Sprintf("%s.%s", n.NamePrefix, n.graphNodeModuleWrappable.Name())
}
// GraphNodeSubPath impl.
func (n *graphNodeModuleFlatWrap) Path() []string {
return n.PathValue
}
func (n *graphNodeModuleFlatWrap) DependableName() []string {
result := n.graphNodeModuleWrappable.DependableName()
n.prefixList(result, n.NamePrefix)
return result
}
func (n *graphNodeModuleFlatWrap) DependentOn() []string {
result := n.graphNodeModuleWrappable.DependentOn()
if n.DependentOnPrefix != "" {
n.prefixList(result, n.DependentOnPrefix)
}
return result
}
func (n *graphNodeModuleFlatWrap) Proxy() bool {
pn, ok := n.graphNodeModuleWrappable.(GraphNodeProxy)
// GraphNodeProvider impl.
func (n *graphNodeModuleFlatWrap) ProviderName() string {
pn, ok := n.Vertex.(GraphNodeProvider)
if !ok {
return false
return ""
}
return pn.Proxy()
return fmt.Sprintf("%s.%s", n.NamePrefix, pn.ProviderName())
}
func (n *graphNodeModuleFlatWrap) prefixList(result []string, prefix string) {
for i, v := range result {
result[i] = fmt.Sprintf("%s.%s", prefix, v)
// GraphNodeProviderConsumer impl.
func (n *graphNodeModuleFlatWrap) ProvidedBy() []string {
pn, ok := n.Vertex.(GraphNodeProviderConsumer)
if !ok {
return nil
}
result := make([]string, 0, 2)
for _, v := range pn.ProvidedBy() {
result = append(result, fmt.Sprintf("%s.%s", n.NamePrefix, v))
}
return result
}
*/
func modulePrefixStr(p []string) string {
parts := make([]string, 0, len(p)*2)
for _, p := range p[1:] {
parts = append(parts, "module", p)
}
return strings.Join(parts, ".")
}
func modulePrefixList(result []string, prefix string) []string {
if prefix != "" {
for i, v := range result {
result[i] = fmt.Sprintf("%s.%s", prefix, v)
}
}
return result
}

View File

@ -1,7 +1,6 @@
package terraform
import (
"reflect"
"strings"
"testing"
@ -59,7 +58,7 @@ func TestGraphNodeConfigModuleExpandFlatten(t *testing.T) {
t.Fatalf("err: %s", err)
}
fg := g.(GraphNodeFlattenable)
fg := g.(GraphNodeFlatGraph)
actual := strings.TrimSpace(fg.FlattenGraph().String())
expected := strings.TrimSpace(testGraphNodeModuleExpandFlattenStr)
@ -68,73 +67,6 @@ func TestGraphNodeConfigModuleExpandFlatten(t *testing.T) {
}
}
func TestGraphNodeModulFlatWrap_Name(t *testing.T) {
n := &graphNodeModuleFlatWrap{
graphNodeModuleWrappable: &testGraphNodeModuleWrappable{
NameValue: "foo",
},
NamePrefix: "module.bar",
}
if v := n.Name(); v != "module.bar.foo" {
t.Fatalf("bad: %s", v)
}
}
func TestGraphNodeModulFlatWrap_DependentOn(t *testing.T) {
n := &graphNodeModuleFlatWrap{
graphNodeModuleWrappable: &testGraphNodeModuleWrappable{
NameValue: "foo",
},
NamePrefix: "module.bar",
DependentOnPrefix: "module.bar",
}
actual := n.DependentOn()
expected := []string{"module.bar.foo"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestGraphNodeModulFlatWrap_DependableName(t *testing.T) {
n := &graphNodeModuleFlatWrap{
graphNodeModuleWrappable: &testGraphNodeModuleWrappable{
NameValue: "foo",
},
NamePrefix: "module.bar",
}
actual := n.DependableName()
expected := []string{"module.bar.foo"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
type testGraphNodeModuleWrappable struct {
NameValue string
}
func (n *testGraphNodeModuleWrappable) ConfigType() GraphNodeConfigType {
return GraphNodeConfigTypeInvalid
}
func (n *testGraphNodeModuleWrappable) Name() string {
return n.NameValue
}
func (n *testGraphNodeModuleWrappable) DependableName() []string {
return []string{"foo"}
}
func (n *testGraphNodeModuleWrappable) DependentOn() []string {
return []string{"foo"}
}
const testGraphNodeModuleExpandStr = `
aws_instance.bar
aws_instance.foo
@ -144,5 +76,5 @@ module inputs
`
const testGraphNodeModuleExpandFlattenStr = `
module.child.aws_instance.foo
aws_instance.foo
`

View File

@ -0,0 +1,99 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
)
// GraphNodeConfigOutput represents an output configured within the
// configuration.
type GraphNodeConfigOutput struct {
Output *config.Output
}
func (n *GraphNodeConfigOutput) Name() string {
return fmt.Sprintf("output.%s", n.Output.Name)
}
func (n *GraphNodeConfigOutput) ConfigType() GraphNodeConfigType {
return GraphNodeConfigTypeOutput
}
func (n *GraphNodeConfigOutput) OutputName() string {
return n.Output.Name
}
func (n *GraphNodeConfigOutput) DependableName() []string {
return []string{n.Name()}
}
func (n *GraphNodeConfigOutput) DependentOn() []string {
vars := n.Output.RawConfig.Variables
result := make([]string, 0, len(vars))
for _, v := range vars {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
return result
}
// GraphNodeEvalable impl.
func (n *GraphNodeConfigOutput) EvalTree() EvalNode {
return &EvalOpFilter{
Ops: []walkOperation{walkRefresh, walkPlan, walkApply},
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalWriteOutput{
Name: n.Output.Name,
Value: n.Output.RawConfig,
},
},
},
}
}
// GraphNodeProxy impl.
func (n *GraphNodeConfigOutput) Proxy() bool {
return true
}
// GraphNodeFlattenable impl.
func (n *GraphNodeConfigOutput) Flatten(p []string) (dag.Vertex, error) {
return &GraphNodeConfigOutputFlat{
GraphNodeConfigOutput: n,
PathValue: p,
}, nil
}
// Same as GraphNodeConfigOutput, but for flattening
type GraphNodeConfigOutputFlat struct {
*GraphNodeConfigOutput
PathValue []string
}
func (n *GraphNodeConfigOutputFlat) Name() string {
return fmt.Sprintf(
"%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigOutput.Name())
}
func (n *GraphNodeConfigOutputFlat) Path() []string {
return n.PathValue
}
func (n *GraphNodeConfigOutputFlat) DependableName() []string {
return modulePrefixList(
n.GraphNodeConfigOutput.DependableName(),
modulePrefixStr(n.PathValue))
}
func (n *GraphNodeConfigOutputFlat) DependentOn() []string {
prefix := modulePrefixStr(n.PathValue)
return modulePrefixList(
n.GraphNodeConfigOutput.DependentOn(),
prefix)
}

View File

@ -0,0 +1,426 @@
package terraform
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/dot"
)
// GraphNodeConfigResource represents a resource within the config graph.
type GraphNodeConfigResource struct {
Resource *config.Resource
// If this is set to anything other than destroyModeNone, then this
// resource represents a resource that will be destroyed in some way.
DestroyMode GraphNodeDestroyMode
// Used during DynamicExpand to target indexes
Targets []ResourceAddress
}
func (n *GraphNodeConfigResource) ConfigType() GraphNodeConfigType {
return GraphNodeConfigTypeResource
}
func (n *GraphNodeConfigResource) DependableName() []string {
return []string{n.Resource.Id()}
}
// GraphNodeDependent impl.
func (n *GraphNodeConfigResource) DependentOn() []string {
result := make([]string, len(n.Resource.DependsOn),
(len(n.Resource.RawCount.Variables)+
len(n.Resource.RawConfig.Variables)+
len(n.Resource.DependsOn))*2)
copy(result, n.Resource.DependsOn)
for _, v := range n.Resource.RawCount.Variables {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
for _, v := range n.Resource.RawConfig.Variables {
if vn := varNameForVar(v); vn != "" {
result = append(result, vn)
}
}
for _, p := range n.Resource.Provisioners {
for _, v := range p.ConnInfo.Variables {
if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() {
result = append(result, vn)
}
}
for _, v := range p.RawConfig.Variables {
if vn := varNameForVar(v); vn != "" && vn != n.Resource.Id() {
result = append(result, vn)
}
}
}
return result
}
// VarWalk calls a callback for all the variables that this resource
// depends on.
func (n *GraphNodeConfigResource) VarWalk(fn func(config.InterpolatedVariable)) {
for _, v := range n.Resource.RawCount.Variables {
fn(v)
}
for _, v := range n.Resource.RawConfig.Variables {
fn(v)
}
for _, p := range n.Resource.Provisioners {
for _, v := range p.ConnInfo.Variables {
fn(v)
}
for _, v := range p.RawConfig.Variables {
fn(v)
}
}
}
func (n *GraphNodeConfigResource) Name() string {
result := n.Resource.Id()
switch n.DestroyMode {
case DestroyNone:
case DestroyPrimary:
result += " (destroy)"
case DestroyTainted:
result += " (destroy tainted)"
default:
result += " (unknown destroy type)"
}
return result
}
// GraphNodeDotter impl.
func (n *GraphNodeConfigResource) DotNode(name string, opts *GraphDotOpts) *dot.Node {
if n.DestroyMode != DestroyNone && !opts.Verbose {
return nil
}
return dot.NewNode(name, map[string]string{
"label": n.Name(),
"shape": "box",
})
}
// GraphNodeFlattenable impl.
func (n *GraphNodeConfigResource) Flatten(p []string) (dag.Vertex, error) {
return &GraphNodeConfigResourceFlat{
GraphNodeConfigResource: n,
PathValue: p,
}, nil
}
// GraphNodeDynamicExpandable impl.
func (n *GraphNodeConfigResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
state, lock := ctx.State()
lock.RLock()
defer lock.RUnlock()
// Start creating the steps
steps := make([]GraphTransformer, 0, 5)
// Primary and non-destroy modes are responsible for creating/destroying
// all the nodes, expanding counts.
switch n.DestroyMode {
case DestroyNone:
fallthrough
case DestroyPrimary:
steps = append(steps, &ResourceCountTransformer{
Resource: n.Resource,
Destroy: n.DestroyMode != DestroyNone,
Targets: n.Targets,
})
}
// Additional destroy modifications.
switch n.DestroyMode {
case DestroyPrimary:
// If we're destroying the primary instance, then we want to
// expand orphans, which have all the same semantics in a destroy
// as a primary.
steps = append(steps, &OrphanTransformer{
State: state,
View: n.Resource.Id(),
Targeting: (len(n.Targets) > 0),
})
steps = append(steps, &DeposedTransformer{
State: state,
View: n.Resource.Id(),
})
case DestroyTainted:
// If we're only destroying tainted resources, then we only
// want to find tainted resources and destroy them here.
steps = append(steps, &TaintedTransformer{
State: state,
View: n.Resource.Id(),
})
}
// Always end with the root being added
steps = append(steps, &RootTransformer{})
// Build the graph
b := &BasicGraphBuilder{Steps: steps}
return b.Build(ctx.Path())
}
// GraphNodeAddressable impl.
func (n *GraphNodeConfigResource) ResourceAddress() *ResourceAddress {
return &ResourceAddress{
// Indicates no specific index; will match on other three fields
Index: -1,
InstanceType: TypePrimary,
Name: n.Resource.Name,
Type: n.Resource.Type,
}
}
// GraphNodeTargetable impl.
func (n *GraphNodeConfigResource) SetTargets(targets []ResourceAddress) {
n.Targets = targets
}
// GraphNodeEvalable impl.
func (n *GraphNodeConfigResource) EvalTree() EvalNode {
return &EvalSequence{
Nodes: []EvalNode{
&EvalInterpolate{Config: n.Resource.RawCount},
&EvalOpFilter{
Ops: []walkOperation{walkValidate},
Node: &EvalValidateCount{Resource: n.Resource},
},
&EvalCountFixZeroOneBoundary{Resource: n.Resource},
},
}
}
// GraphNodeProviderConsumer
func (n *GraphNodeConfigResource) ProvidedBy() []string {
return []string{resourceProvider(n.Resource.Type, n.Resource.Provider)}
}
// GraphNodeProvisionerConsumer
func (n *GraphNodeConfigResource) ProvisionedBy() []string {
result := make([]string, len(n.Resource.Provisioners))
for i, p := range n.Resource.Provisioners {
result[i] = p.Type
}
return result
}
// GraphNodeDestroyable
func (n *GraphNodeConfigResource) DestroyNode(mode GraphNodeDestroyMode) GraphNodeDestroy {
// If we're already a destroy node, then don't do anything
if n.DestroyMode != DestroyNone {
return nil
}
result := &graphNodeResourceDestroy{
GraphNodeConfigResource: *n,
Original: n,
}
result.DestroyMode = mode
return result
}
// Same as GraphNodeConfigResource, but for flattening
type GraphNodeConfigResourceFlat struct {
*GraphNodeConfigResource
PathValue []string
}
func (n *GraphNodeConfigResourceFlat) Name() string {
return fmt.Sprintf(
"%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigResource.Name())
}
func (n *GraphNodeConfigResourceFlat) Path() []string {
return n.PathValue
}
func (n *GraphNodeConfigResourceFlat) DependableName() []string {
return modulePrefixList(
n.GraphNodeConfigResource.DependableName(),
modulePrefixStr(n.PathValue))
}
func (n *GraphNodeConfigResourceFlat) DependentOn() []string {
prefix := modulePrefixStr(n.PathValue)
return modulePrefixList(
n.GraphNodeConfigResource.DependentOn(),
prefix)
}
// graphNodeResourceDestroy represents the logical destruction of a
// resource. This node doesn't mean it will be destroyed for sure, but
// instead that if a destroy were to happen, it must happen at this point.
type graphNodeResourceDestroy struct {
GraphNodeConfigResource
Original *GraphNodeConfigResource
}
func (n *graphNodeResourceDestroy) CreateBeforeDestroy() bool {
// CBD is enabled if the resource enables it in addition to us
// being responsible for destroying the primary state. The primary
// state destroy node is the only destroy node that needs to be
// "shuffled" according to the CBD rules, since tainted resources
// don't have the same inverse dependencies.
return n.Original.Resource.Lifecycle.CreateBeforeDestroy &&
n.DestroyMode == DestroyPrimary
}
func (n *graphNodeResourceDestroy) CreateNode() dag.Vertex {
return n.Original
}
func (n *graphNodeResourceDestroy) DestroyInclude(d *ModuleDiff, s *ModuleState) bool {
switch n.DestroyMode {
case DestroyPrimary:
return n.destroyIncludePrimary(d, s)
case DestroyTainted:
return n.destroyIncludeTainted(d, s)
default:
return true
}
}
func (n *graphNodeResourceDestroy) destroyIncludeTainted(
d *ModuleDiff, s *ModuleState) bool {
// If there is no state, there can't by any tainted.
if s == nil {
return false
}
// Grab the ID which is the prefix (in the case count > 0 at some point)
prefix := n.Original.Resource.Id()
// Go through the resources and find any with our prefix. If there
// are any tainted, we need to keep it.
for k, v := range s.Resources {
if !strings.HasPrefix(k, prefix) {
continue
}
if len(v.Tainted) > 0 {
return true
}
}
// We didn't find any tainted nodes, return
return false
}
func (n *graphNodeResourceDestroy) destroyIncludePrimary(
d *ModuleDiff, s *ModuleState) bool {
// Get the count, and specifically the raw value of the count
// (with interpolations and all). If the count is NOT a static "1",
// then we keep the destroy node no matter what.
//
// The reasoning for this is complicated and not intuitively obvious,
// but I attempt to explain it below.
//
// The destroy transform works by generating the worst case graph,
// with worst case being the case that every resource already exists
// and needs to be destroy/created (force-new). There is a single important
// edge case where this actually results in a real-life cycle: if a
// create-before-destroy (CBD) resource depends on a non-CBD resource.
// Imagine a EC2 instance "foo" with CBD depending on a security
// group "bar" without CBD, and conceptualize the worst case destroy
// order:
//
// 1.) SG must be destroyed (non-CBD)
// 2.) SG must be created/updated
// 3.) EC2 instance must be created (CBD, requires the SG be made)
// 4.) EC2 instance must be destroyed (requires SG be destroyed)
//
// Except, #1 depends on #4, since the SG can't be destroyed while
// an EC2 instance is using it (AWS API requirements). As you can see,
// this is a real life cycle that can't be automatically reconciled
// except under two conditions:
//
// 1.) SG is also CBD. This doesn't work 100% of the time though
// since the non-CBD resource might not support CBD. To make matters
// worse, the entire transitive closure of dependencies must be
// CBD (if the SG depends on a VPC, you have the same problem).
// 2.) EC2 must not CBD. This can't happen automatically because CBD
// is used as a way to ensure zero (or minimal) downtime Terraform
// applies, and it isn't acceptable for TF to ignore this request,
// since it can result in unexpected downtime.
//
// Therefore, we compromise with this edge case here: if there is
// a static count of "1", we prune the diff to remove cycles during a
// graph optimization path if we don't see the resource in the diff.
// If the count is set to ANYTHING other than a static "1" (variable,
// computed attribute, static number greater than 1), then we keep the
// destroy, since it is required for dynamic graph expansion to find
// orphan/tainted count objects.
//
// This isn't ideal logic, but its strictly better without introducing
// new impossibilities. It breaks the cycle in practical cases, and the
// cycle comes back in no cases we've found to be practical, but just
// as the cycle would already exist without this anyways.
count := n.Original.Resource.RawCount
if raw := count.Raw[count.Key]; raw != "1" {
return true
}
// Okay, we're dealing with a static count. There are a few ways
// to include this resource.
prefix := n.Original.Resource.Id()
// If we're present in the diff proper, then keep it.
if d != nil {
for k, _ := range d.Resources {
if strings.HasPrefix(k, prefix) {
return true
}
}
}
// If we're in the state as a primary in any form, then keep it.
// This does a prefix check so it will also catch orphans on count
// decreases to "1".
if s != nil {
for k, v := range s.Resources {
// Ignore exact matches
if k == prefix {
continue
}
// Ignore anything that doesn't have a "." afterwards so that
// we only get our own resource and any counts on it.
if !strings.HasPrefix(k, prefix+".") {
continue
}
// Ignore exact matches and the 0'th index. We only care
// about if there is a decrease in count.
if k == prefix+".0" {
continue
}
if v.Primary != nil {
return true
}
}
// If we're in the state as _both_ "foo" and "foo.0", then
// keep it, since we treat the latter as an orphan.
_, okOne := s.Resources[prefix]
_, okTwo := s.Resources[prefix+".0"]
if okOne && okTwo {
return true
}
}
return false
}

View File

@ -108,11 +108,3 @@ func TestGraphNodeConfigResource_ProvisionedBy(t *testing.T) {
t.Fatalf("bad: %#v", actual)
}
}
func TestGraphNodeConfigVariable_impl(t *testing.T) {
var _ dag.Vertex = new(GraphNodeConfigVariable)
var _ dag.NamedVertex = new(GraphNodeConfigVariable)
var _ graphNodeConfig = new(GraphNodeConfigVariable)
var _ GraphNodeVariable = new(GraphNodeConfigVariable)
var _ GraphNodeProxy = new(GraphNodeConfigVariable)
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/dag"
)
// GraphNodeVariable is the interface that must be implemented by anything
@ -20,6 +21,8 @@ type GraphNodeConfigVariable struct {
// Value, if non-nil, will be used to set the value of the variable
// during evaluation. If this is nil, evaluation will do nothing.
Value *config.RawConfig
depPrefix string
}
func (n *GraphNodeConfigVariable) Name() string {
@ -96,3 +99,44 @@ func (n *GraphNodeConfigVariable) EvalTree() EvalNode {
},
}
}
// GraphNodeFlattenable impl.
func (n *GraphNodeConfigVariable) Flatten(p []string) (dag.Vertex, error) {
return &GraphNodeConfigVariableFlat{
GraphNodeConfigVariable: n,
PathValue: p,
}, nil
}
type GraphNodeConfigVariableFlat struct {
*GraphNodeConfigVariable
PathValue []string
}
func (n *GraphNodeConfigVariableFlat) Name() string {
return fmt.Sprintf(
"%s.%s", modulePrefixStr(n.PathValue), n.GraphNodeConfigVariable.Name())
}
func (n *GraphNodeConfigVariableFlat) DependableName() []string {
return []string{n.Name()}
}
func (n *GraphNodeConfigVariableFlat) DependentOn() []string {
// We only wrap the dependencies and such if we have a path that is
// longer than 2 elements (root, child, more). This is because when
// flattened, variables can point outside the graph.
prefix := ""
if len(n.PathValue) <= 2 {
prefix = modulePrefixStr(n.PathValue[:len(n.PathValue)-1])
}
return modulePrefixList(
n.GraphNodeConfigVariable.DependentOn(),
prefix)
}
func (n *GraphNodeConfigVariableFlat) Path() []string {
return n.PathValue
}

View File

@ -0,0 +1,23 @@
package terraform
import (
"testing"
"github.com/hashicorp/terraform/dag"
)
func TestGraphNodeConfigVariable_impl(t *testing.T) {
var _ dag.Vertex = new(GraphNodeConfigVariable)
var _ dag.NamedVertex = new(GraphNodeConfigVariable)
var _ graphNodeConfig = new(GraphNodeConfigVariable)
var _ GraphNodeVariable = new(GraphNodeConfigVariable)
var _ GraphNodeProxy = new(GraphNodeConfigVariable)
}
func TestGraphNodeConfigVariableFlat_impl(t *testing.T) {
var _ dag.Vertex = new(GraphNodeConfigVariableFlat)
var _ dag.NamedVertex = new(GraphNodeConfigVariableFlat)
var _ graphNodeConfig = new(GraphNodeConfigVariableFlat)
var _ GraphNodeVariable = new(GraphNodeConfigVariableFlat)
var _ GraphNodeProxy = new(GraphNodeConfigVariableFlat)
}

View File

@ -1,15 +1,27 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/dag"
)
// GraphNodeFlattenable must be implemented by nodes that can be flattened
// into the graph.
type GraphNodeFlattenable interface {
// GraphNodeFlatGraph must be implemented by nodes that have subgraphs
// that they want flattened into the graph.
type GraphNodeFlatGraph interface {
FlattenGraph() *Graph
}
// GraphNodeFlattenable must be implemented by all nodes that can be
// flattened. If a FlattenGraph returns any nodes that can't be flattened,
// it will be an error.
//
// If Flatten returns nil for the Vertex along with a nil error, it will
// removed from the graph.
type GraphNodeFlattenable interface {
Flatten(path []string) (dag.Vertex, error)
}
// FlattenTransformer is a transformer that goes through the graph, finds
// subgraphs that can be flattened, and flattens them into this graph,
// removing the prior subgraph node.
@ -17,7 +29,7 @@ type FlattenTransformer struct{}
func (t *FlattenTransformer) Transform(g *Graph) error {
for _, v := range g.Vertices() {
fn, ok := v.(GraphNodeFlattenable)
fn, ok := v.(GraphNodeFlatGraph)
if !ok {
continue
}
@ -39,8 +51,31 @@ func (t *FlattenTransformer) Transform(g *Graph) error {
// Remove the old node
g.Remove(v)
// Flatten the subgraph into this one. Keep any existing
// connections that existed.
// Go through the subgraph and flatten all the nodes
for _, sv := range subgraph.Vertices() {
fn, ok := sv.(GraphNodeFlattenable)
if !ok {
return fmt.Errorf(
"unflattenable node: %s %T",
dag.VertexName(sv), sv)
}
v, err := fn.Flatten(subgraph.Path)
if err != nil {
return fmt.Errorf(
"error flattening %s (%T): %s",
dag.VertexName(sv), sv, err)
}
if v == nil {
subgraph.Remove(v)
} else {
subgraph.Replace(sv, v)
}
}
// Now that we've handled any changes to the graph that are
// needed, we can add them all to our graph along with their edges.
for _, sv := range subgraph.Vertices() {
g.Add(sv)
}

View File

@ -32,7 +32,7 @@ func (t *DisableProviderTransformer) Transform(g *Graph) error {
for _, v := range g.Vertices() {
// We only care about providers
pn, ok := v.(GraphNodeProvider)
if !ok {
if !ok || pn.ProviderName() == "" {
continue
}
@ -130,7 +130,7 @@ type PruneProviderTransformer struct{}
func (t *PruneProviderTransformer) Transform(g *Graph) error {
for _, v := range g.Vertices() {
// We only care about the providers
if _, ok := v.(GraphNodeProvider); !ok {
if pn, ok := v.(GraphNodeProvider); !ok || pn.ProviderName() == "" {
continue
}