Skip to content

Commit

Permalink
core: refactoring.ImpliedMoveStatements replaces NodeCountBoundary
Browse files Browse the repository at this point in the history
Going back a long time we've had a special magic behavior which tries to
recognize a situation where a module author either added or removed the
"count" argument from a resource that already has instances, and to
silently rename the zeroth or no-key instance so that we don't plan to
destroy and recreate the associated object.

Now we have a more general idea of "move statements", and specifically
the idea of "implied" move statements which replicates the same heuristic
we used to use for this behavior, we can treat this magic renaming rule as
just another "move statement", special only in that Terraform generates it
automatically rather than it being written out explicitly in the
configuration.

In return for wiring that in, we can now remove altogether the
NodeCountBoundary graph node type and its associated graph transformer,
CountBoundaryTransformer. We handle moves as a preprocessing step before
building the plan graph, so we no longer need to include any special nodes
in the graph to deal with that situation.

The test updates here are mainly for the graph builders themselves, to
acknowledge that indeed we're no longer inserting the NodeCountBoundary
vertices. The vertices that NodeCountBoundary previously depended on now
become dependencies of the special "root" vertex, although in many cases
here we don't see that explicitly because of the transitive reduction
algorithm, which notices when there's already an equivalent indirect
dependency chain and removes the redundant edge.

We already have plenty of test coverage for these "count boundary" cases
in the context tests whose names start with TestContext2Plan_count and
TestContext2Apply_resourceCount, all of which continued to pass here
without any modification and so are not visible in the diff. The test
functions particularly relevant to this situation are:
 - TestContext2Plan_countIncreaseFromNotSet
 - TestContext2Plan_countDecreaseToOne
 - TestContext2Plan_countOneIndex
 - TestContext2Apply_countDecreaseToOneCorrupted

The last of those in particular deals with the situation where we have
both a no-key instance _and_ a zero-key instance in the prior state, which
is interesting here because to exercises an intentional interaction
between refactoring.ImpliedMoveStatements and refactoring.ApplyMoves,
where we intentionally generate an implied move statement that produces
a collision and then expect ApplyMoves to deal with it in the same way as
it would deal with all other collisions, and thus ensure we handle both
the explicit and implied collisions in the same way.

This does affect some UI-level tests, because a nice side-effect of this
new treatment of this old feature is that we can now report explicitly
in the UI that we're assigning new addresses to these objects, whereas
before we just said nothing and hoped the user would just guess what had
happened and why they therefore weren't seeing a diff.
  • Loading branch information
apparentlymart committed Sep 17, 2021
1 parent f95d0b7 commit e62e3fb
Show file tree
Hide file tree
Showing 12 changed files with 56 additions and 339 deletions.
54 changes: 0 additions & 54 deletions internal/states/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,60 +248,6 @@ func (s *SyncState) RemoveResourceIfEmpty(addr addrs.AbsResource) bool {
return true
}

// MaybeFixUpResourceInstanceAddressForCount deals with the situation where a
// resource has changed from having "count" set to not set, or vice-versa, and
// so we need to rename the zeroth instance key to no key at all, or vice-versa.
//
// Set countEnabled to true if the resource has count set in its new
// configuration, or false if it does not.
//
// The state is modified in-place if necessary, moving a resource instance
// between the two addresses. The return value is true if a change was made,
// and false otherwise.
func (s *SyncState) MaybeFixUpResourceInstanceAddressForCount(addr addrs.ConfigResource, countEnabled bool) bool {
s.lock.Lock()
defer s.lock.Unlock()

// get all modules instances that may match this state
modules := s.state.ModuleInstances(addr.Module)
if len(modules) == 0 {
return false
}

changed := false

for _, ms := range modules {
relAddr := addr.Resource
rs := ms.Resource(relAddr)
if rs == nil {
continue
}

huntKey := addrs.NoKey
replaceKey := addrs.InstanceKey(addrs.IntKey(0))
if !countEnabled {
huntKey, replaceKey = replaceKey, huntKey
}

is, exists := rs.Instances[huntKey]
if !exists {
continue
}

if _, exists := rs.Instances[replaceKey]; exists {
// If the replacement key also exists then we'll do nothing and keep both.
continue
}

// If we get here then we need to "rename" from hunt to replace
rs.Instances[replaceKey] = is
delete(rs.Instances, huntKey)
changed = true
}

return changed
}

