Skip to content

Commit

Permalink
feat: add silence_pr_comments on plan and apply (#4543)
Browse files Browse the repository at this point in the history
  • Loading branch information
anryko committed Jun 1, 2024
1 parent 5a3ac1b commit 78373f6
Show file tree
Hide file tree
Showing 17 changed files with 147 additions and 35 deletions.
3 changes: 3 additions & 0 deletions runatlantis.io/docs/repo-level-atlantis-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ projects:
plan_requirements: [mergeable, approved, undiverged]
apply_requirements: [mergeable, approved, undiverged]
import_requirements: [mergeable, approved, undiverged]
silence_pr_comments: ["apply"]
execution_order_group: 1
depends_on:
- project-1
Expand Down Expand Up @@ -433,6 +434,7 @@ terraform_version: 0.11.0
plan_requirements: ["approved"]
apply_requirements: ["approved"]
import_requirements: ["approved"]
silence_pr_comments: ["apply"]
workflow: myworkflow
```

Expand All @@ -452,6 +454,7 @@ workflow: myworkflow
| plan_requirements<br />*(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.md) for more details. |
| apply_requirements<br />*(restricted)* | array\[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.md) for more details. |
| import_requirements<br />*(restricted)* | array\[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.md) for more details. |
| silence_pr_comments | array\[string\] | none | no | Silence PR comments from defined stages while preserving PR status checks. Supported values are: `plan`, `apply`. |
| workflow <br />*(restricted)* | string | none | no | A custom workflow. If not specified, Atlantis will use its default workflow. |

::: tip
Expand Down
1 change: 1 addition & 0 deletions runatlantis.io/docs/server-side-repo-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ If you set a workflow with the key `default`, it will override this.
| 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. |
| autodiscover | AutoDiscover | none | no | Auto discover settings for this repo |
| silence_pr_comments | []string | none | no | Silence PR comments from defined stages while preserving PR status checks. Useful in large environments with many Atlantis instances and/or projects, when the comments are too big and too many, therefore it is preferable to rely solely on PR status checks. Supported values are: `plan`, `apply`. |

:::tip Notes

Expand Down
10 changes: 8 additions & 2 deletions server/core/config/parser_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,7 +1286,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\", \"repo_locks\", \"policy_check\", and \"custom_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\", \"repo_locks\", \"policy_check\", \"custom_policy_check\", and \"silence_pr_comments\" are supported.).).",
},
"invalid plan_requirement": {
input: `repos:
Expand All @@ -1306,8 +1306,14 @@ func TestParseGlobalCfg(t *testing.T) {
import_requirements: [invalid]`,
expErr: "repos: (0: (import_requirements: \"invalid\" is not a valid import_requirement, only \"approved\", \"mergeable\" and \"undiverged\" are supported.).).",
},
"invalid silence_pr_comments": {
input: `repos:
- id: /.*/
silence_pr_comments: [invalid]`,
expErr: "server-side repo config 'silence_pr_comments' key value of 'invalid' is not supported, supported values are [plan, apply]",
},
"disable autodiscover": {
input: `repos:
input: `repos:
- id: /.*/
autodiscover:
mode: disabled`,
Expand Down
25 changes: 23 additions & 2 deletions server/core/config/raw/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
validation "github.com/go-ozzo/ozzo-validation"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/utils"
)

// GlobalCfg is the raw schema for server-side repo config.
Expand Down Expand Up @@ -38,6 +39,7 @@ type Repo struct {
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 {
Expand Down Expand Up @@ -94,6 +96,24 @@ func (g GlobalCfg) Validate() error {
}
}
}

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

Expand Down Expand Up @@ -195,8 +215,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.RepoLocksKey && o != valid.PolicyCheckKey && o != valid.CustomPolicyCheckKey {
return fmt.Errorf("%q is not a valid override, only %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)
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
Expand Down Expand Up @@ -365,5 +385,6 @@ OuterGlobalImportReqs:
PolicyCheck: r.PolicyCheck,
CustomPolicyCheck: r.CustomPolicyCheck,
AutoDiscover: autoDiscover,
SilencePRComments: r.SilencePRComments,
}
}
5 changes: 5 additions & 0 deletions server/core/config/raw/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Project struct {
ExecutionOrderGroup *int `yaml:"execution_order_group,omitempty"`
PolicyCheck *bool `yaml:"policy_check,omitempty"`
CustomPolicyCheck *bool `yaml:"custom_policy_check,omitempty"`
SilencePRComments []string `yaml:"silence_pr_comments,omitempty"`
}

func (p Project) Validate() error {
Expand Down Expand Up @@ -156,6 +157,10 @@ func (p Project) ToValid() valid.Project {
v.CustomPolicyCheck = p.CustomPolicyCheck
}

if p.SilencePRComments != nil {
v.SilencePRComments = p.SilencePRComments
}

return v
}

Expand Down
2 changes: 2 additions & 0 deletions server/core/config/raw/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type RepoCfg struct {
AllowedRegexpPrefixes []string `yaml:"allowed_regexp_prefixes,omitempty"`
AbortOnExcecutionOrderFail *bool `yaml:"abort_on_execution_order_fail,omitempty"`
RepoLocks *RepoLocks `yaml:"repo_locks,omitempty"`
SilencePRComments []string `yaml:"silence_pr_comments,omitempty"`
}

func (r RepoCfg) Validate() error {
Expand Down Expand Up @@ -96,5 +97,6 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
EmojiReaction: emojiReaction,
AbortOnExcecutionOrderFail: abortOnExcecutionOrderFail,
RepoLocks: repoLocks,
SilencePRComments: r.SilencePRComments,
}
}
65 changes: 58 additions & 7 deletions server/core/config/valid/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const RepoLocksKey = "repo_locks"
const PolicyCheckKey = "policy_check"
const CustomPolicyCheckKey = "custom_policy_check"
const AutoDiscoverKey = "autodiscover"
const SilencePRCommentsKey = "silence_pr_comments"

