Files
atlantis/server/events/project_command_context_builder.go
Sarvar Muminov af2a806870 Implement a new policy check workflow (#1317)
* Adding policy_check support into yaml config

* Added policy check model and runtime structs

* Adding BuildPolicyCheckCommand to ProjectCommandBuilder

* Return incorrectly deleted code

* Remove BuildAutoPolicyPlanCommand from ProjectCommandBuilder

* Split runAutoCommand into two functions

runAutoPlanCommand - does what originally RunAutoplancommand was doing
except now it returns CommandResult and []models.ProjectCommandContext
runAutoPolicyCheckCommand - accepts CommandContext, CommandResult, and
[]models.ProjectCommandContext as arguments and runs PolicyCheckStep runner

* Refactor RunCommentCommand
* Remove BuildPolicyCheckCommand and rename StepCmdExec back to TerraformExec
* Add policy step runner logic and conftest interfaces.
* Add show step runner to policy check stage.

* Adding models.PolicyCheckCommand to buildCtx

This also means buildPlanAllCommands call buildCtx twice once with
models.PlanCommand and once with models.PolicyCheckCommand

* Adding new project_command_builder that supports policy_check

* Refactoring PolicyCheck specific logic into a PolicyCheckProjectCommandBuilder

* Moving events.CommandContext to models.CommandContext this will allow me
to remove buildCtx method and move ProjectCommandContext creation into
models package

* Policy Owners might be different types, for that reason we are
refactoring Owners into its own struct with specific keys defining
different owner types

Co-authored-by: Nish Krishnan <nishk@lyft.com>
Co-authored-by: Nish Krishnan <nishkrishnan@users.noreply.github.com>
2021-02-10 18:13:44 -08:00

221 lines
6.4 KiB
Go

package events
import (
"path/filepath"
"regexp"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-config-inspect/tfconfig"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/yaml/valid"
)
func NewProjectCommandContextBulder(policyCheckEnabled bool, commentBuilder CommentBuilder) ProjectCommandContextBuilder {
projectCommandContextBuilder := &DefaultProjectCommandContextBuilder{
CommentBuilder: commentBuilder,
}
if policyCheckEnabled {
return &PolicyCheckProjectCommandContextBuilder{
CommentBuilder: commentBuilder,
ProjectCommandContextBuilder: projectCommandContextBuilder,
}
}
return projectCommandContextBuilder
}
type ProjectCommandContextBuilder interface {
// BuildProjectContext builds project command contexts for atlantis commands
BuildProjectContext(
ctx *CommandContext,
cmdName models.CommandName,
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelPlan, parallelApply, verbose bool,
) []models.ProjectCommandContext
}
type DefaultProjectCommandContextBuilder struct {
CommentBuilder CommentBuilder
}
func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
ctx *CommandContext,
cmdName models.CommandName,
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose bool,
) (projectCmds []models.ProjectCommandContext) {
ctx.Log.Debug("Building project command context for %s", cmdName)
var steps []valid.Step
switch cmdName {
case models.PlanCommand:
steps = prjCfg.Workflow.Plan.Steps
case models.ApplyCommand:
steps = prjCfg.Workflow.Apply.Steps
}
// If TerraformVersion not defined in config file look for a
// terraform.require_version block.
if prjCfg.TerraformVersion == nil {
prjCfg.TerraformVersion = getTfVersion(ctx, filepath.Join(repoDir, prjCfg.RepoRelDir))
}
projectCmds = append(projectCmds, newProjectCommandContext(
ctx,
cmdName,
cb.CommentBuilder.BuildApplyComment(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,
))
return
}
type PolicyCheckProjectCommandContextBuilder struct {
ProjectCommandContextBuilder *DefaultProjectCommandContextBuilder
CommentBuilder CommentBuilder
}
func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
ctx *CommandContext,
cmdName models.CommandName,
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose bool,
) (projectCmds []models.ProjectCommandContext) {
ctx.Log.Debug("PolicyChecks are enabled")
projectCmds = cb.ProjectCommandContextBuilder.BuildProjectContext(
ctx,
cmdName,
prjCfg,
commentFlags,
repoDir,
automerge,
parallelApply,
parallelPlan,
verbose,
)
if cmdName == models.PlanCommand {
ctx.Log.Debug("Building project command context for %s", models.PolicyCheckCommand)
steps := prjCfg.Workflow.PolicyCheck.Steps
projectCmds = append(projectCmds, newProjectCommandContext(
ctx,
models.PolicyCheckCommand,
cb.CommentBuilder.BuildApplyComment(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,
))
}
return
}
// newProjectCommandContext is a initializer method that handles constructing the
// ProjectCommandContext.
func newProjectCommandContext(ctx *CommandContext,
cmd models.CommandName,
applyCmd string,
planCmd string,
projCfg valid.MergedProjectCfg,
steps []valid.Step,
policySets valid.PolicySets,
escapedCommentArgs []string,
automergeEnabled bool,
parallelApplyEnabled bool,
parallelPlanEnabled bool,
verbose bool,
) models.ProjectCommandContext {
return models.ProjectCommandContext{
CommandName: cmd,
ApplyCmd: applyCmd,
BaseRepo: ctx.Pull.BaseRepo,
EscapedCommentArgs: escapedCommentArgs,
AutomergeEnabled: automergeEnabled,
ParallelApplyEnabled: parallelApplyEnabled,
ParallelPlanEnabled: parallelPlanEnabled,
AutoplanEnabled: projCfg.AutoplanEnabled,
Steps: steps,
HeadRepo: ctx.HeadRepo,
Log: ctx.Log,
PullMergeable: ctx.PullMergeable,
Pull: ctx.Pull,
ProjectName: projCfg.Name,
ApplyRequirements: projCfg.ApplyRequirements,
RePlanCmd: planCmd,
RepoRelDir: projCfg.RepoRelDir,
RepoConfigVersion: projCfg.RepoCfgVersion,
TerraformVersion: projCfg.TerraformVersion,
User: ctx.User,
Verbose: verbose,
Workspace: projCfg.Workspace,
PolicySets: policySets,
}
}
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
}
// Extracts required_version from Terraform configuration.
// Returns nil if unable to determine version from configuration.
func getTfVersion(ctx *CommandContext, absProjDir string) *version.Version {
module, diags := tfconfig.LoadModule(absProjDir)
if diags.HasErrors() {
ctx.Log.Err("trying to detect required version: %s", diags.Error())
return nil
}
if len(module.RequiredCore) != 1 {
ctx.Log.Info("cannot determine which version to use from terraform configuration, detected %d possibilities.", len(module.RequiredCore))
return nil
}
requiredVersionSetting := module.RequiredCore[0]
// We allow `= x.y.z`, `=x.y.z` or `x.y.z` where `x`, `y` and `z` are integers.
re := regexp.MustCompile(`^=?\s*([^\s]+)\s*$`)
matched := re.FindStringSubmatch(requiredVersionSetting)
if len(matched) == 0 {
ctx.Log.Debug("did not specify exact version in terraform configuration, found %q", requiredVersionSetting)
return nil
}
ctx.Log.Debug("found required_version setting of %q", requiredVersionSetting)
version, err := version.NewVersion(matched[1])
if err != nil {
ctx.Log.Debug(err.Error())
return nil
}
ctx.Log.Info("detected module requires version: %q", version.String())
return version
}