From 812db63dfddecba07c2e01812bdf7f07b7bd7bce Mon Sep 17 00:00:00 2001
From: Li Lin
Date: Thu, 15 Dec 2022 12:54:43 -0800
Subject: [PATCH] Fix remote execution on Terraform Cloud after v1.1.0 (#2793)
* Fix remote execution on Terraform Cloud after v1.1.0
* Do not check terraform version for -no-color
* Move string comparison for newer version earlier
* Update remote apply test
---
server/core/runtime/apply_step_runner.go | 2 +-
server/core/runtime/apply_step_runner_test.go | 2 +-
server/core/runtime/plan_step_runner.go | 14 +++-
server/core/runtime/plan_step_runner_test.go | 68 +++++++++++++------
4 files changed, 60 insertions(+), 26 deletions(-)
diff --git a/server/core/runtime/apply_step_runner.go b/server/core/runtime/apply_step_runner.go
index 67e1f5844..a73c82e9f 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 8dd06efc1..d1ad14608 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 a328c09da..722c985c9 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 36e3268fe..50c1083d4 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"