Files
atlantis/server/events/project_finder_test.go
2018-08-30 16:47:17 -06:00

404 lines
9.8 KiB
Go

// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package events_test
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/yaml/valid"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
)
var noopLogger = logging.NewNoopLogger()
var modifiedRepo = "owner/repo"
var m = events.DefaultProjectFinder{}
var nestedModules1 string
var nestedModules2 string
var topLevelModules string
var envDir string
func setupTmpRepos(t *testing.T) {
// Create different repo structures for testing.
// 1. Nested modules directory inside a project
// non-tf
// terraform.tfstate
// terraform.tfstate.backup
// project1/
// main.tf
// terraform.tfstate
// terraform.tfstate.backup
// modules/
// main.tf
var err error
nestedModules1, err = ioutil.TempDir("", "")
Ok(t, err)
err = os.MkdirAll(filepath.Join(nestedModules1, "project1/modules"), 0700)
Ok(t, err)
files := []string{
"non-tf",
"terraform.tfstate.backup",
"project1/main.tf",
"project1/terraform.tfstate",
"project1/terraform.tfstate.backup",
"project1/modules/main.tf",
}
for _, f := range files {
_, err = os.Create(filepath.Join(nestedModules1, f))
Ok(t, err)
}
// 2. Nested modules dir inside top-level project
// main.tf
// modules/
// main.tf
// We can just re-use part of the previous dir structure.
nestedModules2 = filepath.Join(nestedModules1, "project1")
// 3. Top-level modules
// modules/
// main.tf
// project1/
// main.tf
// project2/
// main.tf
topLevelModules, err = ioutil.TempDir("", "")
Ok(t, err)
for _, path := range []string{"modules", "project1", "project2"} {
err = os.MkdirAll(filepath.Join(topLevelModules, path), 0700)
Ok(t, err)
_, err = os.Create(filepath.Join(topLevelModules, path, "main.tf"))
Ok(t, err)
}
// 4. Env/ dir
// main.tf
// env/
// staging.tfvars
// production.tfvars
envDir, err = ioutil.TempDir("", "")
Ok(t, err)
err = os.MkdirAll(filepath.Join(envDir, "env"), 0700)
Ok(t, err)
_, err = os.Create(filepath.Join(envDir, "env/staging.tfvars"))
Ok(t, err)
_, err = os.Create(filepath.Join(envDir, "env/production.tfvars"))
Ok(t, err)
}
func TestDetermineProjects(t *testing.T) {
setupTmpRepos(t)
cases := []struct {
description string
files []string
expProjectPaths []string
repoDir string
}{
{
"If no files were modified then should return an empty list",
nil,
nil,
nestedModules1,
},
{
"Should ignore non .tf files and return an empty list",
[]string{"non-tf"},
nil,
nestedModules1,
},
{
"Should plan in the parent directory from modules if that dir has a main.tf",
[]string{"project1/modules/main.tf"},
[]string{"project1"},
nestedModules1,
},
{
"Should plan in the parent directory from modules if that dir has a main.tf",
[]string{"modules/main.tf"},
[]string{"."},
nestedModules2,
},
{
"Should plan in the parent directory from modules when module is in a subdir if that dir has a main.tf",
[]string{"modules/subdir/main.tf"},
[]string{"."},
nestedModules2,
},
{
"Should not plan in the parent directory from modules if that dir does not have a main.tf",
[]string{"modules/main.tf"},
[]string{},
topLevelModules,
},
{
"Should not plan in the parent directory from modules if that dir does not have a main.tf",
[]string{"modules/main.tf", "project1/main.tf"},
[]string{"project1"},
topLevelModules,
},
{
"Should ignore tfstate files and return an empty list",
[]string{"terraform.tfstate", "terraform.tfstate.backup", "parent/terraform.tfstate", "parent/terraform.tfstate.backup"},
nil,
nestedModules1,
},
{
"Should return '.' when changed file is at root",
[]string{"a.tf"},
[]string{"."},
nestedModules2,
},
{
"Should return directory when changed file is in a dir",
[]string{"project1/a.tf"},
[]string{"project1"},
nestedModules1,
},
{
"Should return parent dir when changed file is in an env/ dir",
[]string{"env/staging.tfvars"},
[]string{"."},
envDir,
},
{
"Should de-duplicate when multiple files changed in the same dir",
[]string{"env/staging.tfvars", "main.tf", "other.tf"},
[]string{"."},
"",
},
{
"Should ignore changes in a dir that was deleted",
[]string{"wasdeleted/main.tf"},
[]string{},
"",
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
projects := m.DetermineProjects(noopLogger, c.files, modifiedRepo, c.repoDir)
// Extract the paths from the projects. We use a slice here instead of a
// map so we can test whether there are duplicates returned.
var paths []string
for _, project := range projects {
paths = append(paths, project.Path)
// Check that the project object has the repo set properly.
Equals(t, modifiedRepo, project.RepoFullName)
}
Assert(t, len(c.expProjectPaths) == len(paths),
"exp %q but found %q", c.expProjectPaths, paths)
for _, expPath := range c.expProjectPaths {
found := false
for _, actPath := range paths {
if expPath == actPath {
found = true
break
}
}
if !found {
t.Fatalf("exp %q but was not in paths %v", expPath, paths)
}
}
})
}
}
func TestDefaultProjectFinder_DetermineProjectsViaConfig(t *testing.T) {
// Create dir structure:
// main.tf
// project1/
// main.tf
// project2/
// main.tf
// terraform.tfvars
// modules/
// module/
// main.tf
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
"main.tf": nil,
"project1": map[string]interface{}{
"main.tf": nil,
},
"project2": map[string]interface{}{
"main.tf": nil,
"terraform.tfvars": nil,
},
"modules": map[string]interface{}{
"module": map[string]interface{}{
"main.tf": nil,
},
},
})
defer cleanup()
cases := []struct {
description string
config valid.Config
modified []string
expProjPaths []string
}{
{
// When autoplan is disabled, we still return the modified project.
// If our caller is interested in autoplan enabled projects, they'll
// need to filter the results.
description: "autoplan disabled",
config: valid.Config{
Projects: []valid.Project{
{
Dir: ".",
Autoplan: valid.Autoplan{
Enabled: false,
WhenModified: []string{"**/*.tf"},
},
},
},
},
modified: []string{"main.tf"},
expProjPaths: []string{"."},
},
{
description: "autoplan default",
config: valid.Config{
Projects: []valid.Project{
{
Dir: ".",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"**/*.tf"},
},
},
},
},
modified: []string{"main.tf"},
expProjPaths: []string{"."},
},
{
description: "parent dir modified",
config: valid.Config{
Projects: []valid.Project{
{
Dir: "project",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"**/*.tf"},
},
},
},
},
modified: []string{"main.tf"},
expProjPaths: nil,
},
{
description: "parent dir modified matches",
config: valid.Config{
Projects: []valid.Project{
{
Dir: "project1",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"../**/*.tf"},
},
},
},
},
modified: []string{"main.tf"},
expProjPaths: []string{"project1"},
},
{
description: "dir deleted",
config: valid.Config{
Projects: []valid.Project{
{
Dir: "project3",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"*.tf"},
},
},
},
},
modified: []string{"project3/main.tf"},
expProjPaths: nil,
},
{
description: "multiple projects",
config: valid.Config{
Projects: []valid.Project{
{
Dir: ".",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"*.tf"},
},
},
{
Dir: "project1",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"../modules/module/*.tf", "**/*.tf"},
},
},
{
Dir: "project2",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"**/*.tf"},
},
},
},
},
modified: []string{"main.tf", "modules/module/another.tf", "project2/nontf.txt"},
expProjPaths: []string{".", "project1"},
},
{
description: ".tfvars file modified",
config: valid.Config{
Projects: []valid.Project{
{
Dir: "project2",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"*.tf*"},
},
},
},
},
modified: []string{"project2/terraform.tfvars"},
expProjPaths: []string{"project2"},
},
}
for _, c := range cases {
if c.description != "autoplan disabled" {
continue
}
t.Run(c.description, func(t *testing.T) {
pf := events.DefaultProjectFinder{}
projects, err := pf.DetermineProjectsViaConfig(logging.NewNoopLogger(), c.modified, c.config, tmpDir)
Ok(t, err)
Equals(t, len(c.expProjPaths), len(projects))
for i, proj := range projects {
Equals(t, c.expProjPaths[i], proj.Dir)
}
})
}
}