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

Use plan graph builder for destroy #31163

Merged
merged 8 commits into from
Jun 1, 2022
Merged

Use plan graph builder for destroy #31163

merged 8 commits into from
Jun 1, 2022

Conversation

jbardin
Copy link
Member

@jbardin jbardin commented May 31, 2022

This PR aims to allow the same components used in PlanGraphBuilder to create the full destroy plan, and eliminate the special case of the DestroyPlanGraph.

The destroy plan was originally envisioned to be completely "offline", where we could generate a simple Delete action for every resource in the state. It turns out that this is not possible, since in order to read each resource from the state, we must call UpgradeResourceState to ensure the schema matches the encoded state, requiring interaction with the provider aside from fetching the schema. Provider developers have also expressed the desire to be able to plan or validate destroy actions, which would also require providers to be correctly configured.

Because the simplistic DestroyPlanGraphBuilder was only intended to create Delete actions, it was not sufficient to ensure we had all the data required to configure a provider before calling UpgradeResourceState. The complete plan graph is where all the setup to properly evaluate the entire config is already done, and since a destroy plan is still a form of planning, we can extend the existing PlanGraphBuilder to accommodate a complete destroy operation rather than duplicating all the logic in the DestroyPlanGraphBuilder. A secondary reason for extending the PlanGraphBuilder (rather than duplicating the logic) is that the destroy graph is often forgotten when changing how plans work, which has led to numerous bugs we can avoid with a single plan codepath.

This does not yet fix the un-configured UpgradeResourceState calls reported in #30460, but does lay the foundation for being able to make that call. This also provides the necessary infrastructure to be able to address #30140, since that also requires a fully configured provider.

Aside from the above mentioned issues, another possibility from this point is to combine the separate refresh step into the destroy plan, removing the special case where we need 2 individual plans to complete a destroy operation. That was purposely left out of consideration for this PR to prevent it from growing too large, and it's not yet clear whether there are any blockers to that being possible.

jbardin added 7 commits May 27, 2022 10:50
Destroy nodes should never participate in references. These edges didn't
come up before, because we weren't building a complete graph including
all temporary values.
We want to use the normal plan graph for destroy, so we need to flag off
configuration for that process.
All instances in state are being removed for destroy, so we can skip
checking for orphans. Because we want to use the normal plan graph, we
need to be able to still call this during destroy, so flag it off.
Rather than maintain a separate graph builder for destroy, use the
normal plan graph with some extra options. Utilize the same pattern as
the validate graph for now, where we take the normal plan graph builder
and inject a new concrete function for the destroy nodes.
@jbardin jbardin requested a review from a team May 31, 2022 18:20
Copy link
Contributor

@apparentlymart apparentlymart left a comment

Choose a reason for hiding this comment

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

This makes sense to me!

Aside from the very nitty nits inline, my inline comments are areas where I stumbled a bit trying to follow what was going on, and so my motivation there is just to share some ideas that might make this code easier to follow in future without having all of the mental context already loaded. They're all subjective, so I'll leave it up to your judgement what (if anything) to do about them.

