Implement atlantis unlock

This command is run on a pr and deletes all locks for the pr
This commit is contained in:
Paris Morali
2020-04-23 14:02:06 +01:00
committed by Luke Kysow
parent ab7016063a
commit aed8d22b52
33 changed files with 617 additions and 266 deletions

1
.gitignore vendored
View File

@@ -13,4 +13,3 @@ helm/test-values.yaml
*.swp
golangci-lint
atlantis
temp.sh

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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```"

View File

@@ -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```"

View File

@@ -623,7 +623,6 @@ func TestPullStatus_UpdateMerge(t *testing.T) {
LockURL: "lock-url",
RePlanCmd: "plan command",
ApplyCmd: "apply command",
DiscardCmd: "discard command",
},
},
})

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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" +
"</details>"))
var discardUnwrappedSuccessTmpl = template.Must(template.New("").Parse(
"```diff\n" +
"{{.Output}}\n" +
"```"))
var unwrappedErrTmplText = "**{{.Command}} Error**\n" +
"```\n" +
"{{.Error}}\n" +

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 ""
}

View File

@@ -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())
}

View File

@@ -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 founddid 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(

View File

@@ -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")
//}
}

View File

@@ -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")

View File

@@ -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")
}

View File

@@ -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,

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`