Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

connect references from config nodes during apply #33403

Merged
merged 2 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/terraform/context_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6463,7 +6463,7 @@ func TestContext2Apply_errorCreateInvalidNew(t *testing.T) {
if got, want := diags.Err().Error(), "forced error"; !strings.Contains(got, want) {
t.Errorf("returned error does not contain %q, but it should\n%s", want, diags.Err())
}
if got, want := len(state.RootModule().Resources), 2; got != want {
if got, want := len(state.RootModule().Resources), 1; got != want {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is because there is no empty resource record in state now that the expand node is part of the instance ordering

t.Errorf("%d resources in state before prune; should have %d\n%s", got, want, spew.Sdump(state))
}
state.PruneResourceHusks() // aws_instance.bar with no instances gets left behind when we bail out, but that's okay
Expand Down
13 changes: 4 additions & 9 deletions internal/terraform/graph_builder_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -766,9 +766,8 @@ const testPlanWithCheckGraphBuilderStr = `
aws_instance.baz
aws_instance.baz
aws_instance.baz (expand)
aws_instance.foo
aws_instance.baz (expand)
provider["registry.terraform.io/hashicorp/aws"]
aws_instance.foo
aws_instance.foo
aws_instance.foo (expand)
aws_instance.foo (expand)
Expand Down Expand Up @@ -798,11 +797,9 @@ module.child.test_object.create (expand)
module.child (expand)
provider["registry.terraform.io/hashicorp/test"]
module.child.test_object.other
module.child.test_object.create
module.child.test_object.other (expand)
module.child.test_object.other (expand)
module.child (expand)
provider["registry.terraform.io/hashicorp/test"]
module.child.test_object.create
provider["registry.terraform.io/hashicorp/test"]
provider["registry.terraform.io/hashicorp/test"] (close)
module.child.test_object.other
Expand All @@ -815,10 +812,9 @@ test_object.create
test_object.create (expand)
provider["registry.terraform.io/hashicorp/test"]
test_object.other
test_object.create
test_object.other (expand)
test_object.other (expand)
provider["registry.terraform.io/hashicorp/test"]
test_object.create
`

const testApplyGraphBuilderDestroyCountStr = `
Expand All @@ -832,9 +828,8 @@ test_object.A (expand)
test_object.A[1] (destroy)
provider["registry.terraform.io/hashicorp/test"]
test_object.B
test_object.A (expand)
test_object.A[1] (destroy)
test_object.B (expand)
test_object.B (expand)
provider["registry.terraform.io/hashicorp/test"]
test_object.A (expand)
`
88 changes: 16 additions & 72 deletions internal/terraform/node_resource_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
package terraform

import (
"log"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/tfdiags"
)

Expand All @@ -21,7 +17,6 @@ type nodeExpandApplyableResource struct {
}

var (
_ GraphNodeDynamicExpandable = (*nodeExpandApplyableResource)(nil)
_ GraphNodeReferenceable = (*nodeExpandApplyableResource)(nil)
_ GraphNodeReferencer = (*nodeExpandApplyableResource)(nil)
_ GraphNodeConfigResource = (*nodeExpandApplyableResource)(nil)
Expand All @@ -34,82 +29,31 @@ func (n *nodeExpandApplyableResource) expandsInstances() {
}

func (n *nodeExpandApplyableResource) References() []*addrs.Reference {
return (&NodeApplyableResource{NodeAbstractResource: n.NodeAbstractResource}).References()
refs := n.NodeAbstractResource.References()

// The expand node needs to connect to the individual resource instances it
// references, but cannot refer to it's own instances without causing
// cycles. It would be preferable to entirely disallow self references
// without the `self` identifier, but those were allowed in provisioners
// for compatibility with legacy configuration. We also can't always just
// filter them out for all resource node types, because the only method we
// have for catching certain invalid configurations are the cycles that
// result from these inter-instance references.
return filterSelfRefs(n.Addr.Resource, refs)
}

func (n *nodeExpandApplyableResource) Name() string {
return n.NodeAbstractResource.Name() + " (expand)"
}

func (n *nodeExpandApplyableResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
var g Graph

func (n *nodeExpandApplyableResource) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
expander := ctx.InstanceExpander()
moduleInstances := expander.ExpandModule(n.Addr.Module)
for _, module := range moduleInstances {
g.Add(&NodeApplyableResource{
NodeAbstractResource: n.NodeAbstractResource,
Addr: n.Addr.Resource.Absolute(module),
})
}
addRootNodeToGraph(&g)

return &g, nil
}

// NodeApplyableResource represents a resource that is "applyable":
// it may need to have its record in the state adjusted to match configuration.
//
// Unlike in the plan walk, this resource node does not DynamicExpand. Instead,
// it should be inserted into the same graph as any instances of the nodes
// with dependency edges ensuring that the resource is evaluated before any
// of its instances, which will turn ensure that the whole-resource record
// in the state is suitably prepared to receive any updates to instances.
type NodeApplyableResource struct {
*NodeAbstractResource

Addr addrs.AbsResource
}

var (
_ GraphNodeModuleInstance = (*NodeApplyableResource)(nil)
_ GraphNodeConfigResource = (*NodeApplyableResource)(nil)
_ GraphNodeExecutable = (*NodeApplyableResource)(nil)
_ GraphNodeProviderConsumer = (*NodeApplyableResource)(nil)
_ GraphNodeAttachResourceConfig = (*NodeApplyableResource)(nil)
_ GraphNodeReferencer = (*NodeApplyableResource)(nil)
)

func (n *NodeApplyableResource) Path() addrs.ModuleInstance {
return n.Addr.Module
}

func (n *NodeApplyableResource) References() []*addrs.Reference {
if n.Config == nil {
log.Printf("[WARN] NodeApplyableResource %q: no configuration, so can't determine References", dag.VertexName(n))
return nil
}

var result []*addrs.Reference

// Since this node type only updates resource-level metadata, we only
// need to worry about the parts of the configuration that affect
// our "each mode": the count and for_each meta-arguments.
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.Config.Count)
result = append(result, refs...)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, n.Config.ForEach)
result = append(result, refs...)

return result
}

// GraphNodeExecutable
func (n *NodeApplyableResource) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics {
if n.Config == nil {
// Nothing to do, then.
log.Printf("[TRACE] NodeApplyableResource: no configuration present for %s", n.Name())
return nil
ctx = ctx.WithPath(module)
diags = diags.Append(n.writeResourceState(ctx, n.Addr.Resource.Absolute(module)))
}

return n.writeResourceState(ctx, n.Addr)
return diags
}
26 changes: 16 additions & 10 deletions internal/terraform/node_resource_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,40 @@ import (
"github.com/hashicorp/terraform/internal/states"
)

func TestNodeApplyableResourceExecute(t *testing.T) {
func TestNodeExpandApplyableResourceExecute(t *testing.T) {
state := states.NewState()
ctx := &MockEvalContext{
StateState: state.SyncWrapper(),
InstanceExpanderExpander: instances.NewExpander(),
}

t.Run("no config", func(t *testing.T) {
node := NodeApplyableResource{
ctx := &MockEvalContext{
StateState: state.SyncWrapper(),
InstanceExpanderExpander: instances.NewExpander(),
}

node := &nodeExpandApplyableResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: mustConfigResourceAddr("test_instance.foo"),
Config: nil,
},
Addr: mustAbsResourceAddr("test_instance.foo"),
}
diags := node.Execute(ctx, walkApply)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err())
}

state.PruneResourceHusks()
if !state.Empty() {
t.Fatalf("expected no state, got:\n %s", state.String())
}
})