var AllowedSilencePRComments = []string{"plan", "apply"}

// DefaultAtlantisFile is the default name of the config file for each repo.
const DefaultAtlantisFile = "atlantis.yaml"
Expand Down Expand Up @@ -85,6 +88,7 @@ type Repo struct {
PolicyCheck *bool
CustomPolicyCheck *bool
AutoDiscover *AutoDiscover
SilencePRComments []string
}

type MergedProjectCfg struct {
Expand All @@ -107,6 +111,7 @@ type MergedProjectCfg struct {
RepoLocks RepoLocks
PolicyCheck bool
CustomPolicyCheck bool
SilencePRComments []string
}

// WorkflowHook is a map of custom run commands to run before or after workflows.
Expand Down Expand Up @@ -212,8 +217,9 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg {
repoLocks := DefaultRepoLocks
customPolicyCheck := false
autoDiscover := AutoDiscover{Mode: AutoDiscoverAutoMode}
var silencePRComments []string
if args.AllowAllRepoSettings {
allowedOverrides = []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, RepoLocksKey, PolicyCheckKey}
allowedOverrides = []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, RepoLocksKey, PolicyCheckKey, SilencePRCommentsKey}
allowCustomWorkflows = true
}

Expand All @@ -237,6 +243,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg {
PolicyCheck: &policyCheck,
CustomPolicyCheck: &customPolicyCheck,
AutoDiscover: &autoDiscover,
SilencePRComments: silencePRComments,
},
},
Workflows: map[string]Workflow{
Expand Down Expand Up @@ -273,7 +280,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, repoLocks, policyCheck, customPolicyCheck, _ := g.getMatchingCfg(log, repoID)
planReqs, applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocks, policyCheck, customPolicyCheck, _, silencePRComments := g.getMatchingCfg(log, repoID)
// If repos are allowed to override certain keys then override them.
for _, key := range allowedOverrides {
switch key {
Expand Down Expand Up @@ -366,12 +373,29 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
log.Debug("overriding server-defined %s with repo settings: [%t]", CustomPolicyCheckKey, *proj.CustomPolicyCheck)
customPolicyCheck = *proj.CustomPolicyCheck
}
case SilencePRCommentsKey:
if proj.SilencePRComments != nil {
log.Debug("overriding repo-root-defined %s with repo settings: [%t]", SilencePRCommentsKey, strings.Join(proj.SilencePRComments, ","))
silencePRComments = proj.SilencePRComments
} else if rCfg.SilencePRComments != nil {
log.Debug("overriding server-defined %s with repo settings: [%s]", SilencePRCommentsKey, strings.Join(rCfg.SilencePRComments, ","))
silencePRComments = rCfg.SilencePRComments
}
}
log.Debug("MergeProjectCfg completed")
}

log.Debug("final settings: %s: [%s], %s: [%s], %s: [%s], %s: %s",
PlanRequirementsKey, strings.Join(planReqs, ","), ApplyRequirementsKey, strings.Join(applyReqs, ","), ImportRequirementsKey, strings.Join(importReqs, ","), WorkflowKey, workflow.Name)
log.Debug("final settings: %s: [%s], %s: [%s], %s: [%s], %s: %s, %s: %t, %s: %s, %s: %t, %s: %t, %s: [%s]",
PlanRequirementsKey, strings.Join(planReqs, ","),
ApplyRequirementsKey, strings.Join(applyReqs, ","),
ImportRequirementsKey, strings.Join(importReqs, ","),
WorkflowKey, workflow.Name,
DeleteSourceBranchOnMergeKey, deleteSourceBranchOnMerge,
RepoLockingKey, repoLocks.Mode,
PolicyCheckKey, policyCheck,
CustomPolicyCheckKey, policyCheck,
SilencePRCommentsKey, strings.Join(silencePRComments, ","),
)

