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

Fix remote execution on Terraform Cloud after v1.1.0 #2793

Merged
merged 5 commits into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/core/runtime/apply_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pa

// TODO: Leverage PlanTypeStepRunnerDelegate here
if IsRemotePlan(contents) {
args := append(append([]string{"apply", "-input=false"}, extraArgs...), ctx.EscapedCommentArgs...)
args := append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...)
out, err = a.runRemoteApply(ctx, args, path, planPath, ctx.TerraformVersion, envs)
if err == nil {
out = a.cleanRemoteApplyOutput(out)
Expand Down
14 changes: 12 additions & 2 deletions server/core/runtime/plan_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,14 @@ func (p *PlanStepRunner) isRemoteOpsErr(output string, err error) bool {
if err == nil {
return false
}
return strings.Contains(output, remoteOpsErr01114) || strings.Contains(output, remoteOpsErr012) || strings.Contains(output, remoteOpsErr100)
return strings.Contains(output, remoteOpsErr110) || strings.Contains(output, remoteOpsErr01114) || strings.Contains(output, remoteOpsErr012) || strings.Contains(output, remoteOpsErr100)
}

// remotePlan runs a terraform plan command compatible with TFE remote
// operations.
func (p *PlanStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []string, path string, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) {
argList := [][]string{
{"plan", "-input=false", "-refresh"},
{"plan", "-input=false", "-refresh", "-no-color"},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before the change, the output in the comment contains color code and doesn't look very nice.
Screen Shot 2022-12-13 at 2 24 11 PM

Also, I was debating whether to check tfVersion before adding the extra -no-color argument, and felt it might be fine to ignore it for better readability in the main code and the test cases. -no-color seems to have been introduced in Terraform since very early on (at least before v0.11).

extraArgs,
ctx.EscapedCommentArgs,
}
Expand Down Expand Up @@ -340,6 +340,16 @@ The "remote" backend does not support saving the generated execution plan
locally at this time.
`

// remoteOpsErr110 is the error terraform plan will return if this project is
// using Terraform Cloud remote operations in TF 1.1.0 and above
var remoteOpsErr110 = `╷
│ Error: Saving a generated plan is currently not supported
│ Terraform Cloud does not support saving the generated execution plan
│ locally at this time.
`

// remoteOpsHeader is the header we add to the planfile if this plan was
// generated using TFE remote operations.
var remoteOpsHeader = "Atlantis: this plan was created by remote ops\n"
68 changes: 46 additions & 22 deletions server/core/runtime/plan_step_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,22 +685,45 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {

// Test plans if using remote ops.
func TestRun_RemoteOps(t *testing.T) {
cases := map[string]string{
"0.11.15 error": `Error: Saving a generated plan is currently not supported!
cases := []struct {
name string
tfVersion string
remoteOpsErr string
}{
{
name: "0.11.15 error",
tfVersion: "0.11.15",
remoteOpsErr: `Error: Saving a generated plan is currently not supported!

The "remote" backend does not support saving the generated execution
plan locally at this time.

`,
"0.12.* error": `Error: Saving a generated plan is currently not supported
},
{
name: "0.12.* error",
tfVersion: "0.12.0",
remoteOpsErr: `Error: Saving a generated plan is currently not supported

The "remote" backend does not support saving the generated execution plan
locally at this time.

`,
},
{
name: "1.1.0 error",
tfVersion: "1.1.0",
remoteOpsErr: `╷
│ Error: Saving a generated plan is currently not supported
│ Terraform Cloud does not support saving the generated execution plan
│ locally at this time.
`,
},
}
for name, remoteOpsErr := range cases {
t.Run(name, func(t *testing.T) {
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {

logger := logging.NewNoopLogger(t)
// Now that mocking is set up, we're ready to run the plan.
Expand All @@ -722,7 +745,7 @@ locally at this time.
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()

tfVersion, _ := version.NewVersion("0.11.12")
tfVersion, _ := version.NewVersion(c.tfVersion)
updater := mocks2.NewMockCommitStatusUpdater()
asyncTf := &remotePlanMock{}
s := runtime.PlanStepRunner{
Expand Down Expand Up @@ -763,16 +786,28 @@ locally at this time.
"comment",
"args",
}
if tfVersion.GreaterThanOrEqual(version.Must(version.NewVersion("0.12.0"))) {
expPlanArgs = []string{"plan",
"-input=false",
"-refresh",
"-out",
fmt.Sprintf("%q", filepath.Join(absProjectPath, "default.tfplan")),
"extra",
"args",
"comment",
"args",
}
}

planErr := errors.New("exit status 1: err")
planOutput := "\n" + remoteOpsErr
planOutput := "\n" + c.remoteOpsErr
asyncTf.LinesToSend = remotePlanOutput
When(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfVersion, "default")).
ThenReturn(planOutput, planErr)

output, err := s.Run(ctx, []string{"extra", "args"}, absProjectPath, map[string]string(nil))
Ok(t, err)
Equals(t, `
Assert(t, strings.Contains(output, `
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy
Expand All @@ -782,26 +817,15 @@ Terraform will perform the following actions:
- null_resource.hi[1]


Plan: 0 to add, 0 to change, 1 to destroy.`, output)
Plan: 0 to add, 0 to change, 1 to destroy.`), "expect plan success")

expRemotePlanArgs := []string{"plan", "-input=false", "-refresh", "extra", "args", "comment", "args"}
expRemotePlanArgs := []string{"plan", "-input=false", "-refresh", "-no-color", "extra", "args", "comment", "args"}
Equals(t, expRemotePlanArgs, asyncTf.CalledArgs)

// Verify that the fake plan file we write has the correct contents.
bytes, err := os.ReadFile(filepath.Join(absProjectPath, "default.tfplan"))
Ok(t, err)
Equals(t, `Atlantis: this plan was created by remote ops

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy

Terraform will perform the following actions:

- null_resource.hi[1]


Plan: 0 to add, 0 to change, 1 to destroy.`, string(bytes))
Assert(t, strings.HasPrefix(string(bytes), "Atlantis: this plan was created by remote ops"), "expect remote plan")

// Ensure that the status was updated with the runURL.
runURL := "https://app.terraform.io/app/lkysow-enterprises/atlantis-tfe-test/runs/run-is4oVvJfrkud1KvE"
Expand Down