diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go index 7fc98bbc67..a9ea9b251a 100644 --- a/server/core/config/raw/project.go +++ b/server/core/config/raw/project.go @@ -35,6 +35,8 @@ type Project struct { Dir *string `yaml:"dir,omitempty"` Workspace *string `yaml:"workspace,omitempty"` Workflow *string `yaml:"workflow,omitempty"` + PullRequestWorkflowName *string `yaml:"pull_request_workflow,omitempty"` + DeploymentWorkflowName *string `yaml:"deployment_workflow,omitempty"` TerraformVersion *string `yaml:"terraform_version,omitempty"` Autoplan *Autoplan `yaml:"autoplan,omitempty"` ApplyRequirements []string `yaml:"apply_requirements,omitempty"` @@ -86,6 +88,8 @@ func (p Project) ToValid() valid.Project { } v.WorkflowName = p.Workflow + v.PullRequestWorkflowName = p.PullRequestWorkflowName + v.DeploymentWorkflowName = p.DeploymentWorkflowName if p.TerraformVersion != nil { v.TerraformVersion, _ = version.NewVersion(*p.TerraformVersion) } diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index c9487ace84..054c35e64c 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -298,7 +298,24 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro } log.Debug("overriding server-defined %s with repo-specified workflow: %q", WorkflowKey, workflow.Name) } + case PullRequestWorkflowKey: + if proj.PullRequestWorkflowName != nil { + name := *proj.PullRequestWorkflowName + if w, ok := g.PullRequestWorkflows[name]; ok { + pullRequestWorkflow = w + } + } + + log.Debug("overriding server-defined %s with repo-specified pull_request_workflow: %q", PullRequestWorkflowKey, workflow.Name) + case DeploymentWorkflowKey: + if proj.DeploymentWorkflowName != nil { + name := *proj.DeploymentWorkflowName + if w, ok := g.DeploymentWorkflows[name]; ok { + deploymentWorkflow = w + } + } + log.Debug("overriding server-defined %s with repo-specified deployment_workflow: %q", DeploymentWorkflowKey, 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. diff --git a/server/core/config/valid/project.go b/server/core/config/valid/project.go index 410c2dc99c..fa719aae84 100644 --- a/server/core/config/valid/project.go +++ b/server/core/config/valid/project.go @@ -9,7 +9,9 @@ import ( type workflowType string const ( - DefaultWorkflowType workflowType = "workflow" + DefaultWorkflowType workflowType = "workflow" + PullRequestWorkflowType workflowType = "pull_request_workflow" + DeploymentWorkflowType workflowType = "deployment_workflow" ) type Project struct { @@ -44,6 +46,12 @@ func (p Project) ValidateAllowedOverrides(allowedOverrides []string) error { if p.WorkflowName != nil && !sliceContains(allowedOverrides, WorkflowKey) { return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", WorkflowKey, AllowedOverridesKey, WorkflowKey) } + if p.PullRequestWorkflowName != nil && !sliceContains(allowedOverrides, PullRequestWorkflowKey) { + return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", PullRequestWorkflowKey, AllowedOverridesKey, PullRequestWorkflowKey) + } + if p.DeploymentWorkflowName != nil && !sliceContains(allowedOverrides, DeploymentWorkflowKey) { + return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", DeploymentWorkflowKey, AllowedOverridesKey, DeploymentWorkflowKey) + } if p.ApplyRequirements != nil && !sliceContains(allowedOverrides, ApplyRequirementsKey) { return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", ApplyRequirementsKey, AllowedOverridesKey, ApplyRequirementsKey) } @@ -59,6 +67,10 @@ func (p Project) getWorkflowName(workflowType workflowType) *string { switch workflowType { case DefaultWorkflowType: name = p.WorkflowName + case PullRequestWorkflowType: + name = p.PullRequestWorkflowName + case DeploymentWorkflowType: + name = p.DeploymentWorkflowName } return name } @@ -67,10 +79,26 @@ func (p Project) ValidateWorkflow(repoWorkflows map[string]Workflow, globalWorkf return p.validateWorkflowForType(DefaultWorkflowType, repoWorkflows, globalWorkflows) } +func (p Project) ValidatePRWorkflow(globalWorkflows map[string]Workflow) error { + return p.validateWorkflowForType(PullRequestWorkflowType, map[string]Workflow{}, globalWorkflows) +} + +func (p Project) ValidateDeploymentWorkflow(globalWorkflows map[string]Workflow) error { + return p.validateWorkflowForType(DeploymentWorkflowType, map[string]Workflow{}, globalWorkflows) +} + func (p Project) ValidateWorkflowAllowed(allowedWorkflows []string) error { return p.validateWorkflowAllowedForType(DefaultWorkflowType, allowedWorkflows) } +func (p Project) ValidatePRWorkflowAllowed(allowedWorkflows []string) error { + return p.validateWorkflowAllowedForType(PullRequestWorkflowType, allowedWorkflows) +} + +func (p Project) ValidateDeploymentWorkflowAllowed(allowedWorkflows []string) error { + return p.validateWorkflowAllowedForType(DeploymentWorkflowType, allowedWorkflows) +} + func (p Project) validateWorkflowForType( workflowType workflowType, repoWorkflows map[string]Workflow, diff --git a/server/core/config/valid/project_test.go b/server/core/config/valid/project_test.go index f1f6e296be..acfe56eb25 100644 --- a/server/core/config/valid/project_test.go +++ b/server/core/config/valid/project_test.go @@ -79,6 +79,115 @@ func TestProject_ValidateWorkflow(t *testing.T) { } } +func TestProject_ValidateDeploymentWorkflow(t *testing.T) { + defaultWorklfow := valid.Workflow{ + Name: "default", + } + customWorkflow := valid.Workflow{ + Name: "custom", + } + undefinedWorkflowName := "undefined" + cases := map[string]struct { + globalWorkflows map[string]valid.Workflow + project valid.Project + expErr string + }{ + "failed validation with undefined workflow": { + globalWorkflows: map[string]valid.Workflow{ + "default": defaultWorklfow, + }, + project: valid.Project{ + DeploymentWorkflowName: &undefinedWorkflowName, + }, + expErr: "deployment_workflow \"undefined\" is not defined anywhere", + }, + "workflow defined in global config": { + globalWorkflows: map[string]valid.Workflow{ + "default": defaultWorklfow, + "custom": customWorkflow, + }, + project: valid.Project{ + DeploymentWorkflowName: &customWorkflow.Name, + }, + }, + "missing workflow name is valid": { + globalWorkflows: map[string]valid.Workflow{ + "default": defaultWorklfow, + "custom": customWorkflow, + }, + project: valid.Project{ + DeploymentWorkflowName: nil, + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + actErr := c.project.ValidateDeploymentWorkflow(c.globalWorkflows) + if c.expErr == "" { + Ok(t, actErr) + } else { + ErrEquals(t, c.expErr, actErr) + } + }) + } +} + +func TestProject_ValidatePRWorkflow(t *testing.T) { + defaultWorklfow := valid.Workflow{ + Name: "default", + } + customWorkflow := valid.Workflow{ + Name: "custom", + } + undefinedWorkflowName := "undefined" + + cases := map[string]struct { + globalWorkflows map[string]valid.Workflow + project valid.Project + expErr string + }{ + "failed validation with undefined workflow": { + globalWorkflows: map[string]valid.Workflow{ + "default": defaultWorklfow, + }, + project: valid.Project{ + PullRequestWorkflowName: &undefinedWorkflowName, + }, + expErr: "pull_request_workflow \"undefined\" is not defined anywhere", + }, + "workflow defined in global config": { + globalWorkflows: map[string]valid.Workflow{ + "default": defaultWorklfow, + "custom": customWorkflow, + }, + project: valid.Project{ + PullRequestWorkflowName: &customWorkflow.Name, + }, + }, + "missing workflow name is valid": { + globalWorkflows: map[string]valid.Workflow{ + "default": defaultWorklfow, + "custom": customWorkflow, + }, + project: valid.Project{ + PullRequestWorkflowName: nil, + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + actErr := c.project.ValidatePRWorkflow(c.globalWorkflows) + if c.expErr == "" { + Ok(t, actErr) + } else { + ErrEquals(t, c.expErr, actErr) + } + }) + } +} + func TestProject_ValidateWorkflowAllowed(t *testing.T) { undefinedWorkflowName := "undefined" customWorkflowName := "custom" @@ -120,6 +229,91 @@ func TestProject_ValidateWorkflowAllowed(t *testing.T) { }) } } + +func TestProject_ValidatePRWorkflowAllowed(t *testing.T) { + undefinedWorkflowName := "undefined" + customWorkflowName := "custom" + + cases := map[string]struct { + allowedWorkflows []string + project valid.Project + expErr string + }{ + "failed validation with undefined workflow": { + allowedWorkflows: []string{"custom"}, + project: valid.Project{ + PullRequestWorkflowName: &undefinedWorkflowName, + }, + expErr: "pull_request_workflow \"undefined\" is not allowed for this repo", + }, + "workflow is allowed": { + allowedWorkflows: []string{"custom"}, + project: valid.Project{ + PullRequestWorkflowName: &customWorkflowName, + }, + }, + "missing workflow name is valid": { + allowedWorkflows: []string{"custom"}, + project: valid.Project{ + PullRequestWorkflowName: nil, + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + actErr := c.project.ValidatePRWorkflowAllowed(c.allowedWorkflows) + if c.expErr == "" { + Ok(t, actErr) + } else { + ErrEquals(t, c.expErr, actErr) + } + }) + } +} + +func TestProject_ValidateDeploymentWorkflowAllowed(t *testing.T) { + undefinedWorkflowName := "undefined" + customWorkflowName := "custom" + + cases := map[string]struct { + allowedWorkflows []string + project valid.Project + expErr string + }{ + "failed validation with undefined workflow": { + allowedWorkflows: []string{"custom"}, + project: valid.Project{ + DeploymentWorkflowName: &undefinedWorkflowName, + }, + expErr: "deployment_workflow \"undefined\" is not allowed for this repo", + }, + "workflow is allowed": { + allowedWorkflows: []string{"custom"}, + project: valid.Project{ + DeploymentWorkflowName: &customWorkflowName, + }, + }, + "missing workflow name is valid": { + allowedWorkflows: []string{"custom"}, + project: valid.Project{ + DeploymentWorkflowName: nil, + }, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + actErr := c.project.ValidateDeploymentWorkflowAllowed(c.allowedWorkflows) + if c.expErr == "" { + Ok(t, actErr) + } else { + ErrEquals(t, c.expErr, actErr) + } + }) + } +} + func TestProject_ValidateAllowedOverrides(t *testing.T) { workflowName := "custom" deleteSourceBranch := true @@ -136,6 +330,20 @@ func TestProject_ValidateAllowedOverrides(t *testing.T) { }, expErr: "repo config not allowed to set 'workflow' key: server-side config needs 'allowed_overrides: [workflow]'", }, + "pull_request_workflow is not allowed override": { + allowedOverrides: []string{}, + project: valid.Project{ + PullRequestWorkflowName: &workflowName, + }, + expErr: "repo config not allowed to set 'pull_request_workflow' key: server-side config needs 'allowed_overrides: [pull_request_workflow]'", + }, + "deployment_workflow is not allowed override": { + allowedOverrides: []string{}, + project: valid.Project{ + DeploymentWorkflowName: &workflowName, + }, + expErr: "repo config not allowed to set 'deployment_workflow' key: server-side config needs 'allowed_overrides: [deployment_workflow]'", + }, "apply_requirements is not allowed override": { allowedOverrides: []string{}, project: valid.Project{ @@ -151,19 +359,23 @@ func TestProject_ValidateAllowedOverrides(t *testing.T) { expErr: "repo config not allowed to set 'delete_source_branch_on_merge' key: server-side config needs 'allowed_overrides: [delete_source_branch_on_merge]'", }, "no errors when allowed override": { - allowedOverrides: []string{"apply_requirements", "workflow", "delete_source_branch_on_merge"}, + allowedOverrides: []string{"apply_requirements", "deployment_workflow", "pull_request_workflow", "workflow", "delete_source_branch_on_merge"}, project: valid.Project{ DeleteSourceBranchOnMerge: &deleteSourceBranch, ApplyRequirements: []string{"mergeable"}, + DeploymentWorkflowName: &workflowName, WorkflowName: &workflowName, + PullRequestWorkflowName: &workflowName, }, }, "no errors if override attributes nil": { - allowedOverrides: []string{"apply_requirements", "workflow", "delete_source_branch_on_merge"}, + allowedOverrides: []string{"apply_requirements", "deployment_workflow", "pull_request_workflow", "workflow", "delete_source_branch_on_merge"}, project: valid.Project{ DeleteSourceBranchOnMerge: nil, ApplyRequirements: nil, + DeploymentWorkflowName: nil, WorkflowName: nil, + PullRequestWorkflowName: nil, }, }, } diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index 3846096d65..77c6925468 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -145,6 +145,50 @@ func (r RepoCfg) ValidateWorkflows( return nil } +// ValidatePRWorkflows ensures that all projects with custom +// pull_request workflow names exists either on server level config +// Additionally it validates that workflow is allowed to be defined +func (r RepoCfg) ValidatePRWorkflows(workflows map[string]Workflow, allowedWorkflows []string) error { + for _, p := range r.Projects { + if err := p.ValidatePRWorkflow(workflows); err != nil { + return err + } + } + + if len(allowedWorkflows) == 0 { + return nil + } + + for _, p := range r.Projects { + if err := p.ValidatePRWorkflowAllowed(allowedWorkflows); err != nil { + return err + } + } + return nil +} + +// ValidateDeploymentWorkflows ensures that all projects with custom +// deployment workflow names exists either on server level config +// Additionally it validates that workflow is allowed to be defined +func (r RepoCfg) ValidateDeploymentWorkflows(workflows map[string]Workflow, allowedWorkflows []string) error { + for _, p := range r.Projects { + if err := p.ValidateDeploymentWorkflow(workflows); err != nil { + return err + } + } + + if len(allowedWorkflows) == 0 { + return nil + } + + for _, p := range r.Projects { + if err := p.ValidateDeploymentWorkflowAllowed(allowedWorkflows); err != nil { + return err + } + } + return nil +} + type Autoplan struct { WhenModified []string Enabled bool