Skip to content

Commit

Permalink
Add the ability to disable Atlantis locking a repo (#1340)
Browse files Browse the repository at this point in the history
* Add the ability to disable Atlantis locking a repo

* Fix incorrect logic when creating locking Client

Co-authored-by: kbaldyga <[email protected]>

* Add tests for noOpLocker to improve coverage

Co-authored-by: Gerald Barker <[email protected]>
Co-authored-by: kbaldyga <[email protected]>
  • Loading branch information
3 people committed Jan 14, 2021
1 parent cf9dd4e commit 1137a82
Show file tree
Hide file tree
Showing 11 changed files with 604 additions and 24 deletions.
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const (
DisableApplyFlag = "disable-apply"
DisableAutoplanFlag = "disable-autoplan"
DisableMarkdownFoldingFlag = "disable-markdown-folding"
DisableRepoLockingFlag = "disable-repo-locking"
GHHostnameFlag = "gh-hostname"
GHTokenFlag = "gh-token"
GHUserFlag = "gh-user"
Expand Down Expand Up @@ -291,6 +292,10 @@ var boolFlags = map[string]boolFlag{
description: "Disable atlantis auto planning feature",
defaultValue: false,
},
DisableRepoLockingFlag: {
description: "Disable atlantis locking repos",
defaultValue: false,
},
AllowDraftPRs: {
description: "Enable autoplan for Github Draft Pull Requests",
defaultValue: false,
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ var testFlags = map[string]interface{}{
DisableApplyAllFlag: true,
DisableApplyFlag: true,
DisableMarkdownFoldingFlag: true,
DisableRepoLockingFlag: true,
GHHostnameFlag: "ghhostname",
GHTokenFlag: "token",
GHUserFlag: "user",
Expand Down
6 changes: 6 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,12 @@ Values are chosen in this order:
```
Disable atlantis auto planning

* ### `--disable-repo-locking`
```bash
atlantis server --disable-repo-locking
```
Stops atlantis locking projects and or workspaces when running terraform

* ### `--gh-hostname`
```bash
atlantis server --gh-hostname="my.github.enterprise.com"
Expand Down
9 changes: 5 additions & 4 deletions server/events/comment_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ package events
import (
"bytes"
"fmt"
"github.com/flynn-archive/go-shlex"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/yaml"
"github.com/spf13/pflag"
"io/ioutil"
"net/url"
"path/filepath"
"regexp"
"strings"
"text/template"

"github.com/flynn-archive/go-shlex"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/yaml"
"github.com/spf13/pflag"
)

const (
Expand Down
44 changes: 44 additions & 0 deletions server/events/locking/locking.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,47 @@ func (c *Client) lockKeyToProjectWorkspace(key string) (models.Project, string,

return models.Project{RepoFullName: matches[1], Path: matches[2]}, matches[3], nil
}

type NoOpLocker struct{}

// NewNoOpLocker returns a new lno operation lockingclient.
func NewNoOpLocker() *NoOpLocker {
return &NoOpLocker{}
}

// TryLock attempts to acquire a lock to a project and workspace.
func (c *NoOpLocker) TryLock(p models.Project, workspace string, pull models.PullRequest, user models.User) (TryLockResponse, error) {
return TryLockResponse{true, models.ProjectLock{}, c.key(p, workspace)}, nil
}

// Unlock attempts to unlock a project and workspace. If successful,
// a pointer to the now deleted lock will be returned. Else, that
// pointer will be nil. An error will only be returned if there was
// an error deleting the lock (i.e. not if there was no lock).
func (c *NoOpLocker) Unlock(key string) (*models.ProjectLock, error) {
return &models.ProjectLock{}, nil
}

// List returns a map of all locks with their lock key as the map key.
// The lock key can be used in GetLock() and Unlock().
func (c *NoOpLocker) List() (map[string]models.ProjectLock, error) {
m := make(map[string]models.ProjectLock)
return m, nil
}

// UnlockByPull deletes all locks associated with that pull request.
func (c *NoOpLocker) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {
return []models.ProjectLock{}, nil
}

// GetLock attempts to get the lock stored at key. If successful,
// a pointer to the lock will be returned. Else, the pointer will be nil.
// An error will only be returned if there was an error getting the lock
// (i.e. not if there was no lock).
func (c *NoOpLocker) GetLock(key string) (*models.ProjectLock, error) {
return nil, nil
}

func (c *NoOpLocker) key(p models.Project, workspace string) string {
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, workspace)
}
37 changes: 37 additions & 0 deletions server/events/locking/locking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,40 @@ func TestGetLock(t *testing.T) {
Ok(t, err)
Equals(t, &pl, lock)
}

func TestTryLock_NoOpLocker(t *testing.T) {
RegisterMockTestingT(t)
currLock := models.ProjectLock{}
l := locking.NewNoOpLocker()
r, err := l.TryLock(project, workspace, pull, user)
Ok(t, err)
Equals(t, locking.TryLockResponse{LockAcquired: true, CurrLock: currLock, LockKey: "owner/repo/path/workspace"}, r)
}

func TestUnlock_NoOpLocker(t *testing.T) {
l := locking.NewNoOpLocker()
lock, err := l.Unlock("owner/repo/path/workspace")
Ok(t, err)
Equals(t, &models.ProjectLock{}, lock)
}

func TestList_NoOpLocker(t *testing.T) {
l := locking.NewNoOpLocker()
list, err := l.List()
Ok(t, err)
Equals(t, map[string]models.ProjectLock{}, list)
}

func TestUnlockByPull_NoOpLocker(t *testing.T) {
l := locking.NewNoOpLocker()
_, err := l.UnlockByPull("owner/repo", 1)
Ok(t, err)
}

func TestGetLock_NoOpLocker(t *testing.T) {
l := locking.NewNoOpLocker()
lock, err := l.GetLock("owner/repo/path/workspace")
Ok(t, err)
var expected *models.ProjectLock = nil
Equals(t, expected, lock)
}
38 changes: 21 additions & 17 deletions server/events/markdown_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,18 @@ type MarkdownRenderer struct {
DisableApplyAll bool
DisableApply bool
DisableMarkdownFolding bool
DisableRepoLocking bool
}

// commonData is data that all responses have.
type commonData struct {
Command string
Verbose bool
Log string
PlansDeleted bool
DisableApplyAll bool
DisableApply bool
Command string
Verbose bool
Log string
PlansDeleted bool
DisableApplyAll bool
DisableApply bool
DisableRepoLocking bool
}

// errData is data about an error response.
Expand All @@ -72,8 +74,9 @@ type resultData struct {

type planSuccessData struct {
models.PlanSuccess
PlanWasDeleted bool
DisableApply bool
PlanWasDeleted bool
DisableApply bool
DisableRepoLocking bool
}

type projectResultTmplData struct {
Expand All @@ -88,12 +91,13 @@ type projectResultTmplData struct {
func (m *MarkdownRenderer) Render(res CommandResult, cmdName models.CommandName, log string, verbose bool, vcsHost models.VCSHostType) string {
commandStr := strings.Title(cmdName.String())
common := commonData{
Command: commandStr,
Verbose: verbose,
Log: log,
PlansDeleted: res.PlansDeleted,
DisableApplyAll: m.DisableApplyAll || m.DisableApply,
DisableApply: m.DisableApply,
Command: commandStr,
Verbose: verbose,
Log: log,
PlansDeleted: res.PlansDeleted,
DisableApplyAll: m.DisableApplyAll || m.DisableApply,
DisableApply: m.DisableApply,
DisableRepoLocking: m.DisableRepoLocking,
}
if res.Error != nil {
return m.renderTemplate(unwrappedErrWithLogTmpl, errData{res.Error.Error(), common})
Expand Down Expand Up @@ -136,9 +140,9 @@ func (m *MarkdownRenderer) renderProjectResults(results []models.ProjectResult,
})
} else if result.PlanSuccess != nil {
if m.shouldUseWrappedTmpl(vcsHost, result.PlanSuccess.TerraformOutput) {
resultData.Rendered = m.renderTemplate(planSuccessWrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply})
resultData.Rendered = m.renderTemplate(planSuccessWrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply, DisableRepoLocking: common.DisableRepoLocking})
} else {
resultData.Rendered = m.renderTemplate(planSuccessUnwrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply})
resultData.Rendered = m.renderTemplate(planSuccessUnwrappedTmpl, planSuccessData{PlanSuccess: *result.PlanSuccess, PlanWasDeleted: common.PlansDeleted, DisableApply: common.DisableApply, DisableRepoLocking: common.DisableRepoLocking})
}
numPlanSuccesses++
} else if result.ApplySuccess != "" {
Expand Down Expand Up @@ -258,7 +262,7 @@ var planSuccessWrappedTmpl = template.Must(template.New("").Parse(
var planNextSteps = "{{ if .PlanWasDeleted }}This plan was not saved because one or more projects failed and automerge requires all plans pass.{{ else }}" +
"{{ if not .DisableApply }}* :arrow_forward: To **apply** this plan, comment:\n" +
" * `{{.ApplyCmd}}`\n{{end}}" +
"* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}})\n" +
"{{ if not .DisableRepoLocking }}* :put_litter_in_its_place: To **delete** this plan click [here]({{.LockURL}})\n{{end}}" +
"* :repeat: To **plan** this project again, comment:\n" +
" * `{{.RePlanCmd}}`{{end}}"
var applyUnwrappedSuccessTmpl = template.Must(template.New("").Parse(
Expand Down
Loading

0 comments on commit 1137a82

Please sign in to comment.