mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 23:58:19 +00:00
Added terraform-config-inspect.
This commit is contained in:
committed by
kennethtxytqw
parent
86a56a39ba
commit
4db1ed87f1
@@ -2,10 +2,14 @@ package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/terraform-config-inspect/tfconfig"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
@@ -137,7 +141,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
|
||||
for _, mp := range matchingProjects {
|
||||
ctx.Log.Debug("determining config for project at dir: %q workspace: %q", mp.Dir, mp.Workspace)
|
||||
mergedCfg := p.GlobalCfg.MergeProjectCfg(ctx.Log, ctx.BaseRepo.ID(), mp, repoCfg)
|
||||
projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, mergedCfg, commentFlags, repoCfg.Automerge, verbose))
|
||||
projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, mergedCfg, commentFlags, repoCfg.Automerge, verbose, repoDir))
|
||||
}
|
||||
} else {
|
||||
// If there is no config file, then we'll plan each project that
|
||||
@@ -148,7 +152,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
|
||||
for _, mp := range modifiedProjects {
|
||||
ctx.Log.Debug("determining config for project at dir: %q", mp.Path)
|
||||
pCfg := p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.BaseRepo.ID(), mp.Path, DefaultWorkspace)
|
||||
projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, pCfg, commentFlags, DefaultAutomergeEnabled, verbose))
|
||||
projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, pCfg, commentFlags, DefaultAutomergeEnabled, verbose, repoDir))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +286,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(
|
||||
if repoCfgPtr != nil {
|
||||
automerge = repoCfgPtr.Automerge
|
||||
}
|
||||
return p.buildCtx(ctx, cmd, projCfg, commentFlags, automerge, verbose), nil
|
||||
return p.buildCtx(ctx, cmd, projCfg, commentFlags, automerge, verbose, repoDir), nil
|
||||
}
|
||||
|
||||
// getCfg returns the atlantis.yaml config (if it exists) for this project. If
|
||||
@@ -371,7 +375,8 @@ func (p *DefaultProjectCommandBuilder) buildCtx(ctx *CommandContext,
|
||||
projCfg valid.MergedProjectCfg,
|
||||
commentArgs []string,
|
||||
automergeEnabled bool,
|
||||
verbose bool) models.ProjectCommandContext {
|
||||
verbose bool,
|
||||
absRepoDir string) models.ProjectCommandContext {
|
||||
|
||||
var steps []valid.Step
|
||||
switch cmd {
|
||||
@@ -381,6 +386,14 @@ func (p *DefaultProjectCommandBuilder) buildCtx(ctx *CommandContext,
|
||||
steps = projCfg.Workflow.Apply.Steps
|
||||
}
|
||||
|
||||
// if TerraformVersion not defined in config file fallback to terraform configuration
|
||||
if projCfg.TerraformVersion == nil {
|
||||
version := p.getTfVersion(ctx, filepath.Join(absRepoDir, projCfg.RepoRelDir))
|
||||
if version != nil {
|
||||
projCfg.TerraformVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
return models.ProjectCommandContext{
|
||||
ApplyCmd: p.CommentBuilder.BuildApplyComment(projCfg.RepoRelDir, projCfg.Workspace, projCfg.Name),
|
||||
BaseRepo: ctx.BaseRepo,
|
||||
@@ -415,3 +428,38 @@ func (p *DefaultProjectCommandBuilder) escapeArgs(args []string) []string {
|
||||
}
|
||||
return escaped
|
||||
}
|
||||
|
||||
// Extracts required_version from Terraform configuration.
|
||||
// Returns nil if unable to determine version from configuation, check warning log for clarification.
|
||||
func (p *DefaultProjectCommandBuilder) getTfVersion(ctx *CommandContext, absProjDir string) *version.Version {
|
||||
module, diags := tfconfig.LoadModule(absProjDir)
|
||||
if diags.HasErrors() {
|
||||
ctx.Log.Debug(diags.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(module.RequiredCore) != 1 {
|
||||
ctx.Log.Info("cannot determine which version to use from terraform configuration, detected %d possibilities.", len(module.RequiredCore))
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Log.Info("verifying if \"%q\" is valid exact version.", module.RequiredCore[0])
|
||||
|
||||
// We allow `= x.y.z`, `=x.y.z` or `x.y.z` where `x`, `y` and `z` are integers
|
||||
re := regexp.MustCompile(`^=?\s*([^\s]+)\s*$`)
|
||||
matched := re.FindStringSubmatch(module.RequiredCore[0])
|
||||
if len(matched) == 0 {
|
||||
ctx.Log.Info("did not specify exact version in terraform configuration.")
|
||||
return nil
|
||||
}
|
||||
|
||||
version, err := version.NewVersion(matched[1])
|
||||
|
||||
if err != nil {
|
||||
ctx.Log.Debug(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Log.Debug("detected version: \"%q\".", version)
|
||||
return version
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -716,3 +717,173 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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.AnyModelsRepo(),
|
||||
matchers.AnyModelsPullRequest(),
|
||||
AnyString())).ThenReturn(tmpDir, nil)
|
||||
|
||||
When(workingDir.GetWorkingDir(
|
||||
matchers.AnyModelsRepo(),
|
||||
matchers.AnyModelsPullRequest(),
|
||||
AnyString())).ThenReturn(tmpDir, nil)
|
||||
|
||||
builder := &events.DefaultProjectCommandBuilder{
|
||||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||||
WorkingDir: workingDir,
|
||||
VCSClient: vcsClient,
|
||||
ParserValidator: &yaml.ParserValidator{},
|
||||
ProjectFinder: &events.DefaultProjectFinder{},
|
||||
CommentBuilder: &events.CommentParser{},
|
||||
GlobalCfg: valid.NewGlobalCfg(true, false, 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.")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,16 +22,22 @@ func TempDir(t *testing.T) (string, func()) {
|
||||
// DirStructure creates a directory structure in a temporary directory.
|
||||
// structure describes the dir structure. If the value is another map, then the
|
||||
// key is the name of a directory. If the value is nil, then the key is the name
|
||||
// of a file. It returns the path to the temp directory containing the defined
|
||||
// of a file. If val is a string then key is a file name and val is the file's content.
|
||||
// It returns the path to the temp directory containing the defined
|
||||
// structure and a cleanup function to delete the directory.
|
||||
// Example usage:
|
||||
// versionConfig := `
|
||||
// terraform {
|
||||
// required_version = "= 0.12.8"
|
||||
// }
|
||||
// `
|
||||
// tmpDir, cleanup := DirStructure(t, map[string]interface{}{
|
||||
// "pulldir": map[string]interface{}{
|
||||
// "project1": map[string]interface{}{
|
||||
// "main.tf": nil,
|
||||
// },
|
||||
// "project2": map[string]interface{}{,
|
||||
// "main.tf": nil,
|
||||
// "main.tf": versionConfig,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
@@ -57,6 +63,10 @@ func dirStructureGo(t *testing.T, parentDir string, structure map[string]interfa
|
||||
Ok(t, os.Mkdir(subDir, 0700))
|
||||
// Recurse and create contents.
|
||||
dirStructureGo(t, subDir, dirContents)
|
||||
} else if fileContent, ok := val.(string); ok {
|
||||
// If val is a string then key is a file name and val is the file's content
|
||||
err := ioutil.WriteFile(filepath.Join(parentDir, key), []byte(fileContent), 0600)
|
||||
Ok(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user