Files
atlantis/server/events/project_command_context_builder.go
Brett Galkowski 22060fe5d2 feat: Add option allowing usage of custom policy check tools (#3765)
* Adding new flag everywhere relevant, implementing policy result workaround

* Fixing unit test str matching, adding custom policy conditional to step_runner

* Adding documentation steps for custom policy tools

* Refactoring ConftestOutput attribute to PolicyOutput
2023-10-06 10:57:29 -04:00

323 lines
10 KiB
Go

package events
import (
"path/filepath"
"github.com/google/uuid"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
tally "github.com/uber-go/tally/v4"
)
func NewProjectCommandContextBuilder(policyCheckEnabled bool, commentBuilder CommentBuilder, scope tally.Scope) ProjectCommandContextBuilder {
projectCommandContextBuilder := &DefaultProjectCommandContextBuilder{
CommentBuilder: commentBuilder,
}
if policyCheckEnabled {
return &PolicyCheckProjectCommandContextBuilder{
CommentBuilder: commentBuilder,
ProjectCommandContextBuilder: projectCommandContextBuilder,
}
}
return &CommandScopedStatsProjectCommandContextBuilder{
ProjectCommandContextBuilder: projectCommandContextBuilder,
ProjectCounter: scope.Counter("projects"),
}
}
type ProjectCommandContextBuilder interface {
// BuildProjectContext builds project command contexts for atlantis commands
BuildProjectContext(
ctx *command.Context,
cmdName command.Name,
subCmdName string,
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool, terraformClient terraform.Client,
) []command.ProjectContext
}
// CommandScopedStatsProjectCommandContextBuilder ensures that project command context contains a scoped stats
// object relevant to the command it applies to.
type CommandScopedStatsProjectCommandContextBuilder struct {
ProjectCommandContextBuilder
// Conciously making this global since it gets flushed periodically anyways
ProjectCounter tally.Counter
}
// BuildProjectContext builds the context and injects the appropriate command level scope after the fact.
func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext(
ctx *command.Context,
cmdName command.Name,
subCmdName string,
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool,
terraformClient terraform.Client,
) (projectCmds []command.ProjectContext) {
cb.ProjectCounter.Inc(1)
cmds := cb.ProjectCommandContextBuilder.BuildProjectContext(
ctx, cmdName, subCmdName, prjCfg, commentFlags, repoDir, automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail, terraformClient,
)
projectCmds = []command.ProjectContext{}
for _, cmd := range cmds {
// specifically use the command name in the context instead of the arg
// since we can return multiple commands worth of contexts for a given command name arg
// to effectively pipeline them.
cmd.Scope = cmd.SetProjectScopeTags(cmd.Scope)
projectCmds = append(projectCmds, cmd)
}
return
}
type DefaultProjectCommandContextBuilder struct {
CommentBuilder CommentBuilder
}
func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
ctx *command.Context,
cmdName command.Name,
subName string,
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool,
terraformClient terraform.Client,
) (projectCmds []command.ProjectContext) {
ctx.Log.Debug("Building project command context for %s", cmdName)
var steps []valid.Step
switch cmdName {
case command.Plan:
steps = prjCfg.Workflow.Plan.Steps
case command.Apply:
steps = prjCfg.Workflow.Apply.Steps
case command.Version:
// Setting statically since there will only be one step
steps = []valid.Step{{
StepName: "version",
}}
case command.Import:
steps = prjCfg.Workflow.Import.Steps
case command.State:
switch subName {
case "rm":
steps = prjCfg.Workflow.StateRm.Steps
default:
// comment_parser prevent invalid subcommand, so not need to handle this.
// if comes here, state_command_runner will respond on PR, so it's enough to do log only.
ctx.Log.Err("unknown state subcommand: %s", subName)
}
}
// If TerraformVersion not defined in config file look for a
// terraform.require_version block.
if prjCfg.TerraformVersion == nil {
prjCfg.TerraformVersion = terraformClient.DetectVersion(ctx.Log, filepath.Join(repoDir, prjCfg.RepoRelDir))
}
projectCmdContext := newProjectCommandContext(
ctx,
cmdName,
cb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled),
cb.CommentBuilder.BuildApprovePoliciesComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name),
cb.CommentBuilder.BuildPlanComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, commentFlags),
prjCfg,
steps,
prjCfg.PolicySets,
escapeArgs(commentFlags),
automerge,
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
ctx.Scope,
ctx.PullRequestStatus,
ctx.PullStatus,
)
projectCmds = append(projectCmds, projectCmdContext)
return
}
type PolicyCheckProjectCommandContextBuilder struct {
ProjectCommandContextBuilder *DefaultProjectCommandContextBuilder
CommentBuilder CommentBuilder
}
func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
ctx *command.Context,
cmdName command.Name,
subCmdName string,
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool,
terraformClient terraform.Client,
) (projectCmds []command.ProjectContext) {
if prjCfg.PolicyCheck {
ctx.Log.Debug("PolicyChecks are enabled")
} else {
// PolicyCheck is disabled at repository level
ctx.Log.Debug("PolicyChecks are disabled on this repository")
}
// If TerraformVersion not defined in config file look for a
// terraform.require_version block.
if prjCfg.TerraformVersion == nil {
prjCfg.TerraformVersion = terraformClient.DetectVersion(ctx.Log, filepath.Join(repoDir, prjCfg.RepoRelDir))
}
projectCmds = cb.ProjectCommandContextBuilder.BuildProjectContext(
ctx,
cmdName,
subCmdName,
prjCfg,
commentFlags,
repoDir,
automerge,
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
terraformClient,
)
if cmdName == command.Plan && prjCfg.PolicyCheck {
ctx.Log.Debug("Building project command context for %s", command.PolicyCheck)
steps := prjCfg.Workflow.PolicyCheck.Steps
projectCmds = append(projectCmds, newProjectCommandContext(
ctx,
command.PolicyCheck,
cb.CommentBuilder.BuildApplyComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, prjCfg.AutoMergeDisabled),
cb.CommentBuilder.BuildApprovePoliciesComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name),
cb.CommentBuilder.BuildPlanComment(prjCfg.RepoRelDir, prjCfg.Workspace, prjCfg.Name, commentFlags),
prjCfg,
steps,
prjCfg.PolicySets,
escapeArgs(commentFlags),
automerge,
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
ctx.Scope,
ctx.PullRequestStatus,
ctx.PullStatus,
))
}
return
}
// newProjectCommandContext is a initializer method that handles constructing the
// ProjectCommandContext.
func newProjectCommandContext(ctx *command.Context,
cmd command.Name,
applyCmd string,
approvePoliciesCmd string,
planCmd string,
projCfg valid.MergedProjectCfg,
steps []valid.Step,
policySets valid.PolicySets,
escapedCommentArgs []string,
automergeEnabled bool,
parallelApplyEnabled bool,
parallelPlanEnabled bool,
verbose bool,
abortOnExcecutionOrderFail bool,
scope tally.Scope,
pullReqStatus models.PullReqStatus,
pullStatus *models.PullStatus,
) command.ProjectContext {
var projectPlanStatus models.ProjectPlanStatus
var projectPolicyStatus []models.PolicySetStatus
if ctx.PullStatus != nil {
for _, project := range ctx.PullStatus.Projects {
// if name is not used, let's match the directory
if projCfg.Name == "" && project.RepoRelDir == projCfg.RepoRelDir {
projectPlanStatus = project.Status
projectPolicyStatus = project.PolicyStatus
break
}
if projCfg.Name != "" && project.ProjectName == projCfg.Name {
projectPlanStatus = project.Status
projectPolicyStatus = project.PolicyStatus
break
}
}
}
return command.ProjectContext{
CommandName: cmd,
ApplyCmd: applyCmd,
ApprovePoliciesCmd: approvePoliciesCmd,
BaseRepo: ctx.Pull.BaseRepo,
EscapedCommentArgs: escapedCommentArgs,
AutomergeEnabled: automergeEnabled,
DeleteSourceBranchOnMerge: projCfg.DeleteSourceBranchOnMerge,
RepoLocking: projCfg.RepoLocking,
CustomPolicyCheck: projCfg.CustomPolicyCheck,
ParallelApplyEnabled: parallelApplyEnabled,
ParallelPlanEnabled: parallelPlanEnabled,
ParallelPolicyCheckEnabled: parallelPlanEnabled,
DependsOn: projCfg.DependsOn,
AutoplanEnabled: projCfg.AutoplanEnabled,
Steps: steps,
HeadRepo: ctx.HeadRepo,
Log: ctx.Log,
Scope: scope,
ProjectPlanStatus: projectPlanStatus,
ProjectPolicyStatus: projectPolicyStatus,
Pull: ctx.Pull,
ProjectName: projCfg.Name,
PlanRequirements: projCfg.PlanRequirements,
ApplyRequirements: projCfg.ApplyRequirements,
ImportRequirements: projCfg.ImportRequirements,
RePlanCmd: planCmd,
RepoRelDir: projCfg.RepoRelDir,
RepoConfigVersion: projCfg.RepoCfgVersion,
TerraformVersion: projCfg.TerraformVersion,
User: ctx.User,
Verbose: verbose,
Workspace: projCfg.Workspace,
PolicySets: policySets,
PolicySetTarget: ctx.PolicySet,
ClearPolicyApproval: ctx.ClearPolicyApproval,
PullReqStatus: pullReqStatus,
PullStatus: pullStatus,
JobID: uuid.New().String(),
ExecutionOrderGroup: projCfg.ExecutionOrderGroup,
AbortOnExcecutionOrderFail: abortOnExcecutionOrderFail,
}
}
func escapeArgs(args []string) []string {
var escaped []string
for _, arg := range args {
var escapedArg string
for i := range arg {
escapedArg += "\\" + string(arg[i])
}
escaped = append(escaped, escapedArg)
}
return escaped
}