Files
atlantis/server/events/project_command_builder_test.go
Nish Krishnan 7ac8106f8a Add structured logger (#54) (#1467)
This is two changes:

Replacing all usages of SimpleLogger with it's interface SimpleLogging which makes it easier to swap out loggers going forward
Nukes SimpleLogger in favor of StructuredLogger which allows us to better analyze logs in our log management system we choose.
2021-04-06 12:02:25 -07:00

1024 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,
},
}
logger := logging.NewNoopLogger(t)
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,
false,
)
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
PullMergeable: true,
Log: logger,
})
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",
},
}
logger := logging.NewNoopLogger(t)
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,
true,
)
var actCtxs []models.ProjectCommandContext
var err error
if cmdName == models.PlanCommand {
actCtxs, err = builder.BuildPlanCommands(&events.CommandContext{
Log: logger,
}, &c.Cmd)
} else {
actCtxs, err = builder.BuildApplyCommands(&events.CommandContext{Log: logger}, &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",
},
},
},
}
logger := logging.NewNoopLogger(t)
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,
false,
)
ctxs, err := builder.BuildPlanCommands(
&events.CommandContext{
Log: logger,
},
&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)
logger := logging.NewNoopLogger(t)
builder := events.NewProjectCommandBuilder(
false,
&yaml.ParserValidator{},
&events.DefaultProjectFinder{},
nil,
workingDir,
events.NewDefaultWorkingDirLocker(),
valid.NewGlobalCfg(false, false, false),
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
false,
false,
)
ctxs, err := builder.BuildApplyCommands(
&events.CommandContext{
Log: logger,
},
&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,
false,
)
ctx := &events.CommandContext{
HeadRepo: models.Repo{},
Pull: models.PullRequest{},
User: models.User{},
Log: logging.NewNoopLogger(t),
}
_, 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`},
},
}
logger := logging.NewNoopLogger(t)
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,
false,
)
var actCtxs []models.ProjectCommandContext
var err error
actCtxs, err = builder.BuildPlanCommands(&events.CommandContext{
Log: logger,
}, &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},
},
}
logger := logging.NewNoopLogger(t)
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,
false,
)
actCtxs, err := builder.BuildPlanCommands(
&events.CommandContext{
Log: logger,
},
&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()
logger := logging.NewNoopLogger(t)
builder := events.NewProjectCommandBuilder(
false,
&yaml.ParserValidator{},
&events.DefaultProjectFinder{},
vcsClient,
workingDir,
events.NewDefaultWorkingDirLocker(),
valid.NewGlobalCfg(true, false, false),
&events.DefaultPendingPlanFinder{},
&events.CommentParser{},
true,
false,
)
var actCtxs []models.ProjectCommandContext
var err error
actCtxs, err = builder.BuildAutoplanCommands(&events.CommandContext{
HeadRepo: models.Repo{},
Pull: models.PullRequest{},
User: models.User{},
Log: logger,
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()
logger := logging.NewNoopLogger(t)
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,
false,
)
ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
PullMergeable: true,
Log: logger,
})
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)
}