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

Apply a confirmable run when given a saved cloud plan #33270

Merged

Conversation

nfagerlund
Copy link
Member

@nfagerlund nfagerlund commented May 26, 2023

This PR is about half? a third? of the Terraform Core portion of the saved cloud plans project. It adds the ability to pass terraform apply a saved cloud plan file (actually a little JSON blob that refers to a run by its ID and its TFC instance hostname), and have Terraform confirm the referenced run and display the outcome of applying it.

Testing instructions

Interestingly, this ability does not depend on the TFC changes to add saved-plan runs as a new run type! It works today, sort of! To test it, you'll need to:

  • Configure the cloud backend in a local config directory.
  • Start a (applicable) run in the currently selected workspace, and allow the plan and checks to finish so that it's awaiting confirmation. (Easiest is probably to terraform apply on the CLI and then ctrl-C instead of approving.)
  • Note the run ID, and create a little json file with the following format:
{
    "remote_plan_format": 1,
    "run_id": "run-y3X9kafnB8hfwxxx",
    "hostname": "app.terraform.io"
}
  • Run terraform apply <PATH TO JSON FILE>
  • Try some more times, with runs that aren't in a good-to-go state. For example: a planned-and-finished run with no changes, a run that was already applied, a discarded run, an errored run, a run that's still planning, etc.

About the automated tests

  • I managed to get a nice concise test for the happy path of applying a run that's ready, but I wasn't able to craft a test for the "error if already applied" case or its siblings; something about crafting mock runs or duplicate Operation structs eluded me.
  • I also couldn't figure out how to test the apply command with this new behavior — up at that highest level of the internal packages, I couldn't see a sensible way to teach Terraform about mocking the TFC APIs! 😓 This might be worthy of an integration test against real TFC, but that won't really be practical until the rest of the work in this project is closer to done.

If you see some way around those issues, or if you spot other things I should be testing in more detail, please let me know!

Sidenotes

I originally had this idea of configuring the cloud backend from ONLY a run ID and hostname, because once you have a client you can just traverse through relationships and get the workspace and organization names from the server. However, it turned out to be more trouble than it was worth — since you HAVE to be running commands from the config working directory anyway, there's no real upside, so we might as well load the backend from the config like we always do.

Target Release

1.6.x

We're now in a decent position to switch behavior for cloud plans within the
apply command.
… so in apply command

Faking up the backend config cty object for this alternate path is kind of
strange and awkward, and these cloud backend setup functions are asking for a
bigger refactor... but this gets it working with a minimally intrusive diff.
- Skips confirmation, like applying a local saved plan.

- Gives more detailed errors than a local plan can; with local, we just know the
  serial and lineage isn't right, but with cloud we can actually tell you what
  happened to that specific run.
As with slices, Append consumes its argument instead of mutating in-place.
`Cloud.setOrgAndWorkspaceFromRun` relies on including the run's workspace, so it
can get the organization name and the workspace name in a single request. This
allows the mocks to cope with that. It doesn't yet add support for all the
possible run includes, since that seems premature.
The extra code complexity doesn't seem worth it, since you need to be inside the
config directory (and able to load the config) in order to apply the run
regardless.
Comment on lines 13 to 14
Local *Reader
Cloud *cloudplan.SavedPlanBookmark
Copy link
Contributor

Choose a reason for hiding this comment

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

I might consider making both of these fields unexported and then changing the two accessor methods to be like func (w *WrappedPlanFile) Local() (*Reader, bool) (and similar for Cloud) so that the caller can't so easily avoid checking which variant is selected, and thus it's hopefully more obvious how to use this API correctly. I don't feel super strongly about it, but just suggesting it in case it seems viable and worthwhile to you.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, thanks! I'll wait to hear from the other reviewers, but it seems like probably a reasonable improvement to make.

Copy link
Contributor

Choose a reason for hiding this comment

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

this sounds good to me!

Additionally, it seems to me that it is currently possible to set both Local & Cloud plan even though the intention is to disallow it, in which case the func OpenWrapped... will pick the Local plan since it is the first block. I think it might be useful to check that both Local and Cloud are not set and return an error if they are, before proceeding to open the local or cloud plan file.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK, I did that refactor! 👍🏼 And as for setting both fields — I made a pair of constructor functions for tests that need to fake up one of these structs, and those plus OpenWrapped are the only things allowed to actually build one of these; that should make it impossible to get ahold of a malformed one.

Copy link
Contributor

Choose a reason for hiding this comment

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

This was really exciting to smoke test! I think I wanted to see the familiar remote execution yellow header that you get with plan. Like, here was the output:

$ ./terraform apply plan.json
null_resource.hello: Destroying... [id=6984397633657598306]
null_resource.hello: Destruction complete after 0s
null_resource.hello: Creating...
null_resource.hello: Creation complete after 0s [id=4736096538695284461]

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

Outputs:
name = "hello"

It think it would be a good idea to re-render the applyDefaultHeader so that I can be reasonably sure this output came from the remote run.

This is awesome! It looks like you found a lot of corner cases I hadn't previously considered.

Copy link
Contributor

@Uk1288 Uk1288 left a comment

Choose a reason for hiding this comment

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

Good work on this!

General suggestions:

  • While testing with speculative runs that have changes, the error message currently says: Error: Saved plan has no changes │ The given plan file contains no changes, so it cannot be applied…. Should the message in this case be something like, speculative plans cannot be applied...

  • When attempting to confirm a plan that is awaiting override approval for soft policy error, I wonder if the error message should be more descriptive, indicating that the run is waiting for override. The current message displays: Error: Saved plan cannot be applied │ Terraform Cloud cannot apply the given plan file. This may mean the plan and checks have not yet completed, or │ may indicate another problem

  • The link to the run only shows up if confirmation resulted in an error, adding the run link even for a successful case might be helpful.