t.Run("simple", func(t *testing.T) {
ctx := &MockEvalContext{
StateState: state.SyncWrapper(),
InstanceExpanderExpander: instances.NewExpander(),
}

node := NodeApplyableResource{
node := &nodeExpandApplyableResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: mustConfigResourceAddr("test_instance.foo"),
Config: &configs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Expand All @@ -49,7 +56,6 @@ func TestNodeApplyableResourceExecute(t *testing.T) {
Module: addrs.RootModule,
},
},
Addr: mustAbsResourceAddr("test_instance.foo"),
}
diags := node.Execute(ctx, walkApply)
if diags.HasErrors() {
Expand Down
8 changes: 8 additions & 0 deletions internal/terraform/transform_destroy_cbd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
)
Expand Down Expand Up @@ -61,6 +62,13 @@ func cbdTestSteps(steps []GraphTransformer) []GraphTransformer {
func filterInstances(g *Graph) *Graph {
for _, v := range g.Vertices() {
if _, ok := v.(GraphNodeResourceInstance); !ok {
// connect around the node to remove it without breaking deps
for _, down := range g.DownEdges(v) {
for _, up := range g.UpEdges(v) {
g.Connect(dag.BasicEdge(up, down))
}
}

g.Remove(v)
}

Expand Down
27 changes: 27 additions & 0 deletions internal/terraform/validate_selfref.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,30 @@ func validateSelfRef(addr addrs.Referenceable, config hcl.Body, providerSchema *

return diags
}

// Legacy provisioner configurations may refer to single instances using the
// resource address. We need to filter these out from the reported references
// to prevent cycles.
func filterSelfRefs(self addrs.Resource, refs []*addrs.Reference) []*addrs.Reference {
for i := 0; i < len(refs); i++ {
ref := refs[i]

var subject addrs.Resource
switch subj := ref.Subject.(type) {
case addrs.Resource:
subject = subj
case addrs.ResourceInstance:
subject = subj.ContainingResource()
default:
continue
}

if self.Equal(subject) {
tail := len(refs) - 1

refs[i], refs[tail] = refs[tail], refs[i]
refs = refs[:tail]
}
}
return refs
}