Files
atlantis/server/events/yaml/parser_validator.go
2018-07-24 17:02:01 +02:00

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)
}