Skip to content

Commit

Permalink
terraform: Callers can force all actions to be deferred
Browse files Browse the repository at this point in the history
When the modules runtime is being used inside the stacks runtime, it's
possible that a component could refer to another component that has
deferred changes in its plan. In that case, we do still want to plan the
downstream component (to give earlier feedback if there are obvious
problems with it) but we need to force all planned actions to be treated
as deferred so that we preserve the correct dependency ordering across
all objects described in a stack.

This commit only deals with the modules runtime handling that case. We'll
make the stacks runtime use it in a subsequent commit.
  • Loading branch information
apparentlymart committed Feb 12, 2024
1 parent 823aa8e commit 2e6cf0a
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 8 deletions.
24 changes: 16 additions & 8 deletions internal/terraform/context_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ type PlanOpts struct {
// the actual graph.
ExternalReferences []*addrs.Reference

// ExternalDependencyDeferred, when set, indicates that the caller
// considers this configuration to depend on some other configuration
// that had at least one deferred change, and therefore everything in
// this configuration must have its changes deferred too so that the
// overall dependency ordering would be correct.
ExternalDependencyDeferred bool

// Overrides provides a set of override objects that should be applied
// during this plan.
Overrides *mocking.Overrides
Expand Down Expand Up @@ -664,14 +671,15 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o
providerFuncResults := providers.NewFunctionResultsTable(nil)

walker, walkDiags := c.walk(graph, walkOp, &graphWalkOpts{
Config: config,
InputState: prevRunState,
ExternalProviderConfigs: externalProviderConfigs,
Changes: changes,
MoveResults: moveResults,
Overrides: opts.Overrides,
PlanTimeTimestamp: timestamp,
ProviderFuncResults: providerFuncResults,
Config: config,
InputState: prevRunState,
ExternalProviderConfigs: externalProviderConfigs,
ExternalDependencyDeferred: opts.ExternalDependencyDeferred,
Changes: changes,
MoveResults: moveResults,
Overrides: opts.Overrides,
PlanTimeTimestamp: timestamp,
ProviderFuncResults: providerFuncResults,
})
diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags)
Expand Down
97 changes: 97 additions & 0 deletions internal/terraform/context_plan2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"

// "github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -4742,6 +4743,102 @@ func TestContext2Plan_externalProviders(t *testing.T) {
}
}

func TestContext2Apply_externalDependencyDeferred(t *testing.T) {
// This test deals with the situation where the stacks runtime knows
// that an upstream component already has deferred actions and so
// it's telling us that we need to artifically treat everything in
// the current configuration as deferred.

cfg := testModuleInline(t, map[string]string{
"main.tf": `
resource "test" "a" {
name = "a"
}
resource "test" "b" {
name = "b"
upstream_names = [test.a.name]
}
resource "test" "c" {
name = "c"
upstream_names = toset([
test.a.name,
test.b.name,
])
}
`,
})

p := &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {
Type: cty.String,
Required: true,
},
"upstream_names": {
Type: cty.Set(cty.String),
Optional: true,
},
},
},
},
},
},
PlanResourceChangeFn: func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
},
}
resourceInstancesActionsInPlan := func(p *plans.Plan) map[string]plans.Action {
ret := make(map[string]plans.Action)
for _, cs := range p.Changes.Resources {
// Anything that was deferred will not appear in the result at
// all. Non-deferred actions that don't actually need to do anything
// _will_ appear, but with action set to [plans.NoOp].
ret[cs.Addr.String()] = cs.Action
}
return ret
}
cmpOpts := cmp.Options{
ctydebug.CmpOptions,
}

ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})

plan, diags := ctx.Plan(cfg, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
ExternalDependencyDeferred: true,
})
assertNoDiagnostics(t, diags)
if plan.Applyable {
t.Fatal("plan is applyable; should not be, because there's nothing to do yet")
}
if plan.Complete {
t.Fatal("plan is complete; should have deferred actions")
}

gotActions := resourceInstancesActionsInPlan(plan)
wantActions := map[string]plans.Action{
// No actions at all, because everything was deferred!
}
if diff := cmp.Diff(wantActions, gotActions, cmpOpts); diff != "" {
t.Fatalf("wrong actions in plan\n%s", diff)
}
// TODO: Once we are including information about the individual
// deferred actions in the plan, this would be a good place to
// assert that they are correct!
}

func TestContext2Plan_removedResourceForgetBasic(t *testing.T) {
addrA := mustResourceInstanceAddr("test_object.a")
m := testModuleInline(t, map[string]string{
Expand Down
9 changes: 9 additions & 0 deletions internal/terraform/context_walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ type graphWalkOpts struct {
// always take into account what walk type it's dealing with.
ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface

// ExternalDependencyDeferred indicates that something that this entire
// configuration depends on (outside the view of this modules runtime)
// has deferred changes, and therefore we must treat _all_ actions
// as deferred to produce the correct overall dependency ordering.
ExternalDependencyDeferred bool

// PlanTimeCheckResults should be populated during the apply phase with
// the snapshot of check results that was generated during the plan step.
//
Expand Down Expand Up @@ -163,6 +169,9 @@ func (c *Context) graphWalker(graph *Graph, operation walkOperation, opts *graph
// blocks, since we need that for deferral tracking.
resourceGraph := graph.ResourceGraph()
deferred := deferring.NewDeferred(resourceGraph)
if opts.ExternalDependencyDeferred {
deferred.SetExternalDependencyDeferred()
}

return &ContextGraphWalker{
Context: c,
Expand Down

0 comments on commit 2e6cf0a

Please sign in to comment.