fix: unused config's create_before_destroy on resource change with no refresh (#2248)

Signed-off-by: ollevche <ollevche@gmail.com>
This commit is contained in:
Oleksandr Levchenkov 2024-12-20 10:47:00 +02:00 committed by GitHub
parent 211ec55a30
commit c5b43b9f1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 102 additions and 17 deletions

View File

@ -9,6 +9,8 @@ ENHANCEMENTS:
BUG FIXES:
* `tofu init` command does not attempt to read encryption keys when `-backend=false` flag is set. (https://github.com/opentofu/opentofu/pull/2293)
* Changes in `create_before_destroy` for resources which require replacement are now properly handled when refresh is disabled. ([#2248](https://github.com/opentofu/opentofu/pull/2248))
## Previous Releases
For information on prior major and minor releases, see their changelogs:

View File

@ -2394,6 +2394,83 @@ func TestApply_showSensitiveArg(t *testing.T) {
}
}
func TestApply_CreateBeforeDestroyWithRefreshFalse(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("apply_cbd_with_refresh_false"), td)
defer testChdir(t, td)()
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "a",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"foo"}`),
Status: states.ObjectReady,
// Create before destroy is set in the state and
// omitted in the configuration.
CreateBeforeDestroy: true,
},
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
addrs.NoKey,
)
})
statePath := testStateFile(t, originalState)
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Computed: true},
},
},
},
},
}
// Resource must be replaced.
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
RequiresReplace: []cty.Path{{cty.GetAttrStep{Name: "id"}}},
}
}
view, done := testView(t)
c := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}
args := []string{
"-state", statePath,
"-auto-approve",
// We are disabling refresh to see if cbd will be updated.
"-refresh=false",
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("Failed to run: \n%s", output.Stderr())
}
// If cbd is not updated, resource becomes detached and
// apply results in 1 added, 0 changed, 0 destroyed.
stdout := output.Stdout()
if !strings.Contains(stdout, "Apply complete! Resources: 1 added, 0 changed, 1 destroyed.") {
t.Fatalf("bad: output should contain 'notsensitive' output\n%s", stdout)
}
}
// applyFixtureSchema returns a schema suitable for processing the
// configuration in testdata/apply . This schema should be
// assigned to a mock provider named "test".

View File

@ -0,0 +1,3 @@
resource "test_instance" "a" {
id = "bar"
}

View File

@ -57,7 +57,7 @@ func (c *Context) Apply(ctx context.Context, plan *plans.Plan, config *configs.C
providerFunctionTracker := make(ProviderFunctionMapping)
graph, operation, diags := c.applyGraph(plan, config, true, providerFunctionTracker)
graph, operation, diags := c.applyGraph(plan, config, providerFunctionTracker)
if diags.HasErrors() {
return nil, diags
}
@ -123,8 +123,7 @@ Note that the -target and -exclude options are not suitable for routine use, and
return newState, diags
}
//nolint:revive,unparam // TODO remove validate bool as it's not used
func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate bool, providerFunctionTracker ProviderFunctionMapping) (*Graph, walkOperation, tfdiags.Diagnostics) {
func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, providerFunctionTracker ProviderFunctionMapping) (*Graph, walkOperation, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
variables := InputValues{}
@ -211,7 +210,7 @@ func (c *Context) ApplyGraphForUI(plan *plans.Plan, config *configs.Config) (*Gr
var diags tfdiags.Diagnostics
graph, _, moreDiags := c.applyGraph(plan, config, false, make(ProviderFunctionMapping))
graph, _, moreDiags := c.applyGraph(plan, config, make(ProviderFunctionMapping))
diags = diags.Append(moreDiags)
return graph, diags
}

View File

@ -291,7 +291,7 @@ func (c *Context) checkApplyGraph(plan *plans.Plan, config *configs.Config) tfdi
return nil
}
log.Println("[DEBUG] building apply graph to check for errors")
_, _, diags := c.applyGraph(plan, config, true, make(ProviderFunctionMapping))
_, _, diags := c.applyGraph(plan, config, make(ProviderFunctionMapping))
return diags
}

View File

@ -187,10 +187,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
Changes: b.Changes,
Operation: b.Operation,
},
&CBDEdgeTransformer{
Config: b.Config,
State: b.State,
},
&CBDEdgeTransformer{},
// We need to remove configuration nodes that are not used at all, as
// they may not be able to evaluate, especially during destroy.

View File

@ -237,7 +237,21 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
log.Printf("[WARN] managedResourceExecute: no Managed config value found in instance state for %q", n.Addr)
} else {
if instanceRefreshState != nil {
prevCreateBeforeDestroy := instanceRefreshState.CreateBeforeDestroy
// This change is usually written to the refreshState and then
// updated value used for further graph execution. However, with
// "refresh=false", refreshState is not being written and then
// some resources with updated configuration could be detached
// due to missaligned create_before_destroy in different graph nodes.
instanceRefreshState.CreateBeforeDestroy = n.Config.Managed.CreateBeforeDestroy || n.ForceCreateBeforeDestroy
if prevCreateBeforeDestroy != instanceRefreshState.CreateBeforeDestroy && n.skipRefresh {
diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, refreshState))
if diags.HasErrors() {
return diags
}
}
}
}

View File

@ -9,9 +9,7 @@ import (
"fmt"
"log"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/dag"
"github.com/opentofu/opentofu/internal/states"
)
// GraphNodeDestroyerCBD must be implemented by nodes that might be
@ -115,12 +113,7 @@ func (t *ForcedCBDTransformer) hasCBDDescendent(g *Graph, v dag.Vertex) bool {
// will get here by recording the CBD-ness of each change in the plan during
// the plan walk and then forcing the nodes into the appropriate setting during
// DiffTransformer when building the apply graph.
type CBDEdgeTransformer struct {
// Module and State are only needed to look up dependencies in
// any way possible. Either can be nil if not available.
Config *configs.Config
State *states.State
}
type CBDEdgeTransformer struct{}
func (t *CBDEdgeTransformer) Transform(g *Graph) error {
// Go through and reverse any destroy edges