Skip to content

Commit 56e58cb

Browse files
authored
Merge pull request #37847 from hashicorp/backport/jbardin/force-replace-instance/repeatedly-next-asp
Backport of Don't carry all `-replace` addresses through to every instance into v1.14
2 parents 4cee658 + c28163b commit 56e58cb

File tree

6 files changed

+22
-37
lines changed

6 files changed

+22
-37
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: Combinations of replace_triggered_by and -replace could result in some instances not being replaced
3+
time: 2025-10-29T17:59:58.326396-04:00
4+
custom:
5+
Issue: "37833"

internal/terraform/graph_builder_apply.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package terraform
55

66
import (
7+
"slices"
8+
79
"github.com/hashicorp/terraform/internal/addrs"
810
"github.com/hashicorp/terraform/internal/configs"
911
"github.com/hashicorp/terraform/internal/dag"
@@ -110,7 +112,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
110112
concreteResourceInstance := func(a *NodeAbstractResourceInstance) dag.Vertex {
111113
return &NodeApplyableResourceInstance{
112114
NodeAbstractResourceInstance: a,
113-
forceReplace: b.ForceReplace,
115+
forceReplace: slices.ContainsFunc(b.ForceReplace, a.Addr.Equal),
114116
}
115117
}
116118

internal/terraform/node_resource_abstract_instance.go

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -791,7 +791,7 @@ func (n *NodeAbstractResourceInstance) plan(
791791
plannedChange *plans.ResourceInstanceChange,
792792
currentState *states.ResourceInstanceObject,
793793
createBeforeDestroy bool,
794-
forceReplace []addrs.AbsResourceInstance,
794+
forceReplace bool,
795795
) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, *providers.Deferred, instances.RepetitionData, tfdiags.Diagnostics) {
796796
var diags tfdiags.Diagnostics
797797
var keyData instances.RepetitionData
@@ -2971,34 +2971,15 @@ func resourceInstancePrevRunAddr(ctx EvalContext, currentAddr addrs.AbsResourceI
29712971
return table.OldAddr(currentAddr)
29722972
}
29732973

2974-
func getAction(addr addrs.AbsResourceInstance, priorVal, plannedNewVal cty.Value, createBeforeDestroy bool, writeOnly cty.PathSet, forceReplace []addrs.AbsResourceInstance, reqRep cty.PathSet) (action plans.Action, actionReason plans.ResourceInstanceChangeActionReason) {
2975-
// The user might also ask us to force replacing a particular resource
2976-
// instance, regardless of whether the provider thinks it needs replacing.
2977-
// For example, users typically do this if they learn a particular object
2978-
// has become degraded in an immutable infrastructure scenario and so
2979-
// replacing it with a new object is a viable repair path.
2980-
matchedForceReplace := false
2981-
for _, candidateAddr := range forceReplace {
2982-
if candidateAddr.Equal(addr) {
2983-
matchedForceReplace = true
2984-
break
2985-
}
2986-
2987-
// For "force replace" purposes we require an exact resource instance
2988-
// address to match. If a user forgets to include the instance key
2989-
// for a multi-instance resource then it won't match here, but we
2990-
// have an earlier check in NodePlannableResource.Execute that should
2991-
// prevent us from getting here in that case.
2992-
}
2993-
2974+
func getAction(addr addrs.AbsResourceInstance, priorVal, plannedNewVal cty.Value, createBeforeDestroy bool, writeOnly cty.PathSet, forceReplace bool, reqRep cty.PathSet) (action plans.Action, actionReason plans.ResourceInstanceChangeActionReason) {
29942975
// Unmark for this test for value equality.
29952976
eqV := plannedNewVal.Equals(priorVal)
29962977
eq := eqV.IsKnown() && eqV.True()
29972978

29982979
switch {
29992980
case priorVal.IsNull():
30002981
action = plans.Create
3001-
case matchedForceReplace || !reqRep.Empty() || !writeOnly.Intersection(reqRep).Empty():
2982+
case forceReplace || !reqRep.Empty() || !writeOnly.Intersection(reqRep).Empty():
30022983
// If the user "forced replace" of this instance of if there are any
30032984
// "requires replace" paths left _after our filtering above_ then this
30042985
// is a replace action.
@@ -3008,12 +2989,12 @@ func getAction(addr addrs.AbsResourceInstance, priorVal, plannedNewVal cty.Value
30082989
action = plans.DeleteThenCreate
30092990
}
30102991
switch {
3011-
case matchedForceReplace:
2992+
case forceReplace:
30122993
actionReason = plans.ResourceInstanceReplaceByRequest
30132994
case !reqRep.Empty():
30142995
actionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate
30152996
}
3016-
case eq && !matchedForceReplace:
2997+
case eq && !forceReplace:
30172998
action = plans.NoOp
30182999
default:
30193000
action = plans.Update

internal/terraform/node_resource_apply_instance.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,9 @@ type NodeApplyableResourceInstance struct {
3131

3232
graphNodeDeposer // implementation of GraphNodeDeposerConfig
3333

34-
// forceReplace are resource instance addresses where the user wants to
35-
// force generating a replace action. This set isn't pre-filtered, so
36-
// it might contain addresses that have nothing to do with the resource
37-
// that this node represents, which the node itself must therefore ignore.
38-
forceReplace []addrs.AbsResourceInstance
34+
// forceReplace indicates that this resource is being replaced for external
35+
// reasons, like a -replace flag or via replace_triggered_by.
36+
forceReplace bool
3937
}
4038

4139
var (

internal/terraform/node_resource_plan.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package terraform
66
import (
77
"fmt"
88
"log"
9+
"slices"
910
"strings"
1011

1112
"github.com/hashicorp/hcl/v2"
@@ -586,7 +587,7 @@ func (n *nodeExpandPlannableResource) concreteResource(ctx EvalContext, knownImp
586587
ForceCreateBeforeDestroy: n.CreateBeforeDestroy(),
587588
skipRefresh: n.skipRefresh,
588589
skipPlanChanges: skipPlanChanges,
589-
forceReplace: n.forceReplace,
590+
forceReplace: slices.ContainsFunc(n.forceReplace, a.Addr.Equal),
590591
}
591592

592593
if importID, ok := knownImports.GetOk(a.Addr); ok {

internal/terraform/node_resource_plan_instance.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,9 @@ type NodePlannableResourceInstance struct {
4040
// for any instances.
4141
skipPlanChanges bool
4242

43-
// forceReplace are resource instance addresses where the user wants to
44-
// force generating a replace action. This set isn't pre-filtered, so
45-
// it might contain addresses that have nothing to do with the resource
46-
// that this node represents, which the node itself must therefore ignore.
47-
forceReplace []addrs.AbsResourceInstance
43+
// forceReplace indicates that this resource is being replaced for external
44+
// reasons, like a -replace flag or via replace_triggered_by.
45+
forceReplace bool
4846

4947
// replaceTriggeredBy stores references from replace_triggered_by which
5048
// triggered this instance to be replaced.
@@ -568,7 +566,7 @@ func (n *NodePlannableResourceInstance) replaceTriggered(ctx EvalContext, repDat
568566
// triggered the replacement in the plan.
569567
// Rather than further complicating the plan method with more
570568
// options, we can refactor both of these features later.
571-
n.forceReplace = append(n.forceReplace, n.Addr)
569+
n.forceReplace = true
572570
log.Printf("[DEBUG] ReplaceTriggeredBy forcing replacement of %s due to change in %s", n.Addr, ref.DisplayString())
573571

574572
n.replaceTriggeredBy = append(n.replaceTriggeredBy, ref)

0 commit comments

Comments
 (0)