// SetResourceInstanceCurrent saves the given instance object as the current
// generation of the resource instance with the given address, simultaneously
// updating the recorded provider configuration address, dependencies, and
Expand Down
19 changes: 16 additions & 3 deletions internal/terraform/context_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2073,9 +2073,22 @@ func TestContext2Apply_countDecreaseToOneX(t *testing.T) {

// https:/PeoplePerHour/terraform/pull/11
//
// This tests a case where both a "resource" and "resource.0" are in
// the state file, which apparently is a reasonable backwards compatibility
// concern found in the above 3rd party repo.
// This tests a rare but possible situation where we have both a no-key and
// a zero-key instance of the same resource in the configuration when we
// disable count.
//
// The main way to get here is for a provider to fail to destroy the zero-key
// instance but succeed in creating the no-key instance, since those two
// can typically happen concurrently. There are various other ways to get here
// that might be considered user error, such as using "terraform state mv"
// to create a strange combination of different key types on the same resource.
//
// This test indirectly exercises an intentional interaction between
// refactoring.ImpliedMoveStatements and refactoring.ApplyMoves: we'll first
// generate an implied move statement from aws_instance.foo[0] to
// aws_instance.foo, but then refactoring.ApplyMoves should notice that and
// ignore the statement, in the same way as it would if an explicit move
// statement specified the same situation.
func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) {
m := testModule(t, "apply-count-dec-one")
p := testProvider("aws")
Expand Down
9 changes: 8 additions & 1 deletion internal/terraform/context_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,14 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State
}

func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState *states.State, targets []addrs.Targetable) ([]refactoring.MoveStatement, map[addrs.UniqueKey]refactoring.MoveResult) {
moveStmts := refactoring.FindMoveStatements(config)
explicitMoveStmts := refactoring.FindMoveStatements(config)
implicitMoveStmts := refactoring.ImpliedMoveStatements(config, prevRunState, explicitMoveStmts)
var moveStmts []refactoring.MoveStatement
if stmtsLen := len(explicitMoveStmts) + len(implicitMoveStmts); stmtsLen > 0 {
moveStmts = make([]refactoring.MoveStatement, 0, stmtsLen)
moveStmts = append(moveStmts, explicitMoveStmts...)
moveStmts = append(moveStmts, implicitMoveStmts...)
}
moveResults := refactoring.ApplyMoves(moveStmts, prevRunState)
return moveStmts, moveResults
}
Expand Down
31 changes: 0 additions & 31 deletions internal/terraform/eval_count.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package terraform

import (
"fmt"
"log"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
Expand Down Expand Up @@ -101,32 +99,3 @@ func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Val

return countVal, diags
}

// fixResourceCountSetTransition is a helper function to fix up the state when a
// resource transitions its "count" from being set to unset or vice-versa,
// treating a 0-key and a no-key instance as aliases for one another across
// the transition.
//
// The correct time to call this function is in the DynamicExpand method for
// a node representing a resource, just after evaluating the count with
// evaluateCountExpression, and before any other analysis of the
// state such as orphan detection.
//
// This function calls methods on the given EvalContext to update the current
// state in-place, if necessary. It is a no-op if there is no count transition
// taking place.
//
// Since the state is modified in-place, this function must take a writer lock
// on the state. The caller must therefore not also be holding a state lock,
// or this function will block forever awaiting the lock.
func fixResourceCountSetTransition(ctx EvalContext, addr addrs.ConfigResource, countEnabled bool) {
state := ctx.State()
if state.MaybeFixUpResourceInstanceAddressForCount(addr, countEnabled) {
log.Printf("[TRACE] renamed first %s instance in transient state due to count argument change", addr)
}

refreshState := ctx.RefreshState()
if refreshState != nil && refreshState.MaybeFixUpResourceInstanceAddressForCount(addr, countEnabled) {
log.Printf("[TRACE] renamed first %s instance in transient state due to count argument change", addr)
}
}
5 changes: 0 additions & 5 deletions internal/terraform/graph_builder_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,6 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
// Target
&TargetsTransformer{Targets: b.Targets},

