diff --git a/server/core/runtime/apply_step_runner.go b/server/core/runtime/apply_step_runner.go index 67e1f58444..a73c82e9f3 100644 --- a/server/core/runtime/apply_step_runner.go +++ b/server/core/runtime/apply_step_runner.go @@ -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) diff --git a/server/core/runtime/apply_step_runner_test.go b/server/core/runtime/apply_step_runner_test.go index 8dd06efc19..d1ad146085 100644 --- a/server/core/runtime/apply_step_runner_test.go +++ b/server/core/runtime/apply_step_runner_test.go @@ -267,7 +267,7 @@ null_resource.dir2[1]: Destruction complete after 0s Apply complete! Resources: 0 added, 0 changed, 1 destroyed. `, output) - Equals(t, []string{"apply", "-input=false", "extra", "args", "comment", "args"}, tfExec.CalledArgs) + Equals(t, []string{"apply", "-input=false", "-no-color", "extra", "args", "comment", "args"}, tfExec.CalledArgs) _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") diff --git a/server/core/runtime/plan_step_runner.go b/server/core/runtime/plan_step_runner.go index a328c09daa..722c985c9f 100644 --- a/server/core/runtime/plan_step_runner.go +++ b/server/core/runtime/plan_step_runner.go @@ -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"}, extraArgs, ctx.EscapedCommentArgs, } @@ -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" diff --git a/server/core/runtime/plan_step_runner_test.go b/server/core/runtime/plan_step_runner_test.go index 36e3268fe0..50c1083d49 100644 --- a/server/core/runtime/plan_step_runner_test.go +++ b/server/core/runtime/plan_step_runner_test.go @@ -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. @@ -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{ @@ -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 @@ -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"