-
Notifications
You must be signed in to change notification settings - Fork 9.6k
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
Apply a confirmable run when given a saved cloud plan #33270
Conversation
f261f81
to
4ecd3bb
Compare
7ba32a2
to
041753b
Compare
041753b
to
765bb2f
Compare
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.
765bb2f
to
76e1a6e
Compare
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.
internal/plans/planfile/wrapped.go
Outdated
Local *Reader | ||
Cloud *cloudplan.SavedPlanBookmark |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
internal/cloud/backend_apply.go
Outdated
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
internal/plans/planfile/wrapped.go
Outdated
Local *Reader | ||
Cloud *cloudplan.SavedPlanBookmark |
There was a problem hiding this comment.
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.
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.
@Uk1288 @brandonc OK, this is ready for re-review!
|
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🎉
Reminder for the merging maintainer: if this is a user-visible change, please update the changelog on the appropriate release branch. |
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.
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.
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.
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.
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. |
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:
terraform apply
on the CLI and then ctrl-C instead of approving.)terraform apply <PATH TO JSON FILE>
About the automated tests
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