// Add the node to fix the state count boundaries
&CountBoundaryTransformer{
Config: b.Config,
},

// Close opened plugin connections
&CloseProviderTransformer{},

Expand Down
29 changes: 12 additions & 17 deletions internal/terraform/graph_builder_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import (
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/zclconf/go-cty/cty"
)

func TestApplyGraphBuilder_impl(t *testing.T) {
Expand Down Expand Up @@ -60,11 +62,10 @@ func TestApplyGraphBuilder(t *testing.T) {
t.Fatalf("wrong path %q", g.Path.String())
}

actual := strings.TrimSpace(g.String())

expected := strings.TrimSpace(testApplyGraphBuilderStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
got := strings.TrimSpace(g.String())
want := strings.TrimSpace(testApplyGraphBuilderStr)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
}

Expand Down Expand Up @@ -352,10 +353,10 @@ func TestApplyGraphBuilder_destroyCount(t *testing.T) {
t.Fatalf("wrong module path %q", g.Path)
}

actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testApplyGraphBuilderDestroyCountStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
got := strings.TrimSpace(g.String())
want := strings.TrimSpace(testApplyGraphBuilderDestroyCountStr)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
}

Expand Down Expand Up @@ -699,9 +700,6 @@ func TestApplyGraphBuilder_orphanedWithProvider(t *testing.T) {
}

const testApplyGraphBuilderStr = `
meta.count-boundary (EachMode fixup)
module.child (close)
test_object.other
module.child (close)
module.child.test_object.other
module.child (expand)
Expand All @@ -721,7 +719,7 @@ provider["registry.terraform.io/hashicorp/test"] (close)
module.child.test_object.other
test_object.other
root
meta.count-boundary (EachMode fixup)
module.child (close)
provider["registry.terraform.io/hashicorp/test"] (close)
test_object.create
test_object.create (expand)
Expand All @@ -735,13 +733,10 @@ test_object.other (expand)
`

const testApplyGraphBuilderDestroyCountStr = `
meta.count-boundary (EachMode fixup)
test_object.B
provider["registry.terraform.io/hashicorp/test"]
provider["registry.terraform.io/hashicorp/test"] (close)
test_object.B
root
meta.count-boundary (EachMode fixup)
provider["registry.terraform.io/hashicorp/test"] (close)
test_object.A (expand)
provider["registry.terraform.io/hashicorp/test"]
Expand Down
5 changes: 0 additions & 5 deletions internal/terraform/graph_builder_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,6 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
// node due to dependency edges, to avoid graph cycles during apply.
&ForcedCBDTransformer{},

// Add the node to fix the state count boundaries
&CountBoundaryTransformer{
Config: b.Config,
},

// Close opened plugin connections
&CloseProviderTransformer{},

Expand Down
54 changes: 20 additions & 34 deletions internal/terraform/graph_builder_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/providers"
"github.com/zclconf/go-cty/cty"
)

func TestPlanGraphBuilder_impl(t *testing.T) {
Expand Down Expand Up @@ -45,10 +47,10 @@ func TestPlanGraphBuilder(t *testing.T) {
t.Fatalf("wrong module path %q", g.Path)
}

actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testPlanGraphBuilderStr)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
got := strings.TrimSpace(g.String())
want := strings.TrimSpace(testPlanGraphBuilderStr)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
}

Expand Down Expand Up @@ -92,15 +94,12 @@ func TestPlanGraphBuilder_dynamicBlock(t *testing.T) {
// is that at the end test_thing.c depends on both test_thing.a and
// test_thing.b. Other details might shift over time as other logic in
// the graph builders changes.
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(`
meta.count-boundary (EachMode fixup)
test_thing.c (expand)
got := strings.TrimSpace(g.String())
want := strings.TrimSpace(`
provider["registry.terraform.io/hashicorp/test"]
provider["registry.terraform.io/hashicorp/test"] (close)
test_thing.c (expand)
root
meta.count-boundary (EachMode fixup)
provider["registry.terraform.io/hashicorp/test"] (close)
test_thing.a (expand)
provider["registry.terraform.io/hashicorp/test"]
Expand All @@ -110,8 +109,8 @@ test_thing.c (expand)
test_thing.a (expand)
test_thing.b (expand)
`)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
}

