diff --git a/runatlantis.io/docs/custom-policy-checks.md b/runatlantis.io/docs/custom-policy-checks.md new file mode 100644 index 0000000000..769558910d --- /dev/null +++ b/runatlantis.io/docs/custom-policy-checks.md @@ -0,0 +1,45 @@ +# Custom Policy Checks +If you want to run custom policy tools or scripts instead of the built-in Conftest integration, you can do so by setting the `custom_policy_check` option and running it in a custom workflow. Note: custom policy tool output is simply parsed for "fail" substrings to determine if the policy set passed. + +This option can be configured either at the server-level in a [repos.yaml config file](server-configuration.md) or at the repo-level in an [atlantis.yaml file.](repo-level-atlantis-yaml.md). + +## Server-side config example +Set the `policy_check` and `custom_policy_check` options to true, and run the custom tool in the policy check steps as seen below. No + +```yaml +repos: + - id: /.*/ + branch: /^main$/ + apply_requirements: [mergeable, undiverged, approved] + policy_check: true + custom_policy_check: true + workflow: custom +workflows: + custom: + policy_check: + steps: + - show + - run: cnspec scan terraform plan $SHOWFILE --policy-bundle example-cnspec-policies.mql.yaml +policies: + owners: + users: + - example_ghuser + policy_sets: + - name: example-set + path: example-cnspec-policies.mql.yaml + source: local +``` + + +## Repo-level atlantis.yaml example +First, you will need to ensure `custom_policy_check` is within the `allowed_overrides` field of the server-side config. Next, just set the custom option to true on the specific project you want as shown in the example `atlantis.yaml` below: + +```yaml +version: 3 +projects: + - name: example + dir: ./example + custom_policy_check: true + autoplan: + when_modified: ["*.tf"] +``` \ No newline at end of file diff --git a/runatlantis.io/docs/policy-checking.md b/runatlantis.io/docs/policy-checking.md index 2530c5969f..c996ef7ee0 100644 --- a/runatlantis.io/docs/policy-checking.md +++ b/runatlantis.io/docs/policy-checking.md @@ -187,7 +187,7 @@ When the policy check workflow runs, a file is created in the working directory [ { "PolicySetName": "policy1", - "ConftestOutput": "", + "PolicyOutput": "", "Passed": false, "ReqApprovals": 1, "CurApprovals": 0 diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index c3908ea89d..890569c2a7 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -59,6 +59,7 @@ projects: terraform_version: v0.11.0 delete_source_branch_on_merge: true repo_locking: true + custom_policy_check: false autoplan: when_modified: ["*.tf", "../modules/**/*.tf", ".terraform.lock.hcl"] enabled: true @@ -311,6 +312,7 @@ workspace: myworkspace execution_order_group: 0 delete_source_branch_on_merge: false repo_locking: true +custom_policy_check: false autoplan: terraform_version: 0.11.0 plan_requirements: ["approved"] @@ -327,7 +329,9 @@ workflow: myworkflow | workspace | string | `"default"` | no | The [Terraform workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) for this project. Atlantis will switch to this workplace when planning/applying and will create it if it doesn't exist. | | execution_order_group | int | `0` | no | Index of execution order group. Projects will be sort by this field before planning/applying. | | delete_source_branch_on_merge | bool | `false` | no | Automatically deletes the source branch on merge. | -| repo_locking | bool | `true` | no | Get a repository lock in this project when plan. | +| repo_locking | bool | `true` | no | Get a repository lock in this project when plan. + +| custom_policy_check | bool | `false` | no | Enable using policy check tools other than Conftest | | autoplan | [Autoplan](#autoplan) | none | no | A custom autoplan configuration. If not specified, will use the autoplan config. See [Autoplanning](autoplanning.html). | | terraform_version | string | none | no | A specific Terraform version to use when running commands for this project. Must be [Semver compatible](https://semver.org/), ex. `v0.11.0`, `0.12.0-beta1`. | | plan_requirements
*(restricted)* | array[string] | none | no | Requirements that must be satisfied before `atlantis plan` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 83cd3ddc5a..01d8a175aa 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -54,7 +54,7 @@ repos: # allowed_overrides specifies which keys can be overridden by this repo in # its atlantis.yaml file. - allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge, repo_locking] + allowed_overrides: [apply_requirements, workflow, delete_source_branch_on_merge, repo_locking, custom_policy_check] # allowed_workflows specifies which workflows the repos that match # are allowed to select. @@ -73,6 +73,10 @@ repos: # If true (default), atlantis try to get a lock. repo_locking: true + # custom_policy_check defines whether policy checking tools besides Conftest are enabled in checks + # If false (default), only Conftest JSON output is allowed + custom_policy_check: false + # pre_workflow_hooks defines arbitrary list of scripts to execute before workflow execution. pre_workflow_hooks: - run: my-pre-workflow-hook-command arg1 @@ -340,6 +344,9 @@ unless you've created your own server-side workflow with that key (overriding it See [Custom Workflows](custom-workflows.html) for more details on writing custom workflows. +### Allow Using Custom Policy Tools +Conftest is the standard policy check application integrated with Atlantis, but custom tools can still be run in custom workflows when the `custom_policy_check` option is set. See the [Custom Policy Checks page](custom-policy-checks.md) for detailed examples. + ### Allow Repos To Define Their Own Workflows If you want repos to be able to define their own workflows you need to allow them to override the `workflow` key and set `allow_custom_workflows` to `true`. @@ -482,12 +489,13 @@ If you set a workflow with the key `default`, it will override this. | plan_requirements | []string | none | no | Requirements that must be satisfied before `atlantis plan` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | | | apply_requirements | []string | none | no | Requirements that must be satisfied before `atlantis apply` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | | import_requirements | []string | none | no | Requirements that must be satisfied before `atlantis import` can be run. Currently the only supported requirements are `approved`, `mergeable`, and `undiverged`. See [Command Requirements](command-requirements.html) for more details. | -| allowed_overrides | []string | none | no | A list of restricted keys that `atlantis.yaml` files can override. The only supported keys are `apply_requirements`, `workflow`, `delete_source_branch_on_merge` and `repo_locking` | +| allowed_overrides | []string | none | no | A list of restricted keys that `atlantis.yaml` files can override. The only supported keys are `apply_requirements`, `workflow`, `delete_source_branch_on_merge`,`repo_locking`, and `custom_policy_check` | | allowed_workflows | []string | none | no | A list of workflows that `atlantis.yaml` files can select from. | | allow_custom_workflows | bool | false | no | Whether or not to allow [Custom Workflows](custom-workflows.html). | | delete_source_branch_on_merge | bool | false | no | Whether or not to delete the source branch on merge. | | repo_locking | bool | false | no | Whether or not to get a lock. | | policy_check | bool | false | no | Whether or not to run policy checks on this repository. | +| custom_policy_check | bool | false | no | Whether or not to enable custom policy check tools outside of Conftest on this repository. | :::tip Notes diff --git a/server/core/config/parser_validator_test.go b/server/core/config/parser_validator_test.go index 13cff30744..53caf9e539 100644 --- a/server/core/config/parser_validator_test.go +++ b/server/core/config/parser_validator_test.go @@ -1309,7 +1309,7 @@ func TestParseGlobalCfg(t *testing.T) { input: `repos: - id: /.*/ allowed_overrides: [invalid]`, - expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"plan_requirements\", \"apply_requirements\", \"import_requirements\", \"workflow\", \"delete_source_branch_on_merge\", \"repo_locking\" and \"policy_check\" are supported.).).", + expErr: "repos: (0: (allowed_overrides: \"invalid\" is not a valid override, only \"plan_requirements\", \"apply_requirements\", \"import_requirements\", \"workflow\", \"delete_source_branch_on_merge\", \"repo_locking\", \"policy_check\", and \"custom_policy_check\" are supported.).).", }, "invalid plan_requirement": { input: `repos: @@ -1573,6 +1573,7 @@ workflows: DeleteSourceBranchOnMerge: Bool(false), RepoLocking: Bool(true), PolicyCheck: Bool(false), + CustomPolicyCheck: Bool(false), }, }, Workflows: map[string]valid.Workflow{ diff --git a/server/core/config/raw/global_cfg.go b/server/core/config/raw/global_cfg.go index 9909b6ed59..92a9ec29b6 100644 --- a/server/core/config/raw/global_cfg.go +++ b/server/core/config/raw/global_cfg.go @@ -35,6 +35,7 @@ type Repo struct { 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"` PolicyCheck *bool `yaml:"policy_check,omitempty" json:"policy_check,omitempty"` + CustomPolicyCheck *bool `yaml:"custom_policy_check,omitempty" json:"custom_policy_check,omitempty"` } func (g GlobalCfg) Validate() error { @@ -192,8 +193,8 @@ func (r Repo) Validate() error { 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.PolicyCheckKey { - return fmt.Errorf("%q is not a valid override, only %q, %q, %q, %q, %q, %q and %q are supported", o, valid.PlanRequirementsKey, valid.ApplyRequirementsKey, valid.ImportRequirementsKey, valid.WorkflowKey, valid.DeleteSourceBranchOnMergeKey, valid.RepoLockingKey, valid.PolicyCheckKey) + if o != valid.PlanRequirementsKey && o != valid.ApplyRequirementsKey && o != valid.ImportRequirementsKey && o != valid.WorkflowKey && o != valid.DeleteSourceBranchOnMergeKey && o != valid.RepoLockingKey && o != valid.PolicyCheckKey && o != valid.CustomPolicyCheckKey { + return fmt.Errorf("%q is not a valid override, only %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.PolicyCheckKey, valid.CustomPolicyCheckKey) } } return nil @@ -331,5 +332,6 @@ OuterGlobalImportReqs: DeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge, RepoLocking: r.RepoLocking, PolicyCheck: r.PolicyCheck, + CustomPolicyCheck: r.CustomPolicyCheck, } } diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go index f331c64483..90784fd747 100644 --- a/server/core/config/raw/project.go +++ b/server/core/config/raw/project.go @@ -36,6 +36,7 @@ type Project struct { RepoLocking *bool `yaml:"repo_locking,omitempty"` ExecutionOrderGroup *int `yaml:"execution_order_group,omitempty"` PolicyCheck *bool `yaml:"policy_check,omitempty"` + CustomPolicyCheck *bool `yaml:"custom_policy_check,omitempty"` } func (p Project) Validate() error { @@ -146,6 +147,10 @@ func (p Project) ToValid() valid.Project { v.PolicyCheck = p.PolicyCheck } + if p.CustomPolicyCheck != nil { + v.CustomPolicyCheck = p.CustomPolicyCheck + } + return v } diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index 937211c00e..5bd0c5bf96 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -24,6 +24,7 @@ const DefaultWorkflowName = "default" const DeleteSourceBranchOnMergeKey = "delete_source_branch_on_merge" const RepoLockingKey = "repo_locking" const PolicyCheckKey = "policy_check" +const CustomPolicyCheckKey = "custom_policy_check" // DefaultAtlantisFile is the default name of the config file for each repo. const DefaultAtlantisFile = "atlantis.yaml" @@ -79,6 +80,7 @@ type Repo struct { DeleteSourceBranchOnMerge *bool RepoLocking *bool PolicyCheck *bool + CustomPolicyCheck *bool } type MergedProjectCfg struct { @@ -100,6 +102,7 @@ type MergedProjectCfg struct { ExecutionOrderGroup int RepoLocking bool PolicyCheck bool + CustomPolicyCheck bool } // WorkflowHook is a map of custom run commands to run before or after workflows. @@ -239,6 +242,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { allowCustomWorkflows := false deleteSourceBranchOnMerge := false repoLockingKey := true + customPolicyCheck := false if args.AllowRepoCfg { allowedOverrides = []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, PolicyCheckKey} allowCustomWorkflows = true @@ -262,6 +266,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { DeleteSourceBranchOnMerge: &deleteSourceBranchOnMerge, RepoLocking: &repoLockingKey, PolicyCheck: &policyCheck, + CustomPolicyCheck: &customPolicyCheck, }, }, Workflows: map[string]Workflow{ @@ -298,7 +303,7 @@ func (r Repo) IDString() string { // 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") - planReqs, applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocking, policyCheck := g.getMatchingCfg(log, repoID) + planReqs, applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocking, policyCheck, customPolicyCheck := g.getMatchingCfg(log, repoID) // If repos are allowed to override certain keys then override them. for _, key := range allowedOverrides { @@ -364,6 +369,11 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro log.Debug("overriding server-defined %s with repo settings: [%t]", PolicyCheckKey, *proj.PolicyCheck) policyCheck = *proj.PolicyCheck } + case CustomPolicyCheckKey: + if proj.CustomPolicyCheck != nil { + log.Debug("overriding server-defined %s with repo settings: [%t]", CustomPolicyCheckKey, *proj.CustomPolicyCheck) + customPolicyCheck = *proj.CustomPolicyCheck + } } log.Debug("MergeProjectCfg completed") } @@ -388,6 +398,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro ExecutionOrderGroup: proj.ExecutionOrderGroup, RepoLocking: repoLocking, PolicyCheck: policyCheck, + CustomPolicyCheck: customPolicyCheck, } } @@ -395,7 +406,7 @@ 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") - planReqs, applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocking, policyCheck := g.getMatchingCfg(log, repoID) + planReqs, applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocking, policyCheck, customPolicyCheck := g.getMatchingCfg(log, repoID) return MergedProjectCfg{ PlanRequirements: planReqs, ApplyRequirements: applyReqs, @@ -410,6 +421,7 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, RepoLocking: repoLocking, PolicyCheck: policyCheck, + CustomPolicyCheck: customPolicyCheck, } } @@ -454,6 +466,9 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { if p.RepoLocking != nil && !utils.SlicesContains(allowedOverrides, RepoLockingKey) { return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", RepoLockingKey, AllowedOverridesKey, RepoLockingKey) } + if p.CustomPolicyCheck != nil && !utils.SlicesContains(allowedOverrides, CustomPolicyCheckKey) { + return fmt.Errorf("repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'", CustomPolicyCheckKey, AllowedOverridesKey, CustomPolicyCheckKey) + } } // Check custom workflows. @@ -512,7 +527,7 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { } // getMatchingCfg returns the key settings for repoID. -func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (planReqs []string, applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocking bool, policyCheck bool) { +func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (planReqs []string, applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocking bool, policyCheck bool, customPolicyCheck bool) { toLog := make(map[string]string) traceF := func(repoIdx int, repoID string, key string, val interface{}) string { from := "default server config" @@ -534,7 +549,7 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla return fmt.Sprintf("setting %s: %s from %s", key, valStr, from) } - for _, key := range []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, PolicyCheckKey} { + for _, key := range []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, PolicyCheckKey, CustomPolicyCheckKey} { for i, repo := range g.Repos { if repo.IDMatches(repoID) { switch key { @@ -583,6 +598,11 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla toLog[PolicyCheckKey] = traceF(i, repo.IDString(), PolicyCheckKey, *repo.PolicyCheck) policyCheck = *repo.PolicyCheck } + case CustomPolicyCheckKey: + if repo.CustomPolicyCheck != nil { + toLog[CustomPolicyCheckKey] = traceF(i, repo.IDString(), CustomPolicyCheckKey, *repo.CustomPolicyCheck) + customPolicyCheck = *repo.CustomPolicyCheck + } } } } diff --git a/server/core/config/valid/global_cfg_test.go b/server/core/config/valid/global_cfg_test.go index 1efa610f8f..d778a9f12d 100644 --- a/server/core/config/valid/global_cfg_test.go +++ b/server/core/config/valid/global_cfg_test.go @@ -81,6 +81,7 @@ func TestNewGlobalCfg(t *testing.T) { DeleteSourceBranchOnMerge: Bool(false), RepoLocking: Bool(true), PolicyCheck: Bool(false), + CustomPolicyCheck: Bool(false), }, }, Workflows: map[string]valid.Workflow{ @@ -707,11 +708,12 @@ policies: }, }, }, - RepoRelDir: ".", - Workspace: "default", - Name: "", - AutoplanEnabled: false, - RepoLocking: true, + RepoRelDir: ".", + Workspace: "default", + Name: "", + AutoplanEnabled: false, + RepoLocking: true, + CustomPolicyCheck: false, }, }, "policies set correct version if specified": { @@ -755,11 +757,12 @@ policies: }, }, }, - RepoRelDir: ".", - Workspace: "default", - Name: "", - AutoplanEnabled: false, - RepoLocking: true, + RepoRelDir: ".", + Workspace: "default", + Name: "", + AutoplanEnabled: false, + RepoLocking: true, + CustomPolicyCheck: false, }, }, } @@ -848,12 +851,13 @@ workflows: Import: valid.DefaultImportStage, StateRm: valid.DefaultStateRmStage, }, - RepoRelDir: ".", - Workspace: "default", - Name: "", - AutoplanEnabled: false, - PolicySets: emptyPolicySets, - RepoLocking: true, + RepoRelDir: ".", + Workspace: "default", + Name: "", + AutoplanEnabled: false, + PolicySets: emptyPolicySets, + RepoLocking: true, + CustomPolicyCheck: false, }, }, "repo-side plan reqs win out if allowed": { @@ -883,6 +887,7 @@ repos: AutoplanEnabled: false, PolicySets: emptyPolicySets, RepoLocking: true, + CustomPolicyCheck: false, }, }, "repo-side apply reqs win out if allowed": { @@ -912,6 +917,7 @@ repos: AutoplanEnabled: false, PolicySets: emptyPolicySets, RepoLocking: true, + CustomPolicyCheck: false, }, }, "repo-side import reqs win out if allowed": { @@ -941,6 +947,7 @@ repos: AutoplanEnabled: false, PolicySets: emptyPolicySets, RepoLocking: true, + CustomPolicyCheck: false, }, }, "repo-side repo_locking win out if allowed": { @@ -957,6 +964,7 @@ repos: ApplyRequirements: []string{}, ImportRequirements: []string{}, RepoLocking: Bool(true), + CustomPolicyCheck: Bool(false), }, repoWorkflows: nil, exp: valid.MergedProjectCfg{ @@ -970,6 +978,7 @@ repos: AutoplanEnabled: false, PolicySets: emptyPolicySets, RepoLocking: false, + CustomPolicyCheck: false, }, }, "last server-side match wins": { @@ -1006,6 +1015,7 @@ repos: AutoplanEnabled: false, PolicySets: emptyPolicySets, RepoLocking: true, + CustomPolicyCheck: false, }, }, "autoplan is set properly": { @@ -1032,6 +1042,7 @@ repos: AutoplanEnabled: true, PolicySets: emptyPolicySets, RepoLocking: true, + CustomPolicyCheck: false, }, }, "execution order group is set": { @@ -1060,6 +1071,7 @@ repos: PolicySets: emptyPolicySets, ExecutionOrderGroup: 10, RepoLocking: true, + CustomPolicyCheck: false, }, }, } @@ -1254,6 +1266,7 @@ repos: PolicySets: emptyPolicySets, RepoLocking: true, PolicyCheck: false, + CustomPolicyCheck: false, }, }, "global policy check enabled": { @@ -1293,6 +1306,7 @@ repos: PolicySets: emptyPolicySets, RepoLocking: true, PolicyCheck: true, + CustomPolicyCheck: false, }, }, "global policy check enabled except current repo": { @@ -1333,6 +1347,7 @@ repos: PolicySets: emptyPolicySets, RepoLocking: true, PolicyCheck: false, + CustomPolicyCheck: false, }, }, "global policy check disabled and disabled on current repo": { @@ -1373,6 +1388,7 @@ repos: PolicySets: emptyPolicySets, RepoLocking: true, PolicyCheck: false, + CustomPolicyCheck: false, }, }, "global policy check disabled and enabled on current repo": { @@ -1413,6 +1429,7 @@ repos: PolicySets: emptyPolicySets, RepoLocking: true, PolicyCheck: true, // Project will have policy check as true but since it is globally disable it wont actually run + CustomPolicyCheck: false, }, }, } diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index 6929fd42b3..1a99e89cda 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -24,6 +24,7 @@ type RepoCfg struct { ParallelPolicyCheck *bool DeleteSourceBranchOnMerge *bool RepoLocking *bool + CustomPolicyCheck *bool EmojiReaction string AllowedRegexpPrefixes []string AbortOnExcecutionOrderFail bool @@ -136,6 +137,7 @@ type Project struct { RepoLocking *bool ExecutionOrderGroup int PolicyCheck *bool + CustomPolicyCheck *bool } // GetName returns the name of the project or an empty string if there is no diff --git a/server/core/runtime/policy/conftest_client.go b/server/core/runtime/policy/conftest_client.go index 3c97bffb0e..9b0f1fa2f8 100644 --- a/server/core/runtime/policy/conftest_client.go +++ b/server/core/runtime/policy/conftest_client.go @@ -208,10 +208,10 @@ func (c *ConfTestExecutorWorkflow) Run(ctx command.ProjectContext, executablePat } policySetResults = append(policySetResults, models.PolicySetResult{ - PolicySetName: policySet.Name, - ConftestOutput: cmdOutput, - Passed: passed, - ReqApprovals: policySet.ApproveCount, + PolicySetName: policySet.Name, + PolicyOutput: cmdOutput, + Passed: passed, + ReqApprovals: policySet.ApproveCount, }) } diff --git a/server/core/runtime/policy/conftest_client_test.go b/server/core/runtime/policy/conftest_client_test.go index 171882c257..143b6e8dbc 100644 --- a/server/core/runtime/policy/conftest_client_test.go +++ b/server/core/runtime/policy/conftest_client_test.go @@ -191,7 +191,7 @@ func TestRun(t *testing.T) { var extraArgs []string expectedOutput := "Success" - expectedResult := `[{"PolicySetName":"policy1","ConftestOutput":"Success","Passed":true,"ReqApprovals":0,"CurApprovals":0},{"PolicySetName":"policy2","ConftestOutput":"Success","Passed":true,"ReqApprovals":0,"CurApprovals":0}]` + expectedResult := `[{"PolicySetName":"policy1","PolicyOutput":"Success","Passed":true,"ReqApprovals":0,"CurApprovals":0},{"PolicySetName":"policy2","PolicyOutput":"Success","Passed":true,"ReqApprovals":0,"CurApprovals":0}]` expectedArgsPolicy1 := []string{executablePath, "test", "-p", localPolicySetPath1, filepath.Join(workdir, "testproj-default.json"), "--no-color"} expectedArgsPolicy2 := []string{executablePath, "test", "-p", localPolicySetPath2, filepath.Join(workdir, "testproj-default.json"), "--no-color"} @@ -216,7 +216,7 @@ func TestRun(t *testing.T) { extraArgs := []string{"--all-namespaces"} expectedOutput := "Success" - expectedResult := `[{"PolicySetName":"policy1","ConftestOutput":"","Passed":true,"ReqApprovals":0,"CurApprovals":0},{"PolicySetName":"policy2","ConftestOutput":"","Passed":true,"ReqApprovals":0,"CurApprovals":0}]` + expectedResult := `[{"PolicySetName":"policy1","PolicyOutput":"","Passed":true,"ReqApprovals":0,"CurApprovals":0},{"PolicySetName":"policy2","PolicyOutput":"","Passed":true,"ReqApprovals":0,"CurApprovals":0}]` expectedArgsPolicy1 := []string{executablePath, "test", "-p", localPolicySetPath1, filepath.Join(workdir, "testproj-default.json"), "--no-color"} expectedArgsPolicy2 := []string{executablePath, "test", "-p", localPolicySetPath2, filepath.Join(workdir, "testproj-default.json"), "--no-color"} @@ -241,7 +241,7 @@ func TestRun(t *testing.T) { var extraArgs []string expectedOutput := "Success" - expectedResult := `[{"PolicySetName":"policy1","ConftestOutput":"Success","Passed":true,"ReqApprovals":0,"CurApprovals":0}]` + expectedResult := `[{"PolicySetName":"policy1","PolicyOutput":"Success","Passed":true,"ReqApprovals":0,"CurApprovals":0}]` expectedArgsPolicy1 := []string{executablePath, "test", "-p", localPolicySetPath1, filepath.Join(workdir, "testproj-default.json"), "--no-color"} expectedArgsPolicy2 := []string{executablePath, "test", "-p", localPolicySetPath2, filepath.Join(workdir, "testproj-default.json"), "--no-color"} @@ -284,7 +284,7 @@ func TestRun(t *testing.T) { expectedOutputPolicy1 := fmt.Sprintf("FAIL - %s - failure\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions", filepath.Join(workdir, "testproj-default.json")) expectedOutputPolicy2 := "Success" - expectedResult := `[{"PolicySetName":"policy1","ConftestOutput":"FAIL - - failure\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions","Passed":false,"ReqApprovals":0,"CurApprovals":0},{"PolicySetName":"policy2","ConftestOutput":"Success","Passed":true,"ReqApprovals":0,"CurApprovals":0}]` + expectedResult := `[{"PolicySetName":"policy1","PolicyOutput":"FAIL - - failure\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions","Passed":false,"ReqApprovals":0,"CurApprovals":0},{"PolicySetName":"policy2","PolicyOutput":"Success","Passed":true,"ReqApprovals":0,"CurApprovals":0}]` expectedArgsPolicy1 := []string{executablePath, "test", "-p", localPolicySetPath1, filepath.Join(workdir, "testproj-default.json"), "--no-color"} expectedArgsPolicy2 := []string{executablePath, "test", "-p", localPolicySetPath2, filepath.Join(workdir, "testproj-default.json"), "--no-color"} @@ -306,7 +306,7 @@ func TestRun(t *testing.T) { var extraArgs []string expectedOutput := fmt.Sprintf("FAIL - %s - failure\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions", filepath.Join(workdir, "testproj-default.json")) - expectedResult := `[{"PolicySetName":"policy1","ConftestOutput":"FAIL - - failure\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions","Passed":false,"ReqApprovals":0,"CurApprovals":0},{"PolicySetName":"policy2","ConftestOutput":"FAIL - - failure\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions","Passed":false,"ReqApprovals":0,"CurApprovals":0}]` + expectedResult := `[{"PolicySetName":"policy1","PolicyOutput":"FAIL - - failure\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions","Passed":false,"ReqApprovals":0,"CurApprovals":0},{"PolicySetName":"policy2","PolicyOutput":"FAIL - - failure\n1 tests, 0 passed, 0 warnings, 1 failure, 0 exceptions","Passed":false,"ReqApprovals":0,"CurApprovals":0}]` expectedArgsPolicy1 := []string{executablePath, "test", "-p", localPolicySetPath1, filepath.Join(workdir, "testproj-default.json"), "--no-color"} expectedArgsPolicy2 := []string{executablePath, "test", "-p", localPolicySetPath2, filepath.Join(workdir, "testproj-default.json"), "--no-color"} diff --git a/server/core/runtime/run_step_runner.go b/server/core/runtime/run_step_runner.go index 32b62f2cc4..b38ba20f24 100644 --- a/server/core/runtime/run_step_runner.go +++ b/server/core/runtime/run_step_runner.go @@ -73,8 +73,12 @@ func (r *RunStepRunner) Run(ctx command.ProjectContext, command string, path str if err != nil { err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, output) - ctx.Log.Debug("error: %s", err) - return "", err + if !ctx.CustomPolicyCheck { + ctx.Log.Debug("error: %s", err) + return "", err + } else { + ctx.Log.Debug("Treating custom policy tool error exit code as a policy failure. Error output: %s", err) + } } switch postProcessOutput { diff --git a/server/events/command/project_context.go b/server/events/command/project_context.go index 5afbe145ac..aa77cb8ff1 100644 --- a/server/events/command/project_context.go +++ b/server/events/command/project_context.go @@ -122,6 +122,8 @@ type ProjectContext struct { ExecutionOrderGroup int // If plans/applies should be aborted if any prior plan/apply fails AbortOnExcecutionOrderFail bool + // Allows custom policy check tools outside of Conftest to run in checks + CustomPolicyCheck bool } // SetProjectScopeTags adds ProjectContext tags to a new returned scope. diff --git a/server/events/markdown_renderer_test.go b/server/events/markdown_renderer_test.go index 548ce03cdb..e5c5b3e03e 100644 --- a/server/events/markdown_renderer_test.go +++ b/server/events/markdown_renderer_test.go @@ -267,7 +267,7 @@ $$$ { PolicySetName: "policy1", // strings.Repeat require to get wrapped result - ConftestOutput: `FAIL - - main - WARNING: Null Resource creation is prohibited. + PolicyOutput: `FAIL - - main - WARNING: Null Resource creation is prohibited. 2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`, Passed: false, @@ -276,9 +276,9 @@ $$$ { PolicySetName: "policy2", // strings.Repeat require to get wrapped result - ConftestOutput: "2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions", - Passed: true, - ReqApprovals: 1, + PolicyOutput: "2 tests, 2 passed, 0 warnings, 0 failure, 0 exceptions", + Passed: true, + ReqApprovals: 1, }, }, LockURL: "lock-url", @@ -335,7 +335,7 @@ $$$ { PolicySetName: "policy1", // strings.Repeat require to get wrapped result - ConftestOutput: strings.Repeat("line\n", 13) + `FAIL - - main - WARNING: Null Resource creation is prohibited. + PolicyOutput: strings.Repeat("line\n", 13) + `FAIL - - main - WARNING: Null Resource creation is prohibited. 2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions`, Passed: false, @@ -565,9 +565,9 @@ $$$ PolicyCheckResults: &models.PolicyCheckResults{ PolicySetResults: []models.PolicySetResult{ models.PolicySetResult{ - PolicySetName: "policy1", - ConftestOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", - Passed: true, + PolicySetName: "policy1", + PolicyOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", + Passed: true, }, }, LockURL: "lock-url", @@ -582,9 +582,9 @@ $$$ PolicyCheckResults: &models.PolicyCheckResults{ PolicySetResults: []models.PolicySetResult{ models.PolicySetResult{ - PolicySetName: "policy1", - ConftestOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", - Passed: true, + PolicySetName: "policy1", + PolicyOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", + Passed: true, }, }, LockURL: "lock-url2", ApplyCmd: "atlantis apply -d path2 -w workspace", @@ -778,9 +778,9 @@ $$$ PolicyCheckResults: &models.PolicyCheckResults{ PolicySetResults: []models.PolicySetResult{ models.PolicySetResult{ - PolicySetName: "policy1", - ConftestOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", - Passed: true, + PolicySetName: "policy1", + PolicyOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", + Passed: true, }, }, LockURL: "lock-url", ApplyCmd: "atlantis apply -d path -w workspace", @@ -794,10 +794,10 @@ $$$ PolicyCheckResults: &models.PolicyCheckResults{ PolicySetResults: []models.PolicySetResult{ models.PolicySetResult{ - PolicySetName: "policy1", - ConftestOutput: "4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions", - Passed: false, - ReqApprovals: 1, + PolicySetName: "policy1", + PolicyOutput: "4 tests, 2 passed, 0 warnings, 2 failures, 0 exceptions", + Passed: false, + ReqApprovals: 1, }, }, LockURL: "lock-url", ApplyCmd: "atlantis apply -d path -w workspace", @@ -1317,9 +1317,9 @@ func TestRenderCustomPolicyCheckTemplate_DisableApplyAll(t *testing.T) { PolicyCheckResults: &models.PolicyCheckResults{ PolicySetResults: []models.PolicySetResult{ models.PolicySetResult{ - PolicySetName: "policy1", - ConftestOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", - Passed: true, + PolicySetName: "policy1", + PolicyOutput: "4 tests, 4 passed, 0 warnings, 0 failures, 0 exceptions", + Passed: true, }, }, LockURL: "lock-url", ApplyCmd: "atlantis apply -d path -w workspace", diff --git a/server/events/models/models.go b/server/events/models/models.go index bdc821c285..549e06a6af 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -368,11 +368,11 @@ type PlanSuccess struct { } type PolicySetResult struct { - PolicySetName string - ConftestOutput string - Passed bool - ReqApprovals int - CurApprovals int + PolicySetName string + PolicyOutput string + Passed bool + ReqApprovals int + CurApprovals int } // PolicySetApproval tracks the number of approvals a given policy set has. @@ -472,7 +472,7 @@ func (p *PolicyCheckResults) CombinedOutput() string { combinedOutput := "" for _, psResult := range p.PolicySetResults { // accounting for json output from conftest. - for _, psResultLine := range strings.Split(psResult.ConftestOutput, "\\n") { + for _, psResultLine := range strings.Split(psResult.PolicyOutput, "\\n") { combinedOutput = fmt.Sprintf("%s\n%s", combinedOutput, psResultLine) } } @@ -484,7 +484,7 @@ func (p *PolicyCheckResults) Summary() string { note := "" for _, policySetResult := range p.PolicySetResults { r := regexp.MustCompile(`\d+ tests?, \d+ passed, \d+ warnings?, \d+ failures?, \d+ exceptions?(, \d skipped)?`) - if match := r.FindString(policySetResult.ConftestOutput); match != "" { + if match := r.FindString(policySetResult.PolicyOutput); match != "" { note = fmt.Sprintf("%s\npolicy set: %s: %s", note, policySetResult.PolicySetName, match) } } diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 126d89c60a..c05016d2af 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -433,8 +433,8 @@ func TestPolicyCheckResults_Summary(t *testing.T) { description: "test single format with single policy set", policysetResults: []models.PolicySetResult{ { - PolicySetName: "policy1", - ConftestOutput: "20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions", + PolicySetName: "policy1", + PolicyOutput: "20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions", }, }, exp: "policy set: policy1: 20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions", @@ -443,16 +443,16 @@ func TestPolicyCheckResults_Summary(t *testing.T) { description: "test multiple formats with multiple policy sets", policysetResults: []models.PolicySetResult{ { - PolicySetName: "policy1", - ConftestOutput: "20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions", + PolicySetName: "policy1", + PolicyOutput: "20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions", }, { - PolicySetName: "policy2", - ConftestOutput: "3 tests, 0 passed, 1 warning, 1 failure, 0 exceptions, 1 skipped", + PolicySetName: "policy2", + PolicyOutput: "3 tests, 0 passed, 1 warning, 1 failure, 0 exceptions, 1 skipped", }, { - PolicySetName: "policy3", - ConftestOutput: "1 test, 0 passed, 1 warning, 1 failure, 1 exception", + PolicySetName: "policy3", + PolicyOutput: "1 test, 0 passed, 1 warning, 1 failure, 1 exception", }, }, exp: `policy set: policy1: 20 tests, 19 passed, 2 warnings, 0 failures, 0 exceptions diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go index 317e8516a1..02245b27fd 100644 --- a/server/events/project_command_context_builder.go +++ b/server/events/project_command_context_builder.go @@ -274,6 +274,7 @@ func newProjectCommandContext(ctx *command.Context, AutomergeEnabled: automergeEnabled, DeleteSourceBranchOnMerge: projCfg.DeleteSourceBranchOnMerge, RepoLocking: projCfg.RepoLocking, + CustomPolicyCheck: projCfg.CustomPolicyCheck, ParallelApplyEnabled: parallelApplyEnabled, ParallelPlanEnabled: parallelPlanEnabled, ParallelPolicyCheckEnabled: parallelPlanEnabled, diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 1e8e2fe315..69ae9e73d4 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -425,7 +425,7 @@ func (p *DefaultProjectCommandRunner) doPolicyCheck(ctx command.ProjectContext) if !lockAttempt.LockAcquired { return nil, lockAttempt.LockFailureReason, nil } - ctx.Log.Debug("acquired lock for project") + ctx.Log.Debug("acquired lock for project.") // Acquire internal lock for the directory we're going to operate in. // We should refactor this to keep the lock for the duration of plan and policy check since as of now @@ -489,14 +489,23 @@ func (p *DefaultProjectCommandRunner) doPolicyCheck(ctx command.ProjectContext) var preConftestOutput []string var postConftestOutput []string var policySetResults []models.PolicySetResult + for i, output := range outputs { index = i - err = json.Unmarshal([]byte(strings.Join([]string{output}, "\n")), &policySetResults) - if err == nil { - break + if !ctx.CustomPolicyCheck { + err = json.Unmarshal([]byte(strings.Join([]string{output}, "\n")), &policySetResults) + if err == nil { + break + } + preConftestOutput = append(preConftestOutput, output) + } else { + // Using a policy tool other than Conftest, manually building result struct + passed := !strings.Contains(strings.ToLower(output), "fail") + policySetResults = append(policySetResults, models.PolicySetResult{PolicySetName: "Custom", PolicyOutput: output, Passed: passed, ReqApprovals: 1, CurApprovals: 0}) + preConftestOutput = append(preConftestOutput, "") } - preConftestOutput = append(preConftestOutput, output) } + if policySetResults == nil { return nil, "", errors.New("unable to unmarshal conftest output") } diff --git a/server/events/templates/policy_check.tmpl b/server/events/templates/policy_check.tmpl index dcabbea963..69ab18cc8d 100644 --- a/server/events/templates/policy_check.tmpl +++ b/server/events/templates/policy_check.tmpl @@ -3,7 +3,7 @@ {{ range $ps, $policy_sets }} #### Policy Set: `{{ $ps.PolicySetName }}` ```diff -{{ $ps.ConftestOutput }} +{{ $ps.PolicyOutput }} ``` {{ end }} {{ end }}