make use of the new state Dependencies

Make use of the new Dependencies field in the instance state.

The inter-instance dependencies will be determined from the complete
reference graph, so that absolute addresses can be stored, rather than
just references within a module. The Dependencies are added to the node
in the same manner as state, i.e. via an "attacher" interface and
transformer.  This is because dependencies are calculated from the graph
itself, and not from the config.
This commit is contained in:
James Bardin 2019-10-17 16:05:27 -04:00
parent 5a0a0020a0
commit 42bb4a644c
15 changed files with 406 additions and 166 deletions

View File

@ -1328,19 +1328,7 @@ func testContext2Apply_destroyDependsOn(t *testing.T) {
// Test that destroy ordering is correct with dependencies only
// in the state.
func TestContext2Apply_destroyDependsOnStateOnly(t *testing.T) {
// It is possible for this to be racy, so we loop a number of times
// just to check.
for i := 0; i < 10; i++ {
testContext2Apply_destroyDependsOnStateOnly(t)
}
}
func testContext2Apply_destroyDependsOnStateOnly(t *testing.T) {
m := testModule(t, "empty")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
state := MustShimLegacyState(&State{
legacyState := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
@ -1368,6 +1356,65 @@ func testContext2Apply_destroyDependsOnStateOnly(t *testing.T) {
},
})
newState := states.NewState()
root := newState.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
Dependencies: []addrs.AbsResource{},
},
addrs.ProviderConfig{
Type: "aws",
}.Absolute(addrs.RootModuleInstance),
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
Dependencies: []addrs.AbsResource{
addrs.AbsResource{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
},
Module: root.Addr,
},
},
},
addrs.ProviderConfig{
Type: "aws",
}.Absolute(addrs.RootModuleInstance),
)
// It is possible for this to be racy, so we loop a number of times
// just to check.
for i := 0; i < 10; i++ {
t.Run("legacy", func(t *testing.T) {
testContext2Apply_destroyDependsOnStateOnly(t, legacyState)
})
t.Run("new", func(t *testing.T) {
testContext2Apply_destroyDependsOnStateOnly(t, newState)
})
}
}
func testContext2Apply_destroyDependsOnStateOnly(t *testing.T, state *states.State) {
m := testModule(t, "empty")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
// Record the order we see Apply
var actual []string
var actualLock sync.Mutex
@ -1408,19 +1455,7 @@ func testContext2Apply_destroyDependsOnStateOnly(t *testing.T) {
// Test that destroy ordering is correct with dependencies only
// in the state within a module (GH-11749)
func TestContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T) {
// It is possible for this to be racy, so we loop a number of times
// just to check.
for i := 0; i < 10; i++ {
testContext2Apply_destroyDependsOnStateOnlyModule(t)
}
}
func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T) {
m := testModule(t, "empty")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
state := MustShimLegacyState(&State{
legacyState := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: []string{"root", "child"},
@ -1448,6 +1483,66 @@ func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T) {
},
})
newState := states.NewState()
child := newState.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
child.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
Dependencies: []addrs.AbsResource{},
},
addrs.ProviderConfig{
Type: "aws",
}.Absolute(addrs.RootModuleInstance),
)
child.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar"}`),
Dependencies: []addrs.AbsResource{
addrs.AbsResource{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
},
Module: child.Addr,
},
},
},
addrs.ProviderConfig{
Type: "aws",
}.Absolute(addrs.RootModuleInstance),
)
// It is possible for this to be racy, so we loop a number of times
// just to check.
for i := 0; i < 10; i++ {
t.Run("legacy", func(t *testing.T) {
testContext2Apply_destroyDependsOnStateOnlyModule(t, legacyState)
})
t.Run("new", func(t *testing.T) {
testContext2Apply_destroyDependsOnStateOnlyModule(t, newState)
})
}
}
func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T, state *states.State) {
m := testModule(t, "empty")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
// Record the order we see Apply
var actual []string
var actualLock sync.Mutex
@ -3466,6 +3561,9 @@ module.B:
provider = provider.aws
foo = foo
type = aws_instance
Dependencies:
module.A.aws_instance.foo
`)
}
@ -8706,7 +8804,7 @@ aws_instance.foo:
type = aws_instance
Dependencies:
module.child
module.child.aws_instance.mod
module.child:
aws_instance.mod:

View File

@ -1964,3 +1964,81 @@ func TestContext2Refresh_dataResourceDependsOn(t *testing.T) {
t.Fatalf("unexpected errors: %s", diags.Err())
}
}
// verify that dependencies are updated in the state during refresh
func TestRefresh_updateDependencies(t *testing.T) {
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"foo"}`),
},
addrs.ProviderConfig{
Type: "aws",
}.Absolute(addrs.RootModuleInstance),
)
root.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"bar","foo":"foo"}`),
},
addrs.ProviderConfig{
Type: "aws",
}.Absolute(addrs.RootModuleInstance),
)
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "aws_instance" "bar" {
foo = aws_instance.foo.id
}
resource "aws_instance" "foo" {
}`,
})
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
ctx := testContext2(t, &ContextOpts{
Config: m,
ProviderResolver: providers.ResolverFixed(
map[string]providers.Factory{
"aws": testProviderFuncFixed(p),
},
),
State: state,
})
result, diags := ctx.Refresh()
if diags.HasErrors() {
t.Fatalf("plan errors: %s", diags.Err())
}
expect := strings.TrimSpace(`
aws_instance.bar:
ID = bar
provider = provider.aws
foo = foo
Dependencies:
aws_instance.foo
aws_instance.foo:
ID = foo
provider = provider.aws
`)
checkStateString(t, result, expect)
}

