Files
atlantis/server/events/command/project_context.go
2023-10-06 11:35:38 -04:00

197 lines
7.7 KiB
Go

package command
import (
"fmt"
"strconv"
"strings"
"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
tally "github.com/uber-go/tally/v4"
)
const (
planfileSlashReplace = "::"
)
// ProjectContext defines the context for a plan or apply stage that will
// be executed for a project.
type ProjectContext struct {
CommandName Name
// ApplyCmd is the command that users should run to apply this plan. If
// this is an apply then this will be empty.
ApplyCmd string
// ApprovePoliciesCmd is the command that users should run to approve policies for this plan. If
// this is an apply then this will be empty.
ApprovePoliciesCmd string
// PlanRequirements is the list of requirements that must be satisfied
// before we will run the plan stage.
PlanRequirements []string
// ApplyRequirements is the list of requirements that must be satisfied
// before we will run the apply stage.
ApplyRequirements []string
// ImportRequirements is the list of requirements that must be satisfied
// before we will run the import stage.
ImportRequirements []string
// AutomergeEnabled is true if automerge is enabled for the repo that this
// project is in.
AutomergeEnabled bool
// ParallelApplyEnabled is true if parallel apply is enabled for this project.
ParallelApplyEnabled bool
// ParallelPlanEnabled is true if parallel plan is enabled for this project.
ParallelPlanEnabled bool
// ParallelPolicyCheckEnabled is true if parallel policy_check is enabled for this project.
ParallelPolicyCheckEnabled bool
// AutoplanEnabled is true if autoplanning is enabled for this project.
AutoplanEnabled bool
// BaseRepo is the repository that the pull request will be merged into.
BaseRepo models.Repo
// EscapedCommentArgs are the extra arguments that were added to the atlantis
// command, ex. atlantis plan -- -target=resource. We then escape them
// by adding a \ before each character so that they can be used within
// sh -c safely, i.e. sh -c "terraform plan $(touch bad)".
EscapedCommentArgs []string
// HeadRepo is the repository that is getting merged into the BaseRepo.
// If the pull request branch is from the same repository then HeadRepo will
// be the same as BaseRepo.
HeadRepo models.Repo
// Log is a logger that's been set up for this context.
Log logging.SimpleLogging
// Scope is the scope for reporting stats setup for this context
Scope tally.Scope
// PullReqStatus holds state about the PR that requires additional computation outside models.PullRequest
PullReqStatus models.PullReqStatus
// CurrentProjectPlanStatus is the status of the current project prior to this command.
ProjectPlanStatus models.ProjectPlanStatus
// ProjectPolicyStatus is the status of policy sets of the current project prior to this command.
ProjectPolicyStatus []models.PolicySetStatus
// Pull is the pull request we're responding to.
Pull models.PullRequest
// ProjectName is the name of the project set in atlantis.yaml. If there was
// no name this will be an empty string.
ProjectName string
// RepoConfigVersion is the version of the repo's atlantis.yaml file. If
// there was no file, this will be 0.
RepoConfigVersion int
// RePlanCmd is the command that users should run to re-plan this project.
// If this is an apply then this will be empty.
RePlanCmd string
// RepoRelDir is the directory of this project relative to the repo root.
RepoRelDir string
// Steps are the sequence of commands we need to run for this project and this
// stage.
Steps []valid.Step
// TerraformVersion is the version of terraform we should use when executing
// commands for this project. This can be set to nil in which case we will
// use the default Atlantis terraform version.
TerraformVersion *version.Version
// Configuration metadata for a given project.
User models.User
// Verbose is true when the user would like verbose output.
Verbose bool
// Workspace is the Terraform workspace this project is in. It will always
// be set.
Workspace string
// PolicySets represent the policies that are run on the plan as part of the
// policy check stage
PolicySets valid.PolicySets
// PolicySetTarget describes which policy sets to target on the approve_policies step.
PolicySetTarget string
// ClearPolicyApproval determines whether policy counts will be incremented or cleared.
ClearPolicyApproval bool
// DeleteSourceBranchOnMerge will attempt to allow a branch to be deleted when merged (AzureDevOps & GitLab Support Only)
DeleteSourceBranchOnMerge bool
// RepoLocking will get a lock when plan
RepoLocking bool
// RepoConfigFile
RepoConfigFile string
// UUID for atlantis logs
JobID string
// The index of order group. Before planning/applying it will use to sort projects. Default is 0.
ExecutionOrderGroup int
// If plans/applies should be aborted if any prior plan/apply fails
AbortOnExcecutionOrderFail bool
// Allows custom policy check tools outside of Conftest to run in checks
CustomPolicyCheck bool
}
// SetProjectScopeTags adds ProjectContext tags to a new returned scope.
func (p ProjectContext) SetProjectScopeTags(scope tally.Scope) tally.Scope {
v := ""
if p.TerraformVersion != nil {
v = p.TerraformVersion.String()
}
tags := ProjectScopeTags{
BaseRepo: p.BaseRepo.FullName,
PrNumber: strconv.Itoa(p.Pull.Num),
Project: p.ProjectName,
ProjectPath: p.RepoRelDir,
TerraformVersion: v,
Workspace: p.Workspace,
}
return scope.Tagged(tags.Loadtags())
}
// GetShowResultFileName returns the filename (not the path) to store the tf show result
func (p ProjectContext) GetShowResultFileName() string {
if p.ProjectName == "" {
return fmt.Sprintf("%s.json", p.Workspace)
}
projName := strings.Replace(p.ProjectName, "/", planfileSlashReplace, -1)
return fmt.Sprintf("%s-%s.json", projName, p.Workspace)
}
// GetPolicyCheckResultFileName returns the filename (not the path) to store the result from conftest_client.
func (p ProjectContext) GetPolicyCheckResultFileName() string {
if p.ProjectName == "" {
return fmt.Sprintf("%s-policyout.json", p.Workspace)
}
projName := strings.Replace(p.ProjectName, "/", planfileSlashReplace, -1)
return fmt.Sprintf("%s-%s-policyout.json", projName, p.Workspace)
}
// Gets a unique identifier for the current pull request as a single string
func (p ProjectContext) PullInfo() string {
normalizedOwner := strings.ReplaceAll(p.BaseRepo.Owner, "/", "-")
normalizedName := strings.ReplaceAll(p.BaseRepo.Name, "/", "-")
projectRepo := fmt.Sprintf("%s/%s", normalizedOwner, normalizedName)
return buildPullInfo(projectRepo, p.Pull.Num, p.ProjectName, p.RepoRelDir, p.Workspace)
}
func buildPullInfo(repoName string, pullNum int, projectName string, relDir string, workspace string) string {
projectIdentifier := getProjectIdentifier(relDir, projectName)
return fmt.Sprintf("%s/%d/%s/%s", repoName, pullNum, projectIdentifier, workspace)
}
func getProjectIdentifier(relRepoDir string, projectName string) string {
if projectName != "" {
return projectName
}
// Replace directory separator / with -
// Replace . with _ to ensure projects with no project name and root dir set to "." have a valid URL
replacer := strings.NewReplacer("/", "-", ".", "_")
return replacer.Replace(relRepoDir)
}
// PolicyCleared returns whether all policies are passing or not.
func (p ProjectContext) PolicyCleared() bool {
passing := true
for _, psStatus := range p.ProjectPolicyStatus {
if psStatus.Passed {
continue
}
for _, psCfg := range p.PolicySets.PolicySets {
if psStatus.PolicySetName == psCfg.Name {
if psStatus.Approvals != psCfg.ApproveCount {
passing = false
}
}
}
}
return passing
}