mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 23:58:19 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -746,6 +746,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
deleteLockCommand,
|
||||
vcsClient,
|
||||
userConfig.SilenceNoProjects,
|
||||
userConfig.DisableUnlockLabel,
|
||||
)
|
||||
|
||||
versionCommandRunner := events.NewVersionCommandRunner(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user