mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 12:48:18 +00:00
* 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>
987 lines
28 KiB
Go
987 lines
28 KiB
Go
package events_test
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
. "github.com/petergtz/pegomock"
|
|
"github.com/runatlantis/atlantis/server/events"
|
|
"github.com/runatlantis/atlantis/server/events/matchers"
|
|
"github.com/runatlantis/atlantis/server/events/mocks"
|
|
"github.com/runatlantis/atlantis/server/events/models"
|
|
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
|
|
"github.com/runatlantis/atlantis/server/events/yaml"
|
|
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
|
"github.com/runatlantis/atlantis/server/logging"
|
|
. "github.com/runatlantis/atlantis/testing"
|
|
)
|
|
|
|
func TestDefaultProjectCommandBuilder_BuildAutoplanCommands(t *testing.T) {
|
|
// expCtxFields define the ctx fields we're going to assert on.
|
|
// Since we're focused on autoplanning here, we don't validate all the
|
|
// fields so the tests are more obvious and targeted.
|
|
type expCtxFields struct {
|
|
ProjectName string
|
|
RepoRelDir string
|
|
Workspace string
|
|
}
|
|
cases := []struct {
|
|
Description string
|
|
AtlantisYAML string
|
|
ServerSideYAML string
|
|
exp []expCtxFields
|
|
}{
|
|
{
|
|
Description: "simple atlantis.yaml",
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- dir: .
|
|
`,
|
|
exp: []expCtxFields{
|
|
{
|
|
ProjectName: "",
|
|
RepoRelDir: ".",
|
|
Workspace: "default",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Description: "some projects disabled",
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- dir: .
|
|
autoplan:
|
|
enabled: false
|
|
- dir: .
|
|
workspace: myworkspace
|
|
autoplan:
|
|
when_modified: ["main.tf"]
|
|
- dir: .
|
|
name: myname
|
|
workspace: myworkspace2
|
|
`,
|
|
exp: []expCtxFields{
|
|
{
|
|
ProjectName: "",
|
|
RepoRelDir: ".",
|
|
Workspace: "myworkspace",
|
|
},
|
|
{
|
|
ProjectName: "myname",
|
|
RepoRelDir: ".",
|
|
Workspace: "myworkspace2",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Description: "some projects disabled",
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- dir: .
|
|
autoplan:
|
|
enabled: false
|
|
- dir: .
|
|
workspace: myworkspace
|
|
autoplan:
|
|
when_modified: ["main.tf"]
|
|
- dir: .
|
|
workspace: myworkspace2
|
|
`,
|
|
exp: []expCtxFields{
|
|
{
|
|
ProjectName: "",
|
|
RepoRelDir: ".",
|
|
Workspace: "myworkspace",
|
|
},
|
|
{
|
|
ProjectName: "",
|
|
RepoRelDir: ".",
|
|
Workspace: "myworkspace2",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Description: "no projects modified",
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- dir: mydir
|
|
`,
|
|
exp: nil,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.Description, func(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
|
|
"main.tf": nil,
|
|
})
|
|
defer cleanup()
|
|
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
When(workingDir.Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, false, nil)
|
|
vcsClient := vcsmocks.NewMockClient()
|
|
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"main.tf"}, nil)
|
|
if c.AtlantisYAML != "" {
|
|
err := ioutil.WriteFile(filepath.Join(tmpDir, yaml.AtlantisYAMLFilename), []byte(c.AtlantisYAML), 0600)
|
|
Ok(t, err)
|
|
}
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
false,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
vcsClient,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
valid.NewGlobalCfg(false, false, false),
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
false,
|
|
)
|
|
|
|
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
|
|
PullMergeable: true,
|
|
})
|
|
Ok(t, err)
|
|
Equals(t, len(c.exp), len(ctxs))
|
|
for i, actCtx := range ctxs {
|
|
expCtx := c.exp[i]
|
|
Equals(t, expCtx.ProjectName, actCtx.ProjectName)
|
|
Equals(t, expCtx.RepoRelDir, actCtx.RepoRelDir)
|
|
Equals(t, expCtx.Workspace, actCtx.Workspace)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test building a plan and apply command for one project.
|
|
func TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand(t *testing.T) {
|
|
cases := []struct {
|
|
Description string
|
|
AtlantisYAML string
|
|
Cmd events.CommentCommand
|
|
ExpCommentArgs []string
|
|
ExpWorkspace string
|
|
ExpDir string
|
|
ExpProjectName string
|
|
ExpErr string
|
|
ExpApplyReqs []string
|
|
}{
|
|
{
|
|
Description: "no atlantis.yaml",
|
|
Cmd: events.CommentCommand{
|
|
RepoRelDir: ".",
|
|
Flags: []string{"commentarg"},
|
|
Name: models.PlanCommand,
|
|
Workspace: "myworkspace",
|
|
},
|
|
AtlantisYAML: "",
|
|
ExpCommentArgs: []string{`\c\o\m\m\e\n\t\a\r\g`},
|
|
ExpWorkspace: "myworkspace",
|
|
ExpDir: ".",
|
|
ExpApplyReqs: []string{},
|
|
},
|
|
{
|
|
Description: "no atlantis.yaml with project flag",
|
|
Cmd: events.CommentCommand{
|
|
RepoRelDir: ".",
|
|
Name: models.PlanCommand,
|
|
ProjectName: "myproject",
|
|
},
|
|
AtlantisYAML: "",
|
|
ExpErr: "cannot specify a project name unless an atlantis.yaml file exists to configure projects",
|
|
},
|
|
{
|
|
Description: "simple atlantis.yaml",
|
|
Cmd: events.CommentCommand{
|
|
RepoRelDir: ".",
|
|
Name: models.PlanCommand,
|
|
Workspace: "myworkspace",
|
|
},
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- dir: .
|
|
workspace: myworkspace
|
|
apply_requirements: [approved]`,
|
|
ExpApplyReqs: []string{"approved"},
|
|
ExpWorkspace: "myworkspace",
|
|
ExpDir: ".",
|
|
},
|
|
{
|
|
Description: "atlantis.yaml wrong dir",
|
|
Cmd: events.CommentCommand{
|
|
RepoRelDir: ".",
|
|
Name: models.PlanCommand,
|
|
Workspace: "myworkspace",
|
|
},
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- dir: notroot
|
|
workspace: myworkspace
|
|
apply_requirements: [approved]`,
|
|
ExpWorkspace: "myworkspace",
|
|
ExpDir: ".",
|
|
ExpApplyReqs: []string{},
|
|
},
|
|
{
|
|
Description: "atlantis.yaml wrong workspace",
|
|
Cmd: events.CommentCommand{
|
|
RepoRelDir: ".",
|
|
Name: models.PlanCommand,
|
|
Workspace: "myworkspace",
|
|
},
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- dir: .
|
|
workspace: notmyworkspace
|
|
apply_requirements: [approved]`,
|
|
ExpErr: "running commands in workspace \"myworkspace\" is not allowed because this directory is only configured for the following workspaces: notmyworkspace",
|
|
},
|
|
{
|
|
Description: "atlantis.yaml with projectname",
|
|
Cmd: events.CommentCommand{
|
|
Name: models.PlanCommand,
|
|
ProjectName: "myproject",
|
|
},
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- name: myproject
|
|
dir: .
|
|
workspace: myworkspace
|
|
apply_requirements: [approved]`,
|
|
ExpApplyReqs: []string{"approved"},
|
|
ExpProjectName: "myproject",
|
|
ExpWorkspace: "myworkspace",
|
|
ExpDir: ".",
|
|
},
|
|
{
|
|
Description: "atlantis.yaml with mergeable apply requirement",
|
|
Cmd: events.CommentCommand{
|
|
Name: models.PlanCommand,
|
|
ProjectName: "myproject",
|
|
},
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- name: myproject
|
|
dir: .
|
|
workspace: myworkspace
|
|
apply_requirements: [mergeable]`,
|
|
ExpApplyReqs: []string{"mergeable"},
|
|
ExpProjectName: "myproject",
|
|
ExpWorkspace: "myworkspace",
|
|
ExpDir: ".",
|
|
},
|
|
{
|
|
Description: "atlantis.yaml with mergeable and approved apply requirements",
|
|
Cmd: events.CommentCommand{
|
|
Name: models.PlanCommand,
|
|
ProjectName: "myproject",
|
|
},
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- name: myproject
|
|
dir: .
|
|
workspace: myworkspace
|
|
apply_requirements: [mergeable, approved]`,
|
|
ExpApplyReqs: []string{"mergeable", "approved"},
|
|
ExpProjectName: "myproject",
|
|
ExpWorkspace: "myworkspace",
|
|
ExpDir: ".",
|
|
},
|
|
{
|
|
Description: "atlantis.yaml with multiple dir/workspaces matching",
|
|
Cmd: events.CommentCommand{
|
|
Name: models.PlanCommand,
|
|
RepoRelDir: ".",
|
|
Workspace: "myworkspace",
|
|
},
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- name: myproject
|
|
dir: .
|
|
workspace: myworkspace
|
|
apply_requirements: [approved]
|
|
- name: myproject2
|
|
dir: .
|
|
workspace: myworkspace
|
|
`,
|
|
ExpErr: "must specify project name: more than one project defined in atlantis.yaml matched dir: \".\" workspace: \"myworkspace\"",
|
|
},
|
|
{
|
|
Description: "atlantis.yaml with project flag not matching",
|
|
Cmd: events.CommentCommand{
|
|
Name: models.PlanCommand,
|
|
RepoRelDir: ".",
|
|
Workspace: "default",
|
|
ProjectName: "notconfigured",
|
|
},
|
|
AtlantisYAML: `
|
|
version: 3
|
|
projects:
|
|
- dir: .
|
|
`,
|
|
ExpErr: "no project with name \"notconfigured\" is defined in atlantis.yaml",
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
// NOTE: we're testing both plan and apply here.
|
|
for _, cmdName := range []models.CommandName{models.PlanCommand, models.ApplyCommand} {
|
|
t.Run(c.Description+"_"+cmdName.String(), func(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
|
|
"main.tf": nil,
|
|
})
|
|
defer cleanup()
|
|
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
When(workingDir.Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, false, nil)
|
|
When(workingDir.GetWorkingDir(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, nil)
|
|
vcsClient := vcsmocks.NewMockClient()
|
|
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"main.tf"}, nil)
|
|
if c.AtlantisYAML != "" {
|
|
err := ioutil.WriteFile(filepath.Join(tmpDir, yaml.AtlantisYAMLFilename), []byte(c.AtlantisYAML), 0600)
|
|
Ok(t, err)
|
|
}
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
false,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
vcsClient,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
valid.NewGlobalCfg(true, false, false),
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
false,
|
|
)
|
|
|
|
var actCtxs []models.ProjectCommandContext
|
|
var err error
|
|
if cmdName == models.PlanCommand {
|
|
actCtxs, err = builder.BuildPlanCommands(&events.CommandContext{}, &c.Cmd)
|
|
} else {
|
|
actCtxs, err = builder.BuildApplyCommands(&events.CommandContext{}, &c.Cmd)
|
|
}
|
|
|
|
if c.ExpErr != "" {
|
|
ErrEquals(t, c.ExpErr, err)
|
|
return
|
|
}
|
|
Ok(t, err)
|
|
Equals(t, 1, len(actCtxs))
|
|
actCtx := actCtxs[0]
|
|
Equals(t, c.ExpDir, actCtx.RepoRelDir)
|
|
Equals(t, c.ExpWorkspace, actCtx.Workspace)
|
|
Equals(t, c.ExpCommentArgs, actCtx.EscapedCommentArgs)
|
|
Equals(t, c.ExpProjectName, actCtx.ProjectName)
|
|
Equals(t, c.ExpApplyReqs, actCtx.ApplyRequirements)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaultProjectCommandBuilder_BuildPlanCommands(t *testing.T) {
|
|
// expCtxFields define the ctx fields we're going to assert on.
|
|
// Since we're focused on autoplanning here, we don't validate all the
|
|
// fields so the tests are more obvious and targeted.
|
|
type expCtxFields struct {
|
|
ProjectName string
|
|
RepoRelDir string
|
|
Workspace string
|
|
}
|
|
cases := map[string]struct {
|
|
DirStructure map[string]interface{}
|
|
AtlantisYAML string
|
|
ModifiedFiles []string
|
|
Exp []expCtxFields
|
|
}{
|
|
"no atlantis.yaml": {
|
|
DirStructure: map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
"project2": map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
},
|
|
ModifiedFiles: []string{"project1/main.tf", "project2/main.tf"},
|
|
Exp: []expCtxFields{
|
|
{
|
|
ProjectName: "",
|
|
RepoRelDir: "project1",
|
|
Workspace: "default",
|
|
},
|
|
{
|
|
ProjectName: "",
|
|
RepoRelDir: "project2",
|
|
Workspace: "default",
|
|
},
|
|
},
|
|
},
|
|
"no modified files": {
|
|
DirStructure: map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
ModifiedFiles: []string{},
|
|
Exp: []expCtxFields{},
|
|
},
|
|
"follow when_modified config": {
|
|
DirStructure: map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
"project2": map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
"project3": map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
},
|
|
AtlantisYAML: `version: 3
|
|
projects:
|
|
- dir: project1 # project1 uses the defaults
|
|
- dir: project2 # project2 has autoplan disabled but should use default when_modified
|
|
autoplan:
|
|
enabled: false
|
|
- dir: project3 # project3 has an empty when_modified
|
|
autoplan:
|
|
enabled: false
|
|
when_modified: []`,
|
|
ModifiedFiles: []string{"project1/main.tf", "project2/main.tf", "project3/main.tf"},
|
|
Exp: []expCtxFields{
|
|
{
|
|
ProjectName: "",
|
|
RepoRelDir: "project1",
|
|
Workspace: "default",
|
|
},
|
|
{
|
|
ProjectName: "",
|
|
RepoRelDir: "project2",
|
|
Workspace: "default",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for name, c := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
tmpDir, cleanup := DirStructure(t, c.DirStructure)
|
|
defer cleanup()
|
|
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
When(workingDir.Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, false, nil)
|
|
When(workingDir.GetWorkingDir(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, nil)
|
|
vcsClient := vcsmocks.NewMockClient()
|
|
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn(c.ModifiedFiles, nil)
|
|
if c.AtlantisYAML != "" {
|
|
err := ioutil.WriteFile(filepath.Join(tmpDir, yaml.AtlantisYAMLFilename), []byte(c.AtlantisYAML), 0600)
|
|
Ok(t, err)
|
|
}
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
false,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
vcsClient,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
valid.NewGlobalCfg(true, false, false),
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
false,
|
|
)
|
|
|
|
ctxs, err := builder.BuildPlanCommands(
|
|
&events.CommandContext{},
|
|
&events.CommentCommand{
|
|
RepoRelDir: "",
|
|
Flags: nil,
|
|
Name: models.PlanCommand,
|
|
Verbose: false,
|
|
Workspace: "",
|
|
ProjectName: "",
|
|
})
|
|
Ok(t, err)
|
|
Equals(t, len(c.Exp), len(ctxs))
|
|
for i, actCtx := range ctxs {
|
|
expCtx := c.Exp[i]
|
|
Equals(t, expCtx.ProjectName, actCtx.ProjectName)
|
|
Equals(t, expCtx.RepoRelDir, actCtx.RepoRelDir)
|
|
Equals(t, expCtx.Workspace, actCtx.Workspace)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test building apply command for multiple projects when the comment
|
|
// isn't for a specific project, i.e. atlantis apply.
|
|
// In this case we should apply all outstanding plans.
|
|
func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
|
|
"workspace1": map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": nil,
|
|
"workspace.tfplan": nil,
|
|
},
|
|
"project2": map[string]interface{}{
|
|
"main.tf": nil,
|
|
"workspace.tfplan": nil,
|
|
},
|
|
},
|
|
"workspace2": map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": nil,
|
|
"workspace.tfplan": nil,
|
|
},
|
|
"project2": map[string]interface{}{
|
|
"main.tf": nil,
|
|
"workspace.tfplan": nil,
|
|
},
|
|
},
|
|
})
|
|
defer cleanup()
|
|
// Initialize git repos in each workspace so that the .tfplan files get
|
|
// picked up.
|
|
runCmd(t, filepath.Join(tmpDir, "workspace1"), "git", "init")
|
|
runCmd(t, filepath.Join(tmpDir, "workspace2"), "git", "init")
|
|
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
When(workingDir.GetPullDir(
|
|
matchers.AnyModelsRepo(),
|
|
matchers.AnyModelsPullRequest())).
|
|
ThenReturn(tmpDir, nil)
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
false,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
nil,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
valid.NewGlobalCfg(false, false, false),
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
false,
|
|
)
|
|
|
|
ctxs, err := builder.BuildApplyCommands(
|
|
&events.CommandContext{},
|
|
&events.CommentCommand{
|
|
RepoRelDir: "",
|
|
Flags: nil,
|
|
Name: models.ApplyCommand,
|
|
Verbose: false,
|
|
Workspace: "",
|
|
ProjectName: "",
|
|
})
|
|
Ok(t, err)
|
|
Equals(t, 4, len(ctxs))
|
|
Equals(t, "project1", ctxs[0].RepoRelDir)
|
|
Equals(t, "workspace1", ctxs[0].Workspace)
|
|
Equals(t, "project2", ctxs[1].RepoRelDir)
|
|
Equals(t, "workspace1", ctxs[1].Workspace)
|
|
Equals(t, "project1", ctxs[2].RepoRelDir)
|
|
Equals(t, "workspace2", ctxs[2].Workspace)
|
|
Equals(t, "project2", ctxs[3].RepoRelDir)
|
|
Equals(t, "workspace2", ctxs[3].Workspace)
|
|
}
|
|
|
|
// Test that if a directory has a list of workspaces configured then we don't
|
|
// allow plans for other workspace names.
|
|
func TestDefaultProjectCommandBuilder_WrongWorkspaceName(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
|
|
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
|
|
"pulldir": map[string]interface{}{
|
|
"notconfigured": map[string]interface{}{},
|
|
},
|
|
})
|
|
defer cleanup()
|
|
repoDir := filepath.Join(tmpDir, "pulldir/notconfigured")
|
|
|
|
yamlCfg := `version: 3
|
|
projects:
|
|
- dir: .
|
|
workspace: default
|
|
- dir: .
|
|
workspace: staging
|
|
`
|
|
err := ioutil.WriteFile(filepath.Join(repoDir, yaml.AtlantisYAMLFilename), []byte(yamlCfg), 0600)
|
|
Ok(t, err)
|
|
|
|
When(workingDir.Clone(
|
|
matchers.AnyPtrToLoggingSimpleLogger(),
|
|
matchers.AnyModelsRepo(),
|
|
matchers.AnyModelsPullRequest(),
|
|
AnyString())).ThenReturn(repoDir, false, nil)
|
|
When(workingDir.GetWorkingDir(
|
|
matchers.AnyModelsRepo(),
|
|
matchers.AnyModelsPullRequest(),
|
|
AnyString())).ThenReturn(repoDir, nil)
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
false,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
nil,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
valid.NewGlobalCfg(true, false, false),
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
false,
|
|
)
|
|
|
|
ctx := &events.CommandContext{
|
|
HeadRepo: models.Repo{},
|
|
Pull: models.PullRequest{},
|
|
User: models.User{},
|
|
Log: logging.NewNoopLogger(),
|
|
}
|
|
_, err = builder.BuildPlanCommands(ctx, &events.CommentCommand{
|
|
RepoRelDir: ".",
|
|
Flags: nil,
|
|
Name: models.PlanCommand,
|
|
Verbose: false,
|
|
Workspace: "notconfigured",
|
|
ProjectName: "",
|
|
})
|
|
ErrEquals(t, "running commands in workspace \"notconfigured\" is not allowed because this directory is only configured for the following workspaces: default, staging", err)
|
|
}
|
|
|
|
// Test that extra comment args are escaped.
|
|
func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
|
|
cases := []struct {
|
|
ExtraArgs []string
|
|
ExpEscapedArgs []string
|
|
}{
|
|
{
|
|
ExtraArgs: []string{"arg1", "arg2"},
|
|
ExpEscapedArgs: []string{`\a\r\g\1`, `\a\r\g\2`},
|
|
},
|
|
{
|
|
ExtraArgs: []string{"-var=$(touch bad)"},
|
|
ExpEscapedArgs: []string{`\-\v\a\r\=\$\(\t\o\u\c\h\ \b\a\d\)`},
|
|
},
|
|
{
|
|
ExtraArgs: []string{"-- ;echo bad"},
|
|
ExpEscapedArgs: []string{`\-\-\ \;\e\c\h\o\ \b\a\d`},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(strings.Join(c.ExtraArgs, " "), func(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
|
|
"main.tf": nil,
|
|
})
|
|
defer cleanup()
|
|
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
When(workingDir.Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, false, nil)
|
|
When(workingDir.GetWorkingDir(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, nil)
|
|
vcsClient := vcsmocks.NewMockClient()
|
|
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"main.tf"}, nil)
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
false,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
vcsClient,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
valid.NewGlobalCfg(true, false, false),
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
false,
|
|
)
|
|
|
|
var actCtxs []models.ProjectCommandContext
|
|
var err error
|
|
actCtxs, err = builder.BuildPlanCommands(&events.CommandContext{}, &events.CommentCommand{
|
|
RepoRelDir: ".",
|
|
Flags: c.ExtraArgs,
|
|
Name: models.PlanCommand,
|
|
Verbose: false,
|
|
Workspace: "default",
|
|
})
|
|
Ok(t, err)
|
|
Equals(t, 1, len(actCtxs))
|
|
actCtx := actCtxs[0]
|
|
Equals(t, c.ExpEscapedArgs, actCtx.EscapedCommentArgs)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test that terraform version is used when specified in terraform configuration
|
|
func TestDefaultProjectCommandBuilder_TerraformVersion(t *testing.T) {
|
|
// For the following tests:
|
|
// If terraform configuration is used, result should be `0.12.8`.
|
|
// If project configuration is used, result should be `0.12.6`.
|
|
// If default is to be used, result should be `nil`.
|
|
baseVersionConfig := `
|
|
terraform {
|
|
required_version = "%s0.12.8"
|
|
}
|
|
`
|
|
|
|
atlantisYamlContent := `
|
|
version: 3
|
|
projects:
|
|
- dir: project1 # project1 uses the defaults
|
|
terraform_version: v0.12.6
|
|
`
|
|
|
|
exactSymbols := []string{"", "="}
|
|
nonExactSymbols := []string{">", ">=", "<", "<=", "~="}
|
|
|
|
type testCase struct {
|
|
DirStructure map[string]interface{}
|
|
AtlantisYAML string
|
|
ModifiedFiles []string
|
|
Exp map[string][]int
|
|
}
|
|
|
|
testCases := make(map[string]testCase)
|
|
|
|
for _, exactSymbol := range exactSymbols {
|
|
testCases[fmt.Sprintf("exact version in terraform config using \"%s\"", exactSymbol)] = testCase{
|
|
DirStructure: map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": fmt.Sprintf(baseVersionConfig, exactSymbol),
|
|
},
|
|
},
|
|
ModifiedFiles: []string{"project1/main.tf"},
|
|
Exp: map[string][]int{
|
|
"project1": {0, 12, 8},
|
|
},
|
|
}
|
|
}
|
|
|
|
for _, nonExactSymbol := range nonExactSymbols {
|
|
testCases[fmt.Sprintf("non-exact version in terraform config using \"%s\"", nonExactSymbol)] = testCase{
|
|
DirStructure: map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": fmt.Sprintf(baseVersionConfig, nonExactSymbol),
|
|
},
|
|
},
|
|
ModifiedFiles: []string{"project1/main.tf"},
|
|
Exp: map[string][]int{
|
|
"project1": nil,
|
|
},
|
|
}
|
|
}
|
|
|
|
// atlantis.yaml should take precedence over terraform config
|
|
testCases["with project config and terraform config"] = testCase{
|
|
DirStructure: map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": fmt.Sprintf(baseVersionConfig, exactSymbols[0]),
|
|
},
|
|
yaml.AtlantisYAMLFilename: atlantisYamlContent,
|
|
},
|
|
ModifiedFiles: []string{"project1/main.tf", "project2/main.tf"},
|
|
Exp: map[string][]int{
|
|
"project1": {0, 12, 6},
|
|
},
|
|
}
|
|
|
|
testCases["with project config only"] = testCase{
|
|
DirStructure: map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
yaml.AtlantisYAMLFilename: atlantisYamlContent,
|
|
},
|
|
ModifiedFiles: []string{"project1/main.tf"},
|
|
Exp: map[string][]int{
|
|
"project1": {0, 12, 6},
|
|
},
|
|
}
|
|
|
|
testCases["neither project config or terraform config"] = testCase{
|
|
DirStructure: map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
},
|
|
ModifiedFiles: []string{"project1/main.tf", "project2/main.tf"},
|
|
Exp: map[string][]int{
|
|
"project1": nil,
|
|
},
|
|
}
|
|
|
|
testCases["project with different terraform config"] = testCase{
|
|
DirStructure: map[string]interface{}{
|
|
"project1": map[string]interface{}{
|
|
"main.tf": fmt.Sprintf(baseVersionConfig, exactSymbols[0]),
|
|
},
|
|
"project2": map[string]interface{}{
|
|
"main.tf": strings.Replace(fmt.Sprintf(baseVersionConfig, exactSymbols[0]), "0.12.8", "0.12.9", -1),
|
|
},
|
|
},
|
|
ModifiedFiles: []string{"project1/main.tf", "project2/main.tf"},
|
|
Exp: map[string][]int{
|
|
"project1": {0, 12, 8},
|
|
"project2": {0, 12, 9},
|
|
},
|
|
}
|
|
|
|
for name, testCase := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
|
|
tmpDir, cleanup := DirStructure(t, testCase.DirStructure)
|
|
|
|
defer cleanup()
|
|
vcsClient := vcsmocks.NewMockClient()
|
|
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn(testCase.ModifiedFiles, nil)
|
|
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
When(workingDir.Clone(
|
|
matchers.AnyPtrToLoggingSimpleLogger(),
|
|
matchers.AnyModelsRepo(),
|
|
matchers.AnyModelsPullRequest(),
|
|
AnyString())).ThenReturn(tmpDir, false, nil)
|
|
|
|
When(workingDir.GetWorkingDir(
|
|
matchers.AnyModelsRepo(),
|
|
matchers.AnyModelsPullRequest(),
|
|
AnyString())).ThenReturn(tmpDir, nil)
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
false,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
vcsClient,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
valid.NewGlobalCfg(true, false, false),
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
false,
|
|
)
|
|
|
|
actCtxs, err := builder.BuildPlanCommands(
|
|
&events.CommandContext{},
|
|
&events.CommentCommand{
|
|
RepoRelDir: "",
|
|
Flags: nil,
|
|
Name: models.PlanCommand,
|
|
Verbose: false,
|
|
})
|
|
|
|
Ok(t, err)
|
|
Equals(t, len(testCase.Exp), len(actCtxs))
|
|
for _, actCtx := range actCtxs {
|
|
if testCase.Exp[actCtx.RepoRelDir] != nil {
|
|
Assert(t, actCtx.TerraformVersion != nil, "TerraformVersion is nil.")
|
|
Equals(t, testCase.Exp[actCtx.RepoRelDir], actCtx.TerraformVersion.Segments())
|
|
} else {
|
|
Assert(t, actCtx.TerraformVersion == nil, "TerraformVersion is supposed to be nil.")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test that we don't clone the repo if there were no changes based on the atlantis.yaml file.
|
|
func TestDefaultProjectCommandBuilder_SkipCloneNoChanges(t *testing.T) {
|
|
atlantisYAML := `
|
|
version: 3
|
|
projects:
|
|
- dir: dir1`
|
|
|
|
RegisterMockTestingT(t)
|
|
vcsClient := vcsmocks.NewMockClient()
|
|
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"main.tf"}, nil)
|
|
When(vcsClient.SupportsSingleFileDownload(matchers.AnyModelsRepo())).ThenReturn(true)
|
|
When(vcsClient.DownloadRepoConfigFile(matchers.AnyModelsPullRequest())).ThenReturn(true, []byte(atlantisYAML), nil)
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
false,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
vcsClient,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
valid.NewGlobalCfg(true, false, false),
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
true,
|
|
)
|
|
|
|
var actCtxs []models.ProjectCommandContext
|
|
var err error
|
|
actCtxs, err = builder.BuildAutoplanCommands(&events.CommandContext{
|
|
HeadRepo: models.Repo{},
|
|
Pull: models.PullRequest{},
|
|
User: models.User{},
|
|
Log: nil,
|
|
PullMergeable: true,
|
|
})
|
|
Ok(t, err)
|
|
Equals(t, 0, len(actCtxs))
|
|
workingDir.VerifyWasCalled(Never()).Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())
|
|
}
|
|
|
|
func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanCommand(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
|
|
"main.tf": nil,
|
|
})
|
|
defer cleanup()
|
|
|
|
workingDir := mocks.NewMockWorkingDir()
|
|
When(workingDir.Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmpDir, false, nil)
|
|
vcsClient := vcsmocks.NewMockClient()
|
|
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"main.tf"}, nil)
|
|
globalCfg := valid.NewGlobalCfg(false, false, false)
|
|
|
|
builder := events.NewProjectCommandBuilder(
|
|
true,
|
|
&yaml.ParserValidator{},
|
|
&events.DefaultProjectFinder{},
|
|
vcsClient,
|
|
workingDir,
|
|
events.NewDefaultWorkingDirLocker(),
|
|
globalCfg,
|
|
&events.DefaultPendingPlanFinder{},
|
|
&events.CommentParser{},
|
|
false,
|
|
)
|
|
|
|
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
|
|
PullMergeable: true,
|
|
})
|
|
|
|
Ok(t, err)
|
|
Equals(t, 2, len(ctxs))
|
|
planCtx := ctxs[0]
|
|
policyCheckCtx := ctxs[1]
|
|
Equals(t, models.PlanCommand, planCtx.CommandName)
|
|
Equals(t, globalCfg.Workflows["default"].Plan.Steps, planCtx.Steps)
|
|
Equals(t, models.PolicyCheckCommand, policyCheckCtx.CommandName)
|
|
Equals(t, globalCfg.Workflows["default"].PolicyCheck.Steps, policyCheckCtx.Steps)
|
|
}
|