Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support regexp in command builder on the project name #1419

Merged
merged 4 commits into from
Mar 6, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const (
DisableMarkdownFoldingFlag = "disable-markdown-folding"
DisableRepoLockingFlag = "disable-repo-locking"
EnablePolicyChecksFlag = "enable-policy-checks"
EnableRegExpCmdFlag = "enable-regexp-cmd"
GHHostnameFlag = "gh-hostname"
GHTokenFlag = "gh-token"
GHUserFlag = "gh-user"
Expand Down Expand Up @@ -300,6 +301,10 @@ var boolFlags = map[string]boolFlag{
description: "Enable atlantis to run user defined policy checks. This is explicitly disabled for TFE/TFC backends since plan files are inaccessible.",
defaultValue: false,
},
EnableRegExpCmdFlag: {
description: "Enable Atlantis to use regular expressions on plan/apply commands when \"-p\" flag is passed with it.",
defaultValue: false,
},
AllowDraftPRs: {
description: "Enable autoplan for Github Draft Pull Requests",
defaultValue: false,
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ var testFlags = map[string]interface{}{
WriteGitCredsFlag: true,
DisableAutoplanFlag: true,
EnablePolicyChecksFlag: false,
EnableRegExpCmdFlag: false,
}

func TestExecute_Defaults(t *testing.T) {
Expand Down
84 changes: 56 additions & 28 deletions server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func NewProjectCommandBuilder(
pendingPlanFinder *DefaultPendingPlanFinder,
commentBuilder CommentBuilder,
skipCloneNoChanges bool,
EnableRegExpCmd bool,
) *DefaultProjectCommandBuilder {
projectCommandBuilder := &DefaultProjectCommandBuilder{
ParserValidator: parserValidator,
Expand All @@ -48,6 +49,7 @@ func NewProjectCommandBuilder(
GlobalCfg: globalCfg,
PendingPlanFinder: pendingPlanFinder,
SkipCloneNoChanges: skipCloneNoChanges,
EnableRegExpCmd: EnableRegExpCmd,
ProjectCommandContextBuilder: NewProjectCommandContextBulder(
policyChecksSupported,
commentBuilder,
Expand Down Expand Up @@ -101,6 +103,7 @@ type DefaultProjectCommandBuilder struct {
PendingPlanFinder *DefaultPendingPlanFinder
ProjectCommandContextBuilder ProjectCommandContextBuilder
SkipCloneNoChanges bool
EnableRegExpCmd bool
}

// See ProjectCommandBuilder.BuildAutoplanCommands.
Expand Down Expand Up @@ -303,7 +306,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectPlanCommand(ctx *CommandConte

// getCfg returns the atlantis.yaml config (if it exists) for this project. If
// there is no config, then projectCfg and repoCfg will be nil.
func (p *DefaultProjectCommandBuilder) getCfg(ctx *CommandContext, projectName string, dir string, workspace string, repoDir string) (projectCfg *valid.Project, repoCfg *valid.RepoCfg, err error) {
func (p *DefaultProjectCommandBuilder) getCfg(ctx *CommandContext, projectName string, dir string, workspace string, repoDir string) (projectsCfg []valid.Project, repoCfg *valid.RepoCfg, err error) {
hasConfigFile, err := p.ParserValidator.HasRepoCfg(repoDir)
if err != nil {
err = errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir)
Expand All @@ -327,8 +330,14 @@ func (p *DefaultProjectCommandBuilder) getCfg(ctx *CommandContext, projectName s
// If they've specified a project by name we look it up. Otherwise we
// use the dir and workspace.
if projectName != "" {
projectCfg = repoCfg.FindProjectByName(projectName)
if projectCfg == nil {
if p.EnableRegExpCmd {
projectsCfg = repoCfg.FindProjectsByName(projectName)
} else {
if p := repoCfg.FindProjectByName(projectName); p != nil {
projectsCfg = append(projectsCfg, *p)
}
}
if len(projectsCfg) == 0 {
err = fmt.Errorf("no project with name %q is defined in %s", projectName, yaml.AtlantisYAMLFilename)
return
}
Expand All @@ -343,7 +352,7 @@ func (p *DefaultProjectCommandBuilder) getCfg(ctx *CommandContext, projectName s
err = fmt.Errorf("must specify project name: more than one project defined in %s matched dir: %q workspace: %q", yaml.AtlantisYAMLFilename, dir, workspace)
return
}
projectCfg = &projCfgs[0]
projectsCfg = projCfgs
return
}

Expand Down Expand Up @@ -418,7 +427,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectApplyCommand(ctx *CommandCont
)
}

// buildProjectCommandCtx builds a context for a single project identified
// buildProjectCommandCtx builds a context for a single or several projects identified
// by the parameters.
func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContext,
cmd models.CommandName,
Expand All @@ -429,47 +438,66 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex
workspace string,
verbose bool) ([]models.ProjectCommandContext, error) {

projCfgPtr, repoCfgPtr, err := p.getCfg(ctx, projectName, repoRelDir, workspace, repoDir)
matchingProjects, repoCfgPtr, err := p.getCfg(ctx, projectName, repoRelDir, workspace, repoDir)
if err != nil {
return []models.ProjectCommandContext{}, err
}

var projCtxs []models.ProjectCommandContext
var projCfg valid.MergedProjectCfg
if projCfgPtr != nil {
automerge := DefaultAutomergeEnabled
parallelApply := DefaultParallelApplyEnabled
parallelPlan := DefaultParallelPlanEnabled
if repoCfgPtr != nil {
automerge = repoCfgPtr.Automerge
parallelApply = repoCfgPtr.ParallelApply
parallelPlan = repoCfgPtr.ParallelPlan
}

if len(matchingProjects) > 0 {
// Override any dir/workspace defined on the comment with what was
// defined in config. This shouldn't matter since we don't allow comments
// with both project name and dir/workspace.
repoRelDir = projCfg.RepoRelDir
workspace = projCfg.Workspace
projCfg = p.GlobalCfg.MergeProjectCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), *projCfgPtr, *repoCfgPtr)
for _, mp := range matchingProjects {
ctx.Log.Debug("Merging config for project at dir: %q workspace: %q", mp.Dir, mp.Workspace)
projCfg = p.GlobalCfg.MergeProjectCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), mp, *repoCfgPtr)

projCtxs = append(projCtxs,
p.ProjectCommandContextBuilder.BuildProjectContext(
ctx,
cmd,
projCfg,
commentFlags,
repoDir,
automerge,
parallelApply,
parallelPlan,
verbose,
)...)
}
} else {
projCfg = p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), repoRelDir, workspace)
projCtxs = append(projCtxs,
p.ProjectCommandContextBuilder.BuildProjectContext(
ctx,
cmd,
projCfg,
commentFlags,
repoDir,
automerge,
parallelApply,
parallelPlan,
verbose,
)...)
}

if err := p.validateWorkspaceAllowed(repoCfgPtr, repoRelDir, workspace); err != nil {
return []models.ProjectCommandContext{}, err
}

automerge := DefaultAutomergeEnabled
parallelApply := DefaultParallelApplyEnabled
parallelPlan := DefaultParallelPlanEnabled
if repoCfgPtr != nil {
automerge = repoCfgPtr.Automerge
parallelApply = repoCfgPtr.ParallelApply
parallelPlan = repoCfgPtr.ParallelPlan
}
return projCtxs, nil

return p.ProjectCommandContextBuilder.BuildProjectContext(
ctx,
cmd,
projCfg,
commentFlags,
repoDir,
automerge,
parallelApply,
parallelPlan,
verbose,
), nil
}

// validateWorkspaceAllowed returns an error if repoCfg defines projects in
Expand Down
187 changes: 187 additions & 0 deletions server/events/project_command_builder_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,7 @@ projects:
&DefaultPendingPlanFinder{},
&CommentParser{},
false,
false,
)

// We run a test for each type of command.
Expand Down Expand Up @@ -640,6 +641,191 @@ projects:
}
}

func TestBuildProjectCmdCtx_WithRegExpCmdEnabled(t *testing.T) {
emptyPolicySets := valid.PolicySets{
Version: nil,
PolicySets: []valid.PolicySet{},
}
baseRepo := models.Repo{
FullName: "owner/repo",
VCSHost: models.VCSHost{
Hostname: "github.com",
},
}
pull := models.PullRequest{
BaseRepo: baseRepo,
}
cases := map[string]struct {
globalCfg string
repoCfg string
expErr string
expCtx models.ProjectCommandContext
expPlanSteps []string
expApplySteps []string
}{

// Test that if we've set global defaults, that they are used but the
// allowed project config values also come through.
"global defaults with repo cfg": {
globalCfg: `
repos:
- id: /.*/
workflow: default
workflows:
default:
plan:
steps:
- init
- plan
apply:
steps:
- apply`,
repoCfg: `
version: 3
automerge: true
projects:
- name: myproject_1
dir: project1
workspace: myworkspace
autoplan:
enabled: true
when_modified: [../modules/**/*.tf]
terraform_version: v10.0
- name: myproject_2
dir: project2
workspace: myworkspace
autoplan:
enabled: true
when_modified: [../modules/**/*.tf]
terraform_version: v10.0
- name: myproject_3
dir: project3
workspace: myworkspace
autoplan:
enabled: true
when_modified: [../modules/**/*.tf]
terraform_version: v10.0
`,
expCtx: models.ProjectCommandContext{
ApplyCmd: "atlantis apply -p myproject_1",
BaseRepo: baseRepo,
EscapedCommentArgs: []string{`\f\l\a\g`},
AutomergeEnabled: true,
AutoplanEnabled: true,
HeadRepo: models.Repo{},
Log: nil,
PullMergeable: true,
Pull: pull,
ProjectName: "myproject_1",
ApplyRequirements: []string{},
RepoConfigVersion: 3,
RePlanCmd: "atlantis plan -p myproject_1 -- flag",
RepoRelDir: "project1",
TerraformVersion: mustVersion("10.0"),
User: models.User{},
Verbose: true,
Workspace: "myworkspace",
PolicySets: emptyPolicySets,
},
expPlanSteps: []string{"init", "plan"},
expApplySteps: []string{"apply"},
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {
tmp, cleanup := DirStructure(t, map[string]interface{}{
"project1": map[string]interface{}{
"main.tf": nil,
},
"modules": map[string]interface{}{
"module": map[string]interface{}{
"main.tf": nil,
},
},
})
defer cleanup()

workingDir := NewMockWorkingDir()
When(workingDir.Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmp, false, nil)
vcsClient := vcsmocks.NewMockClient()
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"modules/module/main.tf"}, nil)

// Write and parse the global config file.
globalCfgPath := filepath.Join(tmp, "global.yaml")
Ok(t, ioutil.WriteFile(globalCfgPath, []byte(c.globalCfg), 0600))
parser := &yaml.ParserValidator{}
globalCfg, err := parser.ParseGlobalCfg(globalCfgPath, valid.NewGlobalCfg(false, false, false))
Ok(t, err)

if c.repoCfg != "" {
Ok(t, ioutil.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600))
}

builder := NewProjectCommandBuilder(
false,
parser,
&DefaultProjectFinder{},
vcsClient,
workingDir,
NewDefaultWorkingDirLocker(),
globalCfg,
&DefaultPendingPlanFinder{},
&CommentParser{},
false,
true,
)

// We run a test for each type of command, again specific projects
for _, cmd := range []models.CommandName{models.PlanCommand, models.ApplyCommand} {
t.Run(cmd.String(), func(t *testing.T) {
ctxs, err := builder.buildProjectCommandCtx(&CommandContext{
Pull: models.PullRequest{
BaseRepo: baseRepo,
},
PullMergeable: true,
}, cmd, "myproject_[1-2]", []string{"flag"}, tmp, "project1", "myworkspace", true)

if c.expErr != "" {
ErrEquals(t, c.expErr, err)
return
}
ctx := ctxs[0]

Ok(t, err)

Equals(t, 2, len(ctxs))
// Construct expected steps.
var stepNames []string
switch cmd {
case models.PlanCommand:
stepNames = c.expPlanSteps
case models.ApplyCommand:
stepNames = c.expApplySteps
}
var expSteps []valid.Step
for _, stepName := range stepNames {
expSteps = append(expSteps, valid.Step{
StepName: stepName,
})
}

c.expCtx.CommandName = cmd
// Init fields we couldn't in our cases map.
c.expCtx.Steps = expSteps
ctx.PolicySets = emptyPolicySets
Equals(t, c.expCtx, ctx)
// Equals() doesn't compare TF version properly so have to
// use .String().
if c.expCtx.TerraformVersion != nil {
Equals(t, c.expCtx.TerraformVersion.String(), ctx.TerraformVersion.String())
}
})
}
})
}
}

func TestBuildProjectCmdCtx_WithPolicCheckEnabled(t *testing.T) {
emptyPolicySets := valid.PolicySets{
Version: nil,
Expand Down Expand Up @@ -790,6 +976,7 @@ workflows:
&DefaultPendingPlanFinder{},
&CommentParser{},
false,
false,
)

cmd := models.PolicyCheckCommand
Expand Down
Loading