return MergedProjectCfg{
PlanRequirements: planReqs,
Expand All @@ -391,14 +415,15 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
RepoLocks: repoLocks,
PolicyCheck: policyCheck,
CustomPolicyCheck: customPolicyCheck,
SilencePRComments: silencePRComments,
}
}

// DefaultProjCfg returns the default project config for all projects under the
// 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, repoLocks, policyCheck, customPolicyCheck, _ := g.getMatchingCfg(log, repoID)
planReqs, applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocks, policyCheck, customPolicyCheck, _, silencePRComments := g.getMatchingCfg(log, repoID)
return MergedProjectCfg{
PlanRequirements: planReqs,
ApplyRequirements: applyReqs,
Expand All @@ -414,6 +439,7 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo
RepoLocks: repoLocks,
PolicyCheck: policyCheck,
CustomPolicyCheck: customPolicyCheck,
SilencePRComments: silencePRComments,
}
}

Expand Down Expand Up @@ -474,6 +500,26 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error {
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)
}
if p.SilencePRComments != nil {
if !utils.SlicesContains(allowedOverrides, SilencePRCommentsKey) {
return fmt.Errorf(
"repo config not allowed to set '%s' key: server-side config needs '%s: [%s]'",
SilencePRCommentsKey,
AllowedOverridesKey,
SilencePRCommentsKey,
)
}
for _, silenceStage := range p.SilencePRComments {
if !utils.SlicesContains(AllowedSilencePRComments, silenceStage) {
return fmt.Errorf(
"repo config '%s' key value of '%s' is not supported, supported values are [%s]",
SilencePRCommentsKey,
silenceStage,
strings.Join(AllowedSilencePRComments, ", "),
)
}
}
}
}

// Check custom workflows.
Expand Down Expand Up @@ -532,7 +578,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, repoLocks RepoLocks, policyCheck bool, customPolicyCheck bool, autoDiscover AutoDiscover) {
func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (planReqs []string, applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocks RepoLocks, policyCheck bool, customPolicyCheck bool, autoDiscover AutoDiscover, silencePRComments []string) {
toLog := make(map[string]string)
traceF := func(repoIdx int, repoID string, key string, val interface{}) string {
from := "default server config"
Expand All @@ -559,7 +605,7 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla
repoLocking := true
repoLocks = DefaultRepoLocks

for _, key := range []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, RepoLocksKey, PolicyCheckKey, CustomPolicyCheckKey} {
for _, key := range []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, RepoLocksKey, PolicyCheckKey, CustomPolicyCheckKey, SilencePRCommentsKey} {
for i, repo := range g.Repos {
if repo.IDMatches(repoID) {
switch key {
Expand Down Expand Up @@ -623,6 +669,11 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla
toLog[AutoDiscoverKey] = traceF(i, repo.IDString(), AutoDiscoverKey, repo.AutoDiscover.Mode)
autoDiscover = *repo.AutoDiscover
}
case SilencePRCommentsKey:
if repo.SilencePRComments != nil {
toLog[SilencePRCommentsKey] = traceF(i, repo.IDString(), SilencePRCommentsKey, repo.SilencePRComments)
silencePRComments = repo.SilencePRComments
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion server/core/config/valid/global_cfg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func TestNewGlobalCfg(t *testing.T) {

if c.allowAllRepoSettings {
exp.Repos[0].AllowCustomWorkflows = Bool(true)
exp.Repos[0].AllowedOverrides = []string{"plan_requirements", "apply_requirements", "import_requirements", "workflow", "delete_source_branch_on_merge", "repo_locking", "repo_locks", "policy_check"}
exp.Repos[0].AllowedOverrides = []string{"plan_requirements", "apply_requirements", "import_requirements", "workflow", "delete_source_branch_on_merge", "repo_locking", "repo_locks", "policy_check", "silence_pr_comments"}
}
if c.policyCheckEnabled {
exp.Repos[0].PlanRequirements = append(exp.Repos[0].PlanRequirements, "policies_passed")
Expand Down
2 changes: 2 additions & 0 deletions server/core/config/valid/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type RepoCfg struct {
EmojiReaction string
AllowedRegexpPrefixes []string
AbortOnExcecutionOrderFail bool
SilencePRComments []string
}

func (r RepoCfg) FindProjectsByDirWorkspace(repoRelDir string, workspace string) []Project {
Expand Down Expand Up @@ -158,6 +159,7 @@ type Project struct {
ExecutionOrderGroup int
PolicyCheck *bool
CustomPolicyCheck *bool
SilencePRComments []string
}

// GetName returns the name of the project or an empty string if there is no
Expand Down
Loading

0 comments on commit 78373f6

Please sign in to comment.