Skip to content

Commit

Permalink
Merge pull request #1091 from runatlantis/unlock-cmd
Browse files Browse the repository at this point in the history
Unlock cmd
  • Loading branch information
lkysow authored Jun 24, 2020
2 parents c70dd5f + aed8d22 commit 4455e8b
Show file tree
Hide file tree
Showing 33 changed files with 618 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ node_modules/
helm/test-values.yaml
*.swp
golangci-lint
atlantis
atlantis
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ docker run --rm -v $(pwd):/go/src/github.com/runatlantis/atlantis -w /go/src/git
## Calling Your Local Atlantis From GitHub
- Create a test terraform repository in your GitHub.
- Create a personal access token for Atlantis. See [Create a GitHub token](https://github.com/runatlantis/atlantis#create-a-github-token).
- Create a personal access token for Atlantis. See [Create a GitHub token](https://github.com/runatlantis/atlantis/tree/master/runatlantis.io/docs/access-credentials.md#generating-an-access-token).
- Start Atlantis in server mode using that token:
```
atlantis server --gh-user <your username> --gh-token <your token> --repo-whitelist <your repo> --gh-webhook-secret <your webhook secret> --log-level debug
Expand All @@ -53,7 +53,7 @@ atlantis server --gh-user <your username> --gh-token <your token> --repo-whiteli
```
ngrok http 4141
```
- Create a Webhook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis#add-github-webhook).
- Create a Webhook in your repo and use the `https` url that `ngrok` printed out after running `ngrok http 4141`. Be sure to append `/events` so your webhook url looks something like `https://efce3bcd.ngrok.io/events`. See [Add GitHub Webhook](https://github.com/runatlantis/atlantis/blob/master/runatlantis.io/docs/configuring-webhooks.md#configuring-webhooks).
- Create a pull request and type `atlantis help`. You should see the request in the `ngrok` and Atlantis logs and you should also see Atlantis comment back.
## Code Style
Expand Down
15 changes: 15 additions & 0 deletions server/events/command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -302,6 +316,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
c.deletePlans(ctx)
result.PlansDeleted = true
}

c.updatePull(
ctx,
cmd,
Expand Down
40 changes: 40 additions & 0 deletions server/events/command_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -80,6 +83,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
WorkingDir: workingDir,
DisableApplyAll: false,
Drainer: drainer,
DeleteLockCommand: deleteLockCommand,
}
return vcsClient
}
Expand Down Expand Up @@ -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) {
Expand Down
35 changes: 28 additions & 7 deletions server/events/comment_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen
return CommentParseResult{CommentResponse: HelpComment}
}

// Need to have a plan or apply at this point.
if !e.stringInSlice(command, []string{models.PlanCommand.String(), models.ApplyCommand.String()}) {
// Need to have a plan, apply or unlock at this point.
if !e.stringInSlice(command, []string{models.PlanCommand.String(), models.ApplyCommand.String(), models.UnlockCommand.String()}) {
return CommentParseResult{CommentResponse: fmt.Sprintf("```\nError: unknown command %q.\nRun 'atlantis --help' for usage.\n```", command)}
}

Expand Down Expand Up @@ -186,6 +186,11 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen
flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Apply the plan for this directory, relative to root of repo, ex. 'child/dir'.")
flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Apply the plan for this project. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", yaml.AtlantisYAMLFilename))
flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.")
case models.UnlockCommand.String():
name = models.UnlockCommand
flagSet = pflag.NewFlagSet(models.UnlockCommand.String(), pflag.ContinueOnError)
flagSet.SetOutput(ioutil.Discard)

default:
return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", command)}
}
Expand All @@ -197,6 +202,9 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen
return CommentParseResult{CommentResponse: fmt.Sprintf("```\nUsage of %s:\n%s\n```", command, flagSet.FlagUsagesWrapped(usagesCols))}
}
if err != nil {
if command == models.UnlockCommand.String() {
return CommentParseResult{CommentResponse: UnlockUsage}
}
return CommentParseResult{CommentResponse: e.errMarkdown(err.Error(), command, flagSet)}
}

Expand Down Expand Up @@ -342,11 +350,13 @@ Examples:
atlantis apply -d . -w staging
Commands:
plan Runs 'terraform plan' for the changes in this pull request.
To plan a specific project, use the -d, -w and -p flags.
apply Runs 'terraform apply' on all unapplied plans from this pull request.
To only apply a specific plan, use the -d, -w and -p flags.
help View help.
plan Runs 'terraform plan' for the changes in this pull request.
To plan a specific project, use the -d, -w and -p flags.
apply Runs 'terraform apply' on all unapplied plans from this pull request.
To only apply a specific plan, use the -d, -w and -p flags.
unlock Removes all atlantis locks and discards all plans for this PR.
To unlock a specific plan you can use the Atlantis UI.
help View help.
Flags:
-h, --help help for atlantis
Expand All @@ -357,3 +367,14 @@ Use "atlantis [command] --help" for more information about a command.` +
// DidYouMeanAtlantisComment is the comment we add to the pull request when
// someone runs a command with terraform instead of atlantis.
var DidYouMeanAtlantisComment = "Did you mean to use `atlantis` instead of `terraform`?"

// UnlockUsage is the comment we add to the pull request when someone runs
// `atlantis unlock` with flags.

var UnlockUsage = "`Usage of unlock:`\n\n ```cmake\n" +
`atlantis unlock
Unlocks the entire PR and discards all plans in this PR.
Arguments or flags are not supported at the moment.
If you need to unlock a specific project please use the atlantis UI.` +
"\n```"
14 changes: 14 additions & 0 deletions server/events/comment_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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```"
79 changes: 79 additions & 0 deletions server/events/delete_lock_command.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 4455e8b

Please sign in to comment.