Expand Down Expand Up @@ -150,23 +149,20 @@ func TestPlanGraphBuilder_attrAsBlocks(t *testing.T) {
// list-of-objects attribute. This requires some special effort
// inside lang.ReferencesInBlock to make sure it searches blocks of
// type "nested" along with an attribute named "nested".
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(`
meta.count-boundary (EachMode fixup)
test_thing.b (expand)
got := strings.TrimSpace(g.String())
want := strings.TrimSpace(`
provider["registry.terraform.io/hashicorp/test"]
provider["registry.terraform.io/hashicorp/test"] (close)
test_thing.b (expand)
root
meta.count-boundary (EachMode fixup)
provider["registry.terraform.io/hashicorp/test"] (close)
test_thing.a (expand)
provider["registry.terraform.io/hashicorp/test"]
test_thing.b (expand)
test_thing.a (expand)
`)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
}

Expand Down Expand Up @@ -211,12 +207,12 @@ func TestPlanGraphBuilder_forEach(t *testing.T) {
t.Fatalf("wrong module path %q", g.Path)
}

actual := strings.TrimSpace(g.String())
got := strings.TrimSpace(g.String())
// We're especially looking for the edge here, where aws_instance.bat
// has a dependency on aws_instance.boo
expected := strings.TrimSpace(testPlanGraphBuilderForEachStr)
if actual != expected {
t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual)
want := strings.TrimSpace(testPlanGraphBuilderForEachStr)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
}

Expand All @@ -230,9 +226,6 @@ aws_security_group.firewall (expand)
provider["registry.terraform.io/hashicorp/aws"]
local.instance_id (expand)
aws_instance.web (expand)
meta.count-boundary (EachMode fixup)
aws_load_balancer.weblb (expand)
output.instance_id
openstack_floating_ip.random (expand)
provider["registry.terraform.io/hashicorp/openstack"]
output.instance_id
Expand All @@ -245,7 +238,7 @@ provider["registry.terraform.io/hashicorp/openstack"]
provider["registry.terraform.io/hashicorp/openstack"] (close)
openstack_floating_ip.random (expand)
root
meta.count-boundary (EachMode fixup)
output.instance_id
provider["registry.terraform.io/hashicorp/aws"] (close)
provider["registry.terraform.io/hashicorp/openstack"] (close)
var.foo
Expand All @@ -263,12 +256,6 @@ aws_instance.boo (expand)
provider["registry.terraform.io/hashicorp/aws"]
aws_instance.foo (expand)
provider["registry.terraform.io/hashicorp/aws"]
meta.count-boundary (EachMode fixup)
aws_instance.bar (expand)
aws_instance.bar2 (expand)
aws_instance.bat (expand)
aws_instance.baz (expand)
aws_instance.foo (expand)
provider["registry.terraform.io/hashicorp/aws"]
provider["registry.terraform.io/hashicorp/aws"] (close)
aws_instance.bar (expand)
Expand All @@ -277,6 +264,5 @@ provider["registry.terraform.io/hashicorp/aws"] (close)
aws_instance.baz (expand)
aws_instance.foo (expand)
root
meta.count-boundary (EachMode fixup)
provider["registry.terraform.io/hashicorp/aws"] (close)
`
Loading

0 comments on commit e62e3fb

Please sign in to comment.