feat: Added disable-unlock-label config option (#3799)

* Added disable-unlock-label config option

* Fixed tests

* Wrote tests + fixed mistakes

* Added docs

* added defaults to docs

---------

Co-authored-by: PePe Amengual <jose.amengual@gmail.com>
This commit is contained in:
Koen van Zuijlen
2023-10-10 17:02:26 +02:00
committed by GitHub
parent 2263d5fd91
commit 786e57c1d6
10 changed files with 127 additions and 14 deletions

View File

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

View File

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

View File

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

View File

@@ -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()
@@ -1460,6 +1462,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
mocks.NewMockDeleteLockCommand(),
e2eVCSClient,
silenceNoProjects,
disableUnlockLabel,
)
versionCommandRunner := events.NewVersionCommandRunner(
@@ -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()

View File

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

View File

@@ -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 {
@@ -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 {
@@ -195,6 +197,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
deleteLockCommand,
vcsClient,
testConfig.SilenceNoProjects,
testConfig.DisableUnlockLabel,
)
versionCommandRunner := events.NewVersionCommandRunner(
@@ -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()

View File

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

View File

@@ -286,6 +286,13 @@ func (mock *MockClient) UpdateStatus(repo models.Repo, pull models.PullRequest,
return ret0
}
func (mock *MockClient) VerifyWasNotCalled() *VerifierMockClient {
return &VerifierMockClient{
mock: mock,
invocationCountMatcher: pegomock.Times(0),
}
}
func (mock *MockClient) VerifyWasCalledOnce() *VerifierMockClient {
return &VerifierMockClient{
mock: mock,

View File

@@ -746,6 +746,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
deleteLockCommand,
vcsClient,
userConfig.SilenceNoProjects,
userConfig.DisableUnlockLabel,
)
versionCommandRunner := events.NewVersionCommandRunner(

View File

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