diff --git a/cmd/server.go b/cmd/server.go index 869d46f3f2..b9949a3908 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -80,6 +80,7 @@ const ( EnableRegExpCmdFlag = "enable-regexp-cmd" EnableDiffMarkdownFormat = "enable-diff-markdown-format" ExecutableName = "executable-name" + FailOnPreWorkflowHookError = "fail-on-pre-workflow-hook-error" HideUnchangedPlanComments = "hide-unchanged-plan-comments" GHHostnameFlag = "gh-hostname" GHTeamAllowlistFlag = "gh-team-allowlist" @@ -457,6 +458,10 @@ var boolFlags = map[string]boolFlag{ description: "Enable Atlantis to format Terraform plan output into a markdown-diff friendly format for color-coding purposes.", defaultValue: false, }, + FailOnPreWorkflowHookError: { + description: "Fail and do not run the requested Atlantis command if any of the pre workflow hooks error.", + defaultValue: false, + }, GHAllowMergeableBypassApply: { description: "Feature flag to enable functionality to allow mergeable check to ignore apply required check", defaultValue: false, diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 7af5d2f235..ad8231b34a 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -429,6 +429,15 @@ and set `--autoplan-modules` to `false`. This is useful when running multiple Atlantis servers against a single repository. +### `--fail-on-pre-workflow-hook-error` + ```bash + atlantis server --fail-on-pre-workflow-hook-error + # or + ATLANTIS_FAIL_ON_PRE_WORKFLOW_HOOK_ERROR=true + ``` + + Fail and do not run the requested Atlantis command if any of the pre workflow hooks error. + ### `--hide-unchanged-plan-comments` ```bash atlantis server --hide-unchanged-plan-comments diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 90ed842872..a61d63ee20 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -96,12 +96,15 @@ type DefaultCommandRunner struct { GithubPullGetter GithubPullGetter AzureDevopsPullGetter AzureDevopsPullGetter GitlabMergeRequestGetter GitlabMergeRequestGetter - DisableAutoplan bool - EventParser EventParsing - Logger logging.SimpleLogging - GlobalCfg valid.GlobalCfg - StatsScope tally.Scope - // AllowForkPRs controls whether we operate on pull requests from forks. + // User config option: Disables autoplan when a pull request is opened or updated. + DisableAutoplan bool + 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 + GlobalCfg valid.GlobalCfg + StatsScope tally.Scope + // User config option: controls whether to operate on pull requests from forks. AllowForkPRs bool // ParallelPoolSize controls the size of the wait group used to run // parallel plans and applies (if enabled). @@ -110,7 +113,7 @@ type DefaultCommandRunner struct { // this in our error message back to the user on a forked PR so they know // how to enable this functionality. AllowForkPRsFlag string - // SilenceForkPRErrors controls whether to comment on Fork PRs when AllowForkPRs = False + // User config option: controls whether to comment on Fork PRs when AllowForkPRs = False SilenceForkPRErrors bool // SilenceForkPRErrorsFlag is the name of the flag that controls fork PR's. We use // this in our error message back to the user on a forked PR so they know @@ -169,7 +172,14 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo err = c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd) if err != nil { - ctx.Log.Err("Error running pre-workflow hooks %s. Proceeding with %s command.", err, command.Plan) + ctx.Log.Err("Error running pre-workflow hooks %s.", err) + + if c.FailOnPreWorkflowHookError { + ctx.Log.Err("'fail-on-pre-workflow-hook-error' set, so not running %s command.", command.Plan) + return + } + + ctx.Log.Err("'fail-on-pre-workflow-hook-error' not set so running %s command.", command.Plan) } autoPlanRunner := buildCommentCommandRunner(c, command.Plan) @@ -293,7 +303,14 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead err = c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd) if err != nil { - ctx.Log.Err("Error running pre-workflow hooks %s. Proceeding with %s command.", err, cmd.Name.String()) + ctx.Log.Err("Error running pre-workflow hooks %s.", err) + + if c.FailOnPreWorkflowHookError { + ctx.Log.Err("'fail-on-pre-workflow-hook-error' set, so not running %s command.", cmd.Name.String()) + return + } + + ctx.Log.Err("'fail-on-pre-workflow-hook-error' not set so running %s command.", cmd.Name.String()) } cmdRunner := buildCommentCommandRunner(c, cmd.CommandName()) diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 141cc31033..90995ba296 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -237,6 +237,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock VCSClient: vcsClient, CommentCommandRunnerByCmd: commentCommandRunnerByCmd, EventParser: eventParsing, + FailOnPreWorkflowHookError: false, GithubPullGetter: githubGetter, GitlabMergeRequestGetter: gitlabGetter, AzureDevopsPullGetter: azuredevopsGetter, @@ -646,6 +647,92 @@ func TestRunAutoplanCommand_DeletePlans(t *testing.T) { lockingLocker.VerifyWasCalledOnce().UnlockByPull(testdata.Pull.BaseRepo.FullName, testdata.Pull.Num) } +func TestRunAutoplanCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_False(t *testing.T) { + setup(t) + tmp := t.TempDir() + boltDB, err := db.New(tmp) + Ok(t, err) + dbUpdater.Backend = boltDB + applyCommandRunner.Backend = boltDB + + When(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())). + ThenReturn([]command.ProjectContext{ + { + CommandName: command.Plan, + }, + }, nil) + When(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectResult{PlanSuccess: &models.PlanSuccess{}}) + When(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil) + When(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New("err")) + testdata.Pull.BaseRepo = testdata.GithubRepo + ch.FailOnPreWorkflowHookError = false + ch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User) + pendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp) + lockingLocker.VerifyWasCalledOnce().UnlockByPull(testdata.Pull.BaseRepo.FullName, testdata.Pull.Num) +} + +func TestRunAutoplanCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_True(t *testing.T) { + setup(t) + tmp := t.TempDir() + boltDB, err := db.New(tmp) + Ok(t, err) + dbUpdater.Backend = boltDB + applyCommandRunner.Backend = boltDB + + When(projectCommandBuilder.BuildAutoplanCommands(Any[*command.Context]())). + ThenReturn([]command.ProjectContext{ + { + CommandName: command.Plan, + }, + }, nil) + When(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New("err")) + testdata.Pull.BaseRepo = testdata.GithubRepo + ch.FailOnPreWorkflowHookError = true + ch.RunAutoplanCommand(testdata.GithubRepo, testdata.GithubRepo, testdata.Pull, testdata.User) + pendingPlanFinder.VerifyWasCalled(Never()).DeletePlans(Any[string]()) + lockingLocker.VerifyWasCalled(Never()).UnlockByPull(Any[string](), Any[int]()) +} + +func TestRunCommentCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_False(t *testing.T) { + setup(t) + tmp := t.TempDir() + boltDB, err := db.New(tmp) + Ok(t, err) + dbUpdater.Backend = boltDB + applyCommandRunner.Backend = boltDB + + When(projectCommandRunner.Plan(Any[command.ProjectContext]())).ThenReturn(command.ProjectResult{PlanSuccess: &models.PlanSuccess{}}) + When(workingDir.GetPullDir(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(tmp, nil) + 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(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New("err")) + testdata.Pull.BaseRepo = testdata.GithubRepo + ch.FailOnPreWorkflowHookError = false + ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan}) + pendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp) + lockingLocker.VerifyWasCalledOnce().UnlockByPull(testdata.Pull.BaseRepo.FullName, testdata.Pull.Num) +} + +func TestRunCommentCommand_FailedPreWorkflowHook_FailOnPreWorkflowHookError_True(t *testing.T) { + setup(t) + tmp := t.TempDir() + boltDB, err := db.New(tmp) + Ok(t, err) + dbUpdater.Backend = boltDB + applyCommandRunner.Backend = boltDB + autoMerger.GlobalAutomerge = true + defer func() { autoMerger.GlobalAutomerge = false }() + + When(preWorkflowHooksCommandRunner.RunPreHooks(Any[*command.Context](), Any[*events.CommentCommand]())).ThenReturn(errors.New("err")) + testdata.Pull.BaseRepo = testdata.GithubRepo + ch.FailOnPreWorkflowHookError = true + ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan}) + pendingPlanFinder.VerifyWasCalled(Never()).DeletePlans(Any[string]()) + lockingLocker.VerifyWasCalled(Never()).UnlockByPull(Any[string](), Any[int]()) +} + func TestRunGenericPlanCommand_DeletePlans(t *testing.T) { setup(t) tmp := t.TempDir() diff --git a/server/server.go b/server/server.go index f3f952b71d..380af5baa0 100644 --- a/server/server.go +++ b/server/server.go @@ -795,6 +795,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { AzureDevopsPullGetter: azuredevopsClient, CommentCommandRunnerByCmd: commentCommandRunnerByCmd, EventParser: eventParser, + FailOnPreWorkflowHookError: userConfig.FailOnPreWorkflowHookError, Logger: logger, GlobalCfg: globalCfg, StatsScope: statsScope.SubScope("cmd"), diff --git a/server/user_config.go b/server/user_config.go index 209482049e..5552df6a2a 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -11,37 +11,39 @@ import ( // The mapstructure tags correspond to flags in cmd/server.go and are used when // the config is parsed from a YAML file. type UserConfig struct { - AllowForkPRs bool `mapstructure:"allow-fork-prs"` - AllowRepoConfig bool `mapstructure:"allow-repo-config"` - AllowCommands string `mapstructure:"allow-commands"` - AtlantisURL string `mapstructure:"atlantis-url"` - Automerge bool `mapstructure:"automerge"` - AutoplanFileList string `mapstructure:"autoplan-file-list"` - AutoplanModules bool `mapstructure:"autoplan-modules"` - AutoplanModulesFromProjects string `mapstructure:"autoplan-modules-from-projects"` - AzureDevopsToken string `mapstructure:"azuredevops-token"` - AzureDevopsUser string `mapstructure:"azuredevops-user"` - AzureDevopsWebhookPassword string `mapstructure:"azuredevops-webhook-password"` - AzureDevopsWebhookUser string `mapstructure:"azuredevops-webhook-user"` - AzureDevOpsHostname string `mapstructure:"azuredevops-hostname"` - BitbucketBaseURL string `mapstructure:"bitbucket-base-url"` - BitbucketToken string `mapstructure:"bitbucket-token"` - BitbucketUser string `mapstructure:"bitbucket-user"` - BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"` - CheckoutDepth int `mapstructure:"checkout-depth"` - CheckoutStrategy string `mapstructure:"checkout-strategy"` - DataDir string `mapstructure:"data-dir"` - DisableApplyAll bool `mapstructure:"disable-apply-all"` - DisableApply bool `mapstructure:"disable-apply"` - DisableAutoplan bool `mapstructure:"disable-autoplan"` - DisableMarkdownFolding bool `mapstructure:"disable-markdown-folding"` - DisableRepoLocking bool `mapstructure:"disable-repo-locking"` - DiscardApprovalOnPlanFlag bool `mapstructure:"discard-approval-on-plan"` - EmojiReaction string `mapstructure:"emoji-reaction"` - EnablePolicyChecksFlag bool `mapstructure:"enable-policy-checks"` - EnableRegExpCmd bool `mapstructure:"enable-regexp-cmd"` - EnableDiffMarkdownFormat bool `mapstructure:"enable-diff-markdown-format"` - ExecutableName string `mapstructure:"executable-name"` + AllowForkPRs bool `mapstructure:"allow-fork-prs"` + AllowRepoConfig bool `mapstructure:"allow-repo-config"` + AllowCommands string `mapstructure:"allow-commands"` + AtlantisURL string `mapstructure:"atlantis-url"` + Automerge bool `mapstructure:"automerge"` + AutoplanFileList string `mapstructure:"autoplan-file-list"` + AutoplanModules bool `mapstructure:"autoplan-modules"` + AutoplanModulesFromProjects string `mapstructure:"autoplan-modules-from-projects"` + AzureDevopsToken string `mapstructure:"azuredevops-token"` + AzureDevopsUser string `mapstructure:"azuredevops-user"` + AzureDevopsWebhookPassword string `mapstructure:"azuredevops-webhook-password"` + AzureDevopsWebhookUser string `mapstructure:"azuredevops-webhook-user"` + AzureDevOpsHostname string `mapstructure:"azuredevops-hostname"` + BitbucketBaseURL string `mapstructure:"bitbucket-base-url"` + BitbucketToken string `mapstructure:"bitbucket-token"` + BitbucketUser string `mapstructure:"bitbucket-user"` + BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"` + CheckoutDepth int `mapstructure:"checkout-depth"` + CheckoutStrategy string `mapstructure:"checkout-strategy"` + DataDir string `mapstructure:"data-dir"` + DisableApplyAll bool `mapstructure:"disable-apply-all"` + DisableApply bool `mapstructure:"disable-apply"` + DisableAutoplan bool `mapstructure:"disable-autoplan"` + DisableMarkdownFolding bool `mapstructure:"disable-markdown-folding"` + DisableRepoLocking bool `mapstructure:"disable-repo-locking"` + DiscardApprovalOnPlanFlag bool `mapstructure:"discard-approval-on-plan"` + EmojiReaction string `mapstructure:"emoji-reaction"` + EnablePolicyChecksFlag bool `mapstructure:"enable-policy-checks"` + EnableRegExpCmd bool `mapstructure:"enable-regexp-cmd"` + EnableDiffMarkdownFormat bool `mapstructure:"enable-diff-markdown-format"` + ExecutableName string `mapstructure:"executable-name"` + // Fail and do not run the Atlantis command request if any of the pre workflow hooks error. + FailOnPreWorkflowHookError bool `mapstructure:"fail-on-pre-workflow-hook-error"` HideUnchangedPlanComments bool `mapstructure:"hide-unchanged-plan-comments"` GithubAllowMergeableBypassApply bool `mapstructure:"gh-allow-mergeable-bypass-apply"` GithubHostname string `mapstructure:"gh-hostname"`