mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 19:38:24 +00:00
228 lines
7.2 KiB
Go
228 lines
7.2 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
validation "github.com/go-ozzo/ozzo-validation"
|
|
shlex "github.com/google/shlex"
|
|
|
|
"github.com/runatlantis/atlantis/server/core/config/raw"
|
|
"github.com/runatlantis/atlantis/server/core/config/valid"
|
|
yaml "gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ParserValidator parses and validates server-side repo config files and
|
|
// repo-level atlantis.yaml files.
|
|
type ParserValidator struct{}
|
|
|
|
// HasRepoCfg returns true if there is a repo config (atlantis.yaml) file
|
|
// for the repo at absRepoDir.
|
|
// Returns an error if for some reason it can't read that directory.
|
|
func (p *ParserValidator) HasRepoCfg(absRepoDir, repoConfigFile string) (bool, error) {
|
|
// Checks for a config file with an invalid extension (atlantis.yml)
|
|
const invalidExtensionFilename = "atlantis.yml"
|
|
_, err := os.Stat(p.repoCfgPath(absRepoDir, invalidExtensionFilename))
|
|
if err == nil {
|
|
return false, fmt.Errorf("found %q as config file; rename using the .yaml extension", invalidExtensionFilename)
|
|
}
|
|
|
|
_, err = os.Stat(p.repoCfgPath(absRepoDir, repoConfigFile))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false, nil
|
|
}
|
|
return err == nil, err
|
|
}
|
|
|
|
// ParseRepoCfg returns the parsed and validated atlantis.yaml config for the
|
|
// repo at absRepoDir.
|
|
// If there was no config file, it will return an os.IsNotExist(error).
|
|
func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.GlobalCfg, repoID string, branch string) (valid.RepoCfg, error) {
|
|
repoConfigFile := globalCfg.RepoConfigFile(repoID)
|
|
configFile := p.repoCfgPath(absRepoDir, repoConfigFile)
|
|
configData, err := os.ReadFile(configFile) // nolint: gosec
|
|
|
|
if err != nil {
|
|
return valid.RepoCfg{}, fmt.Errorf("unable to read %s file: %w", repoConfigFile, err)
|
|
}
|
|
return p.ParseRepoCfgData(configData, globalCfg, repoID, branch)
|
|
}
|
|
|
|
func (p *ParserValidator) ParseRepoCfgData(repoCfgData []byte, globalCfg valid.GlobalCfg, repoID string, branch string) (valid.RepoCfg, error) {
|
|
var rawConfig raw.RepoCfg
|
|
|
|
decoder := yaml.NewDecoder(bytes.NewReader(repoCfgData))
|
|
decoder.KnownFields(true)
|
|
|
|
err := decoder.Decode(&rawConfig)
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return valid.RepoCfg{}, err
|
|
}
|
|
|
|
// Set ErrorTag to yaml so it uses the YAML field names in error messages.
|
|
validation.ErrorTag = "yaml"
|
|
if err := rawConfig.Validate(); err != nil {
|
|
return valid.RepoCfg{}, err
|
|
}
|
|
|
|
validConfig := rawConfig.ToValid()
|
|
|
|
// Filter the repo config's projects based on pull request's branch. Only
|
|
// keep projects that either:
|
|
//
|
|
// - Have no branch regex defined at all (i.e. match all branches), or
|
|
// - Those that have branch regex matching the PR's base branch.
|
|
//
|
|
i := 0
|
|
for _, p := range validConfig.Projects {
|
|
if branch == "" || p.BranchRegex == nil || p.BranchRegex.MatchString(branch) {
|
|
validConfig.Projects[i] = p
|
|
i++
|
|
}
|
|
}
|
|
validConfig.Projects = validConfig.Projects[:i]
|
|
|
|
// We do the project name validation after we get the valid config because
|
|
// we need the defaults of dir and workspace to be populated.
|
|
if err := p.validateProjectNames(validConfig); err != nil {
|
|
return valid.RepoCfg{}, err
|
|
}
|
|
if validConfig.Version == 2 {
|
|
// The only difference between v2 and v3 is how we parse custom run
|
|
// commands.
|
|
if err := p.applyLegacyShellParsing(&validConfig); err != nil {
|
|
return validConfig, err
|
|
}
|
|
}
|
|
|
|
err = globalCfg.ValidateRepoCfg(validConfig, repoID)
|
|
return validConfig, err
|
|
}
|
|
|
|
// ParseGlobalCfg returns the parsed and validated global repo config file at
|
|
// configFile. defaultCfg will be merged into the parsed config.
|
|
// If there is no file at configFile it will return an error.
|
|
func (p *ParserValidator) ParseGlobalCfg(configFile string, defaultCfg valid.GlobalCfg) (valid.GlobalCfg, error) {
|
|
configData, err := os.ReadFile(configFile) // nolint: gosec
|
|
if err != nil {
|
|
return valid.GlobalCfg{}, fmt.Errorf("unable to read %s file: %w", configFile, err)
|
|
}
|
|
if len(configData) == 0 {
|
|
return valid.GlobalCfg{}, fmt.Errorf("file %s was empty", configFile)
|
|
}
|
|
|
|
var rawCfg raw.GlobalCfg
|
|
|
|
decoder := yaml.NewDecoder(bytes.NewReader(configData))
|
|
decoder.KnownFields(true)
|
|
|
|
err = decoder.Decode(&rawCfg)
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return valid.GlobalCfg{}, err
|
|
}
|
|
|
|
return p.validateRawGlobalCfg(rawCfg, defaultCfg, "yaml")
|
|
}
|
|
|
|
// ParseGlobalCfgJSON parses a json string cfgJSON into global config.
|
|
func (p *ParserValidator) ParseGlobalCfgJSON(cfgJSON string, defaultCfg valid.GlobalCfg) (valid.GlobalCfg, error) {
|
|
var rawCfg raw.GlobalCfg
|
|
err := json.Unmarshal([]byte(cfgJSON), &rawCfg)
|
|
if err != nil {
|
|
return valid.GlobalCfg{}, err
|
|
}
|
|
return p.validateRawGlobalCfg(rawCfg, defaultCfg, "json")
|
|
}
|
|
|
|
func (p *ParserValidator) validateRawGlobalCfg(rawCfg raw.GlobalCfg, defaultCfg valid.GlobalCfg, errTag string) (valid.GlobalCfg, error) {
|
|
// Setting ErrorTag means our errors will use the field names defined in
|
|
// the struct tags for yaml/json.
|
|
validation.ErrorTag = errTag
|
|
if err := rawCfg.Validate(); err != nil {
|
|
return valid.GlobalCfg{}, err
|
|
}
|
|
|
|
validCfg := rawCfg.ToValid(defaultCfg)
|
|
return validCfg, nil
|
|
}
|
|
|
|
func (p *ParserValidator) repoCfgPath(repoDir, cfgFilename string) string {
|
|
return filepath.Join(repoDir, cfgFilename)
|
|
}
|
|
|
|
func (p *ParserValidator) validateProjectNames(config valid.RepoCfg) error {
|
|
// First, validate that all names are unique.
|
|
seen := make(map[string]bool)
|
|
for _, project := range config.Projects {
|
|
if project.Name != nil {
|
|
name := *project.Name
|
|
exists := seen[name]
|
|
if exists {
|
|
return fmt.Errorf("found two or more projects with name %q; project names must be unique", name)
|
|
}
|
|
seen[name] = true
|
|
}
|
|
}
|
|
|
|
// Next, validate that all dir/workspace combos are named.
|
|
// This map's keys will be 'dir/workspace' and the values are the names for
|
|
// that project.
|
|
dirWorkspaceToNames := make(map[string][]string)
|
|
for _, project := range config.Projects {
|
|
key := fmt.Sprintf("%s/%s", project.Dir, project.Workspace)
|
|
names := dirWorkspaceToNames[key]
|
|
|
|
// If there is already a project with this dir/workspace then this
|
|
// project must have a name.
|
|
if len(names) > 0 && project.Name == nil {
|
|
return fmt.Errorf("there are two or more projects with dir: %q workspace: %q that are not all named; they must have a 'name' key so they can be targeted for apply's separately", project.Dir, project.Workspace)
|
|
}
|
|
var name string
|
|
if project.Name != nil {
|
|
name = *project.Name
|
|
}
|
|
dirWorkspaceToNames[key] = append(dirWorkspaceToNames[key], name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyLegacyShellParsing changes any custom run commands in cfg to use the old
|
|
// parsing method with shlex.Split().
|
|
func (p *ParserValidator) applyLegacyShellParsing(cfg *valid.RepoCfg) error {
|
|
legacyParseF := func(s *valid.Step) error {
|
|
if s.StepName == "run" {
|
|
split, err := shlex.Split(s.RunCommand)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse %q: %w", s.RunCommand, err)
|
|
}
|
|
s.RunCommand = strings.Join(split, " ")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for k := range cfg.Workflows {
|
|
w := cfg.Workflows[k]
|
|
for i := range w.Plan.Steps {
|
|
s := &w.Plan.Steps[i]
|
|
if err := legacyParseF(s); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i := range w.Apply.Steps {
|
|
s := &w.Apply.Steps[i]
|
|
if err := legacyParseF(s); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
cfg.Workflows[k] = w
|
|
}
|
|
return nil
|
|
}
|