Files
atlantis/server/core/config/raw/global_cfg.go
2025-01-27 22:11:10 -05:00

399 lines
13 KiB
Go

package raw
import (
"errors"
"fmt"
"regexp"
"strings"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/utils"
)
// GlobalCfg is the raw schema for server-side repo config.
type GlobalCfg struct {
Repos []Repo `yaml:"repos" json:"repos"`
Workflows map[string]Workflow `yaml:"workflows" json:"workflows"`
PolicySets PolicySets `yaml:"policies" json:"policies"`
Metrics Metrics `yaml:"metrics" json:"metrics"`
TeamAuthz TeamAuthz `yaml:"team_authz" json:"team_authz"`
}
// Repo is the raw schema for repos in the server-side repo config.
type Repo struct {
ID string `yaml:"id" json:"id"`
Branch string `yaml:"branch" json:"branch"`
RepoConfigFile string `yaml:"repo_config_file" json:"repo_config_file"`
PlanRequirements []string `yaml:"plan_requirements" json:"plan_requirements"`
ApplyRequirements []string `yaml:"apply_requirements" json:"apply_requirements"`
ImportRequirements []string `yaml:"import_requirements" json:"import_requirements"`
PreWorkflowHooks []WorkflowHook `yaml:"pre_workflow_hooks" json:"pre_workflow_hooks"`
Workflow *string `yaml:"workflow,omitempty" json:"workflow,omitempty"`
PostWorkflowHooks []WorkflowHook `yaml:"post_workflow_hooks" json:"post_workflow_hooks"`
AllowedWorkflows []string `yaml:"allowed_workflows,omitempty" json:"allowed_workflows,omitempty"`
AllowedOverrides []string `yaml:"allowed_overrides" json:"allowed_overrides"`
AllowCustomWorkflows *bool `yaml:"allow_custom_workflows,omitempty" json:"allow_custom_workflows,omitempty"`
DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty" json:"delete_source_branch_on_merge,omitempty"`
RepoLocking *bool `yaml:"repo_locking,omitempty" json:"repo_locking,omitempty"`
RepoLocks *RepoLocks `yaml:"repo_locks,omitempty" json:"repo_locks,omitempty"`
PolicyCheck *bool `yaml:"policy_check,omitempty" json:"policy_check,omitempty"`
CustomPolicyCheck *bool `yaml:"custom_policy_check,omitempty" json:"custom_policy_check,omitempty"`
AutoDiscover *AutoDiscover `yaml:"autodiscover,omitempty" json:"autodiscover,omitempty"`
SilencePRComments []string `yaml:"silence_pr_comments,omitempty" json:"silence_pr_comments,omitempty"`
}
func (g GlobalCfg) Validate() error {
err := validation.ValidateStruct(&g,
validation.Field(&g.Repos),
validation.Field(&g.Workflows),
validation.Field(&g.Metrics),
)
if err != nil {
return err
}
// Check that all workflows referenced by repos are actually defined.
for _, repo := range g.Repos {
if repo.Workflow == nil {
continue
}
name := *repo.Workflow
if name == valid.DefaultWorkflowName {
// The 'default' workflow will always be defined.
continue
}
found := false
for w := range g.Workflows {
if w == name {
found = true
break
}
}
if !found {
return fmt.Errorf("workflow %q is not defined", name)
}
}
// Check that all allowed workflows are defined
for _, repo := range g.Repos {
if repo.AllowedWorkflows == nil {
continue
}
for _, name := range repo.AllowedWorkflows {
if name == valid.DefaultWorkflowName {
// The 'default' workflow will always be defined.
continue
}
found := false
for w := range g.Workflows {
if w == name {
found = true
break
}
}
if !found {
return fmt.Errorf("workflow %q is not defined", name)
}
}
}
// Validate supported SilencePRComments values.
for _, repo := range g.Repos {
if repo.SilencePRComments == nil {
continue
}
for _, silenceStage := range repo.SilencePRComments {
if !utils.SlicesContains(valid.AllowedSilencePRComments, silenceStage) {
return fmt.Errorf(
"server-side repo config '%s' key value of '%s' is not supported, supported values are [%s]",
valid.SilencePRCommentsKey,
silenceStage,
strings.Join(valid.AllowedSilencePRComments, ", "),
)
}
}
}
return nil
}
func (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg {
workflows := make(map[string]valid.Workflow)
// assumes: globalcfg is always initialized with one repo .*
globalPlanReqs := defaultCfg.Repos[0].PlanRequirements
applyReqs := defaultCfg.Repos[0].ApplyRequirements
var globalApplyReqs []string
for _, req := range applyReqs {
for _, nonOverridableReq := range valid.NonOverridableApplyReqs {
if req == nonOverridableReq {
globalApplyReqs = append(globalApplyReqs, req)
}
}
}
globalImportReqs := defaultCfg.Repos[0].ImportRequirements
for k, v := range g.Workflows {
validatedWorkflow := v.ToValid(k)
workflows[k] = validatedWorkflow
if k == valid.DefaultWorkflowName {
// Handle the special case where they're redefining the default
// workflow. In this case, our default repo config references
// the "old" default workflow and so needs to be redefined.
defaultCfg.Repos[0].Workflow = &validatedWorkflow
}
}
// Merge in defaults without overriding.
for k, v := range defaultCfg.Workflows {
if _, ok := workflows[k]; !ok {
workflows[k] = v
}
}
var repos []valid.Repo
for _, r := range g.Repos {
repos = append(repos, r.ToValid(workflows, globalPlanReqs, globalApplyReqs, globalImportReqs))
}
repos = append(defaultCfg.Repos, repos...)
return valid.GlobalCfg{
Repos: repos,
Workflows: workflows,
PolicySets: g.PolicySets.ToValid(),
Metrics: g.Metrics.ToValid(),
TeamAuthz: g.TeamAuthz.ToValid(),
}
}
// HasRegexID returns true if r is configured with a regex id instead of an
// exact match id.
func (r Repo) HasRegexID() bool {
return strings.HasPrefix(r.ID, "/") && strings.HasSuffix(r.ID, "/")
}
// HasRegexBranch returns true if a branch regex was set.
func (r Repo) HasRegexBranch() bool {
return strings.HasPrefix(r.Branch, "/") && strings.HasSuffix(r.Branch, "/")
}
func (r Repo) Validate() error {
idValid := func(value interface{}) error {
id := value.(string)
if !r.HasRegexID() {
return nil
}
_, err := regexp.Compile(id[1 : len(id)-1])
if err != nil {
return fmt.Errorf("parsing: %s: %w", id, err)
}
return nil
}
branchValid := func(value interface{}) error {
branch := value.(string)
if branch == "" {
return nil
}
if !strings.HasPrefix(branch, "/") || !strings.HasSuffix(branch, "/") {
return errors.New("regex must begin and end with a slash '/'")
}
withoutSlashes := branch[1 : len(branch)-1]
_, err := regexp.Compile(withoutSlashes)
if err != nil {
return fmt.Errorf("parsing: %s: %w", branch, err)
}
return nil
}
repoConfigFileValid := func(value interface{}) error {
repoConfigFile := value.(string)
if repoConfigFile == "" {
return nil
}
if strings.HasPrefix(repoConfigFile, "/") {
return errors.New("must not starts with a slash '/'")
}
if strings.Contains(repoConfigFile, "../") || strings.Contains(repoConfigFile, "..\\") {
return errors.New("must not contains parent directory path like '../'")
}
return nil
}
overridesValid := func(value interface{}) error {
overrides := value.([]string)
for _, o := range overrides {
if o != valid.PlanRequirementsKey && o != valid.ApplyRequirementsKey && o != valid.ImportRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey && o != valid.RepoLockingKey && o != valid.RepoLocksKey && o != valid.PolicyCheckKey && o != valid.CustomPolicyCheckKey && o != valid.SilencePRCommentsKey {
return fmt.Errorf("%q is not a valid override, only %q, %q, %q, %q, %q, %q, %q, %q, %q, and %q are supported", o, valid.PlanRequirementsKey, valid.ApplyRequirementsKey, valid.ImportRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey, valid.RepoLockingKey, valid.RepoLocksKey, valid.PolicyCheckKey, valid.CustomPolicyCheckKey, valid.SilencePRCommentsKey)
}
}
return nil
}
workflowExists := func(value interface{}) error {
// We validate workflows in ParserValidator.validateRepoWorkflows
// because we need the list of workflows to validate.
return nil
}
deleteSourceBranchOnMergeValid := func(value interface{}) error {
//TOBE IMPLEMENTED
return nil
}
autoDiscoverValid := func(value interface{}) error {
autoDiscover := value.(*AutoDiscover)
if autoDiscover != nil {
return autoDiscover.Validate()
}
return nil
}
repoLocksValid := func(value interface{}) error {
repoLocks := value.(*RepoLocks)
if repoLocks != nil {
return repoLocks.Validate()
}
return nil
}
return validation.ValidateStruct(&r,
validation.Field(&r.ID, validation.Required, validation.By(idValid)),
validation.Field(&r.Branch, validation.By(branchValid)),
validation.Field(&r.RepoConfigFile, validation.By(repoConfigFileValid)),
validation.Field(&r.AllowedOverrides, validation.By(overridesValid)),
validation.Field(&r.PlanRequirements, validation.By(validPlanReq)),
validation.Field(&r.ApplyRequirements, validation.By(validApplyReq)),
validation.Field(&r.ImportRequirements, validation.By(validImportReq)),
validation.Field(&r.Workflow, validation.By(workflowExists)),
validation.Field(&r.DeleteSourceBranchOnMerge, validation.By(deleteSourceBranchOnMergeValid)),
validation.Field(&r.AutoDiscover, validation.By(autoDiscoverValid)),
validation.Field(&r.RepoLocks, validation.By(repoLocksValid)),
)
}
func (r Repo) ToValid(workflows map[string]valid.Workflow, globalPlanReqs []string, globalApplyReqs []string, globalImportReqs []string) valid.Repo {
var id string
var idRegex *regexp.Regexp
if r.HasRegexID() {
withoutSlashes := r.ID[1 : len(r.ID)-1]
// Safe to use MustCompile because we test it in Validate().
idRegex = regexp.MustCompile(withoutSlashes)
} else {
id = r.ID
}
var branchRegex *regexp.Regexp
if r.HasRegexBranch() {
withoutSlashes := r.Branch[1 : len(r.Branch)-1]
// Safe to use MustCompile because we test it in Validate().
branchRegex = regexp.MustCompile(withoutSlashes)
}
var workflow *valid.Workflow
if r.Workflow != nil {
// This key is guaranteed to exist because we test for it in
// ParserValidator.validateRepoWorkflows.
ptr := workflows[*r.Workflow]
workflow = &ptr
}
var preWorkflowHooks []*valid.WorkflowHook
if len(r.PreWorkflowHooks) > 0 {
for _, hook := range r.PreWorkflowHooks {
preWorkflowHooks = append(preWorkflowHooks, hook.ToValid())
}
}
var postWorkflowHooks []*valid.WorkflowHook
if len(r.PostWorkflowHooks) > 0 {
for _, hook := range r.PostWorkflowHooks {
postWorkflowHooks = append(postWorkflowHooks, hook.ToValid())
}
}
var mergedPlanReqs []string
mergedPlanReqs = append(mergedPlanReqs, r.PlanRequirements...)
var mergedApplyReqs []string
mergedApplyReqs = append(mergedApplyReqs, r.ApplyRequirements...)
var mergedImportReqs []string
mergedImportReqs = append(mergedImportReqs, r.ImportRequirements...)
// only add global reqs if they don't exist already.
OuterGlobalPlanReqs:
for _, globalReq := range globalPlanReqs {
for _, currReq := range r.PlanRequirements {
if globalReq == currReq {
continue OuterGlobalPlanReqs
}
}
// dont add policy_check step if repo have it explicitly disabled
if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck {
continue
}
mergedPlanReqs = append(mergedPlanReqs, globalReq)
}
OuterGlobalApplyReqs:
for _, globalReq := range globalApplyReqs {
for _, currReq := range r.ApplyRequirements {
if globalReq == currReq {
continue OuterGlobalApplyReqs
}
}
// dont add policy_check step if repo have it explicitly disabled
if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck {
continue
}
mergedApplyReqs = append(mergedApplyReqs, globalReq)
}
OuterGlobalImportReqs:
for _, globalReq := range globalImportReqs {
for _, currReq := range r.ImportRequirements {
if globalReq == currReq {
continue OuterGlobalImportReqs
}
}
// dont add policy_check step if repo have it explicitly disabled
if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck {
continue
}
mergedImportReqs = append(mergedImportReqs, globalReq)
}
var autoDiscover *valid.AutoDiscover
if r.AutoDiscover != nil {
autoDiscover = r.AutoDiscover.ToValid()
}
var repoLocks *valid.RepoLocks
if r.RepoLocks != nil {
repoLocks = r.RepoLocks.ToValid()
}
return valid.Repo{
ID: id,
IDRegex: idRegex,
BranchRegex: branchRegex,
RepoConfigFile: r.RepoConfigFile,
PlanRequirements: mergedPlanReqs,
ApplyRequirements: mergedApplyReqs,
ImportRequirements: mergedImportReqs,
PreWorkflowHooks: preWorkflowHooks,
Workflow: workflow,
PostWorkflowHooks: postWorkflowHooks,
AllowedWorkflows: r.AllowedWorkflows,
AllowedOverrides: r.AllowedOverrides,
AllowCustomWorkflows: r.AllowCustomWorkflows,
DeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge,
RepoLocking: r.RepoLocking,
RepoLocks: repoLocks,
PolicyCheck: r.PolicyCheck,
CustomPolicyCheck: r.CustomPolicyCheck,
AutoDiscover: autoDiscover,
SilencePRComments: r.SilencePRComments,
}
}