Support regexp in command builder on the project name (#1419)

This commit is contained in:
laurent apollis
2021-03-06 01:13:23 +01:00
committed by GitHub
parent 471646b688
commit d2ef2775c8
10 changed files with 292 additions and 33 deletions

View File

@@ -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"
@@ -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,

View File

@@ -101,6 +101,7 @@ var testFlags = map[string]interface{}{
WriteGitCredsFlag: true,
DisableAutoplanFlag: true,
EnablePolicyChecksFlag: false,
EnableRegExpCmdFlag: false,
}
func TestExecute_Defaults(t *testing.T) {

View File

@@ -233,6 +233,17 @@ Values are chosen in this order:
```
Enables atlantis to run server side policies on the result of a terraform plan. Policies are defined in [server side repo config](https://www.runatlantis.io/docs/server-side-repo-config.html#reference).
* ### `--enable-regexp-cmd`
```bash
atlantis server --enable-regexp-cmd
```
Enable Atlantis to use regular expressions on plan/apply commands when \"-p\" flag is passed with it.
::: warning SECURITY WARNING
It's not supposed to be used with `--disable-apply-all`.
The command `atlantis apply -p .*` will bypass the restriction and run apply on every projects
:::
* ### `--gh-hostname`
```bash
atlantis server --gh-hostname="my.github.enterprise.com"

View File

@@ -38,6 +38,7 @@ func NewProjectCommandBuilder(
pendingPlanFinder *DefaultPendingPlanFinder,
commentBuilder CommentBuilder,
skipCloneNoChanges bool,
EnableRegExpCmd bool,
) *DefaultProjectCommandBuilder {
projectCommandBuilder := &DefaultProjectCommandBuilder{
ParserValidator: parserValidator,
@@ -48,6 +49,7 @@ func NewProjectCommandBuilder(
GlobalCfg: globalCfg,
PendingPlanFinder: pendingPlanFinder,
SkipCloneNoChanges: skipCloneNoChanges,
EnableRegExpCmd: EnableRegExpCmd,
ProjectCommandContextBuilder: NewProjectCommandContextBulder(
policyChecksSupported,
commentBuilder,
@@ -101,6 +103,7 @@ type DefaultProjectCommandBuilder struct {
PendingPlanFinder *DefaultPendingPlanFinder
ProjectCommandContextBuilder ProjectCommandContextBuilder
SkipCloneNoChanges bool
EnableRegExpCmd bool
}
// See ProjectCommandBuilder.BuildAutoplanCommands.
@@ -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)
@@ -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
}
@@ -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
}
@@ -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,
@@ -429,27 +438,12 @@ 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 {
// 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)
} else {
projCfg = p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), repoRelDir, workspace)
}
if err := p.validateWorkspaceAllowed(repoCfgPtr, repoRelDir, workspace); err != nil {
return []models.ProjectCommandContext{}, err
}
automerge := DefaultAutomergeEnabled
parallelApply := DefaultParallelApplyEnabled
parallelPlan := DefaultParallelPlanEnabled
@@ -459,17 +453,51 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex
parallelPlan = repoCfgPtr.ParallelPlan
}
return p.ProjectCommandContextBuilder.BuildProjectContext(
ctx,
cmd,
projCfg,
commentFlags,
repoDir,
automerge,
parallelApply,
parallelPlan,
verbose,
), nil
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
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
}
return projCtxs, nil
}
// validateWorkspaceAllowed returns an error if repoCfg defines projects in

View File

@@ -588,6 +588,7 @@ projects:
&DefaultPendingPlanFinder{},
&CommentParser{},
false,
false,
)
// We run a test for each type of command.
@@ -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,
@@ -790,6 +976,7 @@ workflows:
&DefaultPendingPlanFinder{},
&CommentParser{},
false,
false,
)
cmd := models.PolicyCheckCommand

View File

@@ -145,6 +145,7 @@ projects:
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
false,
)
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
@@ -370,6 +371,7 @@ projects:
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
true,
)
var actCtxs []models.ProjectCommandContext
@@ -506,6 +508,7 @@ projects:
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
false,
)
ctxs, err := builder.BuildPlanCommands(
@@ -580,6 +583,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
false,
)
ctxs, err := builder.BuildApplyCommands(
@@ -649,6 +653,7 @@ projects:
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
false,
)
ctx := &events.CommandContext{
@@ -713,6 +718,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
false,
)
var actCtxs []models.ProjectCommandContext
@@ -879,6 +885,7 @@ projects:
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
false,
)
actCtxs, err := builder.BuildPlanCommands(
@@ -929,6 +936,7 @@ projects:
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
true,
false,
)
var actCtxs []models.ProjectCommandContext
@@ -969,6 +977,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
false,
)
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{

View File

@@ -4,6 +4,7 @@ package valid
import (
"fmt"
"regexp"
"strings"
version "github.com/hashicorp/go-version"
@@ -52,6 +53,20 @@ func (r RepoCfg) FindProjectByName(name string) *Project {
return nil
}
// FindProjectsByName returns all projects that match with name.
func (r RepoCfg) FindProjectsByName(name string) []Project {
var ps []Project
sanitizedName := "^" + name + "$"
for _, p := range r.Projects {
if p.Name != nil {
if match, _ := regexp.MatchString(sanitizedName, *p.Name); match {
ps = append(ps, p)
}
}
}
return ps
}
// validateWorkspaceAllowed returns an error if repoCfg defines projects in
// repoRelDir but none of them use workspace. We want this to be an error
// because if users have gone to the trouble of defining projects in repoRelDir

View File

@@ -640,6 +640,7 @@ func setupE2E(t *testing.T, repoDir string, policyChecksEnabled bool) (server.Ev
&events.DefaultPendingPlanFinder{},
commentParser,
false,
false,
)
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFVersion)

View File

@@ -412,6 +412,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
pendingPlanFinder,
commentParser,
userConfig.SkipCloneNoChanges,
userConfig.EnableRegExpCmd,
)
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfVersion)

View File

@@ -28,6 +28,7 @@ type UserConfig struct {
DisableMarkdownFolding bool `mapstructure:"disable-markdown-folding"`
DisableRepoLocking bool `mapstructure:"disable-repo-locking"`
EnablePolicyChecksFlag bool `mapstructure:"enable-policy-checks"`
EnableRegExpCmd bool `mapstructure:"enable-regexp-cmd"`
GithubHostname string `mapstructure:"gh-hostname"`
GithubToken string `mapstructure:"gh-token"`
GithubUser string `mapstructure:"gh-user"`