case tfe.RunWorkspace:
ws, ok := m.client.Workspaces.workspaceIDs[r.Workspace.ID]
if ok {
r.Workspace = ws
Copy link
Contributor

Choose a reason for hiding this comment

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

should probably break out of the loop once the expected workspace is found and set on the Run.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, should we though?? We're not looping through workspaces here, we're just looping through the list of ?include= parameters that got set on the request. There would only be like, five? total if you set them all... and theoretically we would handle all of them, I just didn't bother coding the others yet because YAGNI.

If we were looping through workspaces, that'd definitely want an early loop exit.


func TestWrappedError(t *testing.T) {
// Open something that isn't a cloud or local planfile: should error
wrongFile := "not a valid zip file"
Copy link
Contributor

Choose a reason for hiding this comment

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

while testing with a yaml file type, I was expecting the error message to describe the expected file format, json in this case: i.e not a valid json file. Is this a general error for wrong file types?

Following the current error message, i attempted to pass in a zip file version of the plan file and I got this error: Error: the given file is not a valid plan file.

Copy link
Member Author

Choose a reason for hiding this comment

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

I ended up addressing this by combining both errors, in the hope that at least one of them will hint at what the user did wrong. That said, because these are both meant as opaque formats, the exact details of the errors aren't really meant for normal users to think too hard about — they hopefully just point the way to remembering which file you meant to click.

Comment on lines 13 to 14
Local *Reader
Cloud *cloudplan.SavedPlanBookmark
Copy link
Contributor

Choose a reason for hiding this comment

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

this sounds good to me!

Additionally, it seems to me that it is currently possible to set both Local & Cloud plan even though the intention is to disallow it, in which case the func OpenWrapped... will pick the Local plan since it is the first block. I think it might be useful to check that both Local and Cloud are not set and return an error if they are, before proceeding to open the local or cloud plan file.

@nfagerlund
Copy link
Member Author

Good catches, all! I'll revise and follow up.

In an earlier draft this wasn't necessary, since we were constructing a backend
from the saved plan runID+host, but now it's a useful extra check to do.
This condition isn't really possible for classic decisive-apply runs, but it IS
possible for saved-plan runs, which are currently feature-flagged.
@nfagerlund
Copy link
Member Author

@Uk1288 @brandonc OK, this is ready for re-review!

  • New error for locked workspace (need to make a "save-plan": true run while another run is current, to be able to see that one. But don't worry about it too much, since that's feature flagged anyway.)
  • Shows a run header (with URL) when starting up, now!
  • Refactored access patterns for planfile.WrappedPlanFile.
  • Some lil stuff.

This reverts commit 83b2763.

Unfortunately, this actually can't be checked safely! A locked workspace check
has to also check whether the workspace is locked by the entity that's about to
attempt locking it, and go-tfe does not support the `locked_by` relationship on
workspaces (probably because it's polymorphic run/team/user).
Copy link
Contributor

@Uk1288 Uk1288 left a comment

Choose a reason for hiding this comment

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

LGTM 🎉

@nfagerlund nfagerlund merged commit f30bbdb into cli-team/saved-cloud-plans Jun 21, 2023
@nfagerlund nfagerlund deleted the nf/may23-apply-a-confirmable-run branch June 21, 2023 02:06
@github-actions
Copy link

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

nfagerlund added a commit that referenced this pull request Jun 21, 2023
It displays a run header with link to web UI, like starting a new plan does, then confirms the run 
and streams the apply logs. If you can't apply the run (it's from a different workspace, is in an
unconfirmable state, etc. etc.), it displays an error instead.

Notable points along the way:

* Implement `WrappedPlanFile` sum type, and update planfile consumers to use it instead of a plain `planfile.Reader`.

* Enable applying a saved cloud plan

* Update TFC mocks — add org name to workspace, and minimal support for includes on MockRuns.ReadWithOptions.
nfagerlund added a commit that referenced this pull request Jun 22, 2023
It displays a run header with link to web UI, like starting a new plan does, then confirms the run 
and streams the apply logs. If you can't apply the run (it's from a different workspace, is in an
unconfirmable state, etc. etc.), it displays an error instead.

Notable points along the way:

* Implement `WrappedPlanFile` sum type, and update planfile consumers to use it instead of a plain `planfile.Reader`.

* Enable applying a saved cloud plan

* Update TFC mocks — add org name to workspace, and minimal support for includes on MockRuns.ReadWithOptions.
nfagerlund added a commit that referenced this pull request Jul 7, 2023
It displays a run header with link to web UI, like starting a new plan does, then confirms the run 
and streams the apply logs. If you can't apply the run (it's from a different workspace, is in an
unconfirmable state, etc. etc.), it displays an error instead.

Notable points along the way:

* Implement `WrappedPlanFile` sum type, and update planfile consumers to use it instead of a plain `planfile.Reader`.

* Enable applying a saved cloud plan

* Update TFC mocks — add org name to workspace, and minimal support for includes on MockRuns.ReadWithOptions.
sebasslash pushed a commit that referenced this pull request Jul 24, 2023
It displays a run header with link to web UI, like starting a new plan does, then confirms the run
and streams the apply logs. If you can't apply the run (it's from a different workspace, is in an
unconfirmable state, etc. etc.), it displays an error instead.

Notable points along the way:

* Implement `WrappedPlanFile` sum type, and update planfile consumers to use it instead of a plain `planfile.Reader`.

* Enable applying a saved cloud plan

* Update TFC mocks — add org name to workspace, and minimal support for includes on MockRuns.ReadWithOptions.
Copy link

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 Dec 14, 2023
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.

4 participants