From 8d02f6b68cd3d7ab5e183d36b7f0920346b4dc69 Mon Sep 17 00:00:00 2001 From: Gerald Barker Date: Mon, 30 Aug 2021 22:53:49 +0100 Subject: [PATCH] Fix regression - Delete .terraform.lock.hcl if it was created by terraform init (#1701) * Delete .terraform.lock.hcl prior to terraform init if it is not staged * Changes suggeted by reviewer --- .../events/events_controller_e2e_test.go | 169 +++++++++++++++++- .../null_provider_lockfile_old_version | 20 +++ .../exp-output-autoplan.txt | 48 +++++ .../simple-with-lockfile/exp-output-plan.txt | 48 +++++ .../test-repos/simple-with-lockfile/main.tf | 18 ++ server/core/runtime/init_step_runner.go | 30 ++-- server/core/runtime/init_step_runner_test.go | 73 +++++++- server/events/mocks/mock_working_dir.go | 85 +++++++++ server/events/runtime/common/common.go | 30 +++- server/events/runtime/common/common_test.go | 61 +++++++ 10 files changed, 557 insertions(+), 25 deletions(-) create mode 100644 server/controllers/events/testfixtures/null_provider_lockfile_old_version create mode 100644 server/controllers/events/testfixtures/test-repos/simple-with-lockfile/exp-output-autoplan.txt create mode 100644 server/controllers/events/testfixtures/test-repos/simple-with-lockfile/exp-output-plan.txt create mode 100644 server/controllers/events/testfixtures/test-repos/simple-with-lockfile/main.tf diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 601e03bc15..baba9f05c5 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -67,8 +67,8 @@ func TestGitHubWorkflow(t *testing.T) { if testing.Short() { t.SkipNow() } - // Ensure we have >= TF 0.12 locally. - ensureRunning012(t) + // Ensure we have >= TF 0.14 locally. + ensureRunning014(t) cases := []struct { Description string @@ -456,12 +456,165 @@ func TestGitHubWorkflow(t *testing.T) { } } +func TestSimlpleWorkflow_terraformLockFile(t *testing.T) { + + if testing.Short() { + t.SkipNow() + } + // Ensure we have >= TF 0.14 locally. + ensureRunning014(t) + + cases := []struct { + Description string + // RepoDir is relative to testfixtures/test-repos. + RepoDir string + // ModifiedFiles are the list of files that have been modified in this + // pull request. + ModifiedFiles []string + // ExpAutoplan is true if we expect Atlantis to autoplan. + ExpAutoplan bool + // Comments are what our mock user writes to the pull request. + Comments []string + // ExpReplies is a list of files containing the expected replies that + // Atlantis writes to the pull request in order. A reply from a parallel operation + // will be matched using a substring check. + ExpReplies [][]string + // LockFileTracked deterims if the `.terraform.lock.hcl` file is tracked in git + // if this is true we dont expect the lockfile to be modified by terraform init + // if false we expect the lock file to be updated + LockFileTracked bool + }{ + { + Description: "simple with plan comment lockfile staged", + RepoDir: "simple-with-lockfile", + ModifiedFiles: []string{"main.tf"}, + ExpAutoplan: true, + Comments: []string{ + "atlantis plan", + }, + ExpReplies: [][]string{ + {"exp-output-autoplan.txt"}, + {"exp-output-plan.txt"}, + }, + LockFileTracked: true, + }, + { + Description: "simple with plan comment lockfile not staged", + RepoDir: "simple-with-lockfile", + ModifiedFiles: []string{"main.tf"}, + Comments: []string{ + "atlantis plan", + }, + ExpReplies: [][]string{ + {"exp-output-autoplan.txt"}, + {"exp-output-plan.txt"}, + }, + LockFileTracked: false, + }, + } + for _, c := range cases { + t.Run(c.Description, func(t *testing.T) { + RegisterMockTestingT(t) + + // reset userConfig + userConfig = server.UserConfig{} + userConfig.DisableApply = true + + ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir) + // Set the repo to be cloned through the testing backdoor. + repoDir, headSHA, cleanup := initializeRepo(t, c.RepoDir) + defer cleanup() + + oldLockFilePath, err := filepath.Abs(filepath.Join("testfixtures", "null_provider_lockfile_old_version")) + Ok(t, err) + oldLockFileContent, err := ioutil.ReadFile(oldLockFilePath) + Ok(t, err) + + if c.LockFileTracked { + runCmd(t, "", "cp", oldLockFilePath, fmt.Sprintf("%s/.terraform.lock.hcl", repoDir)) + runCmd(t, repoDir, "git", "add", ".terraform.lock.hcl") + runCmd(t, repoDir, "git", "commit", "-am", "stage .terraform.lock.hcl") + } + + atlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf("file://%s", repoDir) + + // Setup test dependencies. + w := httptest.NewRecorder() + When(githubGetter.GetPullRequest(AnyRepo(), AnyInt())).ThenReturn(GitHubPullRequestParsed(headSHA), nil) + When(vcsClient.GetModifiedFiles(AnyRepo(), matchers.AnyModelsPullRequest())).ThenReturn(c.ModifiedFiles, nil) + + // First, send the open pull request event which triggers autoplan. + pullOpenedReq := GitHubPullRequestOpenedEvent(t, headSHA) + ctrl.Post(w, pullOpenedReq) + ResponseContains(t, w, 200, "Processing...") + + // check lock file content + actualLockFileContent, err := ioutil.ReadFile(fmt.Sprintf("%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl", atlantisWorkspace.DataDir)) + Ok(t, err) + if c.LockFileTracked { + if string(oldLockFileContent) != string(actualLockFileContent) { + t.Error("Expected terraform.lock.hcl file not to be different as it has been staged") + t.FailNow() + } + } else { + if string(oldLockFileContent) == string(actualLockFileContent) { + t.Error("Expected terraform.lock.hcl file to be different as it should have been updated") + t.FailNow() + } + } + + if !c.LockFileTracked { + // replace the lock file generated by the previous init to simulate + // dependcies needing updating in a latter plan + runCmd(t, "", "cp", oldLockFilePath, fmt.Sprintf("%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl", atlantisWorkspace.DataDir)) + } + + // Now send any other comments. + for _, comment := range c.Comments { + commentReq := GitHubCommentEvent(t, comment) + w = httptest.NewRecorder() + ctrl.Post(w, commentReq) + ResponseContains(t, w, 200, "Processing...") + } + + // check lock file content + actualLockFileContent, err = ioutil.ReadFile(fmt.Sprintf("%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl", atlantisWorkspace.DataDir)) + Ok(t, err) + if c.LockFileTracked { + if string(oldLockFileContent) != string(actualLockFileContent) { + t.Error("Expected terraform.lock.hcl file not to be different as it has been staged") + t.FailNow() + } + } else { + if string(oldLockFileContent) == string(actualLockFileContent) { + t.Error("Expected terraform.lock.hcl file to be different as it should have been updated") + t.FailNow() + } + } + + // Let's verify the pre-workflow hook was called for each comment including the pull request opened event + mockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(runtimematchers.AnyModelsPreWorkflowHookCommandContext(), EqString("some dummy command"), AnyString()) + + // Now we're ready to verify Atlantis made all the comments back (or + // replies) that we expect. We expect each plan to have 1 comment, + // and apply have 1 for each comment plus one for the locks deleted at the + // end. + + _, _, actReplies, _ := vcsClient.VerifyWasCalled(Times(2)).CreateComment(AnyRepo(), AnyInt(), AnyString(), AnyString()).GetAllCapturedArguments() + Assert(t, len(c.ExpReplies) == len(actReplies), "missing expected replies, got %d but expected %d", len(actReplies), len(c.ExpReplies)) + for i, expReply := range c.ExpReplies { + assertCommentEquals(t, expReply, actReplies[i], c.RepoDir, false) + } + }) + } +} + func TestGitHubWorkflowWithPolicyCheck(t *testing.T) { if testing.Short() { t.SkipNow() } - // Ensure we have >= TF 0.12 locally. - ensureRunning012(t) + // Ensure we have >= TF 0.14 locally. + ensureRunning014(t) // Ensure we have >= Conftest 0.21 locally. ensureRunningConftest(t) @@ -1135,11 +1288,11 @@ func ensureRunningConftest(t *testing.T) { } } -// Will fail test if terraform isn't in path and isn't version >= 0.12 -func ensureRunning012(t *testing.T) { +// Will fail test if terraform isn't in path and isn't version >= 0.14 +func ensureRunning014(t *testing.T) { localPath, err := exec.LookPath("terraform") if err != nil { - t.Log("terraform >= 0.12 must be installed to run this test") + t.Log("terraform >= 0.14 must be installed to run this test") t.FailNow() } versionOutBytes, err := exec.Command(localPath, "version").Output() // #nosec @@ -1155,7 +1308,7 @@ func ensureRunning012(t *testing.T) { } localVersion, err := version.NewVersion(match[1]) Ok(t, err) - minVersion, err := version.NewVersion("0.12.0") + minVersion, err := version.NewVersion("0.14.0") Ok(t, err) if localVersion.LessThan(minVersion) { t.Logf("must have terraform version >= %s, you have %s", minVersion, localVersion) diff --git a/server/controllers/events/testfixtures/null_provider_lockfile_old_version b/server/controllers/events/testfixtures/null_provider_lockfile_old_version new file mode 100644 index 0000000000..09c858af04 --- /dev/null +++ b/server/controllers/events/testfixtures/null_provider_lockfile_old_version @@ -0,0 +1,20 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.0.0" + constraints = "3.0.0" + hashes = [ + "h1:ysHGBhBNkIiJLEpthB/IVCLpA1Qoncp3KbCTFGFZTO0=", + "zh:05fb7eab469324c97e9b73a61d2ece6f91de4e9b493e573bfeda0f2077bc3a4c", + "zh:1688aa91885a395c4ae67636d411475d0b831e422e005dcf02eedacaafac3bb4", + "zh:24a0b1292e3a474f57c483a7a4512d797e041bc9c2fbaac42fe12e86a7fb5a3c", + "zh:2fc951bd0d1b9b23427acc93be09b6909d72871e464088171da60fbee4fdde03", + "zh:6db825759425599a326385a68acc6be2d9ba0d7d6ef587191d0cdc6daef9ac63", + "zh:85985763d02618993c32c294072cc6ec51f1692b803cb506fcfedca9d40eaec9", + "zh:a53186599c57058be1509f904da512342cfdc5d808efdaf02dec15f0f3cb039a", + "zh:c2e07b49b6efa676bdc7b00c06333ea1792a983a5720f9e2233db27323d2707c", + "zh:cdc8fe1096103cf5374751e2e8408ec4abd2eb67d5a1c5151fe2c7ecfd525bef", + "zh:dbdef21df0c012b0d08776f3d4f34eb0f2f229adfde07ff252a119e52c0f65b7", + ] +} diff --git a/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/exp-output-autoplan.txt b/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/exp-output-autoplan.txt new file mode 100644 index 0000000000..b301024b0c --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/exp-output-autoplan.txt @@ -0,0 +1,48 @@ +Ran Plan for dir: `.` workspace: `default` + +
Show Output + +```diff + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: ++ create + +Terraform will perform the following actions: + + # null_resource.simple[0] will be created ++ resource "null_resource" "simple" { + + id = (known after apply) + } + + # null_resource.simple2 will be created ++ resource "null_resource" "simple2" { + + id = (known after apply) + } + + # null_resource.simple3 will be created ++ resource "null_resource" "simple3" { + + id = (known after apply) + } + +Plan: 3 to add, 0 to change, 0 to destroy. + +Changes to Outputs: ++ var = "default" ++ workspace = "default" + +``` + +* :arrow_forward: To **apply** this plan, comment: + * `atlantis apply -d .` +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :repeat: To **plan** this project again, comment: + * `atlantis plan -d .` +
+Plan: 3 to add, 0 to change, 0 to destroy. + +--- +* :fast_forward: To **apply** all unapplied plans from this pull request, comment: + * `atlantis apply` +* :put_litter_in_its_place: To delete all plans and locks for the PR, comment: + * `atlantis unlock` diff --git a/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/exp-output-plan.txt b/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/exp-output-plan.txt new file mode 100644 index 0000000000..b301024b0c --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/exp-output-plan.txt @@ -0,0 +1,48 @@ +Ran Plan for dir: `.` workspace: `default` + +
Show Output + +```diff + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: ++ create + +Terraform will perform the following actions: + + # null_resource.simple[0] will be created ++ resource "null_resource" "simple" { + + id = (known after apply) + } + + # null_resource.simple2 will be created ++ resource "null_resource" "simple2" { + + id = (known after apply) + } + + # null_resource.simple3 will be created ++ resource "null_resource" "simple3" { + + id = (known after apply) + } + +Plan: 3 to add, 0 to change, 0 to destroy. + +Changes to Outputs: ++ var = "default" ++ workspace = "default" + +``` + +* :arrow_forward: To **apply** this plan, comment: + * `atlantis apply -d .` +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :repeat: To **plan** this project again, comment: + * `atlantis plan -d .` +
+Plan: 3 to add, 0 to change, 0 to destroy. + +--- +* :fast_forward: To **apply** all unapplied plans from this pull request, comment: + * `atlantis apply` +* :put_litter_in_its_place: To delete all plans and locks for the PR, comment: + * `atlantis unlock` diff --git a/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/main.tf b/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/main.tf new file mode 100644 index 0000000000..2394ee4a7a --- /dev/null +++ b/server/controllers/events/testfixtures/test-repos/simple-with-lockfile/main.tf @@ -0,0 +1,18 @@ +resource "null_resource" "simple" { + count = 1 +} + +resource "null_resource" "simple2" {} +resource "null_resource" "simple3" {} + +variable "var" { + default = "default" +} + +output "var" { + value = var.var +} + +output "workspace" { + value = terraform.workspace +} diff --git a/server/core/runtime/init_step_runner.go b/server/core/runtime/init_step_runner.go index 6d85758238..77e1daad47 100644 --- a/server/core/runtime/init_step_runner.go +++ b/server/core/runtime/init_step_runner.go @@ -16,6 +16,24 @@ type InitStepRunner struct { } func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []string, path string, envs map[string]string) (string, error) { + lockFileName := ".terraform.lock.hcl" + terraformLockfilePath := filepath.Join(path, lockFileName) + terraformLockFileTracked, err := common.IsFileTracked(path, lockFileName) + if err != nil { + ctx.Log.Warn("Error checking if %s is tracked in %s", lockFileName, path) + + } + // If .terraform.lock.hcl is not tracked in git and it exists prior to init + // delete it as it probably has been created by a previous run of + // terraform init + if common.FileExists(terraformLockfilePath) && !terraformLockFileTracked { + ctx.Log.Debug("Deleting `%s` that was generated by previous terraform init", terraformLockfilePath) + delErr := os.Remove(terraformLockfilePath) + if delErr != nil { + ctx.Log.Info("Error Deleting `%s`", lockFileName) + } + } + tfVersion := i.DefaultTFVersion if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion @@ -33,8 +51,7 @@ func (i *InitStepRunner) Run(ctx models.ProjectCommandContext, extraArgs []strin terraformInitArgs = append(terraformInitArgs, "-no-color") - lockfilePath := filepath.Join(path, ".terraform.lock.hcl") - if MustConstraint("< 0.14.0").Check(tfVersion) || fileDoesNotExists(lockfilePath) { + if MustConstraint("< 0.14.0").Check(tfVersion) || !common.FileExists(terraformLockfilePath) { terraformInitArgs = append(terraformInitArgs, "-upgrade") } @@ -50,12 +67,3 @@ 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 -} diff --git a/server/core/runtime/init_step_runner_test.go b/server/core/runtime/init_step_runner_test.go index fff48728c0..496f13f7a7 100644 --- a/server/core/runtime/init_step_runner_test.go +++ b/server/core/runtime/init_step_runner_test.go @@ -2,7 +2,9 @@ package runtime_test import ( "io/ioutil" + "os/exec" "path/filepath" + "strings" "testing" version "github.com/hashicorp/go-version" @@ -98,12 +100,17 @@ func TestRun_ShowInitOutputOnError(t *testing.T) { Equals(t, "output", output) } -func TestRun_InitOmitsUpgradeFlagIfLockFilePresent(t *testing.T) { - tmpDir, cleanup := TempDir(t) +func TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) { + // Initialize the git repo. + repoDir, cleanup := initRepo(t) defer cleanup() - lockFilePath := filepath.Join(tmpDir, ".terraform.lock.hcl") + + lockFilePath := filepath.Join(repoDir, ".terraform.lock.hcl") err := ioutil.WriteFile(lockFilePath, nil, 0600) Ok(t, err) + // commit lock file + runCmd(t, repoDir, "git", "add", ".terraform.lock.hcl") + runCmd(t, repoDir, "git", "commit", "-m", "add .terraform.lock.hcl") RegisterMockTestingT(t) terraform := mocks.NewMockClient() @@ -122,13 +129,13 @@ func TestRun_InitOmitsUpgradeFlagIfLockFilePresent(t *testing.T) { Workspace: "workspace", RepoRelDir: ".", Log: logger, - }, []string{"extra", "args"}, tmpDir, map[string]string(nil)) + }, []string{"extra", "args"}, repoDir, 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") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") } func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) { @@ -260,3 +267,59 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) { }) } } + +func TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) { + // Initialize the git repo. + repoDir, cleanup := initRepo(t) + defer cleanup() + + lockFilePath := filepath.Join(repoDir, ".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"}, repoDir, 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, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") +} + +func runCmd(t *testing.T, dir string, name string, args ...string) string { + t.Helper() + cpCmd := exec.Command(name, args...) + cpCmd.Dir = dir + cpOut, err := cpCmd.CombinedOutput() + Assert(t, err == nil, "err running %q: %s", strings.Join(append([]string{name}, args...), " "), cpOut) + return string(cpOut) +} + +func initRepo(t *testing.T) (string, func()) { + repoDir, cleanup := TempDir(t) + runCmd(t, repoDir, "git", "init") + runCmd(t, repoDir, "touch", ".gitkeep") + runCmd(t, repoDir, "git", "add", ".gitkeep") + runCmd(t, repoDir, "git", "config", "--local", "user.email", "atlantisbot@runatlantis.io") + runCmd(t, repoDir, "git", "config", "--local", "user.name", "atlantisbot") + runCmd(t, repoDir, "git", "commit", "-m", "initial commit") + runCmd(t, repoDir, "git", "branch", "branch") + return repoDir, cleanup +} diff --git a/server/events/mocks/mock_working_dir.go b/server/events/mocks/mock_working_dir.go index 4e743f8900..d9aca75a20 100644 --- a/server/events/mocks/mock_working_dir.go +++ b/server/events/mocks/mock_working_dir.go @@ -120,6 +120,25 @@ func (mock *MockWorkingDir) DeleteForWorkspace(r models.Repo, p models.PullReque return ret0 } +func (mock *MockWorkingDir) IsFileTracked(log logging.SimpleLogging, cloneDir string, filename string) (bool, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockWorkingDir().") + } + params := []pegomock.Param{log, cloneDir, filename} + result := pegomock.GetGenericMockFrom(mock).Invoke("IsFileTracked", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 bool + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + func (mock *MockWorkingDir) VerifyWasCalledOnce() *VerifierMockWorkingDir { return &VerifierMockWorkingDir{ mock: mock, @@ -231,6 +250,37 @@ func (c *MockWorkingDir_GetWorkingDir_OngoingVerification) GetAllCapturedArgumen return } +func (verifier *VerifierMockWorkingDir) HasDiverged(log logging.SimpleLogging, cloneDir string) *MockWorkingDir_HasDiverged_OngoingVerification { + params := []pegomock.Param{log, cloneDir} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasDiverged", params, verifier.timeout) + return &MockWorkingDir_HasDiverged_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockWorkingDir_HasDiverged_OngoingVerification struct { + mock *MockWorkingDir + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockWorkingDir_HasDiverged_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string) { + log, cloneDir := c.GetAllCapturedArguments() + return log[len(log)-1], cloneDir[len(cloneDir)-1] +} + +func (c *MockWorkingDir_HasDiverged_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + _param1 = make([]string, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(string) + } + } + return +} + func (verifier *VerifierMockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) *MockWorkingDir_GetPullDir_OngoingVerification { params := []pegomock.Param{r, p} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullDir", params, verifier.timeout) @@ -327,3 +377,38 @@ func (c *MockWorkingDir_DeleteForWorkspace_OngoingVerification) GetAllCapturedAr } return } + +func (verifier *VerifierMockWorkingDir) IsFileTracked(log logging.SimpleLogging, cloneDir string, filename string) *MockWorkingDir_IsFileTracked_OngoingVerification { + params := []pegomock.Param{log, cloneDir, filename} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsFileTracked", params, verifier.timeout) + return &MockWorkingDir_IsFileTracked_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockWorkingDir_IsFileTracked_OngoingVerification struct { + mock *MockWorkingDir + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockWorkingDir_IsFileTracked_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, string, string) { + log, cloneDir, filename := c.GetAllCapturedArguments() + return log[len(log)-1], cloneDir[len(cloneDir)-1], filename[len(filename)-1] +} + +func (c *MockWorkingDir_IsFileTracked_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []string, _param2 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + _param1 = make([]string, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(string) + } + _param2 = make([]string, len(c.methodInvocations)) + for u, param := range params[2] { + _param2[u] = param.(string) + } + } + return +} diff --git a/server/events/runtime/common/common.go b/server/events/runtime/common/common.go index d459e0e043..cc11f30768 100644 --- a/server/events/runtime/common/common.go +++ b/server/events/runtime/common/common.go @@ -1,6 +1,10 @@ package common -import "strings" +import ( + "os" + "os/exec" + "strings" +) // Looks for any argument in commandArgs that has been overridden by an entry in extra args and replaces them // any extraArgs that are not used as overrides are added yo the end of the final string slice @@ -53,6 +57,30 @@ func DeDuplicateExtraArgs(commandArgs []string, extraArgs []string) []string { return finalArgs } +// returns true if a file at the passed path exists +func FileExists(path string) bool { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +// returns true if the given file is tracked by git +func IsFileTracked(cloneDir string, filename string) (bool, error) { + cmd := exec.Command("git", "ls-files", filename) + cmd.Dir = cloneDir + + output, err := cmd.CombinedOutput() + + if err != nil { + return false, err + } + return len(output) > 0, nil + +} + func stringInSlice(stringSlice []string, target string) bool { for _, value := range stringSlice { if value == target { diff --git a/server/events/runtime/common/common_test.go b/server/events/runtime/common/common_test.go index d426eaf0ed..5df4d1a98b 100644 --- a/server/events/runtime/common/common_test.go +++ b/server/events/runtime/common/common_test.go @@ -1,8 +1,12 @@ package common import ( + "os/exec" "reflect" + "strings" "testing" + + . "github.com/runatlantis/atlantis/testing" ) func Test_DeDuplicateExtraArgs(t *testing.T) { @@ -84,3 +88,60 @@ func Test_DeDuplicateExtraArgs(t *testing.T) { }) } } + +func runCmd(t *testing.T, dir string, name string, args ...string) string { + t.Helper() + cpCmd := exec.Command(name, args...) + cpCmd.Dir = dir + cpOut, err := cpCmd.CombinedOutput() + Assert(t, err == nil, "err running %q: %s", strings.Join(append([]string{name}, args...), " "), cpOut) + return string(cpOut) +} + +func initRepo(t *testing.T) (string, func()) { + repoDir, cleanup := TempDir(t) + runCmd(t, repoDir, "git", "init") + runCmd(t, repoDir, "touch", ".gitkeep") + runCmd(t, repoDir, "git", "add", ".gitkeep") + runCmd(t, repoDir, "git", "config", "--local", "user.email", "atlantisbot@runatlantis.io") + runCmd(t, repoDir, "git", "config", "--local", "user.name", "atlantisbot") + runCmd(t, repoDir, "git", "commit", "-m", "initial commit") + runCmd(t, repoDir, "git", "branch", "branch") + return repoDir, cleanup +} + +func TestIsFileTracked(t *testing.T) { + // Initialize the git repo. + repoDir, cleanup := initRepo(t) + defer cleanup() + + // file1 should not be tracked + tracked, err := IsFileTracked(repoDir, "file1") + Ok(t, err) + Equals(t, tracked, false) + + // stage file1 + runCmd(t, repoDir, "touch", "file1") + runCmd(t, repoDir, "git", "add", "file1") + runCmd(t, repoDir, "git", "commit", "-m", "add file1") + + // file1 should be tracked + tracked, err = IsFileTracked(repoDir, "file1") + Ok(t, err) + Equals(t, tracked, true) + + // .terraform.lock.hcl should not be tracked + tracked, err = IsFileTracked(repoDir, ".terraform.lock.hcl") + Ok(t, err) + Equals(t, tracked, false) + + // stage .terraform.lock.hcl + runCmd(t, repoDir, "touch", ".terraform.lock.hcl") + runCmd(t, repoDir, "git", "add", ".terraform.lock.hcl") + runCmd(t, repoDir, "git", "commit", "-m", "add .terraform.lock.hcl") + + // file1 should be tracked + tracked, err = IsFileTracked(repoDir, ".terraform.lock.hcl") + Ok(t, err) + Equals(t, tracked, true) +}