diff --git a/server/events/runtime/init_step_runner.go b/server/events/runtime/init_step_runner.go index a49477acd4..9fd0be0d9a 100644 --- a/server/events/runtime/init_step_runner.go +++ b/server/events/runtime/init_step_runner.go @@ -1,6 +1,10 @@ package runtime import ( + "os" + "path/filepath" + "strings" + version "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/events/models" ) @@ -16,14 +20,55 @@ func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } - terraformInitCmd := append([]string{"init", "-input=false", "-no-color", "-upgrade"}, extraArgs...) + + terraformInitVerb := []string{"init"} + terraformInitArgs := []string{"-input=false"} // If we're running < 0.9 we have to use `terraform get` instead of `init`. if MustConstraint("< 0.9.0").Check(tfVersion) { ctx.Log.Info("running terraform version %s so will use `get` instead of `init`", tfVersion) - terraformInitCmd = append([]string{"get", "-no-color", "-upgrade"}, extraArgs...) + terraformInitVerb = []string{"get"} + terraformInitArgs = []string{} + } + + terraformInitArgs = append(terraformInitArgs, "-no-color") + + lockfilePath := filepath.Join(path, ".terraform.lock.hcl") + if MustConstraint("< 0.14.0").Check(tfVersion) || fileDoesNotExists(lockfilePath) { + terraformInitArgs = append(terraformInitArgs, "-upgrade") + } + + // work if any of the core args have been overridden + finalArgs := []string{} + usedExtraArgs := []string{} + for _, arg := range terraformInitArgs { + override := "" + prefix := arg + argSplit := strings.Split(arg, "=") + if len(argSplit) == 2 { + prefix = argSplit[0] + } + for _, extraArg := range extraArgs { + if strings.HasPrefix(extraArg, prefix) { + override = extraArg + } + } + if override != "" { + finalArgs = append(finalArgs, override) + usedExtraArgs = append(usedExtraArgs, override) + } else { + finalArgs = append(finalArgs, arg) + } + } + // add any extra args that are not overrides + for _, extraArg := range extraArgs { + if !stringInSlice(usedExtraArgs, extraArg) { + finalArgs = append(finalArgs, extraArg) + } } + terraformInitCmd := append(terraformInitVerb, finalArgs...) + out, err := i.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, terraformInitCmd, envs, tfVersion, ctx.Workspace) // Only include the init output if there was an error. Otherwise it's // unnecessary and lengthens the comment. @@ -32,3 +77,21 @@ func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin } return "", nil } + +func fileDoesNotExists(name string) bool { + if _, err := os.Stat(name); err != nil { + if os.IsNotExist(err) { + return true + } + } + return false +} + +func stringInSlice(stringSlice []string, target string) bool { + for _, value := range stringSlice { + if value == target { + return true + } + } + return false +} diff --git a/server/events/runtime/init_step_runner_test.go b/server/events/runtime/init_step_runner_test.go index 47c9f39a4a..95ba137e9c 100644 --- a/server/events/runtime/init_step_runner_test.go +++ b/server/events/runtime/init_step_runner_test.go @@ -1,6 +1,8 @@ package runtime_test import ( + "io/ioutil" + "path/filepath" "testing" version "github.com/hashicorp/go-version" @@ -95,3 +97,166 @@ func TestRun_ShowInitOutputOnError(t *testing.T) { ErrEquals(t, "error", err) Equals(t, "output", output) } + +func TestRun_InitOmitsUpgradeFlagIfLockFilePresent(t *testing.T) { + tmpDir, cleanup := TempDir(t) + defer cleanup() + lockFilePath := filepath.Join(tmpDir, ".terraform.lock.hcl") + err := ioutil.WriteFile(lockFilePath, nil, 0600) + Ok(t, err) + + RegisterMockTestingT(t) + terraform := mocks.NewMockClient() + + logger := logging.NewNoopLogger(t) + + tfVersion, _ := version.NewVersion("0.14.0") + iso := runtime.InitStepRunner{ + TerraformExecutor: terraform, + DefaultTFVersion: tfVersion, + } + When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + ThenReturn("output", nil) + + output, err := iso.Run(models.ProjectCommandContext{ + Workspace: "workspace", + RepoRelDir: ".", + Log: logger, + }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) + Ok(t, err) + // When there is no error, should not return init output to PR. + Equals(t, "", output) + + expectedArgs := []string{"init", "-input=false", "-no-color", "extra", "args"} + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") +} + +func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) { + tmpDir, cleanup := TempDir(t) + defer cleanup() + + RegisterMockTestingT(t) + terraform := mocks.NewMockClient() + + logger := logging.NewNoopLogger(t) + + tfVersion, _ := version.NewVersion("0.14.0") + iso := runtime.InitStepRunner{ + TerraformExecutor: terraform, + DefaultTFVersion: tfVersion, + } + When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + ThenReturn("output", nil) + + output, err := iso.Run(models.ProjectCommandContext{ + Workspace: "workspace", + RepoRelDir: ".", + Log: logger, + }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) + Ok(t, err) + // When there is no error, should not return init output to PR. + Equals(t, "", output) + + expectedArgs := []string{"init", "-input=false", "-no-color", "-upgrade", "extra", "args"} + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") +} + +func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing.T) { + tmpDir, cleanup := TempDir(t) + defer cleanup() + lockFilePath := filepath.Join(tmpDir, ".terraform.lock.hcl") + err := ioutil.WriteFile(lockFilePath, nil, 0600) + Ok(t, err) + + RegisterMockTestingT(t) + terraform := mocks.NewMockClient() + + logger := logging.NewNoopLogger(t) + + tfVersion, _ := version.NewVersion("0.13.0") + iso := runtime.InitStepRunner{ + TerraformExecutor: terraform, + DefaultTFVersion: tfVersion, + } + When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + ThenReturn("output", nil) + + output, err := iso.Run(models.ProjectCommandContext{ + Workspace: "workspace", + RepoRelDir: ".", + Log: logger, + }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) + Ok(t, err) + // When there is no error, should not return init output to PR. + Equals(t, "", output) + + expectedArgs := []string{"init", "-input=false", "-no-color", "-upgrade", "extra", "args"} + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") +} + +func TestRun_InitExtraArgsDeDupe(t *testing.T) { + RegisterMockTestingT(t) + cases := []struct { + description string + extraArgs []string + expectedArgs []string + }{ + { + "No extra args", + []string{}, + []string{"init", "-input=false", "-no-color", "-upgrade"}, + }, + { + "Override -upgrade", + []string{"-upgrade=false"}, + []string{"init", "-input=false", "-no-color", "-upgrade=false"}, + }, + { + "Override -input", + []string{"-input=true"}, + []string{"init", "-input=true", "-no-color", "-upgrade"}, + }, + { + "Override -input and -upgrade", + []string{"-input=true", "-upgrade=false"}, + []string{"init", "-input=true", "-no-color", "-upgrade=false"}, + }, + { + "Non duplicate extra args", + []string{"extra", "args"}, + []string{"init", "-input=false", "-no-color", "-upgrade", "extra", "args"}, + }, + { + "Override upgrade with extra args", + []string{"extra", "args", "-upgrade=false"}, + []string{"init", "-input=false", "-no-color", "-upgrade=false", "extra", "args"}, + }, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + terraform := mocks.NewMockClient() + + logger := logging.NewNoopLogger(t) + + tfVersion, _ := version.NewVersion("0.10.0") + iso := runtime.InitStepRunner{ + TerraformExecutor: terraform, + DefaultTFVersion: tfVersion, + } + When(terraform.RunCommandWithVersion(logging_matchers.AnyLoggingSimpleLogging(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())). + ThenReturn("output", nil) + + output, err := iso.Run(models.ProjectCommandContext{ + Workspace: "workspace", + RepoRelDir: ".", + Log: logger, + }, c.extraArgs, "/path", map[string]string(nil)) + Ok(t, err) + // When there is no error, should not return init output to PR. + Equals(t, "", output) + + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", c.expectedArgs, map[string]string(nil), tfVersion, "workspace") + }) + } +}