diff --git a/.gitignore b/.gitignore index 6e464bdff1..c23cbfed9c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ node_modules/ helm/test-values.yaml *.swp golangci-lint -atlantis \ No newline at end of file +atlantis diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab034757e3..2cf92271ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ docker run --rm -v $(pwd):/go/src/github.com/runatlantis/atlantis -w /go/src/git ## Calling Your Local Atlantis From GitHub - Create a test terraform repository in your GitHub. -- Create a personal access token for Atlantis. See [Create a GitHub token](https://github.com/runatlantis/atlantis#create-a-github-token). +- Create a personal access token for Atlantis. See [Create a GitHub token](https://github.com/runatlantis/atlantis/tree/master/runatlantis.io/docs/access-credentials.md#generating-an-access-token). - Start Atlantis in server mode using that token: ``` atlantis server --gh-user --gh-token --repo-whitelist --gh-webhook-secret --log-level debug @@ -53,7 +53,7 @@ atlantis server --gh-user --gh-token --repo-whiteli ``` ngrok http 4141 ``` -- Create a Webhook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis#add-github-webhook). +- Create a Webhook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis/blob/master/runatlantis.io/docs/configuring-webhooks.md#configuring-webhooks). - Create a pull request and type `atlantis help`. You should see the request in the `ngrok` and Atlantis logs and you should also see Atlantis comment back. ## Code Style diff --git a/server/events/command_runner.go b/server/events/command_runner.go index c029adcf2f..876f477cde 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -105,6 +105,7 @@ type DefaultCommandRunner struct { WorkingDir WorkingDir DB *db.BoltDB Drainer *Drainer + DeleteLockCommand DeleteLockCommand } // RunAutoplanCommand runs plan when a pull request is opened or updated. @@ -247,6 +248,19 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead return } + if cmd.Name == models.UnlockCommand { + vcsMessage := "`All Atlantis locks for this PR have been released - and plans discarded`" + _, err := c.DeleteLockCommand.DeleteLocksByPull(baseRepo.FullName, pullNum) + if err != nil { + vcsMessage = "Failed to delete PR locks" + log.Err("failed to delete locks by pull %s", err.Error()) + } + if commentErr := c.VCSClient.CreateComment(baseRepo, pullNum, vcsMessage); commentErr != nil { + log.Err("unable to comment: %s", commentErr) + } + return + } + if cmd.CommandName() == models.ApplyCommand { // Get the mergeable status before we set any build statuses of our own. // We do this here because when we set a "Pending" status, if users have @@ -302,6 +316,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead c.deletePlans(ctx) result.PlansDeleted = true } + c.updatePull( ctx, cmd, diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 24d6d4ee65..9fbf2f013a 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -26,6 +26,7 @@ import ( . "github.com/petergtz/pegomock" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/mocks" + eventmocks "github.com/runatlantis/atlantis/server/events/mocks" "github.com/runatlantis/atlantis/server/events/mocks/matchers" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/models/fixtures" @@ -45,6 +46,7 @@ var pullLogger *logging.SimpleLogger var workingDir events.WorkingDir var pendingPlanFinder *mocks.MockPendingPlanFinder var drainer *events.Drainer +var deleteLockCommand *mocks.MockDeleteLockCommand func setup(t *testing.T) *vcsmocks.MockClient { RegisterMockTestingT(t) @@ -60,6 +62,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { workingDir = mocks.NewMockWorkingDir() pendingPlanFinder = mocks.NewMockPendingPlanFinder() drainer = &events.Drainer{} + deleteLockCommand = eventmocks.NewMockDeleteLockCommand() When(logger.GetLevel()).ThenReturn(logging.Info) When(logger.NewLogger("runatlantis/atlantis#1", true, logging.Info)). ThenReturn(pullLogger) @@ -80,6 +83,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { WorkingDir: workingDir, DisableApplyAll: false, Drainer: drainer, + DeleteLockCommand: deleteLockCommand, } return vcsClient } @@ -200,6 +204,42 @@ func TestRunCommentCommand_ClosedPull(t *testing.T) { vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "Atlantis commands can't be run on closed pull requests") } +func TestRunUnlockCommand_VCSComment(t *testing.T) { + t.Log("if unlock PR command is run, atlantis should" + + " invoke the delete command and comment on PR accordingly") + + vcsClient := setup(t) + pull := &github.PullRequest{ + State: github.String("open"), + } + modelPull := models.PullRequest{State: models.OpenPullState} + When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) + When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) + + ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.UnlockCommand}) + + deleteLockCommand.VerifyWasCalledOnce().DeleteLocksByPull(fixtures.GithubRepo.FullName, fixtures.Pull.Num) + vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, fixtures.Pull.Num, "`All Atlantis locks for this PR have been released - and plans discarded`") +} + +func TestRunUnlockCommandFail_VCSComment(t *testing.T) { + t.Log("if unlock PR command is run and delete fails, atlantis should" + + " invoke comment on PR with error message") + + vcsClient := setup(t) + pull := &github.PullRequest{ + State: github.String("open"), + } + modelPull := models.PullRequest{State: models.OpenPullState} + When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil) + When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil) + When(deleteLockCommand.DeleteLocksByPull(fixtures.GithubRepo.FullName, fixtures.Pull.Num)).ThenReturn(models.DeleteLockFail, errors.New("err")) + + ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.UnlockCommand}) + + vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, fixtures.Pull.Num, "Failed to delete PR locks") +} + // Test that if one plan fails and we are using automerge, that // we delete the plans. func TestRunAutoplanCommand_DeletePlans(t *testing.T) { diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index 203ab7f58a..ba608d24ea 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -156,8 +156,8 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen return CommentParseResult{CommentResponse: HelpComment} } - // Need to have a plan or apply at this point. - if !e.stringInSlice(command, []string{models.PlanCommand.String(), models.ApplyCommand.String()}) { + // Need to have a plan, apply or unlock at this point. + if !e.stringInSlice(command, []string{models.PlanCommand.String(), models.ApplyCommand.String(), models.UnlockCommand.String()}) { return CommentParseResult{CommentResponse: fmt.Sprintf("```\nError: unknown command %q.\nRun 'atlantis --help' for usage.\n```", command)} } @@ -186,6 +186,11 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Apply the plan for this directory, relative to root of repo, ex. 'child/dir'.") flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Apply the plan for this project. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", yaml.AtlantisYAMLFilename)) flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") + case models.UnlockCommand.String(): + name = models.UnlockCommand + flagSet = pflag.NewFlagSet(models.UnlockCommand.String(), pflag.ContinueOnError) + flagSet.SetOutput(ioutil.Discard) + default: return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", command)} } @@ -197,6 +202,9 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen return CommentParseResult{CommentResponse: fmt.Sprintf("```\nUsage of %s:\n%s\n```", command, flagSet.FlagUsagesWrapped(usagesCols))} } if err != nil { + if command == models.UnlockCommand.String() { + return CommentParseResult{CommentResponse: UnlockUsage} + } return CommentParseResult{CommentResponse: e.errMarkdown(err.Error(), command, flagSet)} } @@ -342,11 +350,13 @@ Examples: atlantis apply -d . -w staging Commands: - plan Runs 'terraform plan' for the changes in this pull request. - To plan a specific project, use the -d, -w and -p flags. - apply Runs 'terraform apply' on all unapplied plans from this pull request. - To only apply a specific plan, use the -d, -w and -p flags. - help View help. + plan Runs 'terraform plan' for the changes in this pull request. + To plan a specific project, use the -d, -w and -p flags. + apply Runs 'terraform apply' on all unapplied plans from this pull request. + To only apply a specific plan, use the -d, -w and -p flags. + unlock Removes all atlantis locks and discards all plans for this PR. + To unlock a specific plan you can use the Atlantis UI. + help View help. Flags: -h, --help help for atlantis @@ -357,3 +367,14 @@ Use "atlantis [command] --help" for more information about a command.` + // DidYouMeanAtlantisComment is the comment we add to the pull request when // someone runs a command with terraform instead of atlantis. var DidYouMeanAtlantisComment = "Did you mean to use `atlantis` instead of `terraform`?" + +// UnlockUsage is the comment we add to the pull request when someone runs +// `atlantis unlock` with flags. + +var UnlockUsage = "`Usage of unlock:`\n\n ```cmake\n" + + `atlantis unlock + + Unlocks the entire PR and discards all plans in this PR. + Arguments or flags are not supported at the moment. + If you need to unlock a specific project please use the atlantis UI.` + + "\n```" diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index b9c203f2aa..fdaf29ab51 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -126,6 +126,13 @@ func TestParse_UnusedArguments(t *testing.T) { } } +func TestParse_UnknownShorthandFlag(t *testing.T) { + comment := "atlantis unlock -d ." + r := commentParser.Parse(comment, models.Github) + + Equals(t, UnlockUsage, r.CommentResponse) +} + func TestParse_DidYouMeanAtlantis(t *testing.T) { t.Log("given a comment that should result in a 'did you mean atlantis'" + "response, should set CommentParseResult.CommentResult") @@ -693,3 +700,10 @@ var ApplyUsage = `Usage of apply: --verbose Append Atlantis log to comment. -w, --workspace string Apply the plan for this Terraform workspace. ` +var UnlockUsage = "`Usage of unlock:`\n\n ```cmake\n" + + `atlantis unlock + + Unlocks the entire PR and discards all plans in this PR. + Arguments or flags are not supported at the moment. + If you need to unlock a specific project please use the atlantis UI.` + + "\n```" diff --git a/server/events/delete_lock_command.go b/server/events/delete_lock_command.go new file mode 100644 index 0000000000..cb3feb4b85 --- /dev/null +++ b/server/events/delete_lock_command.go @@ -0,0 +1,79 @@ +package events + +import ( + "github.com/runatlantis/atlantis/server/events/db" + "github.com/runatlantis/atlantis/server/events/locking" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/logging" +) + +//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_delete_lock_command.go DeleteLockCommand + +// DeleteLockCommand is the first step after a command request has been parsed. +type DeleteLockCommand interface { + DeleteLock(id string) (models.DeleteLockCommandResult, *models.ProjectLock, error) + DeleteLocksByPull(repoFullName string, pullNum int) (models.DeleteLockCommandResult, error) +} + +// DefaultDeleteLockCommand deletes a specific lock after a request from the LocksController. +type DefaultDeleteLockCommand struct { + Locker locking.Locker + Logger *logging.SimpleLogger + WorkingDir WorkingDir + WorkingDirLocker WorkingDirLocker + DB *db.BoltDB +} + +// DeleteLock handles deleting the lock at id +func (l *DefaultDeleteLockCommand) DeleteLock(id string) (models.DeleteLockCommandResult, *models.ProjectLock, error) { + lock, err := l.Locker.Unlock(id) + if err != nil { + return models.DeleteLockFail, nil, err + } + if lock == nil { + return models.DeleteLockNotFound, nil, nil + } + + l.deleteWorkingDir(*lock) + + return models.DeleteLockSuccess, lock, nil +} + +// DeleteLocksByPull handles deleting all locks for the pull request +func (l *DefaultDeleteLockCommand) DeleteLocksByPull(repoFullName string, pullNum int) (models.DeleteLockCommandResult, error) { + locks, err := l.Locker.UnlockByPull(repoFullName, pullNum) + if err != nil { + return models.DeleteLockFail, err + } + if len(locks) == 0 { + return models.DeleteLockNotFound, nil + } + + for i := 0; i < len(locks); i++ { + lock := locks[i] + l.deleteWorkingDir(lock) + } + + return models.DeleteLockSuccess, nil +} + +func (l *DefaultDeleteLockCommand) deleteWorkingDir(lock models.ProjectLock) { + // NOTE: Because BaseRepo was added to the PullRequest model later, previous + // installations of Atlantis will have locks in their DB that do not have + // this field on PullRequest. We skip deleting the working dir in this case. + if lock.Pull.BaseRepo != (models.Repo{}) { + unlock, err := l.WorkingDirLocker.TryLock(lock.Pull.BaseRepo.FullName, lock.Pull.Num, lock.Workspace) + if err != nil { + l.Logger.Err("unable to obtain working dir lock when trying to delete old plans: %s", err) + } else { + defer unlock() + // nolint: vetshadow + if err := l.WorkingDir.DeleteForWorkspace(lock.Pull.BaseRepo, lock.Pull, lock.Workspace); err != nil { + l.Logger.Err("unable to delete workspace: %s", err) + } + } + if err := l.DB.DeleteProjectStatus(lock.Pull, lock.Workspace, lock.Project.Path); err != nil { + l.Logger.Err("unable to delete project status: %s", err) + } + } +} diff --git a/server/events/delete_lock_command_test.go b/server/events/delete_lock_command_test.go new file mode 100644 index 0000000000..6916356cfe --- /dev/null +++ b/server/events/delete_lock_command_test.go @@ -0,0 +1,132 @@ +package events_test + +import ( + "errors" + "testing" + + . "github.com/petergtz/pegomock" + "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/events/db" + lockmocks "github.com/runatlantis/atlantis/server/events/locking/mocks" + "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +func TestDeleteLock_LockerErr(t *testing.T) { + t.Log("If there is an error retrieving the lock, returned a failed status") + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.Unlock("id")).ThenReturn(nil, errors.New("err")) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + } + deleteLockResult, _, _ := dlc.DeleteLock("id") + Equals(t, models.DeleteLockFail, deleteLockResult) +} + +func TestDeleteLock_None(t *testing.T) { + t.Log("If there is no lock at that ID return no lock found") + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.Unlock("id")).ThenReturn(nil, nil) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + } + deleteLockResult, _, _ := dlc.DeleteLock("id") + Equals(t, models.DeleteLockNotFound, deleteLockResult) +} + +func TestDeleteLock_OldFormat(t *testing.T) { + t.Log("If the lock doesn't have BaseRepo set it is deleted successfully") + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.Unlock("id")).ThenReturn(&models.ProjectLock{}, nil) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + } + deleteLockResult, _, _ := dlc.DeleteLock("id") + Equals(t, models.DeleteLockSuccess, deleteLockResult) +} + +func TestDeleteLock_Success(t *testing.T) { + t.Log("Delete lock deletes successfully the working dir") + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.Unlock("id")).ThenReturn(&models.ProjectLock{}, nil) + workingDir := events.NewMockWorkingDir() + workingDirLocker := events.NewDefaultWorkingDirLocker() + pull := models.PullRequest{ + BaseRepo: models.Repo{FullName: "owner/repo"}, + } + When(l.Unlock("id")).ThenReturn(&models.ProjectLock{ + Pull: pull, + Workspace: "workspace", + Project: models.Project{ + Path: "path", + RepoFullName: "owner/repo", + }, + }, nil) + tmp, cleanup := TempDir(t) + defer cleanup() + db, err := db.New(tmp) + Ok(t, err) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + DB: db, + WorkingDirLocker: workingDirLocker, + WorkingDir: workingDir, + } + deleteLockResult, _, _ := dlc.DeleteLock("id") + Equals(t, models.DeleteLockSuccess, deleteLockResult) + workingDir.VerifyWasCalledOnce().DeleteForWorkspace(pull.BaseRepo, pull, "workspace") +} + +func TestDeleteLocksByPull_LockerErr(t *testing.T) { + t.Log("If there is an error retrieving the lock, returned a failed status") + repoName := "reponame" + pullNum := 2 + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.UnlockByPull(repoName, pullNum)).ThenReturn(nil, errors.New("err")) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + } + deleteLockResult, _ := dlc.DeleteLocksByPull(repoName, pullNum) + Equals(t, models.DeleteLockFail, deleteLockResult) +} + +func TestDeleteLocksByPull_None(t *testing.T) { + t.Log("If there is no lock at that ID return no locks found") + repoName := "reponame" + pullNum := 2 + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.UnlockByPull(repoName, pullNum)).ThenReturn([]models.ProjectLock{}, nil) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + } + deleteLockResult, _ := dlc.DeleteLocksByPull(repoName, pullNum) + Equals(t, models.DeleteLockNotFound, deleteLockResult) +} + +func TestDeleteLocksByPull_OldFormat(t *testing.T) { + t.Log("If the lock doesn't have BaseRepo set it is deleted successfully") + repoName := "reponame" + pullNum := 2 + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.UnlockByPull(repoName, pullNum)).ThenReturn([]models.ProjectLock{{}}, nil) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + } + deleteLockResult, _ := dlc.DeleteLocksByPull(repoName, pullNum) + Equals(t, models.DeleteLockSuccess, deleteLockResult) +} diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go index ff7ab16327..47db159cee 100644 --- a/server/events/markdown_renderer.go +++ b/server/events/markdown_renderer.go @@ -143,7 +143,6 @@ func (m *MarkdownRenderer) renderProjectResults(results []models.ProjectResult, } else { resultData.Rendered = m.renderTemplate(applyUnwrappedSuccessTmpl, struct{ Output string }{result.ApplySuccess}) } - } else { resultData.Rendered = "Found no template. This is a bug!" } @@ -249,7 +248,8 @@ var planSuccessWrappedTmpl = template.Must(template.New("").Parse( // to do next. var planNextSteps = "{{ if .PlanWasDeleted }}This plan was not saved because one or more projects failed and automerge requires all plans pass.{{ else }}* :arrow_forward: To **apply** this plan, comment:\n" + " * `{{.ApplyCmd}}`\n" + - "* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}})\n" + + "* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}}), or to delete all plans and atlantis locks comment:\n" + + " * `atlantis unlock`\n" + "* :repeat: To **plan** this project again, comment:\n" + " * `{{.RePlanCmd}}`{{end}}" var applyUnwrappedSuccessTmpl = template.Must(template.New("").Parse( diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go index 5889be779e..3a501ae152 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/events/markdown_renderer_test.go @@ -152,7 +152,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path -w workspace$ @@ -186,7 +187,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path -w workspace$ @@ -222,7 +224,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path -w workspace$ @@ -309,7 +312,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path -w workspace$ @@ -321,7 +325,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path2 -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url2) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url2), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path2 -w workspace$ @@ -444,7 +449,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path -w workspace$ @@ -614,7 +620,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path -w workspace$ @@ -646,7 +653,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path -w workspace$ @@ -692,7 +700,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path -w workspace$ @@ -703,7 +712,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $atlantis apply -d path2 -w workspace$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url2) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url2), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $atlantis plan -d path2 -w workspace$ @@ -974,7 +984,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $applycmd$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $replancmd$ @@ -992,7 +1003,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $applycmd$ -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $replancmd$ @@ -1118,7 +1130,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $staging-apply-cmd$ -* :put_litter_in_its_place: To **delete** this plan click [here](staging-lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](staging-lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $staging-replan-cmd$ @@ -1133,7 +1146,8 @@ $$$ * :arrow_forward: To **apply** this plan, comment: * $production-apply-cmd$ -* :put_litter_in_its_place: To **delete** this plan click [here](production-lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](production-lock-url), or to delete all plans and atlantis locks comment: + * $atlantis unlock$ * :repeat: To **plan** this project again, comment: * $production-replan-cmd$ diff --git a/server/events/mocks/matchers/models_deletelockcommandresult.go b/server/events/mocks/matchers/models_deletelockcommandresult.go new file mode 100644 index 0000000000..47cab0adff --- /dev/null +++ b/server/events/mocks/matchers/models_deletelockcommandresult.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyModelsDeleteLockCommandResult() models.DeleteLockCommandResult { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.DeleteLockCommandResult))(nil)).Elem())) + var nullValue models.DeleteLockCommandResult + return nullValue +} + +func EqModelsDeleteLockCommandResult(value models.DeleteLockCommandResult) models.DeleteLockCommandResult { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue models.DeleteLockCommandResult + return nullValue +} diff --git a/server/events/mocks/matchers/ptr_to_models_projectlock.go b/server/events/mocks/matchers/ptr_to_models_projectlock.go new file mode 100644 index 0000000000..bf95261d17 --- /dev/null +++ b/server/events/mocks/matchers/ptr_to_models_projectlock.go @@ -0,0 +1,20 @@ +// Code generated by pegomock. DO NOT EDIT. +package matchers + +import ( + "reflect" + "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" +) + +func AnyPtrToModelsProjectLock() *models.ProjectLock { + pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*models.ProjectLock))(nil)).Elem())) + var nullValue *models.ProjectLock + return nullValue +} + +func EqPtrToModelsProjectLock(value *models.ProjectLock) *models.ProjectLock { + pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value}) + var nullValue *models.ProjectLock + return nullValue +} diff --git a/server/events/mocks/mock_delete_lock_command.go b/server/events/mocks/mock_delete_lock_command.go new file mode 100644 index 0000000000..321034921e --- /dev/null +++ b/server/events/mocks/mock_delete_lock_command.go @@ -0,0 +1,163 @@ +// Code generated by pegomock. DO NOT EDIT. +// Source: github.com/runatlantis/atlantis/server/events (interfaces: DeleteLockCommand) + +package mocks + +import ( + pegomock "github.com/petergtz/pegomock" + models "github.com/runatlantis/atlantis/server/events/models" + "reflect" + "time" +) + +type MockDeleteLockCommand struct { + fail func(message string, callerSkip ...int) +} + +func NewMockDeleteLockCommand(options ...pegomock.Option) *MockDeleteLockCommand { + mock := &MockDeleteLockCommand{} + for _, option := range options { + option.Apply(mock) + } + return mock +} + +func (mock *MockDeleteLockCommand) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } +func (mock *MockDeleteLockCommand) FailHandler() pegomock.FailHandler { return mock.fail } + +func (mock *MockDeleteLockCommand) DeleteLock(id string) (models.DeleteLockCommandResult, *models.ProjectLock, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockDeleteLockCommand().") + } + params := []pegomock.Param{id} + result := pegomock.GetGenericMockFrom(mock).Invoke("DeleteLock", params, []reflect.Type{reflect.TypeOf((*models.DeleteLockCommandResult)(nil)).Elem(), reflect.TypeOf((**models.ProjectLock)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.DeleteLockCommandResult + var ret1 *models.ProjectLock + var ret2 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.DeleteLockCommandResult) + } + if result[1] != nil { + ret1 = result[1].(*models.ProjectLock) + } + if result[2] != nil { + ret2 = result[2].(error) + } + } + return ret0, ret1, ret2 +} + +func (mock *MockDeleteLockCommand) DeleteLocksByPull(repoFullName string, pullNum int) (models.DeleteLockCommandResult, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockDeleteLockCommand().") + } + params := []pegomock.Param{repoFullName, pullNum} + result := pegomock.GetGenericMockFrom(mock).Invoke("DeleteLocksByPull", params, []reflect.Type{reflect.TypeOf((*models.DeleteLockCommandResult)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 models.DeleteLockCommandResult + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(models.DeleteLockCommandResult) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockDeleteLockCommand) VerifyWasCalledOnce() *VerifierMockDeleteLockCommand { + return &VerifierMockDeleteLockCommand{ + mock: mock, + invocationCountMatcher: pegomock.Times(1), + } +} + +func (mock *MockDeleteLockCommand) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockDeleteLockCommand { + return &VerifierMockDeleteLockCommand{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + } +} + +func (mock *MockDeleteLockCommand) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockDeleteLockCommand { + return &VerifierMockDeleteLockCommand{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + inOrderContext: inOrderContext, + } +} + +func (mock *MockDeleteLockCommand) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockDeleteLockCommand { + return &VerifierMockDeleteLockCommand{ + mock: mock, + invocationCountMatcher: invocationCountMatcher, + timeout: timeout, + } +} + +type VerifierMockDeleteLockCommand struct { + mock *MockDeleteLockCommand + invocationCountMatcher pegomock.Matcher + inOrderContext *pegomock.InOrderContext + timeout time.Duration +} + +func (verifier *VerifierMockDeleteLockCommand) DeleteLock(id string) *MockDeleteLockCommand_DeleteLock_OngoingVerification { + params := []pegomock.Param{id} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DeleteLock", params, verifier.timeout) + return &MockDeleteLockCommand_DeleteLock_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockDeleteLockCommand_DeleteLock_OngoingVerification struct { + mock *MockDeleteLockCommand + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockDeleteLockCommand_DeleteLock_OngoingVerification) GetCapturedArguments() string { + id := c.GetAllCapturedArguments() + return id[len(id)-1] +} + +func (c *MockDeleteLockCommand_DeleteLock_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]string, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(string) + } + } + return +} + +func (verifier *VerifierMockDeleteLockCommand) DeleteLocksByPull(repoFullName string, pullNum int) *MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification { + params := []pegomock.Param{repoFullName, pullNum} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DeleteLocksByPull", params, verifier.timeout) + return &MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification struct { + mock *MockDeleteLockCommand + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification) GetCapturedArguments() (string, int) { + repoFullName, pullNum := c.GetAllCapturedArguments() + return repoFullName[len(repoFullName)-1], pullNum[len(pullNum)-1] +} + +func (c *MockDeleteLockCommand_DeleteLocksByPull_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []int) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]string, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(string) + } + _param1 = make([]int, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(int) + } + } + return +} diff --git a/server/events/models/models.go b/server/events/models/models.go index 4a49ba8dd4..969478b0c8 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -512,6 +512,8 @@ const ( ApplyCommand CommandName = iota // PlanCommand is a command to run terraform plan. PlanCommand + // UnlockCommand is a command to discard previous plans as well as the atlantis locks. + UnlockCommand // Adding more? Don't forget to update String() below ) @@ -522,6 +524,20 @@ func (c CommandName) String() string { return "apply" case PlanCommand: return "plan" + case UnlockCommand: + return "unlock" } return "" } + +// DeleteLockCommandResult is the result of attempting to delete an atlantis lock. +type DeleteLockCommandResult int + +const ( + // DeleteLockFail if unlock failed for some reason + DeleteLockFail DeleteLockCommandResult = iota + // DeleteLockNotFound if the lock attempting to remove does not exist. + DeleteLockNotFound + // DeleteLockSuccess if deleting an atlantis lock was successful + DeleteLockSuccess +) diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 4ff59190ca..406d6f6157 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -460,3 +460,21 @@ func TestPullStatus_StatusCount(t *testing.T) { Equals(t, 1, ps.StatusCount(models.ErroredApplyStatus)) Equals(t, 0, ps.StatusCount(models.ErroredPlanStatus)) } + +func TestApplyCommand_String(t *testing.T) { + uc := models.ApplyCommand + + Equals(t, "apply", uc.String()) +} + +func TestPlanCommand_String(t *testing.T) { + uc := models.PlanCommand + + Equals(t, "plan", uc.String()) +} + +func TestUnlockCommand_String(t *testing.T) { + uc := models.UnlockCommand + + Equals(t, "unlock", uc.String()) +} diff --git a/server/events/testdata/gitlab-merge-request-comment-event-subgroup.json b/server/events/testdata/gitlab-merge-request-comment-event-subgroup.json index bb31626077..9bd6e6d392 100644 --- a/server/events/testdata/gitlab-merge-request-comment-event-subgroup.json +++ b/server/events/testdata/gitlab-merge-request-comment-event-subgroup.json @@ -48,7 +48,7 @@ "type": null, "updated_at": "2018-08-22 06:14:24 UTC", "updated_by_id": null, - "description": "Ran Plan in dir: `.` workspace: `default`\n\n```diff\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n + create\n\nTerraform will perform the following actions:\n\n+ null_resource.test\n id: \nPlan: 1 to add, 0 to change, 0 to destroy.\n\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n * `atlantis apply -d .`\n* :put_litter_in_its_place: To **delete** this plan click [here](http://Lukes-Macbook-Pro.local:4141/lock?id=lkysow-test%252Fsubgroup%252Fsub-subgroup%252Fatlantis-example%252F.%252Fdefault)\n* :repeat: To **plan** this project again, comment:\n * `atlantis plan -d .`\n\n---\n* :fast_forward: To **apply** all unapplied plans from this pull request, comment:\n * `atlantis apply`", + "description": "Ran Plan in dir: `.` workspace: `default`\n\n```diff\nRefreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n + create\n\nTerraform will perform the following actions:\n\n+ null_resource.test\n id: \nPlan: 1 to add, 0 to change, 0 to destroy.\n\n```\n\n* :arrow_forward: To **apply** this plan, comment:\n * `atlantis apply -d .`\n* :put_litter_in_its_place: To **delete** this plan click [here](http://Lukes-Macbook-Pro.local:4141/lock?id=lkysow-test%252Fsubgroup%252Fsub-subgroup%252Fatlantis-example%252F.%252Fdefault) or comment:\n * `atlantis discard -d .`\n* :repeat: To **plan** this project again, comment:\n * `atlantis plan -d .`\n\n---\n* :fast_forward: To **apply** all unapplied plans from this pull request, comment:\n * `atlantis apply`", "url": "https://gitlab.com/lkysow-test/subgroup/sub-subgroup/atlantis-example/merge_requests/2#note_96056916" }, "repository": { @@ -137,4 +137,4 @@ "human_total_time_spent": null, "human_time_estimate": null } -} +} \ No newline at end of file diff --git a/server/locks_controller.go b/server/locks_controller.go index 628034d938..9d41894b45 100644 --- a/server/locks_controller.go +++ b/server/locks_controller.go @@ -26,6 +26,7 @@ type LocksController struct { WorkingDir events.WorkingDir WorkingDirLocker events.WorkingDirLocker DB *db.BoltDB + DeleteLockCommand events.DeleteLockCommand } // GetLock is the GET /locks/{id} route. It renders the lock detail view. @@ -84,45 +85,33 @@ func (l *LocksController) DeleteLock(w http.ResponseWriter, r *http.Request) { l.respond(w, logging.Warn, http.StatusBadRequest, "Invalid lock id %q. Failed with error: %s", id, err) return } - lock, err := l.Locker.Unlock(idUnencoded) - if err != nil { + + deleteLockResult, lock, err := l.DeleteLockCommand.DeleteLock(idUnencoded) + + switch deleteLockResult { + case models.DeleteLockSuccess: + // NOTE: Because BaseRepo was added to the PullRequest model later, previous + // installations of Atlantis will have locks in their DB that do not have + // this field on PullRequest. We skip commenting and deleting the working dir in this case. + if lock.Pull.BaseRepo != (models.Repo{}) { + // Once the lock has been deleted, comment back on the pull request. + comment := fmt.Sprintf("**Warning**: The plan for dir: `%s` workspace: `%s` was **discarded** via the Atlantis UI.\n\n"+ + "To `apply` this plan you must run `plan` again.", lock.Project.Path, lock.Workspace) + err = l.VCSClient.CreateComment(lock.Pull.BaseRepo, lock.Pull.Num, comment) + if err != nil { + l.respond(w, logging.Error, http.StatusInternalServerError, "Failed commenting on pull request: %s", err) + return + } + } else { + l.Logger.Debug("skipping commenting on pull request and deleting workspace because BaseRepo field is empty") + } + case models.DeleteLockFail: l.respond(w, logging.Error, http.StatusInternalServerError, "deleting lock failed with: %s", err) return - } - if lock == nil { + case models.DeleteLockNotFound: l.respond(w, logging.Info, http.StatusNotFound, "No lock found at id %q", idUnencoded) return } - - // NOTE: Because BaseRepo was added to the PullRequest model later, previous - // installations of Atlantis will have locks in their DB that do not have - // this field on PullRequest. We skip commenting and deleting the working dir in this case. - if lock.Pull.BaseRepo != (models.Repo{}) { - unlock, err := l.WorkingDirLocker.TryLock(lock.Pull.BaseRepo.FullName, lock.Pull.Num, lock.Workspace) - if err != nil { - l.Logger.Err("unable to obtain working dir lock when trying to delete old plans: %s", err) - } else { - defer unlock() - // nolint: vetshadow - if err := l.WorkingDir.DeleteForWorkspace(lock.Pull.BaseRepo, lock.Pull, lock.Workspace); err != nil { - l.Logger.Err("unable to delete workspace: %s", err) - } - } - if err := l.DB.DeleteProjectStatus(lock.Pull, lock.Workspace, lock.Project.Path); err != nil { - l.Logger.Err("unable to delete project status: %s", err) - } - - // Once the lock has been deleted, comment back on the pull request. - comment := fmt.Sprintf("**Warning**: The plan for dir: `%s` workspace: `%s` was **discarded** via the Atlantis UI.\n\n"+ - "To `apply` this plan you must run `plan` again.", lock.Project.Path, lock.Workspace) - err = l.VCSClient.CreateComment(lock.Pull.BaseRepo, lock.Pull.Num, comment) - if err != nil { - l.respond(w, logging.Error, http.StatusInternalServerError, "Failed commenting on pull request: %s", err) - return - } - } else { - l.Logger.Debug("skipping commenting on pull request and deleting workspace because BaseRepo field is empty") - } l.respond(w, logging.Info, http.StatusOK, "Deleted lock id %q", id) } diff --git a/server/locks_controller_test.go b/server/locks_controller_test.go index 1b159018d5..396db3e515 100644 --- a/server/locks_controller_test.go +++ b/server/locks_controller_test.go @@ -143,11 +143,11 @@ func TestDeleteLock_InvalidLockID(t *testing.T) { func TestDeleteLock_LockerErr(t *testing.T) { t.Log("If there is an error retrieving the lock, a 500 is returned") RegisterMockTestingT(t) - l := mocks.NewMockLocker() - When(l.Unlock("id")).ThenReturn(nil, errors.New("err")) + dlc := mocks2.NewMockDeleteLockCommand() + When(dlc.DeleteLock("id")).ThenReturn(models.DeleteLockFail, nil, errors.New("err")) lc := server.LocksController{ - Locker: l, - Logger: logging.NewNoopLogger(), + DeleteLockCommand: dlc, + Logger: logging.NewNoopLogger(), } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) @@ -159,11 +159,11 @@ func TestDeleteLock_LockerErr(t *testing.T) { func TestDeleteLock_None(t *testing.T) { t.Log("If there is no lock at that ID we get a 404") RegisterMockTestingT(t) - l := mocks.NewMockLocker() - When(l.Unlock("id")).ThenReturn(nil, nil) + dlc := mocks2.NewMockDeleteLockCommand() + When(dlc.DeleteLock("id")).ThenReturn(models.DeleteLockNotFound, nil, nil) lc := server.LocksController{ - Locker: l, - Logger: logging.NewNoopLogger(), + DeleteLockCommand: dlc, + Logger: logging.NewNoopLogger(), } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) @@ -175,14 +175,13 @@ func TestDeleteLock_None(t *testing.T) { func TestDeleteLock_OldFormat(t *testing.T) { t.Log("If the lock doesn't have BaseRepo set it is deleted successfully") RegisterMockTestingT(t) - cp := vcsmocks.NewMockClient() - l := mocks.NewMockLocker() - When(l.Unlock("id")).ThenReturn(&models.ProjectLock{}, nil) + dlc := mocks2.NewMockDeleteLockCommand() + When(dlc.DeleteLock("id")).ThenReturn(models.DeleteLockSuccess, &models.ProjectLock{}, nil) lc := server.LocksController{ - Locker: l, - Logger: logging.NewNoopLogger(), - VCSClient: cp, + DeleteLockCommand: dlc, + Logger: logging.NewNoopLogger(), + VCSClient: cp, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) @@ -195,28 +194,27 @@ func TestDeleteLock_OldFormat(t *testing.T) { func TestDeleteLock_CommentFailed(t *testing.T) { t.Log("If the commenting fails we return an error") RegisterMockTestingT(t) - - cp := vcsmocks.NewMockClient() - workingDir := mocks2.NewMockWorkingDir() - workingDirLocker := events.NewDefaultWorkingDirLocker() - When(cp.CreateComment(AnyRepo(), AnyInt(), AnyString())).ThenReturn(errors.New("err")) - l := mocks.NewMockLocker() - When(l.Unlock("id")).ThenReturn(&models.ProjectLock{ + dlc := mocks2.NewMockDeleteLockCommand() + When(dlc.DeleteLock("id")).ThenReturn(models.DeleteLockSuccess, &models.ProjectLock{ Pull: models.PullRequest{ BaseRepo: models.Repo{FullName: "owner/repo"}, }, }, nil) + cp := vcsmocks.NewMockClient() + workingDir := mocks2.NewMockWorkingDir() + workingDirLocker := events.NewDefaultWorkingDirLocker() + When(cp.CreateComment(AnyRepo(), AnyInt(), AnyString())).ThenReturn(errors.New("err")) tmp, cleanup := TempDir(t) defer cleanup() db, err := db.New(tmp) Ok(t, err) lc := server.LocksController{ - Locker: l, - Logger: logging.NewNoopLogger(), - VCSClient: cp, - WorkingDir: workingDir, - WorkingDirLocker: workingDirLocker, - DB: db, + DeleteLockCommand: dlc, + Logger: logging.NewNoopLogger(), + VCSClient: cp, + WorkingDir: workingDir, + WorkingDirLocker: workingDirLocker, + DB: db, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) @@ -228,15 +226,12 @@ func TestDeleteLock_CommentFailed(t *testing.T) { func TestDeleteLock_CommentSuccess(t *testing.T) { t.Log("We should comment back on the pull request if the lock is deleted") RegisterMockTestingT(t) - cp := vcsmocks.NewMockClient() - l := mocks.NewMockLocker() - workingDir := mocks2.NewMockWorkingDir() - workingDirLocker := events.NewDefaultWorkingDirLocker() + dlc := mocks2.NewMockDeleteLockCommand() pull := models.PullRequest{ BaseRepo: models.Repo{FullName: "owner/repo"}, } - When(l.Unlock("id")).ThenReturn(&models.ProjectLock{ + When(dlc.DeleteLock("id")).ThenReturn(models.DeleteLockSuccess, &models.ProjectLock{ Pull: pull, Workspace: "workspace", Project: models.Project{ @@ -249,12 +244,10 @@ func TestDeleteLock_CommentSuccess(t *testing.T) { db, err := db.New(tmp) Ok(t, err) lc := server.LocksController{ - Locker: l, - Logger: logging.NewNoopLogger(), - VCSClient: cp, - WorkingDirLocker: workingDirLocker, - WorkingDir: workingDir, - DB: db, + DeleteLockCommand: dlc, + Logger: logging.NewNoopLogger(), + VCSClient: cp, + DB: db, } req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil)) req = mux.SetURLVars(req, map[string]string{"id": "id"}) @@ -264,5 +257,4 @@ func TestDeleteLock_CommentSuccess(t *testing.T) { cp.VerifyWasCalled(Once()).CreateComment(pull.BaseRepo, pull.Num, "**Warning**: The plan for dir: `path` workspace: `workspace` was **discarded** via the Atlantis UI.\n\n"+ "To `apply` this plan you must run `plan` again.") - workingDir.VerifyWasCalledOnce().DeleteForWorkspace(pull.BaseRepo, pull, "workspace") } diff --git a/server/server.go b/server/server.go index c5ef287542..7e812ad7a2 100644 --- a/server/server.go +++ b/server/server.go @@ -251,6 +251,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Locker: lockingClient, VCSClient: vcsClient, } + deleteLockCommand := &events.DefaultDeleteLockCommand{ + Locker: lockingClient, + Logger: logger, + WorkingDir: workingDir, + WorkingDirLocker: workingDirLocker, + DB: boltdb, + } + parsedURL, err := ParseAtlantisURL(userConfig.AtlantisURL) if err != nil { return nil, errors.Wrapf(err, @@ -371,6 +379,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { WorkingDir: workingDir, PendingPlanFinder: pendingPlanFinder, DB: boltdb, + DeleteLockCommand: deleteLockCommand, GlobalAutomerge: userConfig.Automerge, Drainer: drainer, } @@ -388,6 +397,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { WorkingDir: workingDir, WorkingDirLocker: workingDirLocker, DB: boltdb, + DeleteLockCommand: deleteLockCommand, } eventsController := &EventsController{ CommandRunner: commandRunner, diff --git a/server/testfixtures/test-repos/automerge/exp-output-autoplan.txt b/server/testfixtures/test-repos/automerge/exp-output-autoplan.txt index 5c0f9fe035..859333c60e 100644 --- a/server/testfixtures/test-repos/automerge/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/automerge/exp-output-autoplan.txt @@ -25,7 +25,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d dir1` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d dir1` @@ -53,7 +54,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d dir2` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d dir2` diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt index 9f0f0df8bd..8b4a71d408 100644 --- a/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt @@ -25,7 +25,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d staging` @@ -53,7 +54,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d production` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d production` diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-plan-production.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-plan-production.txt index f08e2c50ae..a9f12f532b 100644 --- a/server/testfixtures/test-repos/modules-yaml/exp-output-plan-production.txt +++ b/server/testfixtures/test-repos/modules-yaml/exp-output-plan-production.txt @@ -21,7 +21,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d production` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d production` diff --git a/server/testfixtures/test-repos/modules-yaml/exp-output-plan-staging.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-plan-staging.txt index de773736db..03e157300e 100644 --- a/server/testfixtures/test-repos/modules-yaml/exp-output-plan-staging.txt +++ b/server/testfixtures/test-repos/modules-yaml/exp-output-plan-staging.txt @@ -21,7 +21,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d staging` diff --git a/server/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt b/server/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt index 5d53aae696..6b84495e3e 100644 --- a/server/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt +++ b/server/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt @@ -21,7 +21,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d staging` diff --git a/server/testfixtures/test-repos/modules/exp-output-plan-production.txt b/server/testfixtures/test-repos/modules/exp-output-plan-production.txt index 41847cb335..432f818960 100644 --- a/server/testfixtures/test-repos/modules/exp-output-plan-production.txt +++ b/server/testfixtures/test-repos/modules/exp-output-plan-production.txt @@ -21,7 +21,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d production` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d production` diff --git a/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt b/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt index 5d53aae696..6b84495e3e 100644 --- a/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt +++ b/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt @@ -21,7 +21,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d staging` diff --git a/server/testfixtures/test-repos/server-side-cfg/exp-output-autoplan.txt b/server/testfixtures/test-repos/server-side-cfg/exp-output-autoplan.txt index 7ce9f84f69..29a63491d0 100644 --- a/server/testfixtures/test-repos/server-side-cfg/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/server-side-cfg/exp-output-autoplan.txt @@ -29,7 +29,8 @@ postplan custom * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d .` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d .` @@ -59,7 +60,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -w staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -w staging` diff --git a/server/testfixtures/test-repos/simple-yaml/exp-output-autoplan.txt b/server/testfixtures/test-repos/simple-yaml/exp-output-autoplan.txt index f7a80a450d..c7ff94887d 100644 --- a/server/testfixtures/test-repos/simple-yaml/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/simple-yaml/exp-output-autoplan.txt @@ -29,7 +29,8 @@ postplan * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d .` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d .` @@ -57,7 +58,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -w staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -w staging` diff --git a/server/testfixtures/test-repos/simple/exp-output-atlantis-plan-new-workspace.txt b/server/testfixtures/test-repos/simple/exp-output-atlantis-plan-new-workspace.txt index aceca399c2..35662fef85 100644 --- a/server/testfixtures/test-repos/simple/exp-output-atlantis-plan-new-workspace.txt +++ b/server/testfixtures/test-repos/simple/exp-output-atlantis-plan-new-workspace.txt @@ -31,7 +31,8 @@ Plan: 3 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -w new_workspace` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -w new_workspace -- -var var=new_workspace` diff --git a/server/testfixtures/test-repos/simple/exp-output-atlantis-plan-var-overridden.txt b/server/testfixtures/test-repos/simple/exp-output-atlantis-plan-var-overridden.txt index 09aaabba6a..0d8c82af21 100644 --- a/server/testfixtures/test-repos/simple/exp-output-atlantis-plan-var-overridden.txt +++ b/server/testfixtures/test-repos/simple/exp-output-atlantis-plan-var-overridden.txt @@ -31,7 +31,8 @@ Plan: 3 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d .` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d . -- -var var=overridden` diff --git a/server/testfixtures/test-repos/simple/exp-output-atlantis-plan.txt b/server/testfixtures/test-repos/simple/exp-output-atlantis-plan.txt index d51f44ff8e..b5801c9ca1 100644 --- a/server/testfixtures/test-repos/simple/exp-output-atlantis-plan.txt +++ b/server/testfixtures/test-repos/simple/exp-output-atlantis-plan.txt @@ -31,7 +31,8 @@ Plan: 3 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d .` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d . -- -var var=default_workspace` diff --git a/server/testfixtures/test-repos/simple/exp-output-autoplan.txt b/server/testfixtures/test-repos/simple/exp-output-autoplan.txt index d20e62e185..8f16d4737e 100644 --- a/server/testfixtures/test-repos/simple/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/simple/exp-output-autoplan.txt @@ -31,7 +31,8 @@ Plan: 3 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d .` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d .` diff --git a/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-default.txt b/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-default.txt index 7a33d16436..c70fbc2657 100644 --- a/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-default.txt +++ b/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-default.txt @@ -21,7 +21,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -p default` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -p default` diff --git a/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-staging.txt b/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-staging.txt index b3cb4f5059..6bdc21e80b 100644 --- a/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-staging.txt +++ b/server/testfixtures/test-repos/tfvars-yaml-no-autoplan/exp-output-plan-staging.txt @@ -21,7 +21,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -p staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -p staging` diff --git a/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt b/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt index 9dcbc2bbc8..85af35b277 100644 --- a/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt @@ -27,7 +27,8 @@ workspace=default * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -p default` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -p default` @@ -55,7 +56,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -p staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -p staging` diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt index 136c895af7..2cf6288ee2 100644 --- a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt +++ b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-production.txt @@ -19,7 +19,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d production -w production` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d production -w production` diff --git a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt index 8ba8f0312d..29fc525b11 100644 --- a/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt +++ b/server/testfixtures/test-repos/workspace-parallel-yaml/exp-output-autoplan-staging.txt @@ -19,7 +19,8 @@ Plan: 1 to add, 0 to change, 0 to destroy. * :arrow_forward: To **apply** this plan, comment: * `atlantis apply -d staging -w staging` -* :put_litter_in_its_place: To **delete** this plan click [here](lock-url) +* :put_litter_in_its_place: To **delete** this plan click [here](lock-url), or to delete all plans and atlantis locks comment: + * `atlantis unlock` * :repeat: To **plan** this project again, comment: * `atlantis plan -d staging -w staging`