type = map(string)
default = {
"a" = "first"
"b" = "second"
Copy link
Contributor

Choose a reason for hiding this comment

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

The nittiest nit: this line seems to have a tab, while the others have spaces.

Copy link
Member Author

Choose a reason for hiding this comment

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

thanks, my vim setup doesn't like switching contexts like that

Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, I've typically just used Go-style tab-based indentation for situations where we have configuration embedded in raw strings inside Go code, because it avoids this sort of editor fighting and isn't really important unless the test is actually testing something related to formatting.

So making this all be tabs would also have been an acceptable answer as far as I'm concerned; it was the inconsistency that bothered me. 😀

Comment on lines +1045 to +1046
// TODO: extend this to ensure the otherProvider is always properly
// configured during the destroy plan
Copy link
Contributor

Choose a reason for hiding this comment

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

Heh... this comment represents exactly what I thought when I first read through this test and saw that it's only testing that the plan succeeds, not that it succeeded for the right reason.

Did you TODO this because there's something blocking us from actually configuring the provider right now? My first instinct here was that it should be possible to check otherProvider.ConfigureProviderResponse after the plan succeeds, but probably there's some extra complexity here I'm not understanding! If so, it would be nice to capture that in this comment too so we can see what needs to be true in order for us TODO what this comment says. 😀

Copy link
Member Author

Choose a reason for hiding this comment

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

yup, the provider still won't be configured yet, that will be enabled in a follow up PR. This was intended to be a drop-in replacement of the destroy plan graph with no change in behavior.

@@ -574,7 +574,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
}).Build(addrs.RootModuleInstance)
return graph, walkPlan, diags
case plans.DestroyMode:
graph, diags := (&DestroyPlanGraphBuilder{
graph, diags := DestroyPlanGraphBuilder(&PlanGraphBuilder{
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be reasonable to make this be consistent with how we deal with RefreshOnlyMode above and have this instead "just" set a flag in the PlanGraphBuilder struct and let the Build method worry about the details?

I think I ended up leaving it in this odd shape where NormalMode and RefreshOnlyMode are largely the same just because the DestroyMode case was a drastically different shape, but if we normalize these to all just be conditional flags on the struct then we could potentially rework this to avoid duplicating the common fields for each case and only set skipPlanChanges / destroy in the non-normal modes.

Of course fine to leave this for another day if it seems too risky, since it's just some further cleanup after all.

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 was following the pattern used by the validate graph, because I thought I would end up injecting more concrete functions that I ended using. I think as part of the follow up here we should get rid of both the validate and destroy builder entry points, and just make everything a plan ;)

Comment on lines 32 to 33
// destroyPlan indicated this is being called from a destroy plan.
destroyPlan bool
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels a little weird to me that this flag essentially means to disable the behavior of this transformer entirely, but it's named as if it's going to modify the behavior in a more subtle way than that.

I guess being able to flag it off is probably easier than dynamically omitting it from the list of graph builders given that we currently just write those out as a slice constructor, but maybe it would read more clearly to just call this disable, ignoreConfig or similar instead, so that e.g. the caller will say ignoreConfig: b.destroy which (to me at least) reads as "ignore configuration if we are in destroy mode".

Copy link
Member Author

Choose a reason for hiding this comment

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

That makes sense to me. Adding conditional logic to the builder steps is quite cumbersome, and we managed to get all that out of the way a while back. The old flags were called destroy, but making them a bit more descriptive can definitely help.

@@ -4,9 +4,7 @@ import (
"log"

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/states"

Copy link
Contributor

Choose a reason for hiding this comment

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

Another nitty nit: it isn't clear to me why this blank line was here in the first place, but in any event maybe now we can remove it along with the two lines that were around it. 😀

@@ -26,9 +26,17 @@ type OrphanResourceInstanceTransformer struct {
// Config is the root node in the configuration tree. We'll look up
// the appropriate note in this tree using the path in each node.
Config *configs.Config

// There are no orphans when doing a full destroy
destroyPlan bool
Copy link
Contributor

Choose a reason for hiding this comment

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

Same thought here as about ConfigTransformer above: can we make this flag have a name that describes more specifically what effect it has?

@@ -21,7 +21,7 @@ type OutputTransformer struct {

// If this is a planned destroy, root outputs are still in the configuration
// so we need to record that we wish to remove them
Destroy bool
destroyPlan bool
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe forceDestroyAll bool to represent that this field specifies to destroy all outputs regardless of what action we might otherwise have planned for them? (and then maybe nodeExpandOutput itself has forceDestroy instead of just Destroy, for consistency?

@jbardin jbardin merged commit 93ff272 into main Jun 1, 2022
@jbardin jbardin deleted the jbardin/plan-destroy branch June 1, 2022 19:37
@github-actions
Copy link
Contributor

github-actions bot commented Jun 1, 2022

Reminder for the merging maintainer: if this is a user-visible change, please update the changelog on the appropriate release branch.

@github-actions
Copy link
Contributor

github-actions bot commented Jul 2, 2022

I'm going to lock this pull request because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants