mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-30 00:48:01 +00:00
Problem When `projects` are specified and `when_modified` is not specified via the config file, changing `.terraform.lock.hcl` file won't trigger auto plan. This is because the default `raw.WhenModified` does not include `.terraform.lock.hcl`. Note that when projects are auto detected, changing `.terraform.lock.hcl` triggers auto plan. This is because the `cmd.DefaultAutoplanFileList` includes `.terraform.lock.hcl`. Solution Include `.terraform.lock.hcl` to the default `raw.WhenModified`.
567 lines
14 KiB
Go
567 lines
14 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 (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/runatlantis/atlantis/server/core/config/raw"
|
|
"github.com/runatlantis/atlantis/server/core/config/valid"
|
|
"github.com/runatlantis/atlantis/server/events"
|
|
"github.com/runatlantis/atlantis/server/logging"
|
|
. "github.com/runatlantis/atlantis/testing"
|
|
)
|
|
|
|
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
|
|
nestedModules1 = t.TempDir()
|
|
err := os.MkdirAll(filepath.Join(nestedModules1, "project1/modules"), 0700)
|
|
Ok(t, err)
|
|
files := []string{
|
|
"non-tf",
|
|
".tflint.hcl",
|
|
"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 = t.TempDir()
|
|
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
|
|
// global-env-config.auto.tfvars.json
|
|
envDir = t.TempDir()
|
|
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 TestDetermineWorkspaceFromHCL(t *testing.T) {
|
|
noopLogger := logging.NewNoopLogger(t)
|
|
cases := []struct {
|
|
description string
|
|
repoDir string
|
|
expectedWorkspace string
|
|
}{
|
|
{
|
|
"Should use configured Terraform Cloud workspace",
|
|
"workspace-configured",
|
|
"test-workspace",
|
|
},
|
|
{
|
|
"If no 'cloud' block was configured, it should use 'default' workspace",
|
|
"no-cloud-block",
|
|
"default",
|
|
},
|
|
{
|
|
"If 'cloud' was specify but without `name` attribute, it should use 'default' workspace",
|
|
"cloud-block-without-workspace-name",
|
|
"default",
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
fullPath := filepath.Join("testdata/test-repos", c.repoDir)
|
|
got, err := m.DetermineWorkspaceFromHCL(noopLogger, fullPath)
|
|
if err != nil {
|
|
t.Error("got error:", err)
|
|
break
|
|
}
|
|
if got != c.expectedWorkspace {
|
|
t.Fatalf("Expected %s but got %s", c.expectedWorkspace, got)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestDetermineProjects(t *testing.T) {
|
|
noopLogger := logging.NewNoopLogger(t)
|
|
setupTmpRepos(t)
|
|
|
|
defaultAutoplanFileList := "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl"
|
|
|
|
cases := []struct {
|
|
description string
|
|
files []string
|
|
expProjectPaths []string
|
|
repoDir string
|
|
autoplanFileList string
|
|
}{
|
|
{
|
|
"If no files were modified then should return an empty list",
|
|
nil,
|
|
nil,
|
|
nestedModules1,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should ignore non .tf files and return an empty list",
|
|
[]string{"non-tf", "non.tf.suffix"},
|
|
nil,
|
|
nestedModules1,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should ignore .tflint.hcl files and return an empty list",
|
|
[]string{".tflint.hcl", "project1/.tflint.hcl"},
|
|
nil,
|
|
nestedModules1,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should plan in the parent directory from modules if that dir has a main.tf",
|
|
[]string{"project1/modules/main.tf"},
|
|
[]string{"project1"},
|
|
nestedModules1,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should plan in the parent directory from modules if that dir has a main.tf",
|
|
[]string{"modules/main.tf"},
|
|
[]string{"."},
|
|
nestedModules2,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"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,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should not plan in the parent directory from modules if that dir does not have a main.tf",
|
|
[]string{"modules/main.tf"},
|
|
[]string{},
|
|
topLevelModules,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"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,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should ignore tfstate files and return an empty list",
|
|
[]string{"terraform.tfstate", "terraform.tfstate.backup", "parent/terraform.tfstate", "parent/terraform.tfstate.backup"},
|
|
nil,
|
|
nestedModules1,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should return '.' when changed file is at root",
|
|
[]string{"a.tf"},
|
|
[]string{"."},
|
|
nestedModules2,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should return directory when changed file is in a dir",
|
|
[]string{"project1/a.tf"},
|
|
[]string{"project1"},
|
|
nestedModules1,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should return parent dir when changed file is in an env/ dir",
|
|
[]string{"env/staging.tfvars"},
|
|
[]string{"."},
|
|
envDir,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should de-duplicate when multiple files changed in the same dir",
|
|
[]string{"env/staging.tfvars", "main.tf", "other.tf"},
|
|
[]string{"."},
|
|
"",
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should ignore changes in a dir that was deleted",
|
|
[]string{"wasdeleted/main.tf"},
|
|
[]string{},
|
|
"",
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should not ignore terragrunt.hcl files",
|
|
[]string{"terragrunt.hcl"},
|
|
[]string{"."},
|
|
nestedModules2,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should find terragrunt.hcl file inside a nested directory",
|
|
[]string{"project1/terragrunt.hcl"},
|
|
[]string{"project1"},
|
|
nestedModules1,
|
|
defaultAutoplanFileList,
|
|
},
|
|
{
|
|
"Should find packer files and ignore default tf files",
|
|
[]string{"project1/image.pkr.hcl", "project2/main.tf"},
|
|
[]string{"project1"},
|
|
topLevelModules,
|
|
"**/*.pkr.hcl",
|
|
},
|
|
{
|
|
"Should find yaml files in addition to defaults",
|
|
[]string{"project1/ansible.yml", "project2/main.tf"},
|
|
[]string{"project1", "project2"},
|
|
topLevelModules,
|
|
"**/*.tf,**/*.yml",
|
|
},
|
|
{
|
|
"Should find yaml files unless excluded",
|
|
[]string{"project1/ansible.yml", "project2/config.yml"},
|
|
[]string{"project1"},
|
|
topLevelModules,
|
|
"**/*.yml,!project2/*.yml",
|
|
},
|
|
{
|
|
"Should not ignore .terraform.lock.hcl files",
|
|
[]string{"project1/.terraform.lock.hcl"},
|
|
[]string{"project1"},
|
|
nestedModules1,
|
|
defaultAutoplanFileList,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.description, func(t *testing.T) {
|
|
projects := m.DetermineProjects(noopLogger, c.files, modifiedRepo, c.repoDir, c.autoplanFileList, nil)
|
|
|
|
// 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
|
|
// terraform.tfvars.json
|
|
// project2/
|
|
// main.tf
|
|
// terraform.tfvars
|
|
// modules/
|
|
// module/
|
|
// main.tf
|
|
tmpDir := DirStructure(t, map[string]interface{}{
|
|
"main.tf": nil,
|
|
"project1": map[string]interface{}{
|
|
"main.tf": nil,
|
|
"terraform.tfvars.json": nil,
|
|
},
|
|
"project2": map[string]interface{}{
|
|
"main.tf": nil,
|
|
"terraform.tfvars": nil,
|
|
},
|
|
"modules": map[string]interface{}{
|
|
"module": map[string]interface{}{
|
|
"main.tf": nil,
|
|
},
|
|
},
|
|
})
|
|
|
|
cases := []struct {
|
|
description string
|
|
config valid.RepoCfg
|
|
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.RepoCfg{
|
|
Projects: []valid.Project{
|
|
{
|
|
Dir: ".",
|
|
Autoplan: valid.Autoplan{
|
|
Enabled: false,
|
|
WhenModified: []string{"**/*.tf"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
modified: []string{"main.tf"},
|
|
expProjPaths: []string{"."},
|
|
},
|
|
{
|
|
description: "autoplan default",
|
|
config: valid.RepoCfg{
|
|
Projects: []valid.Project{
|
|
{
|
|
Dir: ".",
|
|
Autoplan: valid.Autoplan{
|
|
Enabled: true,
|
|
WhenModified: []string{"**/*.tf"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
modified: []string{"main.tf"},
|
|
expProjPaths: []string{"."},
|
|
},
|
|
{
|
|
description: "parent dir modified",
|
|
config: valid.RepoCfg{
|
|
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.RepoCfg{
|
|
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.RepoCfg{
|
|
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.RepoCfg{
|
|
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.RepoCfg{
|
|
Projects: []valid.Project{
|
|
{
|
|
Dir: "project2",
|
|
Autoplan: valid.Autoplan{
|
|
Enabled: true,
|
|
WhenModified: []string{"*.tf*"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
modified: []string{"project2/terraform.tfvars"},
|
|
expProjPaths: []string{"project2"},
|
|
},
|
|
{
|
|
description: ".terraform.lock.hcl file modified",
|
|
config: valid.RepoCfg{
|
|
Projects: []valid.Project{
|
|
{
|
|
Dir: "project2",
|
|
Autoplan: valid.Autoplan{
|
|
Enabled: true,
|
|
WhenModified: raw.DefaultAutoPlanWhenModified,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
modified: []string{"project2/.terraform.lock.hcl"},
|
|
expProjPaths: []string{"project2"},
|
|
},
|
|
{
|
|
description: "file excluded",
|
|
config: valid.RepoCfg{
|
|
Projects: []valid.Project{
|
|
{
|
|
Dir: "project1",
|
|
Autoplan: valid.Autoplan{
|
|
Enabled: true,
|
|
WhenModified: []string{"*.tf", "!exclude-me.tf"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
modified: []string{"project1/exclude-me.tf"},
|
|
expProjPaths: nil,
|
|
},
|
|
{
|
|
description: "some files excluded and others included",
|
|
config: valid.RepoCfg{
|
|
Projects: []valid.Project{
|
|
{
|
|
Dir: "project1",
|
|
Autoplan: valid.Autoplan{
|
|
Enabled: true,
|
|
WhenModified: []string{"*.tf", "!exclude-me.tf"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
modified: []string{"project1/exclude-me.tf", "project1/include-me.tf"},
|
|
expProjPaths: []string{"project1"},
|
|
},
|
|
{
|
|
description: "multiple dirs excluded",
|
|
config: valid.RepoCfg{
|
|
Projects: []valid.Project{
|
|
{
|
|
Dir: "project1",
|
|
Autoplan: valid.Autoplan{
|
|
Enabled: true,
|
|
WhenModified: []string{"**/*.tf", "!subdir1/*", "!subdir2/*"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
modified: []string{"project1/subdir1/main.tf", "project1/subdir2/main.tf"},
|
|
expProjPaths: nil,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.description, func(t *testing.T) {
|
|
pf := events.DefaultProjectFinder{}
|
|
projects, err := pf.DetermineProjectsViaConfig(logging.NewNoopLogger(t), c.modified, c.config, tmpDir, nil)
|
|
Ok(t, err)
|
|
Equals(t, len(c.expProjPaths), len(projects))
|
|
for i, proj := range projects {
|
|
Equals(t, c.expProjPaths[i], proj.Dir)
|
|
}
|
|
})
|
|
}
|
|
}
|