diff --git a/.gitignore b/.gitignore index 66060be56..c23cbfed9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,3 @@ helm/test-values.yaml *.swp golangci-lint atlantis -temp.sh \ No newline at end of file diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 835a8606f..2d815caff 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 unlocked 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 @@ -273,8 +287,6 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead projectCmds, err = c.ProjectCommandBuilder.BuildPlanCommands(ctx, cmd) case models.ApplyCommand: projectCmds, err = c.ProjectCommandBuilder.BuildApplyCommands(ctx, cmd) - case models.DiscardCommand: - projectCmds, err = c.ProjectCommandBuilder.BuildDiscardCommands(ctx, cmd) default: ctx.Log.Err("failed to determine desired command, neither plan nor apply") return @@ -305,13 +317,6 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead result.PlansDeleted = true } - // If this was a successful discard command, delete plans anyway - if cmd.Name == models.DiscardCommand && !result.HasErrors() { - c.deletePlans(ctx) - result.PlansDeleted = true - } - - // TODO: check here for updating PR with discard c.updatePull( ctx, cmd, @@ -435,8 +440,6 @@ func (c *DefaultCommandRunner) runProjectCmds(cmds []models.ProjectCommandContex res = c.ProjectCommandRunner.Plan(pCmd) case models.ApplyCommand: res = c.ProjectCommandRunner.Apply(pCmd) - case models.DiscardCommand: - res = c.ProjectCommandRunner.Discard(pCmd) } results = append(results, res) } diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 24d6d4ee6..bdf6fc57a 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 unlocked 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(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 386af2cbe..ba608d24e 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -61,8 +61,6 @@ type CommentBuilder interface { BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string // BuildApplyComment builds an apply comment for the specified args. BuildApplyComment(repoRelDir string, workspace string, project string) string - // BuildDiscardComment builds a discard comment for the specified args. - BuildDiscardComment(repoRelDir string, workspace string, project string, commentArgs []string) string } // CommentParser implements CommentParsing @@ -158,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(), models.DiscardCommand.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)} } @@ -188,13 +186,10 @@ 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.DiscardCommand.String(): - name = models.DiscardCommand - flagSet = pflag.NewFlagSet(models.DiscardCommand.String(), pflag.ContinueOnError) + case models.UnlockCommand.String(): + name = models.UnlockCommand + flagSet = pflag.NewFlagSet(models.UnlockCommand.String(), pflag.ContinueOnError) flagSet.SetOutput(ioutil.Discard) - flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before planning.") - flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run plan in relative to root of repo, ex. 'child/dir'.") - flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Which project to discard the plan for. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", yaml.AtlantisYAMLFilename)) default: return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", command)} @@ -207,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)} } @@ -274,22 +272,6 @@ func (e *CommentParser) BuildApplyComment(repoRelDir string, workspace string, p return fmt.Sprintf("%s %s%s", atlantisExecutable, models.ApplyCommand.String(), flags) } -// BuildDiscardComment builds discard comment for the specified args. -func (e *CommentParser) BuildDiscardComment(repoRelDir string, workspace string, project string, commentArgs []string) string { - flags := e.buildFlags(repoRelDir, workspace, project) - commentFlags := "" - if len(commentArgs) > 0 { - var flagsWithoutQuotes []string - for _, f := range commentArgs { - f = strings.TrimPrefix(f, "\"") - f = strings.TrimSuffix(f, "\"") - flagsWithoutQuotes = append(flagsWithoutQuotes, f) - } - commentFlags = fmt.Sprintf(" -- %s", strings.Join(flagsWithoutQuotes, " ")) - } - return fmt.Sprintf("%s %s%s%s", atlantisExecutable, models.DiscardCommand.String(), flags, commentFlags) -} - func (e *CommentParser) buildFlags(repoRelDir string, workspace string, project string) string { // Add quotes if dir has spaces. if strings.Contains(repoRelDir, " ") { @@ -371,9 +353,9 @@ 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. - discard Discards a previous plan as well as the atlantis lock. - To discard a specific plan and atlantis lock use the -d flag. + 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: @@ -385,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 b9c203f2a..fdaf29ab5 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/db/boltdb_test.go b/server/events/db/boltdb_test.go index e165101b9..5eaac70a3 100644 --- a/server/events/db/boltdb_test.go +++ b/server/events/db/boltdb_test.go @@ -623,7 +623,6 @@ func TestPullStatus_UpdateMerge(t *testing.T) { LockURL: "lock-url", RePlanCmd: "plan command", ApplyCmd: "apply command", - DiscardCmd: "discard command", }, }, }) diff --git a/server/events/delete_lock_command.go b/server/events/delete_lock_command.go new file mode 100644 index 000000000..583eba1e1 --- /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.ProjectLock, error) + DeleteLocksByPull(repoFullName string, pullNum int) 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.ProjectLock, error) { + lock, err := l.Locker.Unlock(id) + if err != nil { + return nil, err + } + if lock == nil { + return nil, nil + } + + l.deleteWorkingDir(*lock) + return lock, nil +} + +// DeleteLocksByPull handles deleting all locks for the pull request +func (l *DefaultDeleteLockCommand) DeleteLocksByPull(repoFullName string, pullNum int) error { + locks, err := l.Locker.UnlockByPull(repoFullName, pullNum) + if err != nil { + return err + } + if len(locks) == 0 { + return nil + } + + for i := 0; i < len(locks); i++ { + lock := locks[i] + l.deleteWorkingDir(lock) + } + + return 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{}) { + return + } + 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 000000000..ba2acd8c0 --- /dev/null +++ b/server/events/delete_lock_command_test.go @@ -0,0 +1,135 @@ +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, we return the error") + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.Unlock("id")).ThenReturn(nil, errors.New("err")) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + } + _, err := dlc.DeleteLock("id") + ErrEquals(t, "err", err) +} + +func TestDeleteLock_None(t *testing.T) { + t.Log("If there is no lock at that ID we return nil") + RegisterMockTestingT(t) + l := lockmocks.NewMockLocker() + When(l.Unlock("id")).ThenReturn(nil, nil) + dlc := events.DefaultDeleteLockCommand{ + Locker: l, + Logger: logging.NewNoopLogger(), + } + lock, err := dlc.DeleteLock("id") + Ok(t, err) + Assert(t, lock == nil, "lock was not nil") +} + +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(), + } + lock, err := dlc.DeleteLock("id") + Ok(t, err) + Assert(t, lock != nil, "lock was nil") +} + +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, + } + lock, err := dlc.DeleteLock("id") + Ok(t, err) + Assert(t, lock != nil, "lock was nil") + 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(), + } + err := dlc.DeleteLocksByPull(repoName, pullNum) + ErrEquals(t, "err", err) +} + +func TestDeleteLocksByPull_None(t *testing.T) { + t.Log("If there is no lock at that ID there is no error") + 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(), + } + err := dlc.DeleteLocksByPull(repoName, pullNum) + Ok(t, err) +} + +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(), + } + err := dlc.DeleteLocksByPull(repoName, pullNum) + Ok(t, err) +} diff --git a/server/events/markdown_renderer.go b/server/events/markdown_renderer.go index 32725bf6b..e5bd72eff 100644 --- a/server/events/markdown_renderer.go +++ b/server/events/markdown_renderer.go @@ -24,9 +24,8 @@ import ( ) const ( - planCommandTitle = "Plan" - applyCommandTitle = "Apply" - discardCommandTitle = "Discard" + planCommandTitle = "Plan" + applyCommandTitle = "Apply" // maxUnwrappedLines is the maximum number of lines the Terraform output // can be before we wrap it in an expandable template. maxUnwrappedLines = 12 @@ -144,9 +143,6 @@ func (m *MarkdownRenderer) renderProjectResults(results []models.ProjectResult, } else { resultData.Rendered = m.renderTemplate(applyUnwrappedSuccessTmpl, struct{ Output string }{result.ApplySuccess}) } - - } else if result.DiscardSuccess != "" { - resultData.Rendered = m.renderTemplate(discardUnwrappedSuccessTmpl, struct{ Output string }{result.DiscardSuccess}) } else { resultData.Rendered = "Found no template. This is a bug!" } @@ -159,8 +155,6 @@ func (m *MarkdownRenderer) renderProjectResults(results []models.ProjectResult, tmpl = singleProjectPlanSuccessTmpl case len(resultsTmplData) == 1 && common.Command == planCommandTitle && numPlanSuccesses == 0: tmpl = singleProjectPlanUnsuccessfulTmpl - case len(resultsTmplData) == 1 && common.Command == discardCommandTitle: - tmpl = singleProjectDiscardTmpl case len(resultsTmplData) == 1 && common.Command == applyCommandTitle: tmpl = singleProjectApplyTmpl case common.Command == planCommandTitle: @@ -210,12 +204,12 @@ var singleProjectPlanSuccessTmpl = template.Must(template.New("").Parse( "\n" + "{{ if ne .DisableApplyAll true }}---\n" + "* :fast_forward: To **apply** all unapplied plans from this pull request, comment:\n" + - " * `atlantis apply`{{ end }}" + logTmpl)) + " * `atlantis apply`\n" + + "* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:\n" + + " * `atlantis unlock`{{ end }}" + logTmpl)) var singleProjectPlanUnsuccessfulTmpl = template.Must(template.New("").Parse( "{{$result := index .Results 0}}Ran {{.Command}} for dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n" + "{{$result.Rendered}}\n" + logTmpl)) -var singleProjectDiscardTmpl = template.Must(template.New("").Parse( - "{{$result := index .Results 0}}Ran {{.Command}} for {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n\n{{$result.Rendered}}\n" + logTmpl)) var multiProjectPlanTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse( "Ran {{.Command}} for {{ len .Results }} projects:\n\n" + "{{ range $result := .Results }}" + @@ -225,7 +219,10 @@ var multiProjectPlanTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap "### {{add $i 1}}. {{ if $result.ProjectName }}project: `{{$result.ProjectName}}` {{ end }}dir: `{{$result.RepoRelDir}}` workspace: `{{$result.Workspace}}`\n" + "{{$result.Rendered}}\n\n" + "{{ if ne $disableApplyAll true }}---\n{{end}}{{end}}{{ if ne .DisableApplyAll true }}{{ if and (gt (len .Results) 0) (not .PlansDeleted) }}* :fast_forward: To **apply** all unapplied plans from this pull request, comment:\n" + - " * `atlantis apply`{{end}}{{end}}" + + " * `atlantis apply`\n" + + "* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:\n" + + " * `atlantis unlock`" + + "{{end}}{{end}}" + logTmpl)) var multiProjectApplyTmpl = template.Must(template.New("").Funcs(sprig.TxtFuncMap()).Parse( "Ran {{.Command}} for {{ len .Results }} projects:\n\n" + @@ -256,8 +253,7 @@ 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}}), or comment:\n" + - " * `{{.DiscardCmd}}`\n" + + "* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}})\n" + "* :repeat: To **plan** this project again, comment:\n" + " * `{{.RePlanCmd}}`{{end}}" var applyUnwrappedSuccessTmpl = template.Must(template.New("").Parse( @@ -270,10 +266,6 @@ var applyWrappedSuccessTmpl = template.Must(template.New("").Parse( "{{.Output}}\n" + "```\n" + "")) -var discardUnwrappedSuccessTmpl = template.Must(template.New("").Parse( - "```diff\n" + - "{{.Output}}\n" + - "```")) var unwrappedErrTmplText = "**{{.Command}} Error**\n" + "```\n" + "{{.Error}}\n" + diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go index 5889be779..46b88444e 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/events/markdown_renderer_test.go @@ -159,6 +159,8 @@ $$$ --- * :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$ `, }, { @@ -195,6 +197,8 @@ $$$ --- * :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$ `, }, { @@ -229,6 +233,8 @@ $$$ --- * :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$ `, }, { @@ -328,6 +334,8 @@ $$$ --- * :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$ `, }, { @@ -462,6 +470,8 @@ $$$ --- * :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$ `, }, { @@ -982,6 +992,8 @@ $$$ --- * :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$ ` } else { exp = `Ran Plan for dir: $.$ workspace: $default$ @@ -999,6 +1011,8 @@ $$$ --- * :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$ ` } case models.ApplyCommand: @@ -1141,6 +1155,8 @@ $$$ --- * :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$ ` expWithBackticks := strings.Replace(exp, "$", "`", -1) Equals(t, expWithBackticks, rendered) 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 000000000..bf95261d1 --- /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 000000000..a6664300d --- /dev/null +++ b/server/events/mocks/mock_delete_lock_command.go @@ -0,0 +1,155 @@ +// 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.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.ProjectLock)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 *models.ProjectLock + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(*models.ProjectLock) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + +func (mock *MockDeleteLockCommand) DeleteLocksByPull(repoFullName string, pullNum int) 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((*error)(nil)).Elem()}) + var ret0 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(error) + } + } + return ret0 +} + +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 245508f18..5cf469ce8 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -316,9 +316,6 @@ type ProjectCommandContext struct { AutoplanEnabled bool // BaseRepo is the repository that the pull request will be merged into. BaseRepo Repo - // DiscardCmd is the command that users should run to discard a plan. - // If this is an apply then this will be empty. - DiscardCmd string // EscapedCommentArgs are the extra arguments that were added to the atlantis // command, ex. atlantis plan -- -target=resource. We then escape them // by adding a \ before each character so that they can be used within @@ -378,15 +375,14 @@ func SplitRepoFullName(repoFullName string) (owner string, repo string) { // ProjectResult is the result of executing a plan/apply for a specific project. type ProjectResult struct { - Command CommandName - RepoRelDir string - Workspace string - Error error - Failure string - PlanSuccess *PlanSuccess - ApplySuccess string - DiscardSuccess string - ProjectName string + Command CommandName + RepoRelDir string + Workspace string + Error error + Failure string + PlanSuccess *PlanSuccess + ApplySuccess string + ProjectName string } // CommitStatus returns the vcs commit status of this project result. @@ -439,8 +435,6 @@ type PlanSuccess struct { RePlanCmd string // ApplyCmd is the command that users should run to apply this plan. ApplyCmd string - // DiscardCmd is the command that users should run to discard this plan. - DiscardCmd string // HasDiverged is true if we're using the checkout merge strategy and the // branch we're merging into has been updated since we cloned and merged // it. @@ -518,8 +512,8 @@ const ( ApplyCommand CommandName = iota // PlanCommand is a command to run terraform plan. PlanCommand - // DiscardCommand is a command to discard a previous plan as well as the atlantis lock. - DiscardCommand + // UnlockCommand is a command to discard previous plans as well as the atlantis locks. + UnlockCommand // Adding more? Don't forget to update String() below ) @@ -530,8 +524,8 @@ func (c CommandName) String() string { return "apply" case PlanCommand: return "plan" - case DiscardCommand: - return "discard" + case UnlockCommand: + return "unlock" } return "" } diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 4ff59190c..406d6f615 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/project_command_builder.go b/server/events/project_command_builder.go index 5d4dc4b23..4980067aa 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -2,6 +2,7 @@ package events import ( "fmt" + "os" "path/filepath" "regexp" "strings" @@ -46,10 +47,6 @@ type ProjectCommandBuilder interface { // comment doesn't specify one project then there may be multiple commands // to be run. BuildApplyCommands(ctx *CommandContext, comment *CommentCommand) ([]models.ProjectCommandContext, error) - // BuildDiscardCommands builds project discard commands for ctx and comment. If - // comment doesn't specify one project then there may be multiple commands - // to be run. - BuildDiscardCommands(ctx *CommandContext, comment *CommentCommand) ([]models.ProjectCommandContext, error) } // DefaultProjectCommandBuilder implements ProjectCommandBuilder. @@ -101,15 +98,6 @@ func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, c return []models.ProjectCommandContext{pac}, err } -// See ProjectCommandBuilder.BuildDiscardCommands. -func (p *DefaultProjectCommandBuilder) BuildDiscardCommands(ctx *CommandContext, cmd *CommentCommand) ([]models.ProjectCommandContext, error) { - //if !cmd.IsForSpecificProject() { - // return p.buildDiscardAllCommands(ctx, cmd.Flags, cmd.Verbose) - //} - pcc, err := p.buildProjectDiscardCommand(ctx, cmd) - return []models.ProjectCommandContext{pcc}, err -} - // buildPlanAllCommands builds plan contexts for all projects we determine were // modified in this ctx. func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) { @@ -254,7 +242,9 @@ func (p *DefaultProjectCommandBuilder) buildProjectApplyCommand(ctx *CommandCont defer unlockFn() repoDir, err := p.WorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, workspace) - if err != nil { + if os.IsNotExist(errors.Cause(err)) { + return projCtx, errors.New("no working directory found–did you run plan?") + } else if err != nil { return projCtx, err } @@ -266,35 +256,6 @@ func (p *DefaultProjectCommandBuilder) buildProjectApplyCommand(ctx *CommandCont return p.buildProjectCommandCtx(ctx, models.ApplyCommand, cmd.ProjectName, cmd.Flags, repoDir, repoRelDir, workspace, cmd.Verbose) } -// cmd must be for only one project. -func (p *DefaultProjectCommandBuilder) buildProjectDiscardCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) { - workspace := DefaultWorkspace - if cmd.Workspace != "" { - workspace = cmd.Workspace - } - - var pcc models.ProjectCommandContext - ctx.Log.Debug("building plan command") - unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, ctx.Pull.Num, workspace) - if err != nil { - return pcc, err - } - defer unlockFn() - - ctx.Log.Debug("cloning repository") - repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace) - if err != nil { - return pcc, err - } - - repoRelDir := DefaultRepoRelDir - if cmd.RepoRelDir != "" { - repoRelDir = cmd.RepoRelDir - } - - return p.buildProjectCommandCtx(ctx, models.PlanCommand, cmd.ProjectName, cmd.Flags, repoDir, repoRelDir, workspace, cmd.Verbose) -} - // buildProjectCommandCtx builds a context for a single project identified // by the parameters. func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx( diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 6f71d617e..1c84f2487 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -87,8 +87,6 @@ type ProjectCommandRunner interface { Plan(ctx models.ProjectCommandContext) models.ProjectResult // Apply runs terraform apply for the project described by ctx. Apply(ctx models.ProjectCommandContext) models.ProjectResult - // Discard discards the plan for project described by ctx. - Discard(ctx models.ProjectCommandContext) models.ProjectResult } // DefaultProjectCommandRunner implements ProjectCommandRunner. @@ -98,7 +96,6 @@ type DefaultProjectCommandRunner struct { InitStepRunner StepRunner PlanStepRunner StepRunner ApplyStepRunner StepRunner - DiscardStepRunner StepRunner RunStepRunner CustomStepRunner EnvStepRunner EnvStepRunner PullApprovedChecker runtime.PullApprovedChecker @@ -135,20 +132,6 @@ func (p *DefaultProjectCommandRunner) Apply(ctx models.ProjectCommandContext) mo } } -// Discard deletes the atlantis plan and discards the lock for the project described by ctx. -func (p *DefaultProjectCommandRunner) Discard(ctx models.ProjectCommandContext) models.ProjectResult { - discardOut, failure, err := p.doDiscard(ctx) - return models.ProjectResult{ - Command: models.PlanCommand, - Error: err, - Failure: failure, - DiscardSuccess: discardOut, - RepoRelDir: ctx.RepoRelDir, - Workspace: ctx.Workspace, - ProjectName: ctx.ProjectName, - } -} - func (p *DefaultProjectCommandRunner) doPlan(ctx models.ProjectCommandContext) (*models.PlanSuccess, string, error) { // Acquire Atlantis lock for this repo/dir/workspace. lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.BaseRepo.FullName, ctx.RepoRelDir)) @@ -193,7 +176,6 @@ func (p *DefaultProjectCommandRunner) doPlan(ctx models.ProjectCommandContext) ( TerraformOutput: strings.Join(outputs, "\n"), RePlanCmd: ctx.RePlanCmd, ApplyCmd: ctx.ApplyCmd, - DiscardCmd: ctx.DiscardCmd, HasDiverged: hasDiverged, }, "", nil } @@ -281,68 +263,3 @@ func (p *DefaultProjectCommandRunner) doApply(ctx models.ProjectCommandContext) } return strings.Join(outputs, "\n"), "", nil } - -func (p *DefaultProjectCommandRunner) doDiscard(ctx models.ProjectCommandContext) (discardOut string, failure string, err error) { - // Definitely need this to prevent applying. But need to do something more to - // Lead to generation of message: plan is required - //if err := p.WorkingDir.Delete(ctx.BaseRepo, ctx.Pull); err != nil { - // return "", "", errors.Wrap(err, "cleaning workspace") - //} - - // TryLock is idembpotent so OK to run even if lock already exists - which will normally be the case - // TODO try to expose a method to identify if a lock is there to begin with, if not error - lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.BaseRepo.FullName, ctx.RepoRelDir)) - ctx.Log.Err("discard: attempting to lock") - if err != nil { - ctx.Log.Err("discard: failed to lock: %v", err) - return "", "", errors.Wrap(err, "acquiring lock") - } - - if !lockAttempt.LockAcquired { - return "", lockAttempt.LockFailureReason, nil - } - ctx.Log.Debug("discard: acquired lock for project") - ctx.Log.Debug("discard: attempting to unlock project") - if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil { - ctx.Log.Err("error unlocking state after plan error: %v", unlockErr) - } - - /* - if ctx.BaseRepo != (models.Repo{}) { - unlock, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace) - if err != nil { - ctx.Log.Err("unable to obtain working dir lock when trying to delete plans: %s", err) - } else { - defer unlock() - // nolint: vetshadow - if err := p.WorkingDir.DeleteForWorkspace(ctx.BaseRepo, ctx.Pull, ctx.Workspace); err != nil { - ctx.Log.Err("unable to delete workspace: %s", err) - } - } - - if err := p.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 - } - - } - */ - - return "discard successful", "", nil - - // Finally, delete locks. We do this last because when someone - // unlocks a project, right now we don't actually delete the plan - // so we might have plans laying around but no locks. - //locks, err := p.Locker(repo.FullName, pull.Num) - //if err != nil { - // return nil, "", errors.Wrap(err, "cleaning up locks") - //} -} diff --git a/server/locks_controller.go b/server/locks_controller.go index 628034d93..6aa0f5abd 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,11 +85,13 @@ 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) + + lock, err := l.DeleteLockCommand.DeleteLock(idUnencoded) if err != nil { l.respond(w, logging.Error, http.StatusInternalServerError, "deleting lock failed with: %s", err) return } + if lock == nil { l.respond(w, logging.Info, http.StatusNotFound, "No lock found at id %q", idUnencoded) return @@ -96,29 +99,12 @@ func (l *LocksController) DeleteLock(w http.ResponseWriter, r *http.Request) { // 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. + // this field on PullRequest. We skip commenting 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 + if err = l.VCSClient.CreateComment(lock.Pull.BaseRepo, lock.Pull.Num, comment); err != nil { + l.Logger.Warn("failed commenting on pull request: %s", err) } } else { l.Logger.Debug("skipping commenting on pull request and deleting workspace because BaseRepo field is empty") diff --git a/server/locks_controller_test.go b/server/locks_controller_test.go index 1b159018d..c117fa1b4 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(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(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.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"}) @@ -193,50 +192,46 @@ func TestDeleteLock_OldFormat(t *testing.T) { } func TestDeleteLock_CommentFailed(t *testing.T) { - t.Log("If the commenting fails we return an error") + t.Log("If the commenting fails we still return success") 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.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"}) w := httptest.NewRecorder() lc.DeleteLock(w, req) - responseContains(t, w, http.StatusInternalServerError, "Failed commenting on pull request: err") + responseContains(t, w, http.StatusOK, "Deleted lock id \"id\"") } 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.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 c5ef28754..7e812ad7a 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 5c0f9fe03..9517ae553 100644 --- a/server/testfixtures/test-repos/automerge/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/automerge/exp-output-autoplan.txt @@ -61,3 +61,5 @@ Plan: 1 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/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt b/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt index 9f0f0df8b..d6bf0de5b 100644 --- a/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/modules-yaml/exp-output-autoplan.txt @@ -61,3 +61,5 @@ Plan: 1 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/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt b/server/testfixtures/test-repos/modules/exp-output-autoplan-only-staging.txt index 5d53aae69..ff6dc802d 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 @@ -29,3 +29,5 @@ Plan: 1 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/testfixtures/test-repos/modules/exp-output-plan-production.txt b/server/testfixtures/test-repos/modules/exp-output-plan-production.txt index 41847cb33..0b6a2c522 100644 --- a/server/testfixtures/test-repos/modules/exp-output-plan-production.txt +++ b/server/testfixtures/test-repos/modules/exp-output-plan-production.txt @@ -29,3 +29,5 @@ Plan: 1 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/testfixtures/test-repos/modules/exp-output-plan-staging.txt b/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt index 5d53aae69..ff6dc802d 100644 --- a/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt +++ b/server/testfixtures/test-repos/modules/exp-output-plan-staging.txt @@ -29,3 +29,5 @@ Plan: 1 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/testfixtures/test-repos/server-side-cfg/exp-output-autoplan.txt b/server/testfixtures/test-repos/server-side-cfg/exp-output-autoplan.txt index 7ce9f84f6..dcf03dcc1 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 @@ -67,3 +67,5 @@ Plan: 1 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/testfixtures/test-repos/simple-yaml/exp-output-autoplan.txt b/server/testfixtures/test-repos/simple-yaml/exp-output-autoplan.txt index f7a80a450..2f4c57e87 100644 --- a/server/testfixtures/test-repos/simple-yaml/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/simple-yaml/exp-output-autoplan.txt @@ -65,3 +65,5 @@ Plan: 1 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/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 aceca399c..f65d52ded 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 @@ -39,3 +39,5 @@ 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/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 09aaabba6..35edc4838 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 @@ -39,3 +39,5 @@ 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/testfixtures/test-repos/simple/exp-output-atlantis-plan.txt b/server/testfixtures/test-repos/simple/exp-output-atlantis-plan.txt index d51f44ff8..4d25f0458 100644 --- a/server/testfixtures/test-repos/simple/exp-output-atlantis-plan.txt +++ b/server/testfixtures/test-repos/simple/exp-output-atlantis-plan.txt @@ -39,3 +39,5 @@ 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/testfixtures/test-repos/simple/exp-output-autoplan.txt b/server/testfixtures/test-repos/simple/exp-output-autoplan.txt index d20e62e18..88124493d 100644 --- a/server/testfixtures/test-repos/simple/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/simple/exp-output-autoplan.txt @@ -39,3 +39,5 @@ 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/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 7a33d1643..07fb3d7fa 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 @@ -29,3 +29,5 @@ Plan: 1 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/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 b3cb4f505..ced8df0b8 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 @@ -29,3 +29,5 @@ Plan: 1 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/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt b/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt index 9dcbc2bbc..ba1a86b23 100644 --- a/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt +++ b/server/testfixtures/test-repos/tfvars-yaml/exp-output-autoplan.txt @@ -63,3 +63,5 @@ Plan: 1 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`