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 }