View File

@ -24,7 +24,6 @@ import (
type EvalApply struct {
Addr addrs.ResourceInstance
Config *configs.Resource
Dependencies []addrs.Referenceable
State **states.ResourceInstanceObject
Change **plans.ResourceInstanceChange
ProviderAddr addrs.AbsProviderConfig
@ -274,7 +273,6 @@ func (n *EvalApply) Eval(ctx EvalContext) (interface{}, error) {
Status: states.ObjectReady,
Value: newVal,
Private: resp.Private,
Dependencies: n.Dependencies, // Should be populated by the caller from the StateDependencies method on the resource instance node
}
}

View File

@ -21,7 +21,6 @@ import (
type EvalReadData struct {
Addr addrs.ResourceInstance
Config *configs.Resource
Dependencies []addrs.Referenceable
Provider *providers.Interface
ProviderAddr addrs.AbsProviderConfig
ProviderSchema **ProviderSchema
@ -163,7 +162,6 @@ func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) {
state := &states.ResourceInstanceObject{
Value: change.After,
Status: states.ObjectPlanned, // because the partial value in the plan must be used for now
Dependencies: n.Dependencies,
}
*n.OutputState = state
}
@ -277,7 +275,6 @@ func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) {
state := &states.ResourceInstanceObject{
Value: change.After,
Status: states.ObjectReady, // because we completed the read from the provider
Dependencies: n.Dependencies,
}
err = ctx.Hook(func(h Hook) (HookAction, error) {
@ -313,7 +310,6 @@ type EvalReadDataApply struct {
Output **states.ResourceInstanceObject
Config *configs.Resource
Change **plans.ResourceInstanceChange
StateReferences []addrs.Referenceable
}
func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) {
@ -387,7 +383,6 @@ func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) {
*n.Output = &states.ResourceInstanceObject{
Value: newVal,
Status: states.ObjectReady,
Dependencies: n.StateReferences,
}
}

View File

@ -200,6 +200,10 @@ type EvalWriteState struct {
// ProviderAddr is the address of the provider configuration that
// produced the given object.
ProviderAddr addrs.AbsProviderConfig
// Dependencies are the inter-resource dependencies to be stored in the
// state.
Dependencies []addrs.AbsResource
}
func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
@ -215,7 +219,6 @@ func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
if n.ProviderAddr.ProviderConfig.Type == "" {
return nil, fmt.Errorf("failed to write state for %s, missing provider type", absAddr)
}
obj := *n.State
if obj == nil || obj.Value.IsNull() {
// No need to encode anything: we'll just write it directly.
@ -223,6 +226,10 @@ func (n *EvalWriteState) Eval(ctx EvalContext) (interface{}, error) {
log.Printf("[TRACE] EvalWriteState: removing state object for %s", absAddr)
return nil, nil
}
// store the new deps in the state
obj.Dependencies = n.Dependencies
if n.ProviderSchema == nil || *n.ProviderSchema == nil {
// Should never happen, unless our state object is nil
panic("EvalWriteState used with pointer to nil ProviderSchema object")

View File

@ -155,6 +155,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
// Connect references so ordering is correct
&ReferenceTransformer{},
&AttachDependenciesTransformer{},
// Destruction ordering
&DestroyEdgeTransformer{

View File

@ -165,6 +165,7 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
// Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on.
&ReferenceTransformer{},
&AttachDependenciesTransformer{},
// Target
&TargetsTransformer{

View File

@ -169,7 +169,6 @@ func (n *NodeRefreshableDataResourceInstance) EvalTree() EvalNode {
&EvalReadData{
Addr: addr.Resource,
Config: n.Config,
Dependencies: n.StateReferences(),
Provider: &provider,
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,

View File

@ -3,7 +3,6 @@ package terraform
import (
"fmt"
"log"
"sort"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
@ -93,8 +92,8 @@ type NodeAbstractResourceInstance struct {
// The fields below will be automatically set using the Attach
// interfaces if you're running those transforms, but also be explicitly
// set if you already have that information.
ResourceState *states.Resource
Dependencies []addrs.AbsResource
}
var (
@ -220,7 +219,8 @@ func (n *NodeAbstractResource) References() []*addrs.Reference {
func (n *NodeAbstractResourceInstance) References() []*addrs.Reference {
// If we have a configuration attached then we'll delegate to our
// embedded abstract resource, which knows how to extract dependencies
// from configuration.
// from configuration. If there is no config, then the dependencies will
// be connected during destroy from those stored in the state.
if n.Config != nil {
if n.Schema == nil {
// We'll produce a log message about this out here so that
@ -232,8 +232,10 @@ func (n *NodeAbstractResourceInstance) References() []*addrs.Reference {
return n.NodeAbstractResource.References()
}
// Otherwise, if we have state then we'll use the values stored in state
// as a fallback.
// FIXME: remove once the deprecated DependsOn values have been removed from state
// The state dependencies are now connected in a separate transformation as
// absolute addresses, but we need to keep this here until we can be sure
// that no state will need to use the old depends_on references.
if rs := n.ResourceState; rs != nil {
if s := rs.Instance(n.InstanceKey); s != nil {
// State is still storing dependencies as old-style strings, so we'll
@ -248,8 +250,9 @@ func (n *NodeAbstractResourceInstance) References() []*addrs.Reference {
// https://github.com/hashicorp/terraform/issues/21407
if s.Current == nil {
log.Printf("[WARN] no current state found for %s", n.Name())
} else {
for _, addr := range s.Current.Dependencies {
return nil
}
for _, addr := range s.Current.DependsOn {
if addr == nil {
// Should never happen; indicates a bug in the state loader
panic(fmt.Sprintf("dependencies for current object on %s contains nil address", n.ResourceInstanceAddr()))
@ -265,7 +268,6 @@ func (n *NodeAbstractResourceInstance) References() []*addrs.Reference {
},
})
}
}
return result
}
}
@ -288,67 +290,17 @@ func dottedInstanceAddr(tr addrs.ResourceInstance) string {
return tr.Resource.String() + suffix
}
// StateReferences returns the dependencies to put into the state for
// this resource.
func (n *NodeAbstractResourceInstance) StateReferences() []addrs.Referenceable {
selfAddrs := n.ReferenceableAddrs()
// Since we don't include the source location references in our
// results from this method, we'll also filter out duplicates:
// there's no point in listing the same object twice without
// that additional context.
seen := map[string]struct{}{}
// Pretend that we've already "seen" all of our own addresses so that we
// won't record self-references in the state. This can arise if, for
// example, a provisioner for a resource refers to the resource itself,
// which is valid (since provisioners always run after apply) but should
// not create an explicit dependency edge.
for _, selfAddr := range selfAddrs {
seen[selfAddr.String()] = struct{}{}
if riAddr, ok := selfAddr.(addrs.ResourceInstance); ok {
seen[riAddr.ContainingResource().String()] = struct{}{}
// StateDependencies returns the dependencies saved in the state.
func (n *NodeAbstractResourceInstance) StateDependencies() []addrs.AbsResource {
if rs := n.ResourceState; rs != nil {
if s := rs.Instance(n.InstanceKey); s != nil {
if s.Current != nil {
return s.Current.Dependencies
}
}
}
depsRaw := n.References()
deps := make([]addrs.Referenceable, 0, len(depsRaw))
for _, d := range depsRaw {
subj := d.Subject
if mco, isOutput := subj.(addrs.ModuleCallOutput); isOutput {
// For state dependencies, we simplify outputs to just refer
// to the module as a whole. It's not really clear why we do this,
// but this logic is preserved from before the 0.12 rewrite of
// this function.
subj = mco.Call
}
k := subj.String()
if _, exists := seen[k]; exists {
continue
}
seen[k] = struct{}{}
switch tr := subj.(type) {
case addrs.ResourceInstance:
deps = append(deps, tr)
case addrs.Resource:
deps = append(deps, tr)
case addrs.ModuleCallInstance:
deps = append(deps, tr)
default:
// No other reference types are recorded in the state.
}
}
// We'll also sort them, since that'll avoid creating changes in the
// serialized state that make no semantic difference.
sort.Slice(deps, func(i, j int) bool {
// Simple string-based sort because we just care about consistency,
// not user-friendliness.
return deps[i].String() < deps[j].String()
})
return deps
return nil
}
func (n *NodeAbstractResource) SetProvider(p addrs.AbsProviderConfig) {

View File

@ -34,6 +34,7 @@ var (
_ GraphNodeReferencer = (*NodeApplyableResourceInstance)(nil)
_ GraphNodeDeposer = (*NodeApplyableResourceInstance)(nil)
_ GraphNodeEvalable = (*NodeApplyableResourceInstance)(nil)
_ GraphNodeAttachDependencies = (*NodeApplyableResourceInstance)(nil)
)
// GraphNodeAttachDestroyer
@ -97,6 +98,11 @@ func (n *NodeApplyableResourceInstance) References() []*addrs.Reference {
return ret
}
// GraphNodeAttachDependencies
func (n *NodeApplyableResourceInstance) AttachDependencies(deps []addrs.AbsResource) {
n.Dependencies = deps
}
// GraphNodeEvalable
func (n *NodeApplyableResourceInstance) EvalTree() EvalNode {
addr := n.ResourceInstanceAddr()
@ -171,7 +177,6 @@ func (n *NodeApplyableResourceInstance) evalTreeDataResource(addr addrs.AbsResou
&EvalReadData{
Addr: addr.Resource,
Config: n.Config,
Dependencies: n.StateReferences(),
Planned: &change, // setting this indicates that the result must be complete
Provider: &provider,
ProviderAddr: n.ResolvedProvider,
@ -341,7 +346,6 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe
&EvalApply{
Addr: addr.Resource,
Config: n.Config,
Dependencies: n.StateReferences(),
State: &state,
Change: &diffApply,
Provider: &provider,
@ -363,6 +367,7 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,
State: &state,
Dependencies: n.Dependencies,
},
&EvalApplyProvisioners{
Addr: addr.Resource,
@ -384,6 +389,7 @@ func (n *NodeApplyableResourceInstance) evalTreeManagedResource(addr addrs.AbsRe
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,
State: &state,
Dependencies: n.Dependencies,
},
&EvalIf{
If: func(ctx EvalContext) (bool, error) {

View File

@ -78,8 +78,8 @@ func (n *NodePlannableResourceInstance) evalTreeDataResource(addr addrs.AbsResou
// Check and see if any of our dependencies have changes.
changes := ctx.Changes()
for _, d := range n.StateReferences() {
ri, ok := d.(addrs.ResourceInstance)
for _, d := range n.References() {
ri, ok := d.Subject.(addrs.ResourceInstance)
if !ok {
continue
}
@ -114,7 +114,6 @@ func (n *NodePlannableResourceInstance) evalTreeDataResource(addr addrs.AbsResou
&EvalReadData{
Addr: addr.Resource,
Config: n.Config,
Dependencies: n.StateReferences(),
Provider: &provider,
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,

View File

@ -18,6 +18,10 @@ import (
// NodeRefreshableManagedResourceInstance. Resource count orphans are also added.
type NodeRefreshableManagedResource struct {
*NodeAbstractResource
// We attach dependencies to the Resource during refresh, since the
// instances are instantiated during DynamicExpand.
Dependencies []addrs.AbsResource
}
var (
@ -27,8 +31,14 @@ var (
_ GraphNodeReferencer = (*NodeRefreshableManagedResource)(nil)
_ GraphNodeResource = (*NodeRefreshableManagedResource)(nil)
_ GraphNodeAttachResourceConfig = (*NodeRefreshableManagedResource)(nil)
_ GraphNodeAttachDependencies = (*NodeRefreshableManagedResource)(nil)
)
// GraphNodeAttachDependencies
func (n *NodeRefreshableManagedResource) AttachDependencies(deps []addrs.AbsResource) {
n.Dependencies = deps
}
// GraphNodeDynamicExpandable
func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
var diags tfdiags.Diagnostics
@ -58,6 +68,7 @@ func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph,
// Add the config and state since we don't do that via transforms
a.Config = n.Config
a.ResolvedProvider = n.ResolvedProvider
a.Dependencies = n.Dependencies
return &NodeRefreshableManagedResourceInstance{
NodeAbstractResourceInstance: a,

View File

@ -608,9 +608,6 @@ aws_instance.bar:
foo = true
type = aws_instance
Dependencies:
module.child
module.child:
<no state>
Outputs:
@ -666,6 +663,9 @@ module.child:
provider = provider.aws
type = aws_instance
value = bar
Dependencies:
aws_instance.foo
`
const testTerraformApplyOutputOrphanStr = `
@ -784,17 +784,11 @@ aws_instance.foo.1:
provider = provider.aws
foo = number 1
type = aws_instance
Dependencies:
aws_instance.foo[0]
aws_instance.foo.2:
ID = foo
provider = provider.aws
foo = number 2
type = aws_instance
Dependencies:
aws_instance.foo[0]
`
const testTerraformApplyProvisionerDiffStr = `
@ -859,7 +853,7 @@ aws_instance.a:
type = aws_instance
Dependencies:
module.child
module.child.aws_instance.child
module.child:
aws_instance.child:
@ -877,7 +871,7 @@ aws_instance.a:
type = aws_instance
Dependencies:
module.child
module.child.module.grandchild.aws_instance.c
module.child.grandchild:
aws_instance.c:
@ -897,7 +891,7 @@ module.child:
type = aws_instance
Dependencies:
module.grandchild
module.child.module.grandchild.aws_instance.c
module.child.grandchild:
aws_instance.c:
ID = foo
@ -1305,9 +1299,6 @@ data.null_data_source.bar:
ID = foo
provider = provider.null
bar = yes
Dependencies:
data.null_data_source.foo
data.null_data_source.foo:
ID = foo
provider = provider.null

View File

@ -54,10 +54,14 @@ type DestroyEdgeTransformer struct {
func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
// Build a map of what is being destroyed (by address string) to
// the list of destroyers. Usually there will be at most one destroyer
// per node, but we allow multiple if present for completeness.
// the list of destroyers.
destroyers := make(map[string][]GraphNodeDestroyer)
destroyerAddrs := make(map[string]addrs.AbsResourceInstance)
// destroyersByResource records each destroyer by the AbsResourceAddress.
// We use this because dependencies are only referenced as resources, but we
// will want to connect all the individual instances for correct ordering.
destroyersByResource := make(map[string][]GraphNodeDestroyer)
for _, v := range g.Vertices() {
dn, ok := v.(GraphNodeDestroyer)
if !ok {
@ -66,6 +70,7 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
addrP := dn.DestroyAddr()
if addrP == nil {
log.Printf("[WARN] DestroyEdgeTransformer: %q (%T) has no destroy address", dag.VertexName(dn), v)
continue
}
addr := *addrP
@ -74,6 +79,9 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
log.Printf("[TRACE] DestroyEdgeTransformer: %q (%T) destroys %s", dag.VertexName(dn), v, key)
destroyers[key] = append(destroyers[key], dn)
destroyerAddrs[key] = addr
resAddr := addr.Resource.Absolute(addr.Module).String()
destroyersByResource[resAddr] = append(destroyersByResource[resAddr], dn)
}
// If we aren't destroying anything, there will be no edges to make
@ -82,6 +90,29 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
return nil
}
type withDeps interface {
StateDependencies() []addrs.AbsResource
}
// Connect destroy dependencies directly as stored in the state in case
// there's no configuration to create the edges otherwise.
for _, ds := range destroyers {
for _, des := range ds {
ri, ok := des.(withDeps)
if !ok {
continue
}
for _, resAddr := range ri.StateDependencies() {
for _, desDep := range destroyersByResource[resAddr.String()] {
log.Printf("[TRACE] DestroyEdgeTransformer: %s depends on %s\n", dag.VertexName(des), dag.VertexName(desDep))
g.Connect(dag.BasicEdge(desDep, des))
}
}
}
}
// Go through and connect creators to destroyers. Going along with
// our example, this makes: A_d => A
for _, v := range g.Vertices() {
@ -95,13 +126,7 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
continue
}
key := addr.String()
ds := destroyers[key]
if len(ds) == 0 {
continue
}
for _, d := range ds {
for _, d := range destroyers[addr.String()] {
// For illustrating our example
a_d := d.(dag.Vertex)
a := v
@ -124,6 +149,8 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error {
}
}
// FIXME: connect StateDependencies in here somewhere
// This is strange but is the easiest way to get the dependencies
// of a node that is being destroyed. We use another graph to make sure
// the resource is in the graph and ask for references. We have to do this

View File

@ -6,9 +6,11 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/lang"
"github.com/hashicorp/terraform/states"
)
// GraphNodeReferenceable must be implemented by any node that represents
@ -37,6 +39,11 @@ type GraphNodeReferencer interface {
References() []*addrs.Reference
}
type GraphNodeAttachDependencies interface {
GraphNodeResource
AttachDependencies([]addrs.AbsResource)
}
// GraphNodeReferenceOutside is an interface that can optionally be implemented.
// A node that implements it can specify that its own referenceable addresses
// and/or the addresses it references are in a different module than the
@ -84,11 +91,81 @@ func (t *ReferenceTransformer) Transform(g *Graph) error {
for _, parent := range parents {
g.Connect(dag.BasicEdge(v, parent))
}
if len(parents) > 0 {
continue
}
}
return nil
}
// AttachDependenciesTransformer records all resource dependencies for each
// instance, and attaches the addresses to the node itself. Managed resource
// will record these in the state for proper ordering of destroy operations.
type AttachDependenciesTransformer struct {
Config *configs.Config
State *states.State
Schemas *Schemas
}
func (t AttachDependenciesTransformer) Transform(g *Graph) error {
for _, v := range g.Vertices() {
attacher, ok := v.(GraphNodeAttachDependencies)
if !ok {
continue
}
selfAddr := attacher.ResourceAddr()
// Data sources don't need to track destroy dependencies
if selfAddr.Resource.Mode == addrs.DataResourceMode {
continue
}
ans, err := g.Ancestors(v)
if err != nil {
return err
}
// dedupe addrs when there's multiple instances involved, or
// multiple paths in the un-reduced graph
depMap := map[string]addrs.AbsResource{}
for _, d := range ans.List() {
var addr addrs.AbsResource
switch d := d.(type) {
case GraphNodeResourceInstance:
instAddr := d.ResourceInstanceAddr()
addr = instAddr.Resource.Resource.Absolute(instAddr.Module)
case GraphNodeResource:
addr = d.ResourceAddr()
default:
continue
}
// Data sources don't need to track destroy dependencies
if addr.Resource.Mode == addrs.DataResourceMode {
continue
}
if addr.Equal(selfAddr) {
continue
}
depMap[addr.String()] = addr
}
deps := make([]addrs.AbsResource, 0, len(depMap))
for _, d := range depMap {
deps = append(deps, d)
}
log.Printf("[TRACE] AttachDependenciesTransformer: %s depends on %s", attacher.ResourceAddr(), deps)
attacher.AttachDependencies(deps)
}
return nil
}
// DestroyReferenceTransformer is a GraphTransformer that reverses the edges
// for locals and outputs that depend on other nodes which will be
// removed during destroy. If a destroy node is evaluated before the local or