mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-04 15:38:44 +00:00
147 lines
4.2 KiB
Go
147 lines
4.2 KiB
Go
package yaml
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/go-ozzo/ozzo-validation"
|
|
"github.com/pkg/errors"
|
|
"github.com/runatlantis/atlantis/server/events/yaml/raw"
|
|
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// AtlantisYAMLFilename is the name of the config file for each repo.
|
|
const AtlantisYAMLFilename = "atlantis.yaml"
|
|
|
|
type ParserValidator struct{}
|
|
|
|
// ReadConfig returns the parsed and validated atlantis.yaml config for repoDir.
|
|
// If there was no config file, then this can be detected by checking the type
|
|
// of error: os.IsNotExist(error) but it's instead preferred to check with
|
|
// HasConfigFile.
|
|
func (p *ParserValidator) ReadConfig(repoDir string) (valid.Config, error) {
|
|
configFile := p.configFilePath(repoDir)
|
|
configData, err := ioutil.ReadFile(configFile) // nolint: gosec
|
|
|
|
// NOTE: the error we return here must also be os.IsNotExist since that's
|
|
// what our callers use to detect a missing config file.
|
|
if err != nil && os.IsNotExist(err) {
|
|
return valid.Config{}, err
|
|
}
|
|
|
|
// If it exists but we couldn't read it return an error.
|
|
if err != nil {
|
|
return valid.Config{}, errors.Wrapf(err, "unable to read %s file", AtlantisYAMLFilename)
|
|
}
|
|
|
|
// If the config file exists, parse it.
|
|
config, err := p.parseAndValidate(configData)
|
|
if err != nil {
|
|
return valid.Config{}, errors.Wrapf(err, "parsing %s", AtlantisYAMLFilename)
|
|
}
|
|
return config, err
|
|
}
|
|
|
|
func (p *ParserValidator) HasConfigFile(repoDir string) (bool, error) {
|
|
_, err := os.Stat(p.configFilePath(repoDir))
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (p *ParserValidator) configFilePath(repoDir string) string {
|
|
return filepath.Join(repoDir, AtlantisYAMLFilename)
|
|
}
|
|
|
|
func (p *ParserValidator) parseAndValidate(configData []byte) (valid.Config, error) {
|
|
var rawConfig raw.Config
|
|
if err := yaml.UnmarshalStrict(configData, &rawConfig); err != nil {
|
|
return valid.Config{}, 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.Config{}, err
|
|
}
|
|
|
|
// Top level validation.
|
|
if err := p.validateWorkflows(rawConfig); err != nil {
|
|
return valid.Config{}, err
|
|
}
|
|
|
|
validConfig := rawConfig.ToValid()
|
|
if err := p.validateProjectNames(validConfig); err != nil {
|
|
return valid.Config{}, err
|
|
}
|
|
|
|
return validConfig, nil
|
|
}
|
|
|
|
func (p *ParserValidator) validateProjectNames(config valid.Config) 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
|
|
}
|
|
|
|
func (p *ParserValidator) validateWorkflows(config raw.Config) error {
|
|
for _, project := range config.Projects {
|
|
if err := p.validateWorkflowExists(project, config.Workflows); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *ParserValidator) validateWorkflowExists(project raw.Project, workflows map[string]raw.Workflow) error {
|
|
if project.Workflow == nil {
|
|
return nil
|
|
}
|
|
workflow := *project.Workflow
|
|
for k := range workflows {
|
|
if k == workflow {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("workflow %q is not defined", workflow)
|
|
}
|