Skip to content

Commit

Permalink
feat: Added disable-unlock-label config option (runatlantis#3799)
Browse files Browse the repository at this point in the history
* Added disable-unlock-label config option

* Fixed tests

* Wrote tests + fixed mistakes

* Added docs

* added defaults to docs

---------

Co-authored-by: PePe Amengual <[email protected]>
  • Loading branch information
2 people authored and ijames-gc committed Feb 13, 2024
1 parent af3d16f commit 4a43f8a
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 14 deletions.
7 changes: 6 additions & 1 deletion cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const (
DisableAutoplanLabelFlag = "disable-autoplan-label"
DisableMarkdownFoldingFlag = "disable-markdown-folding"
DisableRepoLockingFlag = "disable-repo-locking"
DisableUnlockLabelFlag = "disable-unlock-label"
DiscardApprovalOnPlanFlag = "discard-approval-on-plan"
EmojiReaction = "emoji-reaction"
EnablePolicyChecksFlag = "enable-policy-checks"
Expand Down Expand Up @@ -135,7 +136,7 @@ const (
RestrictFileList = "restrict-file-list"
TFDownloadFlag = "tf-download"
TFDownloadURLFlag = "tf-download-url"
UseTFPluginCache = "use-tf-plugin-cache"
UseTFPluginCache = "use-tf-plugin-cache"
VarFileAllowlistFlag = "var-file-allowlist"
VCSStatusName = "vcs-status-name"
TFEHostnameFlag = "tfe-hostname"
Expand Down Expand Up @@ -262,6 +263,10 @@ var stringFlags = map[string]stringFlag{
description: "Pull request label to disable atlantis auto planning feature only if present.",
defaultValue: "",
},
DisableUnlockLabelFlag: {
description: "Pull request label to disable atlantis unlock feature only if present.",
defaultValue: "",
},
EmojiReaction: {
description: "Emoji Reaction to use to react to comments",
defaultValue: DefaultEmojiReaction,
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ var testFlags = map[string]interface{}{
WriteGitCredsFlag: true,
DisableAutoplanFlag: true,
DisableAutoplanLabelFlag: "no-auto-plan",
DisableUnlockLabelFlag: "do-not-unlock",
EnablePolicyChecksFlag: false,
EnableRegExpCmdFlag: false,
EnableDiffMarkdownFormat: false,
Expand Down
8 changes: 8 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ and set `--autoplan-modules` to `false`.
```
Stops atlantis from locking projects and or workspaces when running terraform.

### `--disable-unlock-label`
```bash
atlantis server --disable-unlock-label do-not-unlock
# or
ATLANTIS_DISABLE_UNLOCK_LABEL="do-not-unlock"
```
Stops atlantis from unlocking a pull request with this label. Defaults to "" (feature disabled).

### `--emoji-reaction`
```bash
atlantis server --emoji-reaction thumbsup
Expand Down
5 changes: 4 additions & 1 deletion server/controllers/events/events_controller_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,8 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
parallelPoolSize := 1
silenceNoProjects := false

disableUnlockLabel := "do-not-unlock"

statusUpdater := runtimemocks.NewMockStatusUpdater()
commitStatusUpdater := mocks.NewMockCommitStatusUpdater()
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
Expand Down Expand Up @@ -1460,6 +1462,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
mocks.NewMockDeleteLockCommand(),
e2eVCSClient,
silenceNoProjects,
disableUnlockLabel,
)

versionCommandRunner := events.NewVersionCommandRunner(
Expand Down Expand Up @@ -1739,7 +1742,7 @@ func ensureRunningConftest(t *testing.T) {
_, err := exec.LookPath(conftestCommand)
if err != nil {
t.Logf(`%s must be installed to run this test
- on local, please install contest command or run 'make docker/test-all'
- on local, please install conftest command or run 'make docker/test-all'
- on CI, please check testing-env docker image contains conftest command. see testing/Dockerfile
`, conftestCommand)
t.FailNow()
Expand Down
6 changes: 3 additions & 3 deletions server/events/command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ type DefaultCommandRunner struct {
AzureDevopsPullGetter AzureDevopsPullGetter
GitlabMergeRequestGetter GitlabMergeRequestGetter
// User config option: Disables autoplan when a pull request is opened or updated.
DisableAutoplan bool
DisableAutoplanLabel string
EventParser EventParsing
DisableAutoplan bool
DisableAutoplanLabel string
EventParser EventParsing
// User config option: Fail and do not run the Atlantis command request if any of the pre workflow hooks error
FailOnPreWorkflowHookError bool
Logger logging.SimpleLogging
Expand Down
62 changes: 62 additions & 0 deletions server/events/command_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type TestConfig struct {
StatusName string
discardApprovalOnPlan bool
backend locking.Backend
DisableUnlockLabel string
}

func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.MockClient {
Expand All @@ -93,6 +94,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
StatusName: "atlantis-test",
discardApprovalOnPlan: false,
backend: defaultBoltDB,
DisableUnlockLabel: "do-not-unlock",
}

for _, op := range options {
Expand Down Expand Up @@ -195,6 +197,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
deleteLockCommand,
vcsClient,
testConfig.SilenceNoProjects,
testConfig.DisableUnlockLabel,
)

versionCommandRunner := events.NewVersionCommandRunner(
Expand Down Expand Up @@ -670,6 +673,65 @@ func TestRunUnlockCommandFail_VCSComment(t *testing.T) {
vcsClient.VerifyWasCalledOnce().CreateComment(testdata.GithubRepo, testdata.Pull.Num, "Failed to delete PR locks", "unlock")
}

func TestRunUnlockCommandFail_DisableUnlockLabel(t *testing.T) {
t.Log("if PR has label equal to disable-unlock-label unlock should fail")

doNotUnlock := "do-not-unlock"

vcsClient := setup(t)
pull := &github.PullRequest{
State: github.String("open"),
}
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
When(githubGetter.GetPullRequest(testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)
When(deleteLockCommand.DeleteLocksByPull(testdata.GithubRepo.FullName, testdata.Pull.Num)).ThenReturn(0, errors.New("err"))
When(ch.VCSClient.GetPullLabels(testdata.GithubRepo, modelPull)).ThenReturn([]string{doNotUnlock, "need-help"}, nil)

ch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Unlock})

vcsClient.VerifyWasCalledOnce().CreateComment(testdata.GithubRepo, testdata.Pull.Num, "Not allowed to unlock PR with "+doNotUnlock+" label", "unlock")
}

func TestRunUnlockCommandFail_GetLabelsFail(t *testing.T) {
t.Log("if GetPullLabels fails do not unlock PR")

vcsClient := setup(t)
pull := &github.PullRequest{
State: github.String("open"),
}
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
When(githubGetter.GetPullRequest(testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)
When(deleteLockCommand.DeleteLocksByPull(testdata.GithubRepo.FullName, testdata.Pull.Num)).ThenReturn(0, errors.New("err"))
When(ch.VCSClient.GetPullLabels(testdata.GithubRepo, modelPull)).ThenReturn(nil, errors.New("err"))

ch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Unlock})

vcsClient.VerifyWasCalledOnce().CreateComment(testdata.GithubRepo, testdata.Pull.Num, "Failed to retrieve PR labels... Not unlocking", "unlock")
}

func TestRunUnlockCommandDoesntRetrieveLabelsIfDisableUnlockLabelNotSet(t *testing.T) {
t.Log("if disable-unlock-label is not set do not call GetPullLabels")

doNotUnlock := "do-not-unlock"

vcsClient := setup(t)
pull := &github.PullRequest{
State: github.String("open"),
}
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
When(githubGetter.GetPullRequest(testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)
When(deleteLockCommand.DeleteLocksByPull(testdata.GithubRepo.FullName, testdata.Pull.Num)).ThenReturn(0, errors.New("err"))
When(ch.VCSClient.GetPullLabels(testdata.GithubRepo, modelPull)).ThenReturn([]string{doNotUnlock, "need-help"}, nil)
unlockCommandRunner.DisableUnlockLabel = ""

ch.RunCommentCommand(testdata.GithubRepo, &testdata.GithubRepo, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Unlock})

vcsClient.VerifyWasNotCalled().GetPullLabels(testdata.GithubRepo, modelPull)
}

func TestRunAutoplanCommand_DeletePlans(t *testing.T) {
setup(t)
tmp := t.TempDir()
Expand Down
41 changes: 33 additions & 8 deletions server/events/unlock_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ package events
import (
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/vcs"
"slices"
)

func NewUnlockCommandRunner(
deleteLockCommand DeleteLockCommand,
vcsClient vcs.Client,
SilenceNoProjects bool,
DisableUnlockLabel string,
) *UnlockCommandRunner {
return &UnlockCommandRunner{
deleteLockCommand: deleteLockCommand,
vcsClient: vcsClient,
SilenceNoProjects: SilenceNoProjects,
deleteLockCommand: deleteLockCommand,
vcsClient: vcsClient,
SilenceNoProjects: SilenceNoProjects,
DisableUnlockLabel: DisableUnlockLabel,
}
}

Expand All @@ -22,7 +25,8 @@ type UnlockCommandRunner struct {
deleteLockCommand DeleteLockCommand
// SilenceNoProjects is whether Atlantis should respond to PRs if no projects
// are found
SilenceNoProjects bool
SilenceNoProjects bool
DisableUnlockLabel string
}

func (u *UnlockCommandRunner) Run(
Expand All @@ -31,13 +35,34 @@ func (u *UnlockCommandRunner) Run(
) {
baseRepo := ctx.Pull.BaseRepo
pullNum := ctx.Pull.Num
disableUnlockLabel := u.DisableUnlockLabel

ctx.Log.Info("Unlocking all locks")
vcsMessage := "All Atlantis locks for this PR have been unlocked and plans discarded"
numLocks, err := u.deleteLockCommand.DeleteLocksByPull(baseRepo.FullName, pullNum)
if err != nil {
vcsMessage = "Failed to delete PR locks"
ctx.Log.Err("failed to delete locks by pull %s", err.Error())

var hasLabel bool
var err error
if disableUnlockLabel != "" {
var labels []string
labels, err = u.vcsClient.GetPullLabels(baseRepo, ctx.Pull)
if err != nil {
vcsMessage = "Failed to retrieve PR labels... Not unlocking"
ctx.Log.Err("Failed to retrieve PR labels for pull %s", err.Error())
}
hasLabel = slices.Contains(labels, disableUnlockLabel)
if hasLabel {
vcsMessage = "Not allowed to unlock PR with " + disableUnlockLabel + " label"
ctx.Log.Info("Not allowed to unlock PR with %v label", disableUnlockLabel)
}
}

var numLocks int
if err == nil && !hasLabel {
numLocks, err = u.deleteLockCommand.DeleteLocksByPull(baseRepo.FullName, pullNum)
if err != nil {
vcsMessage = "Failed to delete PR locks"
ctx.Log.Err("failed to delete locks by pull %s", err.Error())
}
}

// if there are no locks to delete, no errors, and SilenceNoProjects is enabled, don't comment
Expand Down
7 changes: 7 additions & 0 deletions server/events/vcs/mocks/mock_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
deleteLockCommand,
vcsClient,
userConfig.SilenceNoProjects,
userConfig.DisableUnlockLabel,
)

versionCommandRunner := events.NewVersionCommandRunner(
Expand Down
3 changes: 2 additions & 1 deletion server/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type UserConfig struct {
DisableAutoplanLabel string `mapstructure:"disable-autoplan-label"`
DisableMarkdownFolding bool `mapstructure:"disable-markdown-folding"`
DisableRepoLocking bool `mapstructure:"disable-repo-locking"`
DisableUnlockLabel string `mapstructure:"disable-unlock-label"`
DiscardApprovalOnPlanFlag bool `mapstructure:"discard-approval-on-plan"`
EmojiReaction string `mapstructure:"emoji-reaction"`
EnablePolicyChecksFlag bool `mapstructure:"enable-policy-checks"`
Expand Down Expand Up @@ -126,7 +127,7 @@ type UserConfig struct {
WebPassword string `mapstructure:"web-password"`
WriteGitCreds bool `mapstructure:"write-git-creds"`
WebsocketCheckOrigin bool `mapstructure:"websocket-check-origin"`
UseTFPluginCache bool `mapstructure:"use-tf-plugin-cache"`
UseTFPluginCache bool `mapstructure:"use-tf-plugin-cache"`
}

// ToAllowCommandNames parse AllowCommands into a slice of CommandName
Expand Down

0 comments on commit 4a43f8a

Please sign in to comment.