From aae2fd7c2b1e1b4137fba5ee2b4e6515895fbfdc Mon Sep 17 00:00:00 2001
From: X-Guardian <32168619+X-Guardian@users.noreply.github.com>
Date: Sat, 1 Feb 2025 16:35:36 +0000
Subject: [PATCH] Format fix
Signed-off-by: X-Guardian <32168619+X-Guardian@users.noreply.github.com>
---
server/events/apply_command_runner.copy | 197 ++++
server/events/command_runner.copy | 602 ++++++++++
server/events/pull_closed_executor_test.go | 2 +-
server/metrics/debug.go | 12 +-
server/server.copy | 1247 ++++++++++++++++++++
5 files changed, 2053 insertions(+), 7 deletions(-)
create mode 100644 server/events/apply_command_runner.copy
create mode 100644 server/events/command_runner.copy
create mode 100644 server/server.copy
diff --git a/server/events/apply_command_runner.copy b/server/events/apply_command_runner.copy
new file mode 100644
index 0000000000..d6e9d38388
--- /dev/null
+++ b/server/events/apply_command_runner.copy
@@ -0,0 +1,197 @@
+package events
+
+import (
+ "github.com/runatlantis/atlantis/server/core/locking"
+ "github.com/runatlantis/atlantis/server/events/command"
+ "github.com/runatlantis/atlantis/server/events/models"
+ "github.com/runatlantis/atlantis/server/events/vcs"
+)
+
+func NewApplyCommandRunner(
+ vcsClient vcs.Client,
+ disableApplyAll bool,
+ applyCommandLocker locking.ApplyLockChecker,
+ commitStatusUpdater CommitStatusUpdater,
+ prjCommandBuilder ProjectApplyCommandBuilder,
+ prjCmdRunner ProjectApplyCommandRunner,
+ autoMerger *AutoMerger,
+ pullUpdater *PullUpdater,
+ dbUpdater *DBUpdater,
+ backend locking.Backend,
+ parallelPoolSize int,
+ SilenceNoProjects bool,
+ silenceVCSStatusNoProjects bool,
+ pullReqStatusFetcher vcs.PullReqStatusFetcher,
+) *ApplyCommandRunner {
+ return &ApplyCommandRunner{
+ vcsClient: vcsClient,
+ DisableApplyAll: disableApplyAll,
+ locker: applyCommandLocker,
+ commitStatusUpdater: commitStatusUpdater,
+ prjCmdBuilder: prjCommandBuilder,
+ prjCmdRunner: prjCmdRunner,
+ autoMerger: autoMerger,
+ pullUpdater: pullUpdater,
+ dbUpdater: dbUpdater,
+ Backend: backend,
+ parallelPoolSize: parallelPoolSize,
+ SilenceNoProjects: SilenceNoProjects,
+ silenceVCSStatusNoProjects: silenceVCSStatusNoProjects,
+ pullReqStatusFetcher: pullReqStatusFetcher,
+ }
+}
+
+type ApplyCommandRunner struct {
+ DisableApplyAll bool
+ Backend locking.Backend
+ locker locking.ApplyLockChecker
+ vcsClient vcs.Client
+ commitStatusUpdater CommitStatusUpdater
+ prjCmdBuilder ProjectApplyCommandBuilder
+ prjCmdRunner ProjectApplyCommandRunner
+ autoMerger *AutoMerger
+ pullUpdater *PullUpdater
+ dbUpdater *DBUpdater
+ parallelPoolSize int
+ pullReqStatusFetcher vcs.PullReqStatusFetcher
+ // SilenceNoProjects is whether Atlantis should respond to PRs if no projects
+ // are found
+ SilenceNoProjects bool
+ // SilenceVCSStatusNoPlans is whether any plan should set commit status if no projects
+ // are found
+ silenceVCSStatusNoProjects bool
+ SilencePRComments []string
+}
+
+func (a *ApplyCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
+ var err error
+ baseRepo := ctx.Pull.BaseRepo
+ pull := ctx.Pull
+
+ // Get the mergeable status before we set any build statuses of our own.
+ // We do this here because when we set a "Pending" status, if users have
+ // required the Atlantis status checks to pass, then we've now changed
+ // the mergeability status of the pull request.
+ // This sets the approved, mergeable, and sqlocked status in the context.
+ ctx.PullRequestStatus, err = a.pullReqStatusFetcher.FetchPullStatus(ctx.Log, pull)
+ if err != nil {
+ // On error we continue the request with mergeable assumed false.
+ // We want to continue because not all apply's will need this status,
+ // only if they rely on the mergeability requirement.
+ // All PullRequestStatus fields are set to false by default when error.
+ ctx.Log.Warn("unable to get pull request status: %s. Continuing with mergeable and approved assumed false", err)
+ }
+
+ var projectCmds []command.ProjectContext
+ projectCmds, err = a.prjCmdBuilder.BuildApplyCommands(ctx, cmd)
+
+ if err != nil {
+ if statusErr := a.commitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, cmd.CommandName()); statusErr != nil {
+ ctx.Log.Warn("unable to update commit status: %s", statusErr)
+ }
+ a.pullUpdater.updatePull(ctx, cmd, command.Result{Error: err})
+ return
+ }
+
+ // If there are no projects to apply, don't respond to the PR and ignore
+ if len(projectCmds) == 0 && a.SilenceNoProjects {
+ ctx.Log.Info("determined there was no project to run plan in")
+ if !a.silenceVCSStatusNoProjects {
+ if cmd.IsForSpecificProject() {
+ // With a specific apply, just reset the status so it's not stuck in pending state
+ pullStatus, err := a.Backend.GetPullStatus(pull)
+ if err != nil {
+ ctx.Log.Warn("unable to fetch pull status: %s", err)
+ return
+ }
+ if pullStatus == nil {
+ // default to 0/0
+ ctx.Log.Debug("setting VCS status to 0/0 success as no previous state was found")
+ if err := a.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {
+ ctx.Log.Warn("unable to update commit status: %s", err)
+ }
+ return
+ }
+ ctx.Log.Debug("resetting VCS status")
+ a.updateCommitStatus(ctx, *pullStatus)
+ } else {
+ // With a generic apply, we set successful commit statuses
+ // with 0/0 projects planned successfully because some users require
+ // the Atlantis status to be passing for all pull requests.
+ // Does not apply to skipped runs for specific projects
+ ctx.Log.Debug("setting VCS status to success with no projects found")
+ if err := a.commitStatusUpdater.UpdateCombinedCount(ctx.Log, baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {
+ ctx.Log.Warn("unable to update commit status: %s", err)
+ }
+ }
+ }
+ return
+ }
+
+ // Only run commands in parallel if enabled
+ var result command.Result
+ if a.isParallelEnabled(projectCmds) {
+ ctx.Log.Info("Running applies in parallel")
+ result = runProjectCmdsParallelGroups(ctx, projectCmds, a.prjCmdRunner.Apply, a.parallelPoolSize)
+ } else {
+ result = runProjectCmds(projectCmds, a.prjCmdRunner.Apply)
+ }
+
+ a.pullUpdater.updatePull(
+ ctx,
+ cmd,
+ result)
+
+ pullStatus, err := a.dbUpdater.updateDB(ctx, pull, result.ProjectResults)
+ if err != nil {
+ ctx.Log.Err("writing results: %s", err)
+ return
+ }
+
+ a.updateCommitStatus(ctx, pullStatus)
+
+ if a.autoMerger.automergeEnabled(projectCmds) && !cmd.AutoMergeDisabled {
+ a.autoMerger.automerge(ctx, pullStatus, a.autoMerger.deleteSourceBranchOnMergeEnabled(projectCmds), cmd.AutoMergeMethod)
+ }
+}
+
+func (a *ApplyCommandRunner) isParallelEnabled(projectCmds []command.ProjectContext) bool {
+ return len(projectCmds) > 0 && projectCmds[0].ParallelApplyEnabled
+}
+
+func (a *ApplyCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus models.PullStatus) {
+ var numSuccess int
+ var numErrored int
+ status := models.SuccessCommitStatus
+
+ numSuccess = pullStatus.StatusCount(models.AppliedPlanStatus) + pullStatus.StatusCount(models.PlannedNoChangesPlanStatus)
+ numErrored = pullStatus.StatusCount(models.ErroredApplyStatus)
+
+ if numErrored > 0 {
+ status = models.FailedCommitStatus
+ } else if numSuccess < len(pullStatus.Projects) {
+ // If there are plans that haven't been applied yet, we'll use a pending
+ // status.
+ status = models.PendingCommitStatus
+ }
+
+ if err := a.commitStatusUpdater.UpdateCombinedCount(
+ ctx.Log,
+ ctx.Pull.BaseRepo,
+ ctx.Pull,
+ status,
+ command.Apply,
+ numSuccess,
+ len(pullStatus.Projects),
+ ); err != nil {
+ ctx.Log.Warn("unable to update commit status: %s", err)
+ }
+}
+
+// applyAllDisabledComment is posted when apply all commands (i.e. "atlantis apply")
+// are disabled and an apply all command is issued.
+var applyAllDisabledComment = "**Error:** Running `atlantis apply` without flags is disabled." +
+ " You must specify which project to apply via the `-d
`, `-w ` or `-p ` flags."
+
+// applyDisabledComment is posted when apply commands are disabled globally and an apply command is issued.
+var applyDisabledComment = "**Error:** Running `atlantis apply` is disabled."
diff --git a/server/events/command_runner.copy b/server/events/command_runner.copy
new file mode 100644
index 0000000000..b975a8fee7
--- /dev/null
+++ b/server/events/command_runner.copy
@@ -0,0 +1,602 @@
+// Copyright 2017 HootSuite Media Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the License);
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an AS IS BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// Modified hereafter by contributors to runatlantis/atlantis.
+
+package events
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/google/go-github/v67/github"
+ "github.com/mcdafydd/go-azuredevops/azuredevops"
+ "github.com/pkg/errors"
+ "github.com/runatlantis/atlantis/server/core/config/valid"
+ "github.com/runatlantis/atlantis/server/core/locking"
+ "github.com/runatlantis/atlantis/server/events/command"
+ "github.com/runatlantis/atlantis/server/events/models"
+ "github.com/runatlantis/atlantis/server/events/vcs"
+ "github.com/runatlantis/atlantis/server/events/vcs/gitea"
+ "github.com/runatlantis/atlantis/server/logging"
+ "github.com/runatlantis/atlantis/server/metrics"
+ "github.com/runatlantis/atlantis/server/recovery"
+ "github.com/runatlantis/atlantis/server/utils"
+ tally "github.com/uber-go/tally/v4"
+ gitlab "github.com/xanzy/go-gitlab"
+)
+
+const (
+ ShutdownComment = "Atlantis server is shutting down, please try again later."
+)
+
+//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_command_runner.go CommandRunner
+
+// CommandRunner is the first step after a command request has been parsed.
+type CommandRunner interface {
+ // RunCommentCommand is the first step after a command request has been parsed.
+ // It handles gathering additional information needed to execute the command
+ // and then calling the appropriate services to finish executing the command.
+ RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *CommentCommand)
+ RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User)
+}
+
+//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_github_pull_getter.go GithubPullGetter
+
+// GithubPullGetter makes API calls to get pull requests.
+type GithubPullGetter interface {
+ // GetPullRequest gets the pull request with id pullNum for the repo.
+ GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*github.PullRequest, error)
+}
+
+//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_azuredevops_pull_getter.go AzureDevopsPullGetter
+
+// AzureDevopsPullGetter makes API calls to get pull requests.
+type AzureDevopsPullGetter interface {
+ // GetPullRequest gets the pull request with id pullNum for the repo.
+ GetPullRequest(logger logging.SimpleLogging, repo models.Repo, pullNum int) (*azuredevops.GitPullRequest, error)
+}
+
+//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_gitlab_merge_request_getter.go GitlabMergeRequestGetter
+
+// GitlabMergeRequestGetter makes API calls to get merge requests.
+type GitlabMergeRequestGetter interface {
+ // GetMergeRequest gets the pull request with the id pullNum for the repo.
+ GetMergeRequest(logger logging.SimpleLogging, repoFullName string, pullNum int) (*gitlab.MergeRequest, error)
+}
+
+// CommentCommandRunner runs individual command workflows.
+type CommentCommandRunner interface {
+ Run(*command.Context, *CommentCommand)
+}
+
+func buildCommentCommandRunner(
+ cmdRunner *DefaultCommandRunner,
+ cmdName command.Name,
+) CommentCommandRunner {
+ // panic here, we want to fail fast and hard since
+ // this would be an internal service configuration error.
+ runner, ok := cmdRunner.CommentCommandRunnerByCmd[cmdName]
+
+ if !ok {
+ panic(fmt.Sprintf("command runner not configured for command %s", cmdName.String()))
+ }
+
+ return runner
+}
+
+// DefaultCommandRunner is the first step when processing a comment command.
+type DefaultCommandRunner struct {
+ VCSClient vcs.Client
+ GithubPullGetter GithubPullGetter
+ AzureDevopsPullGetter AzureDevopsPullGetter
+ GitlabMergeRequestGetter GitlabMergeRequestGetter
+ GiteaPullGetter *gitea.GiteaClient
+ // User config option: Disables autoplan when a pull request is opened or updated.
+ 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
+ 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).
+ ParallelPoolSize int
+ // AllowForkPRsFlag 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
+ // how to enable this functionality.
+ AllowForkPRsFlag string
+ // 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
+ // how to disable error comment
+ SilenceForkPRErrorsFlag string
+ CommentCommandRunnerByCmd map[command.Name]CommentCommandRunner
+ Drainer *Drainer
+ PreWorkflowHooksCommandRunner PreWorkflowHooksCommandRunner
+ PostWorkflowHooksCommandRunner PostWorkflowHooksCommandRunner
+ PullStatusFetcher PullStatusFetcher
+ TeamAllowlistChecker command.TeamAllowlistChecker
+ VarFileAllowlistChecker *VarFileAllowlistChecker
+ CommitStatusUpdater CommitStatusUpdater
+ ApplyLocker locking.ApplyLockChecker
+}
+
+// RunAutoplanCommand runs plan and policy_checks when a pull request is opened or updated.
+func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo models.Repo, pull models.PullRequest, user models.User) {
+ if opStarted := c.Drainer.StartOp(); !opStarted {
+ if commentErr := c.VCSClient.CreateComment(c.Logger, baseRepo, pull.Num, ShutdownComment, command.Plan.String()); commentErr != nil {
+ c.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr)
+ }
+ return
+ }
+ defer c.Drainer.OpDone()
+
+ log := c.buildLogger(baseRepo.FullName, pull.Num)
+ defer c.logPanics(baseRepo, pull.Num, log)
+ status, err := c.PullStatusFetcher.GetPullStatus(pull)
+
+ if err != nil {
+ log.Err("Unable to fetch pull status, this is likely a bug.", err)
+ }
+
+ scope := c.StatsScope.SubScope("autoplan")
+ timer := scope.Timer(metrics.ExecutionTimeMetric).Start()
+ defer timer.Stop()
+
+ // Check if the user who triggered the autoplan has permissions to run 'plan'.
+ if c.TeamAllowlistChecker != nil && c.TeamAllowlistChecker.HasRules() {
+ err := c.fetchUserTeams(baseRepo, &user)
+ if err != nil {
+ log.Err("Unable to fetch user teams: %s", err)
+ return
+ }
+
+ ok, err := c.checkUserPermissions(baseRepo, user, "plan")
+ if err != nil {
+ log.Err("Unable to check user permissions: %s", err)
+ return
+ }
+ if !ok {
+ return
+ }
+ }
+
+ ctx := &command.Context{
+ User: user,
+ Log: log,
+ Scope: scope,
+ Pull: pull,
+ HeadRepo: headRepo,
+ PullStatus: status,
+ Trigger: command.AutoTrigger,
+ }
+ if !c.validateCtxAndComment(ctx, command.Autoplan) {
+ return
+ }
+ if c.DisableAutoplan {
+ return
+ }
+ if len(c.DisableAutoplanLabel) > 0 {
+ labels, err := c.VCSClient.GetPullLabels(ctx.Log, baseRepo, pull)
+ if err != nil {
+ ctx.Log.Err("Unable to get VCS pull/merge request labels: %s. Proceeding with autoplan.", err)
+ } else if utils.SlicesContains(labels, c.DisableAutoplanLabel) {
+ ctx.Log.Info("Pull/merge request has disable auto plan label '%s' so not running autoplan.", c.DisableAutoplanLabel)
+ return
+ }
+ }
+
+ ctx.Log.Info("Running autoplan...")
+ cmd := &CommentCommand{
+ Name: command.Autoplan,
+ }
+
+ // Update the combined plan commit status to pending
+ if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil {
+ ctx.Log.Warn("unable to update plan commit status: %s", err)
+ }
+
+ err = c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd)
+
+ if err != nil {
+ if c.FailOnPreWorkflowHookError {
+ ctx.Log.Err("'fail-on-pre-workflow-hook-error' set, so not running %s command.", command.Plan)
+
+ // Update the plan or apply commit status to failed
+ switch cmd.Name {
+ case command.Plan:
+ if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Plan); err != nil {
+ ctx.Log.Warn("Unable to update plan commit status: %s", err)
+ }
+ case command.Apply:
+ if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Apply); err != nil {
+ ctx.Log.Warn("Unable to update apply commit status: %s", err)
+ }
+ }
+
+ return
+ }
+
+ ctx.Log.Err("'fail-on-pre-workflow-hook-error' not set so running %s command.", command.Plan)
+ }
+
+ autoPlanRunner := buildCommentCommandRunner(c, command.Plan)
+
+ autoPlanRunner.Run(ctx, nil)
+
+ c.PostWorkflowHooksCommandRunner.RunPostHooks(ctx, cmd) // nolint: errcheck
+}
+
+// commentUserDoesNotHavePermissions comments on the pull request that the user
+// is not allowed to execute the command.
+func (c *DefaultCommandRunner) commentUserDoesNotHavePermissions(baseRepo models.Repo, pullNum int, user models.User, cmd *CommentCommand) {
+ errMsg := fmt.Sprintf("```\nError: User @%s does not have permissions to execute '%s' command.\n```", user.Username, cmd.Name.String())
+ if err := c.VCSClient.CreateComment(c.Logger, baseRepo, pullNum, errMsg, ""); err != nil {
+ c.Logger.Err("unable to comment on pull request: %s", err)
+ }
+}
+
+// checkUserPermissions checks if the user has permissions to execute the command
+func (c *DefaultCommandRunner) checkUserPermissions(repo models.Repo, user models.User, cmdName string) (bool, error) {
+ if c.TeamAllowlistChecker == nil || !c.TeamAllowlistChecker.HasRules() {
+ // allowlist restriction is not enabled
+ return true, nil
+ }
+ ctx := models.TeamAllowlistCheckerContext{
+ BaseRepo: repo,
+ CommandName: cmdName,
+ Log: c.Logger,
+ Pull: models.PullRequest{},
+ User: user,
+ Verbose: false,
+ API: false,
+ }
+ ok := c.TeamAllowlistChecker.IsCommandAllowedForAnyTeam(ctx, user.Teams, cmdName)
+ if !ok {
+ return false, nil
+ }
+ return true, nil
+}
+
+// checkVarFilesInPlanCommandAllowlisted checks if paths in a 'plan' command are allowlisted.
+func (c *DefaultCommandRunner) checkVarFilesInPlanCommandAllowlisted(cmd *CommentCommand) error {
+ if cmd == nil || cmd.CommandName() != command.Plan {
+ return nil
+ }
+
+ return c.VarFileAllowlistChecker.Check(cmd.Flags)
+}
+
+// RunCommentCommand executes the command.
+// We take in a pointer for maybeHeadRepo because for some events there isn't
+// enough data to construct the Repo model and callers might want to wait until
+// the event is further validated before making an additional (potentially
+// wasteful) call to get the necessary data.
+func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHeadRepo *models.Repo, maybePull *models.PullRequest, user models.User, pullNum int, cmd *CommentCommand) {
+ if opStarted := c.Drainer.StartOp(); !opStarted {
+ if commentErr := c.VCSClient.CreateComment(c.Logger, baseRepo, pullNum, ShutdownComment, ""); commentErr != nil {
+ c.Logger.Log(logging.Error, "unable to comment that Atlantis is shutting down: %s", commentErr)
+ }
+ return
+ }
+ defer c.Drainer.OpDone()
+
+ log := c.buildLogger(baseRepo.FullName, pullNum)
+ defer c.logPanics(baseRepo, pullNum, log)
+
+ scope := c.StatsScope.SubScope("comment")
+
+ if cmd != nil {
+ scope = scope.SubScope(cmd.Name.String())
+ }
+ timer := scope.Timer(metrics.ExecutionTimeMetric).Start()
+ defer timer.Stop()
+
+ // Check if the user who commented has the permissions to execute the 'plan' or 'apply' commands
+ if c.TeamAllowlistChecker != nil && c.TeamAllowlistChecker.HasRules() {
+ err := c.fetchUserTeams(baseRepo, &user)
+ if err != nil {
+ c.Logger.Err("Unable to fetch user teams: %s", err)
+ return
+ }
+
+ ok, err := c.checkUserPermissions(baseRepo, user, cmd.Name.String())
+ if err != nil {
+ c.Logger.Err("Unable to check user permissions: %s", err)
+ return
+ }
+ if !ok {
+ c.commentUserDoesNotHavePermissions(baseRepo, pullNum, user, cmd)
+ return
+ }
+ }
+
+ // Check if the provided var files in a 'plan' command are allowlisted
+ if err := c.checkVarFilesInPlanCommandAllowlisted(cmd); err != nil {
+ errMsg := fmt.Sprintf("```\n%s\n```", err.Error())
+ if commentErr := c.VCSClient.CreateComment(c.Logger, baseRepo, pullNum, errMsg, ""); commentErr != nil {
+ c.Logger.Err("unable to comment on pull request: %s", commentErr)
+ }
+ return
+ }
+
+ headRepo, pull, err := c.ensureValidRepoMetadata(baseRepo, maybeHeadRepo, maybePull, user, pullNum, log)
+ if err != nil {
+ return
+ }
+
+ status, err := c.PullStatusFetcher.GetPullStatus(pull)
+
+ if err != nil {
+ log.Err("Unable to fetch pull status, this is likely a bug.", err)
+ }
+
+ ctx := &command.Context{
+ User: user,
+ Log: log,
+ Pull: pull,
+ PullStatus: status,
+ HeadRepo: headRepo,
+ Scope: scope,
+ Trigger: command.CommentTrigger,
+ PolicySet: cmd.PolicySet,
+ ClearPolicyApproval: cmd.ClearPolicyApproval,
+ TeamAllowlistChecker: c.TeamAllowlistChecker,
+ }
+
+ if !c.validateCtxAndComment(ctx, cmd.Name) {
+ return
+ }
+
+ if cmd.Name == command.Apply {
+ locked, err := c.IsApplyLocked()
+ // CheckApplyLock falls back to AllowedCommand flag if fetching the lock
+ // raises an error
+ // We will log failure as warning
+ if err != nil {
+ ctx.Log.Warn("checking global apply lock: %s", err)
+ }
+
+ if locked {
+ ctx.Log.Info("ignoring apply command since apply disabled globally")
+ if err := c.VCSClient.CreateComment(ctx.Log, baseRepo, pull.Num, applyDisabledComment, command.Apply.String()); err != nil {
+ ctx.Log.Err("unable to comment on pull request: %s", err)
+ }
+ return
+ }
+
+ if c.CommentCommandRunnerByCmd[command.Apply].(*ApplyCommandRunner).DisableApplyAll && !cmd.IsForSpecificProject() {
+ ctx.Log.Info("ignoring apply command without flags since apply all is disabled")
+ if err := c.VCSClient.CreateComment(ctx.Log, baseRepo, pull.Num, applyAllDisabledComment, command.Apply.String()); err != nil {
+ ctx.Log.Err("unable to comment on pull request: %s", err)
+ }
+ return
+ }
+ }
+
+ // Update the combined plan or apply commit status to pending
+ switch cmd.Name {
+ case command.Plan:
+ if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Plan); err != nil {
+ ctx.Log.Warn("unable to update plan commit status: %s", err)
+ }
+ case command.Apply:
+ if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.PendingCommitStatus, command.Apply); err != nil {
+ ctx.Log.Warn("unable to update apply commit status: %s", err)
+ }
+ }
+
+ err = c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx, cmd)
+
+ if err != nil {
+ if c.FailOnPreWorkflowHookError {
+ ctx.Log.Err("'fail-on-pre-workflow-hook-error' set, so not running %s command.", cmd.Name.String())
+
+ // Update the plan or apply commit status to failed
+ switch cmd.Name {
+ case command.Plan:
+ if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Plan); err != nil {
+ ctx.Log.Warn("unable to update plan commit status: %s", err)
+ }
+ case command.Apply:
+ if err := c.CommitStatusUpdater.UpdateCombined(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, command.Apply); err != nil {
+ ctx.Log.Warn("unable to update apply commit status: %s", err)
+ }
+ }
+
+ return
+ }
+
+ ctx.Log.Err("'fail-on-pre-workflow-hook-error' not set so running %s command.", cmd.Name.String())
+ }
+
+ cmdRunner := buildCommentCommandRunner(c, cmd.CommandName())
+
+ cmdRunner.Run(ctx, cmd)
+
+ c.PostWorkflowHooksCommandRunner.RunPostHooks(ctx, cmd) // nolint: errcheck
+}
+
+func (c *DefaultCommandRunner) getGithubData(logger logging.SimpleLogging, baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) {
+ if c.GithubPullGetter == nil {
+ return models.PullRequest{}, models.Repo{}, errors.New("Atlantis not configured to support GitHub")
+ }
+ ghPull, err := c.GithubPullGetter.GetPullRequest(logger, baseRepo, pullNum)
+ if err != nil {
+ return models.PullRequest{}, models.Repo{}, errors.Wrap(err, "making pull request API call to GitHub")
+ }
+ pull, _, headRepo, err := c.EventParser.ParseGithubPull(logger, ghPull)
+ if err != nil {
+ return pull, headRepo, errors.Wrap(err, "extracting required fields from comment data")
+ }
+ return pull, headRepo, nil
+}
+
+func (c *DefaultCommandRunner) getGiteaData(logger logging.SimpleLogging, baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) {
+ if c.GiteaPullGetter == nil {
+ return models.PullRequest{}, models.Repo{}, errors.New("Atlantis not configured to support Gitea")
+ }
+ giteaPull, err := c.GiteaPullGetter.GetPullRequest(logger, baseRepo, pullNum)
+ if err != nil {
+ return models.PullRequest{}, models.Repo{}, errors.Wrap(err, "making pull request API call to Gitea")
+ }
+ pull, _, headRepo, err := c.EventParser.ParseGiteaPull(giteaPull)
+ if err != nil {
+ return pull, headRepo, errors.Wrap(err, "extracting required fields from comment data")
+ }
+ return pull, headRepo, nil
+}
+
+func (c *DefaultCommandRunner) getGitlabData(logger logging.SimpleLogging, baseRepo models.Repo, pullNum int) (models.PullRequest, error) {
+ if c.GitlabMergeRequestGetter == nil {
+ return models.PullRequest{}, errors.New("Atlantis not configured to support GitLab")
+ }
+ mr, err := c.GitlabMergeRequestGetter.GetMergeRequest(logger, baseRepo.FullName, pullNum)
+ if err != nil {
+ return models.PullRequest{}, errors.Wrap(err, "making merge request API call to GitLab")
+ }
+ pull := c.EventParser.ParseGitlabMergeRequest(mr, baseRepo)
+ return pull, nil
+}
+
+func (c *DefaultCommandRunner) getAzureDevopsData(logger logging.SimpleLogging, baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) {
+ if c.AzureDevopsPullGetter == nil {
+ return models.PullRequest{}, models.Repo{}, errors.New("atlantis not configured to support Azure DevOps")
+ }
+ adPull, err := c.AzureDevopsPullGetter.GetPullRequest(logger, baseRepo, pullNum)
+ if err != nil {
+ return models.PullRequest{}, models.Repo{}, errors.Wrap(err, "making pull request API call to Azure DevOps")
+ }
+ pull, _, headRepo, err := c.EventParser.ParseAzureDevopsPull(adPull)
+ if err != nil {
+ return pull, headRepo, errors.Wrap(err, "extracting required fields from comment data")
+ }
+ return pull, headRepo, nil
+}
+
+func (c *DefaultCommandRunner) buildLogger(repoFullName string, pullNum int) logging.SimpleLogging {
+
+ return c.Logger.WithHistory(
+ "repo", repoFullName,
+ "pull", strconv.Itoa(pullNum),
+ )
+}
+
+func (c *DefaultCommandRunner) ensureValidRepoMetadata(
+ baseRepo models.Repo,
+ maybeHeadRepo *models.Repo,
+ maybePull *models.PullRequest,
+ _ models.User,
+ pullNum int,
+ log logging.SimpleLogging,
+) (headRepo models.Repo, pull models.PullRequest, err error) {
+ if maybeHeadRepo != nil {
+ headRepo = *maybeHeadRepo
+ }
+
+ switch baseRepo.VCSHost.Type {
+ case models.Github:
+ pull, headRepo, err = c.getGithubData(log, baseRepo, pullNum)
+ case models.Gitlab:
+ pull, err = c.getGitlabData(log, baseRepo, pullNum)
+ case models.BitbucketCloud, models.BitbucketServer:
+ if maybePull == nil {
+ err = errors.New("pull request should not be nil–this is a bug")
+ break
+ }
+ pull = *maybePull
+ case models.AzureDevops:
+ pull, headRepo, err = c.getAzureDevopsData(log, baseRepo, pullNum)
+ case models.Gitea:
+ pull, headRepo, err = c.getGiteaData(log, baseRepo, pullNum)
+ default:
+ err = errors.New("Unknown VCS type–this is a bug")
+ }
+
+ if err != nil {
+ log.Err(err.Error())
+ if commentErr := c.VCSClient.CreateComment(c.Logger, baseRepo, pullNum, fmt.Sprintf("`Error: %s`", err), ""); commentErr != nil {
+ log.Err("unable to comment: %s", commentErr)
+ }
+ }
+
+ return
+}
+
+func (c *DefaultCommandRunner) fetchUserTeams(repo models.Repo, user *models.User) error {
+ teams, err := c.VCSClient.GetTeamNamesForUser(repo, *user)
+ if err != nil {
+ return err
+ }
+
+ user.Teams = teams
+ return nil
+}
+
+func (c *DefaultCommandRunner) validateCtxAndComment(ctx *command.Context, commandName command.Name) bool {
+ if !c.AllowForkPRs && ctx.HeadRepo.Owner != ctx.Pull.BaseRepo.Owner {
+ if c.SilenceForkPRErrors {
+ return false
+ }
+ ctx.Log.Info("command was run on a fork pull request which is disallowed")
+ if err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, fmt.Sprintf("Atlantis commands can't be run on fork pull requests. To enable, set --%s or, to disable this message, set --%s", c.AllowForkPRsFlag, c.SilenceForkPRErrorsFlag), ""); err != nil {
+ ctx.Log.Err("unable to comment: %s", err)
+ }
+ return false
+ }
+
+ if ctx.Pull.State != models.OpenPullState && commandName != command.Unlock {
+ ctx.Log.Info("command was run on closed pull request")
+ if err := c.VCSClient.CreateComment(ctx.Log, ctx.Pull.BaseRepo, ctx.Pull.Num, "Atlantis commands can't be run on closed pull requests", ""); err != nil {
+ ctx.Log.Err("unable to comment: %s", err)
+ }
+ return false
+ }
+
+ repo := c.GlobalCfg.MatchingRepo(ctx.Pull.BaseRepo.ID())
+ if !repo.BranchMatches(ctx.Pull.BaseBranch) {
+ ctx.Log.Info("command was run on a pull request which doesn't match base branches")
+ // just ignore it to allow us to use any git workflows without malicious intentions.
+ return false
+ }
+ return true
+}
+
+// logPanics logs and creates a comment on the pull request for panics.
+func (c *DefaultCommandRunner) logPanics(baseRepo models.Repo, pullNum int, logger logging.SimpleLogging) {
+ if err := recover(); err != nil {
+ stack := recovery.Stack(3)
+ logger.Err("PANIC: %s\n%s", err, stack)
+ if commentErr := c.VCSClient.CreateComment(
+ logger,
+ baseRepo,
+ pullNum,
+ fmt.Sprintf("**Error: goroutine panic. This is a bug.**\n```\n%s\n%s```", err, stack),
+ "",
+ ); commentErr != nil {
+ logger.Err("unable to comment: %s", commentErr)
+ }
+ }
+}
+
+func (c *DefaultCommandRunner) IsApplyLocked() (bool, error) {
+ lock, err := c.ApplyLocker.CheckApplyLock()
+
+ return lock.Locked, err
+}
+
+var automergeComment = `Automatically merging because all plans have been successfully applied.`
diff --git a/server/events/pull_closed_executor_test.go b/server/events/pull_closed_executor_test.go
index 583bd4e078..add595ea67 100644
--- a/server/events/pull_closed_executor_test.go
+++ b/server/events/pull_closed_executor_test.go
@@ -194,7 +194,7 @@ func TestCleanUpPullComments(t *testing.T) {
t.Cleanup(func() {
db.Close()
})
- Ok(t, err)
+ Ok(t, err)
pce := events.PullClosedExecutor{
Locker: l,
VCSClient: cp,
diff --git a/server/metrics/debug.go b/server/metrics/debug.go
index 08203e1599..112d79b4e8 100644
--- a/server/metrics/debug.go
+++ b/server/metrics/debug.go
@@ -38,18 +38,18 @@ func (r *debugReporter) Flush() {
}
func (r *debugReporter) ReportCounter(name string, tags map[string]string, value int64) {
- log := r.log.With("name", name, "value", value, "tags", tags, "type", "counter")
- log.Debug("counter")
+ //log := r.log.With("name", name, "value", value, "tags", tags, "type", "counter")
+ //log.Debug("counter")
}
func (r *debugReporter) ReportGauge(name string, tags map[string]string, value float64) {
- log := r.log.With("name", name, "value", value, "tags", tags, "type", "gauge")
- log.Debug("gauge")
+ //log := r.log.With("name", name, "value", value, "tags", tags, "type", "gauge")
+ //log.Debug("gauge")
}
func (r *debugReporter) ReportTimer(name string, tags map[string]string, interval time.Duration) {
- log := r.log.With("name", name, "value", interval, "tags", tags, "type", "timer")
- log.Debug("timer")
+ //log := r.log.With("name", name, "value", interval, "tags", tags, "type", "timer")
+ //log.Debug("timer")
}
func (r *debugReporter) ReportHistogramValueSamples(
diff --git a/server/server.copy b/server/server.copy
new file mode 100644
index 0000000000..aadce43534
--- /dev/null
+++ b/server/server.copy
@@ -0,0 +1,1247 @@
+// Copyright 2017 HootSuite Media Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the License);
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an AS IS BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// Modified hereafter by contributors to runatlantis/atlantis.
+
+// Package server handles the web server and executing commands that come in
+// via webhooks.
+package server
+
+import (
+ "context"
+ "crypto/tls"
+ "embed"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "sort"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/mitchellh/go-homedir"
+ tally "github.com/uber-go/tally/v4"
+ prometheus "github.com/uber-go/tally/v4/prometheus"
+ "github.com/urfave/negroni/v3"
+
+ cfg "github.com/runatlantis/atlantis/server/core/config"
+ "github.com/runatlantis/atlantis/server/core/config/valid"
+ "github.com/runatlantis/atlantis/server/core/db"
+ "github.com/runatlantis/atlantis/server/core/redis"
+ "github.com/runatlantis/atlantis/server/jobs"
+ "github.com/runatlantis/atlantis/server/metrics"
+ "github.com/runatlantis/atlantis/server/scheduled"
+
+ "github.com/gorilla/mux"
+ "github.com/pkg/errors"
+ "github.com/runatlantis/atlantis/server/controllers"
+ events_controllers "github.com/runatlantis/atlantis/server/controllers/events"
+ "github.com/runatlantis/atlantis/server/controllers/web_templates"
+ "github.com/runatlantis/atlantis/server/controllers/websocket"
+ "github.com/runatlantis/atlantis/server/core/locking"
+ "github.com/runatlantis/atlantis/server/core/runtime"
+ "github.com/runatlantis/atlantis/server/core/runtime/policy"
+ "github.com/runatlantis/atlantis/server/core/terraform"
+ "github.com/runatlantis/atlantis/server/events"
+ "github.com/runatlantis/atlantis/server/events/command"
+ "github.com/runatlantis/atlantis/server/events/models"
+ "github.com/runatlantis/atlantis/server/events/vcs"
+ "github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud"
+ "github.com/runatlantis/atlantis/server/events/vcs/bitbucketserver"
+ "github.com/runatlantis/atlantis/server/events/vcs/gitea"
+ "github.com/runatlantis/atlantis/server/events/webhooks"
+ "github.com/runatlantis/atlantis/server/logging"
+)
+
+const (
+ // LockViewRouteName is the named route in mux.Router for the lock view.
+ // The route can be retrieved by this name, ex:
+ // mux.Router.Get(LockViewRouteName)
+ LockViewRouteName = "lock-detail"
+ // LockViewRouteIDQueryParam is the query parameter needed to construct the lock view
+ // route. ex:
+ // mux.Router.Get(LockViewRouteName).URL(LockViewRouteIDQueryParam, "my id")
+ LockViewRouteIDQueryParam = "id"
+ // ProjectJobsViewRouteName is the named route in mux.Router for the log stream view.
+ ProjectJobsViewRouteName = "project-jobs-detail"
+ // binDirName is the name of the directory inside our data dir where
+ // we download binaries.
+ BinDirName = "bin"
+ // terraformPluginCacheDir is the name of the dir inside our data dir
+ // where we tell terraform to cache plugins and modules.
+ TerraformPluginCacheDirName = "plugin-cache"
+)
+
+// Server runs the Atlantis web server.
+type Server struct {
+ AtlantisVersion string
+ AtlantisURL *url.URL
+ Router *mux.Router
+ Port int
+ PostWorkflowHooksCommandRunner *events.DefaultPostWorkflowHooksCommandRunner
+ PreWorkflowHooksCommandRunner *events.DefaultPreWorkflowHooksCommandRunner
+ CommandRunner *events.DefaultCommandRunner
+ Logger logging.SimpleLogging
+ StatsScope tally.Scope
+ StatsReporter tally.BaseStatsReporter
+ StatsCloser io.Closer
+ Locker locking.Locker
+ ApplyLocker locking.ApplyLocker
+ VCSEventsController *events_controllers.VCSEventsController
+ GithubAppController *controllers.GithubAppController
+ LocksController *controllers.LocksController
+ StatusController *controllers.StatusController
+ JobsController *controllers.JobsController
+ APIController *controllers.APIController
+ IndexTemplate web_templates.TemplateWriter
+ LockDetailTemplate web_templates.TemplateWriter
+ ProjectJobsTemplate web_templates.TemplateWriter
+ ProjectJobsErrorTemplate web_templates.TemplateWriter
+ SSLCertFile string
+ SSLKeyFile string
+ CertLastRefreshTime time.Time
+ KeyLastRefreshTime time.Time
+ SSLCert *tls.Certificate
+ Drainer *events.Drainer
+ WebAuthentication bool
+ WebUsername string
+ WebPassword string
+ ProjectCmdOutputHandler jobs.ProjectCommandOutputHandler
+ ScheduledExecutorService *scheduled.ExecutorService
+ DisableGlobalApplyLock bool
+}
+
+// Config holds config for server that isn't passed in by the user.
+type Config struct {
+ AllowForkPRsFlag string
+ AtlantisURLFlag string
+ AtlantisVersion string
+ DefaultTFVersionFlag string
+ RepoConfigJSONFlag string
+ SilenceForkPRErrorsFlag string
+}
+
+// WebhookConfig is nested within UserConfig. It's used to configure webhooks.
+type WebhookConfig struct {
+ // Event is the type of event we should send this webhook for, ex. apply.
+ Event string `mapstructure:"event"`
+ // WorkspaceRegex is a regex that is used to match against the workspace
+ // that is being modified for this event. If the regex matches, we'll
+ // send the webhook, ex. "production.*".
+ WorkspaceRegex string `mapstructure:"workspace-regex"`
+ // BranchRegex is a regex that is used to match against the base branch
+ // that is being modified for this event. If the regex matches, we'll
+ // send the webhook, ex. "main.*".
+ BranchRegex string `mapstructure:"branch-regex"`
+ // Kind is the type of webhook we should send, ex. slack.
+ Kind string `mapstructure:"kind"`
+ // Channel is the channel to send this webhook to. It only applies to
+ // slack webhooks. Should be without '#'.
+ Channel string `mapstructure:"channel"`
+}
+
+//go:embed static
+var staticAssets embed.FS
+
+// NewServer returns a new server. If there are issues starting the server or
+// its dependencies an error will be returned. This is like the main() function
+// for the server CLI command because it injects all the dependencies.
+func NewServer(userConfig UserConfig, config Config) (*Server, error) {
+ logging.SuppressDefaultLogging()
+ logger, err := logging.NewStructuredLoggerFromLevel(userConfig.ToLogLevel())
+
+ if err != nil {
+ return nil, err
+ }
+
+ var supportedVCSHosts []models.VCSHostType
+ var githubClient vcs.IGithubClient
+ var githubAppEnabled bool
+ var githubConfig vcs.GithubConfig
+ var githubCredentials vcs.GithubCredentials
+ var gitlabClient *vcs.GitlabClient
+ var bitbucketCloudClient *bitbucketcloud.Client
+ var bitbucketServerClient *bitbucketserver.Client
+ var azuredevopsClient *vcs.AzureDevopsClient
+ var giteaClient *gitea.GiteaClient
+
+ policyChecksEnabled := false
+ if userConfig.EnablePolicyChecksFlag {
+ logger.Info("Policy Checks are enabled")
+ policyChecksEnabled = true
+ }
+
+ allowCommands, err := userConfig.ToAllowCommandNames()
+ if err != nil {
+ return nil, err
+ }
+ disableApply := true
+ for _, allowCommand := range allowCommands {
+ if allowCommand == command.Apply {
+ disableApply = false
+ break
+ }
+ }
+
+ validator := &cfg.ParserValidator{}
+
+ globalCfg := valid.NewGlobalCfgFromArgs(
+ valid.GlobalCfgArgs{
+ PolicyCheckEnabled: userConfig.EnablePolicyChecksFlag,
+ })
+ if userConfig.RepoConfig != "" {
+ globalCfg, err = validator.ParseGlobalCfg(userConfig.RepoConfig, globalCfg)
+ if err != nil {
+ return nil, errors.Wrapf(err, "parsing %s file", userConfig.RepoConfig)
+ }
+ } else if userConfig.RepoConfigJSON != "" {
+ globalCfg, err = validator.ParseGlobalCfgJSON(userConfig.RepoConfigJSON, globalCfg)
+ if err != nil {
+ return nil, errors.Wrapf(err, "parsing --%s", config.RepoConfigJSONFlag)
+ }
+ }
+
+ statsScope, statsReporter, closer, err := metrics.NewScope(globalCfg.Metrics, logger, userConfig.StatsNamespace)
+
+ if err != nil {
+ return nil, errors.Wrapf(err, "instantiating metrics scope")
+ }
+
+ if userConfig.GithubUser != "" || userConfig.GithubAppID != 0 {
+ if userConfig.GithubAllowMergeableBypassApply {
+ githubConfig = vcs.GithubConfig{
+ AllowMergeableBypassApply: true,
+ }
+ }
+ supportedVCSHosts = append(supportedVCSHosts, models.Github)
+ if userConfig.GithubUser != "" {
+ githubCredentials = &vcs.GithubUserCredentials{
+ User: userConfig.GithubUser,
+ Token: userConfig.GithubToken,
+ TokenFile: userConfig.GithubTokenFile,
+ }
+ } else if userConfig.GithubAppID != 0 && userConfig.GithubAppKeyFile != "" {
+ privateKey, err := os.ReadFile(userConfig.GithubAppKeyFile)
+ if err != nil {
+ return nil, err
+ }
+ githubCredentials = &vcs.GithubAppCredentials{
+ AppID: userConfig.GithubAppID,
+ InstallationID: userConfig.GithubAppInstallationID,
+ Key: privateKey,
+ Hostname: userConfig.GithubHostname,
+ AppSlug: userConfig.GithubAppSlug,
+ }
+ githubAppEnabled = true
+ } else if userConfig.GithubAppID != 0 && userConfig.GithubAppKey != "" {
+ githubCredentials = &vcs.GithubAppCredentials{
+ AppID: userConfig.GithubAppID,
+ InstallationID: userConfig.GithubAppInstallationID,
+ Key: []byte(userConfig.GithubAppKey),
+ Hostname: userConfig.GithubHostname,
+ AppSlug: userConfig.GithubAppSlug,
+ }
+ githubAppEnabled = true
+ }
+
+ var err error
+ rawGithubClient, err := vcs.NewGithubClient(userConfig.GithubHostname, githubCredentials, githubConfig, userConfig.MaxCommentsPerCommand, logger)
+ if err != nil {
+ return nil, err
+ }
+
+ githubClient = vcs.NewInstrumentedGithubClient(rawGithubClient, statsScope, logger)
+ }
+ if userConfig.GitlabUser != "" {
+ supportedVCSHosts = append(supportedVCSHosts, models.Gitlab)
+ var err error
+ gitlabClient, err = vcs.NewGitlabClient(userConfig.GitlabHostname, userConfig.GitlabToken, logger)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if userConfig.BitbucketUser != "" {
+ if userConfig.BitbucketBaseURL == bitbucketcloud.BaseURL {
+ supportedVCSHosts = append(supportedVCSHosts, models.BitbucketCloud)
+ bitbucketCloudClient = bitbucketcloud.NewClient(
+ http.DefaultClient,
+ userConfig.BitbucketUser,
+ userConfig.BitbucketToken,
+ userConfig.AtlantisURL)
+ } else {
+ supportedVCSHosts = append(supportedVCSHosts, models.BitbucketServer)
+ var err error
+ bitbucketServerClient, err = bitbucketserver.NewClient(
+ http.DefaultClient,
+ userConfig.BitbucketUser,
+ userConfig.BitbucketToken,
+ userConfig.BitbucketBaseURL,
+ userConfig.AtlantisURL)
+ if err != nil {
+ return nil, errors.Wrapf(err, "setting up Bitbucket Server client")
+ }
+ }
+ }
+ if userConfig.AzureDevopsUser != "" {
+ supportedVCSHosts = append(supportedVCSHosts, models.AzureDevops)
+
+ var err error
+ azuredevopsClient, err = vcs.NewAzureDevopsClient(userConfig.AzureDevOpsHostname, userConfig.AzureDevopsUser, userConfig.AzureDevopsToken)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if userConfig.GiteaToken != "" {
+ supportedVCSHosts = append(supportedVCSHosts, models.Gitea)
+
+ giteaClient, err = gitea.NewClient(userConfig.GiteaBaseURL, userConfig.GiteaUser, userConfig.GiteaToken, userConfig.GiteaPageSize, logger)
+ if err != nil {
+ fmt.Println("error setting up gitea client", "error", err)
+ return nil, errors.Wrapf(err, "setting up Gitea client")
+ } else {
+ logger.Info("gitea client configured successfully")
+ }
+ }
+
+ var supportedVCSHostsStr []string
+ for _, host := range supportedVCSHosts {
+ supportedVCSHostsStr = append(supportedVCSHostsStr, host.String())
+ }
+
+ logger.Info("Supported VCS Hosts: %s", strings.Join(supportedVCSHostsStr, ", "))
+
+ home, err := homedir.Dir()
+ if err != nil {
+ return nil, errors.Wrap(err, "getting home dir to write ~/.git-credentials file")
+ }
+
+ if userConfig.WriteGitCreds {
+ if userConfig.GithubUser != "" {
+ if err := vcs.WriteGitCreds(userConfig.GithubUser, userConfig.GithubToken, userConfig.GithubHostname, home, logger, false); err != nil {
+ return nil, err
+ }
+ }
+ if userConfig.GitlabUser != "" {
+ if err := vcs.WriteGitCreds(userConfig.GitlabUser, userConfig.GitlabToken, userConfig.GitlabHostname, home, logger, false); err != nil {
+ return nil, err
+ }
+ }
+ if userConfig.BitbucketUser != "" {
+ // The default BitbucketBaseURL is https://api.bitbucket.org which can't actually be used for git
+ // so we override it here only if it's that to be bitbucket.org
+ bitbucketBaseURL := userConfig.BitbucketBaseURL
+ if bitbucketBaseURL == "https://api.bitbucket.org" {
+ bitbucketBaseURL = "bitbucket.org"
+ }
+ if err := vcs.WriteGitCreds(userConfig.BitbucketUser, userConfig.BitbucketToken, bitbucketBaseURL, home, logger, false); err != nil {
+ return nil, err
+ }
+ }
+ if userConfig.AzureDevopsUser != "" {
+ if err := vcs.WriteGitCreds(userConfig.AzureDevopsUser, userConfig.AzureDevopsToken, "dev.azure.com", home, logger, false); err != nil {
+ return nil, err
+ }
+ }
+ if userConfig.GiteaUser != "" {
+ if err := vcs.WriteGitCreds(userConfig.GiteaUser, userConfig.GiteaToken, userConfig.GiteaBaseURL, home, logger, false); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ // default the project files used to generate the module index to the autoplan-file-list if autoplan-modules is true
+ // but no files are specified
+ if userConfig.AutoplanModules && userConfig.AutoplanModulesFromProjects == "" {
+ userConfig.AutoplanModulesFromProjects = userConfig.AutoplanFileList
+ }
+
+ var webhooksConfig []webhooks.Config
+ for _, c := range userConfig.Webhooks {
+ config := webhooks.Config{
+ Channel: c.Channel,
+ BranchRegex: c.BranchRegex,
+ Event: c.Event,
+ Kind: c.Kind,
+ WorkspaceRegex: c.WorkspaceRegex,
+ }
+ webhooksConfig = append(webhooksConfig, config)
+ }
+ webhooksManager, err := webhooks.NewMultiWebhookSender(webhooksConfig, webhooks.NewSlackClient(userConfig.SlackToken))
+ if err != nil {
+ return nil, errors.Wrap(err, "initializing webhooks")
+ }
+ vcsClient := vcs.NewClientProxy(githubClient, gitlabClient, bitbucketCloudClient, bitbucketServerClient, azuredevopsClient, giteaClient)
+ commitStatusUpdater := &events.DefaultCommitStatusUpdater{Client: vcsClient, StatusName: userConfig.VCSStatusName}
+
+ binDir, err := mkSubDir(userConfig.DataDir, BinDirName)
+
+ if err != nil {
+ return nil, err
+ }
+
+ cacheDir, err := mkSubDir(userConfig.DataDir, TerraformPluginCacheDirName)
+
+ if err != nil {
+ return nil, err
+ }
+
+ parsedURL, err := ParseAtlantisURL(userConfig.AtlantisURL)
+ if err != nil {
+ return nil, errors.Wrapf(err,
+ "parsing --%s flag %q", config.AtlantisURLFlag, userConfig.AtlantisURL)
+ }
+
+ underlyingRouter := mux.NewRouter()
+ router := &Router{
+ AtlantisURL: parsedURL,
+ LockViewRouteIDQueryParam: LockViewRouteIDQueryParam,
+ LockViewRouteName: LockViewRouteName,
+ ProjectJobsViewRouteName: ProjectJobsViewRouteName,
+ Underlying: underlyingRouter,
+ }
+
+ var projectCmdOutputHandler jobs.ProjectCommandOutputHandler
+
+ if userConfig.TFEToken != "" && !userConfig.TFELocalExecutionMode {
+ // When TFE is enabled and using remote execution mode log streaming is not necessary.
+ projectCmdOutputHandler = &jobs.NoopProjectOutputHandler{}
+ } else {
+ projectCmdOutput := make(chan *jobs.ProjectCmdOutputLine)
+ projectCmdOutputHandler = jobs.NewAsyncProjectCommandOutputHandler(
+ projectCmdOutput,
+ logger,
+ )
+ }
+
+ distribution := terraform.NewDistributionTerraform()
+ if userConfig.TFDistribution == "opentofu" {
+ distribution = terraform.NewDistributionOpenTofu()
+ }
+
+ terraformClient, err := terraform.NewClient(
+ logger,
+ distribution,
+ binDir,
+ cacheDir,
+ userConfig.TFEToken,
+ userConfig.TFEHostname,
+ userConfig.DefaultTFVersion,
+ config.DefaultTFVersionFlag,
+ userConfig.TFDownloadURL,
+ userConfig.TFDownload,
+ userConfig.UseTFPluginCache,
+ projectCmdOutputHandler)
+ // The flag.Lookup call is to detect if we're running in a unit test. If we
+ // are, then we don't error out because we don't have/want terraform
+ // installed on our CI system where the unit tests run.
+ if err != nil && flag.Lookup("test.v") == nil {
+ return nil, errors.Wrap(err, fmt.Sprintf("initializing %s", userConfig.TFDistribution))
+ }
+ markdownRenderer := events.NewMarkdownRenderer(
+ gitlabClient.SupportsCommonMark(),
+ userConfig.DisableApplyAll,
+ disableApply,
+ userConfig.DisableMarkdownFolding,
+ userConfig.DisableRepoLocking,
+ userConfig.EnableDiffMarkdownFormat,
+ userConfig.MarkdownTemplateOverridesDir,
+ userConfig.ExecutableName,
+ userConfig.HideUnchangedPlanComments,
+ )
+
+ var lockingClient locking.Locker
+ var applyLockingClient locking.ApplyLocker
+ var backend locking.Backend
+
+ switch dbtype := userConfig.LockingDBType; dbtype {
+ case "redis":
+ logger.Info("Utilizing Redis DB")
+ backend, err = redis.New(userConfig.RedisHost, userConfig.RedisPort, userConfig.RedisPassword, userConfig.RedisTLSEnabled, userConfig.RedisInsecureSkipVerify, userConfig.RedisDB)
+ if err != nil {
+ return nil, err
+ }
+ case "boltdb":
+ logger.Info("Utilizing BoltDB")
+ backend, err = db.New(userConfig.DataDir)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ noOpLocker := locking.NewNoOpLocker()
+ if userConfig.DisableRepoLocking {
+ logger.Info("Repo Locking is disabled")
+ lockingClient = noOpLocker
+ } else {
+ lockingClient = locking.NewClient(backend)
+ }
+ disableGlobalApplyLock := false
+ if userConfig.DisableGlobalApplyLock {
+ disableGlobalApplyLock = true
+ }
+
+ applyLockingClient = locking.NewApplyClient(backend, disableApply, disableGlobalApplyLock)
+ workingDirLocker := events.NewDefaultWorkingDirLocker()
+
+ var workingDir events.WorkingDir = &events.FileWorkspace{
+ DataDir: userConfig.DataDir,
+ CheckoutMerge: userConfig.CheckoutStrategy == "merge",
+ CheckoutDepth: userConfig.CheckoutDepth,
+ GithubAppEnabled: githubAppEnabled,
+ }
+
+ scheduledExecutorService := scheduled.NewExecutorService(
+ statsScope,
+ logger,
+ )
+
+ // provide fresh tokens before clone from the GitHub Apps integration, proxy workingDir
+ if githubAppEnabled {
+ if !userConfig.WriteGitCreds {
+ return nil, errors.New("Github App requires --write-git-creds to support cloning")
+ }
+ workingDir = &events.GithubAppWorkingDir{
+ WorkingDir: workingDir,
+ Credentials: githubCredentials,
+ GithubHostname: userConfig.GithubHostname,
+ }
+
+ githubAppTokenRotator := vcs.NewGithubTokenRotator(logger, githubCredentials, userConfig.GithubHostname, "x-access-token", home)
+ tokenJd, err := githubAppTokenRotator.GenerateJob()
+ if err != nil {
+ return nil, errors.Wrap(err, "could not write credentials")
+ }
+ scheduledExecutorService.AddJob(tokenJd)
+ }
+
+ if userConfig.GithubUser != "" && userConfig.GithubTokenFile != "" && userConfig.WriteGitCreds {
+ githubTokenRotator := vcs.NewGithubTokenRotator(logger, githubCredentials, userConfig.GithubHostname, userConfig.GithubUser, home)
+ tokenJd, err := githubTokenRotator.GenerateJob()
+ if err != nil {
+ return nil, errors.Wrap(err, "could not write credentials")
+ }
+ scheduledExecutorService.AddJob(tokenJd)
+ }
+
+ projectLocker := &events.DefaultProjectLocker{
+ Locker: lockingClient,
+ NoOpLocker: noOpLocker,
+ VCSClient: vcsClient,
+ }
+ deleteLockCommand := &events.DefaultDeleteLockCommand{
+ Locker: lockingClient,
+ WorkingDir: workingDir,
+ WorkingDirLocker: workingDirLocker,
+ Backend: backend,
+ }
+
+ pullClosedExecutor := events.NewInstrumentedPullClosedExecutor(
+ statsScope,
+ logger,
+ &events.PullClosedExecutor{
+ Locker: lockingClient,
+ WorkingDir: workingDir,
+ Backend: backend,
+ PullClosedTemplate: &events.PullClosedEventTemplate{},
+ LogStreamResourceCleaner: projectCmdOutputHandler,
+ VCSClient: vcsClient,
+ },
+ )
+
+ eventParser := &events.EventParser{
+ GithubUser: userConfig.GithubUser,
+ GithubToken: userConfig.GithubToken,
+ GithubTokenFile: userConfig.GithubTokenFile,
+ GitlabUser: userConfig.GitlabUser,
+ GitlabToken: userConfig.GitlabToken,
+ GiteaUser: userConfig.GiteaUser,
+ GiteaToken: userConfig.GiteaToken,
+ AllowDraftPRs: userConfig.PlanDrafts,
+ BitbucketUser: userConfig.BitbucketUser,
+ BitbucketToken: userConfig.BitbucketToken,
+ BitbucketServerURL: userConfig.BitbucketBaseURL,
+ AzureDevopsUser: userConfig.AzureDevopsUser,
+ AzureDevopsToken: userConfig.AzureDevopsToken,
+ }
+ commentParser := events.NewCommentParser(
+ userConfig.GithubUser,
+ userConfig.GitlabUser,
+ userConfig.GiteaUser,
+ userConfig.BitbucketUser,
+ userConfig.AzureDevopsUser,
+ userConfig.ExecutableName,
+ allowCommands,
+ )
+ defaultTfVersion := terraformClient.DefaultVersion()
+ pendingPlanFinder := &events.DefaultPendingPlanFinder{}
+ runStepRunner := &runtime.RunStepRunner{
+ TerraformExecutor: terraformClient,
+ DefaultTFVersion: defaultTfVersion,
+ TerraformBinDir: terraformClient.TerraformBinDir(),
+ ProjectCmdOutputHandler: projectCmdOutputHandler,
+ }
+ drainer := &events.Drainer{}
+ statusController := &controllers.StatusController{
+ Logger: logger,
+ Drainer: drainer,
+ AtlantisVersion: config.AtlantisVersion,
+ }
+ preWorkflowHooksCommandRunner := &events.DefaultPreWorkflowHooksCommandRunner{
+ VCSClient: vcsClient,
+ GlobalCfg: globalCfg,
+ WorkingDirLocker: workingDirLocker,
+ WorkingDir: workingDir,
+ PreWorkflowHookRunner: runtime.DefaultPreWorkflowHookRunner{
+ OutputHandler: projectCmdOutputHandler,
+ },
+ CommitStatusUpdater: commitStatusUpdater,
+ Router: router,
+ }
+ postWorkflowHooksCommandRunner := &events.DefaultPostWorkflowHooksCommandRunner{
+ VCSClient: vcsClient,
+ GlobalCfg: globalCfg,
+ WorkingDirLocker: workingDirLocker,
+ WorkingDir: workingDir,
+ PostWorkflowHookRunner: runtime.DefaultPostWorkflowHookRunner{
+ OutputHandler: projectCmdOutputHandler,
+ },
+ CommitStatusUpdater: commitStatusUpdater,
+ Router: router,
+ }
+ projectCommandBuilder := events.NewInstrumentedProjectCommandBuilder(
+ logger,
+ policyChecksEnabled,
+ validator,
+ &events.DefaultProjectFinder{},
+ vcsClient,
+ workingDir,
+ workingDirLocker,
+ globalCfg,
+ pendingPlanFinder,
+ commentParser,
+ userConfig.SkipCloneNoChanges,
+ userConfig.EnableRegExpCmd,
+ userConfig.Automerge,
+ userConfig.ParallelPlan,
+ userConfig.ParallelApply,
+ userConfig.AutoplanModulesFromProjects,
+ userConfig.AutoplanFileList,
+ userConfig.RestrictFileList,
+ userConfig.SilenceNoProjects,
+ userConfig.IncludeGitUntrackedFiles,
+ userConfig.AutoDiscoverModeFlag,
+ statsScope,
+ terraformClient,
+ )
+
+ showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfVersion)
+
+ if err != nil {
+ return nil, errors.Wrap(err, "initializing show step runner")
+ }
+
+ policyCheckStepRunner, err := runtime.NewPolicyCheckStepRunner(
+ defaultTfVersion,
+ policy.NewConfTestExecutorWorkflow(logger, binDir, &policy.ConfTestGoGetterVersionDownloader{}),
+ )
+
+ if err != nil {
+ return nil, errors.Wrap(err, "initializing policy check step runner")
+ }
+
+ applyRequirementHandler := &events.DefaultCommandRequirementHandler{
+ WorkingDir: workingDir,
+ }
+
+ projectCommandRunner := &events.DefaultProjectCommandRunner{
+ VcsClient: vcsClient,
+ Locker: projectLocker,
+ LockURLGenerator: router,
+ InitStepRunner: &runtime.InitStepRunner{
+ TerraformExecutor: terraformClient,
+ DefaultTFVersion: defaultTfVersion,
+ },
+ PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfVersion, commitStatusUpdater, terraformClient),
+ ShowStepRunner: showStepRunner,
+ PolicyCheckStepRunner: policyCheckStepRunner,
+ ApplyStepRunner: &runtime.ApplyStepRunner{
+ TerraformExecutor: terraformClient,
+ DefaultTFVersion: defaultTfVersion,
+ CommitStatusUpdater: commitStatusUpdater,
+ AsyncTFExec: terraformClient,
+ },
+ RunStepRunner: runStepRunner,
+ EnvStepRunner: &runtime.EnvStepRunner{
+ RunStepRunner: runStepRunner,
+ },
+ MultiEnvStepRunner: &runtime.MultiEnvStepRunner{
+ RunStepRunner: runStepRunner,
+ },
+ VersionStepRunner: &runtime.VersionStepRunner{
+ TerraformExecutor: terraformClient,
+ DefaultTFVersion: defaultTfVersion,
+ },
+ ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfVersion),
+ StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTfVersion),
+ WorkingDir: workingDir,
+ Webhooks: webhooksManager,
+ WorkingDirLocker: workingDirLocker,
+ CommandRequirementHandler: applyRequirementHandler,
+ }
+
+ dbUpdater := &events.DBUpdater{
+ Backend: backend,
+ }
+
+ pullUpdater := &events.PullUpdater{
+ HidePrevPlanComments: userConfig.HidePrevPlanComments,
+ VCSClient: vcsClient,
+ MarkdownRenderer: markdownRenderer,
+ }
+
+ autoMerger := &events.AutoMerger{
+ VCSClient: vcsClient,
+ GlobalAutomerge: userConfig.Automerge,
+ }
+
+ projectOutputWrapper := &events.ProjectOutputWrapper{
+ JobMessageSender: projectCmdOutputHandler,
+ ProjectCommandRunner: projectCommandRunner,
+ JobURLSetter: jobs.NewJobURLSetter(router, commitStatusUpdater),
+ }
+ instrumentedProjectCmdRunner := events.NewInstrumentedProjectCommandRunner(
+ statsScope,
+ projectOutputWrapper,
+ )
+
+ policyCheckCommandRunner := events.NewPolicyCheckCommandRunner(
+ dbUpdater,
+ pullUpdater,
+ commitStatusUpdater,
+ instrumentedProjectCmdRunner,
+ userConfig.ParallelPoolSize,
+ userConfig.SilenceVCSStatusNoProjects,
+ userConfig.QuietPolicyChecks,
+ )
+
+ pullReqStatusFetcher := vcs.NewPullReqStatusFetcher(vcsClient, userConfig.VCSStatusName, strings.Split(userConfig.IgnoreVCSStatusNames, ","))
+ planCommandRunner := events.NewPlanCommandRunner(
+ userConfig.SilenceVCSStatusNoPlans,
+ userConfig.SilenceVCSStatusNoProjects,
+ vcsClient,
+ pendingPlanFinder,
+ workingDir,
+ commitStatusUpdater,
+ projectCommandBuilder,
+ instrumentedProjectCmdRunner,
+ dbUpdater,
+ pullUpdater,
+ policyCheckCommandRunner,
+ autoMerger,
+ userConfig.ParallelPoolSize,
+ userConfig.SilenceNoProjects,
+ backend,
+ lockingClient,
+ userConfig.DiscardApprovalOnPlanFlag,
+ pullReqStatusFetcher,
+ )
+
+ applyCommandRunner := events.NewApplyCommandRunner(
+ vcsClient,
+ userConfig.DisableApplyAll,
+ applyLockingClient,
+ commitStatusUpdater,
+ projectCommandBuilder,
+ instrumentedProjectCmdRunner,
+ autoMerger,
+ pullUpdater,
+ dbUpdater,
+ backend,
+ userConfig.ParallelPoolSize,
+ userConfig.SilenceNoProjects,
+ userConfig.SilenceVCSStatusNoProjects,
+ pullReqStatusFetcher,
+ )
+
+ approvePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner(
+ commitStatusUpdater,
+ projectCommandBuilder,
+ instrumentedProjectCmdRunner,
+ pullUpdater,
+ dbUpdater,
+ userConfig.SilenceNoProjects,
+ userConfig.SilenceVCSStatusNoPlans,
+ vcsClient,
+ )
+
+ unlockCommandRunner := events.NewUnlockCommandRunner(
+ deleteLockCommand,
+ vcsClient,
+ userConfig.SilenceNoProjects,
+ userConfig.DisableUnlockLabel,
+ )
+
+ versionCommandRunner := events.NewVersionCommandRunner(
+ pullUpdater,
+ projectCommandBuilder,
+ projectOutputWrapper,
+ userConfig.ParallelPoolSize,
+ userConfig.SilenceNoProjects,
+ )
+
+ importCommandRunner := events.NewImportCommandRunner(
+ pullUpdater,
+ pullReqStatusFetcher,
+ projectCommandBuilder,
+ instrumentedProjectCmdRunner,
+ userConfig.SilenceNoProjects,
+ )
+
+ stateCommandRunner := events.NewStateCommandRunner(
+ pullUpdater,
+ projectCommandBuilder,
+ instrumentedProjectCmdRunner,
+ )
+
+ commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{
+ command.Plan: planCommandRunner,
+ command.Apply: applyCommandRunner,
+ command.ApprovePolicies: approvePoliciesCommandRunner,
+ command.Unlock: unlockCommandRunner,
+ command.Version: versionCommandRunner,
+ command.Import: importCommandRunner,
+ command.State: stateCommandRunner,
+ }
+
+ var teamAllowlistChecker command.TeamAllowlistChecker
+ if globalCfg.TeamAuthz.Command != "" {
+ teamAllowlistChecker = &events.ExternalTeamAllowlistChecker{
+ Command: globalCfg.TeamAuthz.Command,
+ ExtraArgs: globalCfg.TeamAuthz.Args,
+ ExternalTeamAllowlistRunner: &runtime.DefaultExternalTeamAllowlistRunner{},
+ }
+ } else {
+ teamAllowlistChecker, err = command.NewTeamAllowlistChecker(userConfig.GithubTeamAllowlist)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ varFileAllowlistChecker, err := events.NewVarFileAllowlistChecker(userConfig.VarFileAllowlist)
+ if err != nil {
+ return nil, err
+ }
+
+ commandRunner := &events.DefaultCommandRunner{
+ VCSClient: vcsClient,
+ GithubPullGetter: githubClient,
+ GitlabMergeRequestGetter: gitlabClient,
+ AzureDevopsPullGetter: azuredevopsClient,
+ GiteaPullGetter: giteaClient,
+ CommentCommandRunnerByCmd: commentCommandRunnerByCmd,
+ EventParser: eventParser,
+ FailOnPreWorkflowHookError: userConfig.FailOnPreWorkflowHookError,
+ Logger: logger,
+ GlobalCfg: globalCfg,
+ StatsScope: statsScope.SubScope("cmd"),
+ AllowForkPRs: userConfig.AllowForkPRs,
+ AllowForkPRsFlag: config.AllowForkPRsFlag,
+ SilenceForkPRErrors: userConfig.SilenceForkPRErrors,
+ SilenceForkPRErrorsFlag: config.SilenceForkPRErrorsFlag,
+ DisableAutoplan: userConfig.DisableAutoplan,
+ DisableAutoplanLabel: userConfig.DisableAutoplanLabel,
+ Drainer: drainer,
+ PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
+ PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,
+ PullStatusFetcher: backend,
+ TeamAllowlistChecker: teamAllowlistChecker,
+ VarFileAllowlistChecker: varFileAllowlistChecker,
+ CommitStatusUpdater: commitStatusUpdater,
+ ApplyLocker: applyLockingClient,
+ }
+ repoAllowlist, err := events.NewRepoAllowlistChecker(userConfig.RepoAllowlist)
+ if err != nil {
+ return nil, err
+ }
+ locksController := &controllers.LocksController{
+ AtlantisVersion: config.AtlantisVersion,
+ AtlantisURL: parsedURL,
+ Locker: lockingClient,
+ ApplyLocker: applyLockingClient,
+ Logger: logger,
+ VCSClient: vcsClient,
+ LockDetailTemplate: web_templates.LockTemplate,
+ WorkingDir: workingDir,
+ WorkingDirLocker: workingDirLocker,
+ Backend: backend,
+ DeleteLockCommand: deleteLockCommand,
+ }
+
+ wsMux := websocket.NewMultiplexor(
+ logger,
+ controllers.JobIDKeyGenerator{},
+ projectCmdOutputHandler,
+ userConfig.WebsocketCheckOrigin,
+ )
+
+ jobsController := &controllers.JobsController{
+ AtlantisVersion: config.AtlantisVersion,
+ AtlantisURL: parsedURL,
+ Logger: logger,
+ ProjectJobsTemplate: web_templates.ProjectJobsTemplate,
+ ProjectJobsErrorTemplate: web_templates.ProjectJobsErrorTemplate,
+ Backend: backend,
+ WsMux: wsMux,
+ KeyGenerator: controllers.JobIDKeyGenerator{},
+ StatsScope: statsScope.SubScope("api"),
+ }
+ apiController := &controllers.APIController{
+ APISecret: []byte(userConfig.APISecret),
+ Locker: lockingClient,
+ Logger: logger,
+ Parser: eventParser,
+ ProjectCommandBuilder: projectCommandBuilder,
+ ProjectPlanCommandRunner: instrumentedProjectCmdRunner,
+ ProjectApplyCommandRunner: instrumentedProjectCmdRunner,
+ FailOnPreWorkflowHookError: userConfig.FailOnPreWorkflowHookError,
+ PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
+ PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,
+ RepoAllowlistChecker: repoAllowlist,
+ Scope: statsScope.SubScope("api"),
+ VCSClient: vcsClient,
+ }
+
+ eventsController := &events_controllers.VCSEventsController{
+ CommandRunner: commandRunner,
+ PullCleaner: pullClosedExecutor,
+ Parser: eventParser,
+ CommentParser: commentParser,
+ Logger: logger,
+ Scope: statsScope,
+ ApplyDisabled: disableApply,
+ GithubWebhookSecret: []byte(userConfig.GithubWebhookSecret),
+ GithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},
+ GitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},
+ GitlabWebhookSecret: []byte(userConfig.GitlabWebhookSecret),
+ RepoAllowlistChecker: repoAllowlist,
+ SilenceAllowlistErrors: userConfig.SilenceAllowlistErrors,
+ EmojiReaction: userConfig.EmojiReaction,
+ ExecutableName: userConfig.ExecutableName,
+ SupportedVCSHosts: supportedVCSHosts,
+ VCSClient: vcsClient,
+ BitbucketWebhookSecret: []byte(userConfig.BitbucketWebhookSecret),
+ AzureDevopsWebhookBasicUser: []byte(userConfig.AzureDevopsWebhookUser),
+ AzureDevopsWebhookBasicPassword: []byte(userConfig.AzureDevopsWebhookPassword),
+ AzureDevopsRequestValidator: &events_controllers.DefaultAzureDevopsRequestValidator{},
+ GiteaWebhookSecret: []byte(userConfig.GiteaWebhookSecret),
+ }
+ githubAppController := &controllers.GithubAppController{
+ AtlantisURL: parsedURL,
+ Logger: logger,
+ GithubSetupComplete: githubAppEnabled,
+ GithubHostname: userConfig.GithubHostname,
+ GithubOrg: userConfig.GithubOrg,
+ }
+
+ return &Server{
+ AtlantisVersion: config.AtlantisVersion,
+ AtlantisURL: parsedURL,
+ Router: underlyingRouter,
+ Port: userConfig.Port,
+ PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,
+ PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
+ CommandRunner: commandRunner,
+ Logger: logger,
+ StatsScope: statsScope,
+ StatsReporter: statsReporter,
+ StatsCloser: closer,
+ Locker: lockingClient,
+ ApplyLocker: applyLockingClient,
+ VCSEventsController: eventsController,
+ GithubAppController: githubAppController,
+ LocksController: locksController,
+ JobsController: jobsController,
+ StatusController: statusController,
+ APIController: apiController,
+ IndexTemplate: web_templates.IndexTemplate,
+ LockDetailTemplate: web_templates.LockTemplate,
+ ProjectJobsTemplate: web_templates.ProjectJobsTemplate,
+ ProjectJobsErrorTemplate: web_templates.ProjectJobsErrorTemplate,
+ SSLKeyFile: userConfig.SSLKeyFile,
+ SSLCertFile: userConfig.SSLCertFile,
+ DisableGlobalApplyLock: userConfig.DisableGlobalApplyLock,
+ Drainer: drainer,
+ ProjectCmdOutputHandler: projectCmdOutputHandler,
+ WebAuthentication: userConfig.WebBasicAuth,
+ WebUsername: userConfig.WebUsername,
+ WebPassword: userConfig.WebPassword,
+ ScheduledExecutorService: scheduledExecutorService,
+ }, nil
+}
+
+// Start creates the routes and starts serving traffic.
+func (s *Server) Start() error {
+ s.Router.HandleFunc("/", s.Index).Methods("GET").MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) bool {
+ return r.URL.Path == "/" || r.URL.Path == "/index.html"
+ })
+ s.Router.HandleFunc("/healthz", s.Healthz).Methods("GET")
+ s.Router.HandleFunc("/status", s.StatusController.Get).Methods("GET")
+ s.Router.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
+ s.Router.HandleFunc("/events", s.VCSEventsController.Post).Methods("POST")
+ s.Router.HandleFunc("/api/plan", s.APIController.Plan).Methods("POST")
+ s.Router.HandleFunc("/api/apply", s.APIController.Apply).Methods("POST")
+ s.Router.HandleFunc("/github-app/exchange-code", s.GithubAppController.ExchangeCode).Methods("GET")
+ s.Router.HandleFunc("/github-app/setup", s.GithubAppController.New).Methods("GET")
+ s.Router.HandleFunc("/locks", s.LocksController.DeleteLock).Methods("DELETE").Queries("id", "{id:.*}")
+ s.Router.HandleFunc("/lock", s.LocksController.GetLock).Methods("GET").
+ Queries(LockViewRouteIDQueryParam, fmt.Sprintf("{%s}", LockViewRouteIDQueryParam)).Name(LockViewRouteName)
+ s.Router.HandleFunc("/jobs/{job-id}", s.JobsController.GetProjectJobs).Methods("GET").Name(ProjectJobsViewRouteName)
+ s.Router.HandleFunc("/jobs/{job-id}/ws", s.JobsController.GetProjectJobsWS).Methods("GET")
+
+ r, ok := s.StatsReporter.(prometheus.Reporter)
+ if ok {
+ s.Router.Handle(s.CommandRunner.GlobalCfg.Metrics.Prometheus.Endpoint, r.HTTPHandler())
+ }
+ if !s.DisableGlobalApplyLock {
+ s.Router.HandleFunc("/apply/lock", s.LocksController.LockApply).Methods("POST").Queries()
+ s.Router.HandleFunc("/apply/unlock", s.LocksController.UnlockApply).Methods("DELETE").Queries()
+ }
+
+ n := negroni.New(&negroni.Recovery{
+ Logger: log.New(os.Stdout, "", log.LstdFlags),
+ PrintStack: false,
+ StackAll: false,
+ StackSize: 1024 * 8,
+ }, NewRequestLogger(s))
+ n.UseHandler(s.Router)
+
+ defer s.Logger.Flush()
+
+ // Ensure server gracefully drains connections when stopped.
+ stop := make(chan os.Signal, 1)
+ // Stop on SIGINTs and SIGTERMs.
+ signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
+
+ go s.ScheduledExecutorService.Run()
+
+ go func() {
+ s.ProjectCmdOutputHandler.Handle()
+ }()
+
+ tlsConfig := &tls.Config{GetCertificate: s.GetSSLCertificate, MinVersion: tls.VersionTLS12}
+
+ server := &http.Server{Addr: fmt.Sprintf(":%d", s.Port), Handler: n, TLSConfig: tlsConfig, ReadHeaderTimeout: 10 * time.Second}
+ go func() {
+ s.Logger.Info("Atlantis started - listening on port %v", s.Port)
+
+ var err error
+ if s.SSLCertFile != "" && s.SSLKeyFile != "" {
+ err = server.ListenAndServeTLS("", "")
+ } else {
+ err = server.ListenAndServe()
+ }
+
+ if err != nil && err != http.ErrServerClosed {
+ s.Logger.Err(err.Error())
+ }
+ }()
+ <-stop
+
+ s.Logger.Warn("Received interrupt. Waiting for in-progress operations to complete")
+ s.waitForDrain()
+
+ // flush stats before shutdown
+ if err := s.StatsCloser.Close(); err != nil {
+ s.Logger.Err(err.Error())
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ if err := server.Shutdown(ctx); err != nil {
+ return fmt.Errorf("while shutting down: %s", err)
+ }
+ return nil
+}
+
+// waitForDrain blocks until draining is complete.
+func (s *Server) waitForDrain() {
+ drainComplete := make(chan bool, 1)
+ go func() {
+ s.Drainer.ShutdownBlocking()
+ drainComplete <- true
+ }()
+ ticker := time.NewTicker(5 * time.Second)
+ for {
+ select {
+ case <-drainComplete:
+ s.Logger.Info("All in-progress operations complete, shutting down")
+ return
+ case <-ticker.C:
+ s.Logger.Info("Waiting for in-progress operations to complete, current in-progress ops: %d", s.Drainer.GetStatus().InProgressOps)
+ }
+ }
+}
+
+// Index is the / route.
+func (s *Server) Index(w http.ResponseWriter, _ *http.Request) {
+ locks, err := s.Locker.List()
+ if err != nil {
+ w.WriteHeader(http.StatusServiceUnavailable)
+ fmt.Fprintf(w, "Could not retrieve locks: %s", err)
+ return
+ }
+
+ var lockResults []web_templates.LockIndexData
+ for id, v := range locks {
+ lockURL, _ := s.Router.Get(LockViewRouteName).URL("id", url.QueryEscape(id))
+ lockResults = append(lockResults, web_templates.LockIndexData{
+ // NOTE: must use .String() instead of .Path because we need the
+ // query params as part of the lock URL.
+ LockPath: lockURL.String(),
+ RepoFullName: v.Project.RepoFullName,
+ LockedBy: v.Pull.Author,
+ PullNum: v.Pull.Num,
+ Path: v.Project.Path,
+ Workspace: v.Workspace,
+ Time: v.Time,
+ TimeFormatted: v.Time.Format("2006-01-02 15:04:05"),
+ })
+ }
+
+ applyCmdLock, err := s.ApplyLocker.CheckApplyLock()
+ s.Logger.Debug("Apply Lock: %v", applyCmdLock)
+ if err != nil {
+ w.WriteHeader(http.StatusServiceUnavailable)
+ fmt.Fprintf(w, "Could not retrieve global apply lock: %s", err)
+ return
+ }
+
+ applyLockData := web_templates.ApplyLockData{
+ Time: applyCmdLock.Time,
+ Locked: applyCmdLock.Locked,
+ GlobalApplyLockEnabled: applyCmdLock.GlobalApplyLockEnabled,
+ TimeFormatted: applyCmdLock.Time.Format("2006-01-02 15:04:05"),
+ }
+ //Sort by date - newest to oldest.
+ sort.SliceStable(lockResults, func(i, j int) bool { return lockResults[i].Time.After(lockResults[j].Time) })
+
+ err = s.IndexTemplate.Execute(w, web_templates.IndexData{
+ Locks: lockResults,
+ PullToJobMapping: preparePullToJobMappings(s),
+ ApplyLock: applyLockData,
+ AtlantisVersion: s.AtlantisVersion,
+ CleanedBasePath: s.AtlantisURL.Path,
+ })
+ if err != nil {
+ s.Logger.Err(err.Error())
+ }
+}
+
+func preparePullToJobMappings(s *Server) []jobs.PullInfoWithJobIDs {
+
+ pullToJobMappings := s.ProjectCmdOutputHandler.GetPullToJobMapping()
+
+ for i := range pullToJobMappings {
+ for j := range pullToJobMappings[i].JobIDInfos {
+ jobUrl, _ := s.Router.Get(ProjectJobsViewRouteName).URL("job-id", pullToJobMappings[i].JobIDInfos[j].JobID)
+ pullToJobMappings[i].JobIDInfos[j].JobIDUrl = jobUrl.String()
+ pullToJobMappings[i].JobIDInfos[j].TimeFormatted = pullToJobMappings[i].JobIDInfos[j].Time.Format("2006-01-02 15:04:05")
+ }
+
+ //Sort by date - newest to oldest.
+ sort.SliceStable(pullToJobMappings[i].JobIDInfos, func(x, y int) bool {
+ return pullToJobMappings[i].JobIDInfos[x].Time.After(pullToJobMappings[i].JobIDInfos[y].Time)
+ })
+ }
+
+ //Sort by repository, project, path, workspace then date.
+ sort.SliceStable(pullToJobMappings, func(x, y int) bool {
+ if pullToJobMappings[x].Pull.RepoFullName != pullToJobMappings[y].Pull.RepoFullName {
+ return pullToJobMappings[x].Pull.RepoFullName < pullToJobMappings[y].Pull.RepoFullName
+ }
+ if pullToJobMappings[x].Pull.ProjectName != pullToJobMappings[y].Pull.ProjectName {
+ return pullToJobMappings[x].Pull.ProjectName < pullToJobMappings[y].Pull.ProjectName
+ }
+ if pullToJobMappings[x].Pull.Path != pullToJobMappings[y].Pull.Path {
+ return pullToJobMappings[x].Pull.Path < pullToJobMappings[y].Pull.Path
+ }
+ return pullToJobMappings[x].Pull.Workspace < pullToJobMappings[y].Pull.Workspace
+ })
+
+ return pullToJobMappings
+}
+
+func mkSubDir(parentDir string, subDir string) (string, error) {
+ fullDir := filepath.Join(parentDir, subDir)
+ if err := os.MkdirAll(fullDir, 0700); err != nil {
+ return "", errors.Wrapf(err, "unable to create dir %q", fullDir)
+ }
+
+ return fullDir, nil
+}
+
+// Healthz returns the health check response. It always returns a 200 currently.
+func (s *Server) Healthz(w http.ResponseWriter, _ *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Write(healthzData) // nolint: errcheck
+}
+
+var healthzData = []byte(`{
+ "status": "ok"
+}`)
+
+func (s *Server) GetSSLCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
+ certStat, err := os.Stat(s.SSLCertFile)
+ if err != nil {
+ return nil, fmt.Errorf("while getting cert file modification time: %w", err)
+ }
+
+ keyStat, err := os.Stat(s.SSLKeyFile)
+ if err != nil {
+ return nil, fmt.Errorf("while getting key file modification time: %w", err)
+ }
+
+ if s.SSLCert == nil || certStat.ModTime() != s.CertLastRefreshTime || keyStat.ModTime() != s.KeyLastRefreshTime {
+ cert, err := tls.LoadX509KeyPair(s.SSLCertFile, s.SSLKeyFile)
+ if err != nil {
+ return nil, fmt.Errorf("while loading tls cert: %w", err)
+ }
+
+ s.SSLCert = &cert
+ s.CertLastRefreshTime = certStat.ModTime()
+ s.KeyLastRefreshTime = keyStat.ModTime()
+ }
+ return s.SSLCert, nil
+}
+
+// ParseAtlantisURL parses the user-passed atlantis URL to ensure it is valid
+// and we can use it in our templates.
+// It removes any trailing slashes from the path so we can concatenate it
+// with other paths without checking.
+func ParseAtlantisURL(u string) (*url.URL, error) {
+ parsed, err := url.Parse(u)
+ if err != nil {
+ return nil, err
+ }
+ if !(parsed.Scheme == "http" || parsed.Scheme == "https") {
+ return nil, errors.New("http or https must be specified")
+ }
+ // We want the path to end without a trailing slash so we know how to
+ // use it in the rest of the program.
+ parsed.Path = strings.TrimSuffix(parsed.Path, "/")
+ return parsed, nil
+}