Skip to content

Commit

Permalink
Orca 4417 refactor merge config (#183)
Browse files Browse the repository at this point in the history
* Add platform mode flag to GlobalCfg

* Added server side config support for platform mode

* Adding flag to enable platform mode

* fix tests

* Moved some of the project level validations to project struct

Added platform mode config support for project level workflow overrides.
We do not support custom platform mode workflows on repo configs

* fix broken test

* moved validations to repo_cfg

* moved some repetitive code to helper functino

* remove platform mode changes

* clean up for loop

* remove platform mode values from project
  • Loading branch information
msarvar authored Feb 22, 2022
1 parent 7caee98 commit 78eea78
Show file tree
Hide file tree
Showing 6 changed files with 444 additions and 208 deletions.
6 changes: 3 additions & 3 deletions server/core/config/raw/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,23 @@ type Repo struct {

func (g GlobalCfg) GetWorkflowNames() []string {
names := make([]string, 0)
for name, _ := range g.Workflows {
for name := range g.Workflows {
names = append(names, name)
}
return names
}

func (g GlobalCfg) GetPullRequestWorkflowNames() []string {
names := make([]string, 0)
for name, _ := range g.PullRequestWorkflows {
for name := range g.PullRequestWorkflows {
names = append(names, name)
}
return names
}

func (g GlobalCfg) GetDeploymentWorkflowNames() []string {
names := make([]string, 0)
for name, _ := range g.DeploymentWorkflows {
for name := range g.DeploymentWorkflows {
names = append(names, name)
}
return names
Expand Down
252 changes: 88 additions & 164 deletions server/core/config/valid/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,29 @@ func (g GlobalCfg) PlatformModeEnabled() bool {
// final config. It assumes that all configs have been validated.
func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, proj Project, rCfg RepoCfg) MergedProjectCfg {
log.Debug("MergeProjectCfg started")
applyReqs, workflow, pullRequestWorkflow, deploymentWorkflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge := g.getMatchingCfg(log, repoID)
var applyReqs []string
var workflow Workflow
var pullRequestWorkflow Workflow
var deploymentWorkflow Workflow
var allowCustomWorkflows bool
var deleteSourceBranchOnMerge bool

repo := g.foldMatchingRepos(repoID)

applyReqs = repo.ApplyRequirements
allowCustomWorkflows = *repo.AllowCustomWorkflows
deleteSourceBranchOnMerge = *repo.DeleteSourceBranchOnMerge
workflow = *repo.Workflow

// If platform mode is enabled there will be at least default workflows,
// otherwise values will be nil
if g.PlatformModeEnabled() {
pullRequestWorkflow = *repo.PullRequestWorkflow
deploymentWorkflow = *repo.DeploymentWorkflow
}

// If repos are allowed to override certain keys then override them.
for _, key := range allowedOverrides {
for _, key := range repo.AllowedOverrides {
switch key {
case ApplyRequirementsKey:
if proj.ApplyRequirements != nil {
Expand All @@ -270,20 +289,16 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
// define its own workflow. We also know that a workflow will
// exist with this name due to earlier validation.
name := *proj.WorkflowName
for k, v := range g.Workflows {
if k == name {
workflow = v
}
if w, ok := g.Workflows[name]; ok {
workflow = w
}
if allowCustomWorkflows {
for k, v := range rCfg.Workflows {
if k == name {
workflow = v
}
}

if w, ok := rCfg.Workflows[name]; allowCustomWorkflows && ok {
workflow = w
}
log.Debug("overriding server-defined %s with repo-specified workflow: %q", WorkflowKey, workflow.Name)
}

case DeleteSourceBranchOnMergeKey:
//We check whether the server configured value and repo-root level
//config is different. If it is then we change to the more granular.
Expand All @@ -302,8 +317,14 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
log.Debug("MergeProjectCfg completed")
}

log.Debug("final settings: %s: [%s], %s: %s",
ApplyRequirementsKey, strings.Join(applyReqs, ","), WorkflowKey, workflow.Name)
if g.PlatformModeEnabled() {
log.Debug("final settings: %s: %s, %s: %s, %s: %s",
WorkflowKey, workflow.Name, DeploymentWorkflowKey, deploymentWorkflow.Name, PullRequestWorkflowKey, pullRequestWorkflow.Name)

} else {
log.Debug("final settings: %s: [%s], %s: %s",
ApplyRequirementsKey, strings.Join(applyReqs, ","), WorkflowKey, workflow.Name)
}

return MergedProjectCfg{
ApplyRequirements: applyReqs,
Expand All @@ -326,186 +347,89 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
// repo with id repoID. It is used when there is no repo config.
func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repoRelDir string, workspace string) MergedProjectCfg {
log.Debug("building config based on server-side config")
applyReqs, workflow, pullRequestWorkflow, deploymentWorkflow, _, _, deleteSourceBranchOnMerge := g.getMatchingCfg(log, repoID)
return MergedProjectCfg{
ApplyRequirements: applyReqs,
Workflow: workflow,
PullRequestWorkflow: pullRequestWorkflow,
DeploymentWorkflow: deploymentWorkflow,
repo := g.foldMatchingRepos(repoID)

mrgPrj := MergedProjectCfg{
ApplyRequirements: repo.ApplyRequirements,
Workflow: *repo.Workflow,
RepoRelDir: repoRelDir,
Workspace: workspace,
Name: "",
AutoplanEnabled: DefaultAutoPlanEnabled,
TerraformVersion: nil,
PolicySets: g.PolicySets,
DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge,
}
}

// ValidateRepoCfg validates that rCfg for repo with id repoID is valid based
// on our global config.
func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error {

sliceContainsF := func(slc []string, str string) bool {
for _, s := range slc {
if s == str {
return true
}
}
return false
}
mapContainsF := func(m map[string]Workflow, key string) bool {
for k := range m {
if k == key {
return true
}
}
return false
}

// Check allowed overrides.
var allowedOverrides []string
for _, repo := range g.Repos {
if repo.IDMatches(repoID) {
if repo.AllowedOverrides != nil {
allowedOverrides = repo.AllowedOverrides
}
}
}
for _, p := range rCfg.Projects {
if p.WorkflowName != nil && !sliceContainsF(allowedOverrides, WorkflowKey) {
return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", WorkflowKey, AllowedOverridesKey, WorkflowKey)
}
if p.ApplyRequirements != nil && !sliceContainsF(allowedOverrides, ApplyRequirementsKey) {
return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", ApplyRequirementsKey, AllowedOverridesKey, ApplyRequirementsKey)
}
if p.DeleteSourceBranchOnMerge != nil && !sliceContainsF(allowedOverrides, DeleteSourceBranchOnMergeKey) {
return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", DeleteSourceBranchOnMergeKey, AllowedOverridesKey, DeleteSourceBranchOnMergeKey)
}
}

// Check custom workflows.
var allowCustomWorkflows bool
for _, repo := range g.Repos {
if repo.IDMatches(repoID) {
if repo.AllowCustomWorkflows != nil {
allowCustomWorkflows = *repo.AllowCustomWorkflows
}
}
DeleteSourceBranchOnMerge: *repo.DeleteSourceBranchOnMerge,
}

if len(rCfg.Workflows) > 0 && !allowCustomWorkflows {
return fmt.Errorf("repo config not allowed to define custom workflows: server-side config needs '%s: true'", AllowCustomWorkflowsKey)
}

// Check if the repo has set a workflow name that doesn't exist.
for _, p := range rCfg.Projects {
if p.WorkflowName != nil {
name := *p.WorkflowName
if !mapContainsF(rCfg.Workflows, name) && !mapContainsF(g.Workflows, name) {
return fmt.Errorf("workflow %q is not defined anywhere", name)
}
}
}

// Check workflow is allowed
var allowedWorkflows []string
for _, repo := range g.Repos {
if repo.IDMatches(repoID) {

if repo.AllowedWorkflows != nil {
allowedWorkflows = repo.AllowedWorkflows
}
}
}

for _, p := range rCfg.Projects {
// default is always allowed
if p.WorkflowName != nil && len(allowedWorkflows) != 0 {
name := *p.WorkflowName
if allowCustomWorkflows {
// If we allow CustomWorkflows we need to check that workflow name is defined inside repo and not global.
if mapContainsF(rCfg.Workflows, name) {
break
}
}

if !sliceContainsF(allowedWorkflows, name) {
return fmt.Errorf("workflow '%s' is not allowed for this repo", name)
}
}
}

return nil
return mrgPrj
}

// getMatchingCfg returns the key settings for repoID.
func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (
applyReqs []string,
workflow Workflow,
pullRequestWorkflow Workflow,
deploymentWorkflow Workflow,
allowedOverrides []string,
allowCustomWorkflows bool,
deleteSourceBranchOnMerge bool,
) {
toLog := make(map[string]string)
traceF := func(repoIdx int, repoID string, key string, val interface{}) string {
from := "default server config"
if repoIdx > 0 {
from = fmt.Sprintf("repos[%d], id: %s", repoIdx, repoID)
}
var valStr string
switch v := val.(type) {
case string:
valStr = fmt.Sprintf("%q", v)
case []string:
valStr = fmt.Sprintf("[%s]", strings.Join(v, ","))
case bool:
valStr = fmt.Sprintf("%t", v)
default:
valStr = "this is a bug"
}

return fmt.Sprintf("setting %s: %s from %s", key, valStr, from)
// foldMatchingRepos will return a pseudo repo instance that will iterate over
// the matching repositories and assign relevant fields if they're defined.
// This means returned object will contain the last matching repo's value as a it's fields
func (g GlobalCfg) foldMatchingRepos(repoID string) Repo {
foldedRepo := Repo{
AllowedWorkflows: make([]string, 0),
AllowedOverrides: make([]string, 0),
ApplyRequirements: make([]string, 0),
}

for i, repo := range g.Repos {
for _, repo := range g.Repos {
if repo.IDMatches(repoID) {
if repo.ApplyRequirements != nil {
toLog[ApplyRequirementsKey] = traceF(i, repo.IDString(), ApplyRequirementsKey, repo.ApplyRequirements)
applyReqs = repo.ApplyRequirements
foldedRepo.ApplyRequirements = repo.ApplyRequirements
}
if repo.Workflow != nil {
toLog[WorkflowKey] = traceF(i, repo.IDString(), WorkflowKey, repo.Workflow.Name)
workflow = *repo.Workflow
foldedRepo.Workflow = repo.Workflow
}
if repo.PullRequestWorkflow != nil {
toLog[PullRequestWorkflowKey] = traceF(i, repo.IDString(), PullRequestWorkflowKey, repo.PullRequestWorkflow.Name)
pullRequestWorkflow = *repo.PullRequestWorkflow
foldedRepo.PullRequestWorkflow = repo.PullRequestWorkflow
}
if repo.DeploymentWorkflow != nil {
toLog[DeploymentWorkflowKey] = traceF(i, repo.IDString(), DeploymentWorkflowKey, repo.DeploymentWorkflow.Name)
deploymentWorkflow = *repo.DeploymentWorkflow
foldedRepo.DeploymentWorkflow = repo.DeploymentWorkflow
}
if repo.AllowedWorkflows != nil {
foldedRepo.AllowedWorkflows = repo.AllowedWorkflows
}
if repo.AllowedOverrides != nil {
toLog[AllowedOverridesKey] = traceF(i, repo.IDString(), AllowedOverridesKey, repo.AllowedOverrides)
allowedOverrides = repo.AllowedOverrides
foldedRepo.AllowedOverrides = repo.AllowedOverrides
}
if repo.AllowCustomWorkflows != nil {
toLog[AllowCustomWorkflowsKey] = traceF(i, repo.IDString(), AllowCustomWorkflowsKey, *repo.AllowCustomWorkflows)
allowCustomWorkflows = *repo.AllowCustomWorkflows
foldedRepo.AllowCustomWorkflows = repo.AllowCustomWorkflows
}
if repo.DeleteSourceBranchOnMerge != nil {
toLog[DeleteSourceBranchOnMergeKey] = traceF(i, repo.IDString(), DeleteSourceBranchOnMergeKey, *repo.DeleteSourceBranchOnMerge)
deleteSourceBranchOnMerge = *repo.DeleteSourceBranchOnMerge
foldedRepo.DeleteSourceBranchOnMerge = repo.DeleteSourceBranchOnMerge
}
}
}
for _, l := range toLog {
log.Debug(l)

return foldedRepo
}

// ValidateRepoCfg validates that rCfg for repo with id repoID is valid based
// on our global config.
func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error {
repo := g.foldMatchingRepos(repoID)

// Check allowed overrides.
allowedOverrides := repo.AllowedOverrides

if err := rCfg.ValidateAllowedOverrides(allowedOverrides); err != nil {
return err
}

allowCustomWorkflows := *repo.AllowCustomWorkflows
// Check custom workflows.
if len(rCfg.Workflows) > 0 && !allowCustomWorkflows {
return fmt.Errorf("repo config not allowed to define custom workflows: server-side config needs '%s: true'", AllowCustomWorkflowsKey)
}

// Check if the repo has set a workflow name that doesn't exist and if workflow is allowed
if err := rCfg.ValidateWorkflows(g.Workflows, repo.AllowedWorkflows, allowCustomWorkflows); err != nil {
return err
}
return

return nil
}

// MatchingRepo returns an instance of Repo which matches a given repoID.
Expand Down
27 changes: 10 additions & 17 deletions server/core/config/valid/global_cfg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ func TestGlobalCfg_ValidateRepoCfg(t *testing.T) {
},
},
repoID: "github.com/owner/repo",
expErr: "workflow 'forbidden' is not allowed for this repo",
expErr: "workflow \"forbidden\" is not allowed for this repo",
},
"repo uses workflow that is defined server side but not allowed (without custom workflows)": {
gCfg: valid.GlobalCfg{
Expand Down Expand Up @@ -419,7 +419,7 @@ func TestGlobalCfg_ValidateRepoCfg(t *testing.T) {
},
},
repoID: "github.com/owner/repo",
expErr: "workflow 'forbidden' is not allowed for this repo",
expErr: "workflow \"forbidden\" is not allowed for this repo",
},
"repo uses workflow that is defined in both places with same name (without custom workflows)": {
gCfg: valid.GlobalCfg{
Expand Down Expand Up @@ -1024,28 +1024,21 @@ repos:
tmp, cleanup := TempDir(t)
defer cleanup()
var global valid.GlobalCfg
globalCfgArgs := valid.GlobalCfgArgs{
AllowRepoCfg: false,
MergeableReq: false,
ApprovedReq: false,
UnDivergedReq: false,
PlatformModeEnabled: c.platformMode,
}

if c.gCfg != "" {
path := filepath.Join(tmp, "config.yaml")
Ok(t, ioutil.WriteFile(path, []byte(c.gCfg), 0600))
var err error
globalCfgArgs := valid.GlobalCfgArgs{
AllowRepoCfg: false,
MergeableReq: false,
ApprovedReq: false,
UnDivergedReq: false,
PlatformModeEnabled: c.platformMode,
}

global, err = (&config.ParserValidator{}).ParseGlobalCfg(path, valid.NewGlobalCfgFromArgs(globalCfgArgs))
Ok(t, err)
} else {
globalCfgArgs := valid.GlobalCfgArgs{
AllowRepoCfg: false,
MergeableReq: false,
ApprovedReq: false,
UnDivergedReq: false,
PlatformModeEnabled: c.platformMode,
}
global = valid.NewGlobalCfgFromArgs(globalCfgArgs)
}

Expand Down
Loading

0 comments on commit 78eea78

Please sign in to comment.