github.com/jenkins-x/test-infra@v0.0.7/prow/config/branch_protection.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package config 18 19 import ( 20 "errors" 21 "fmt" 22 23 "github.com/sirupsen/logrus" 24 "k8s.io/apimachinery/pkg/util/sets" 25 ) 26 27 // Policy for the config/org/repo/branch. 28 // When merging policies, a nil value results in inheriting the parent policy. 29 type Policy struct { 30 // Protect overrides whether branch protection is enabled if set. 31 Protect *bool `json:"protect,omitempty"` 32 // RequiredStatusChecks configures github contexts 33 RequiredStatusChecks *ContextPolicy `json:"required_status_checks,omitempty"` 34 // Admins overrides whether protections apply to admins if set. 35 Admins *bool `json:"enforce_admins,omitempty"` 36 // Restrictions limits who can merge 37 Restrictions *Restrictions `json:"restrictions,omitempty"` 38 // RequiredPullRequestReviews specifies github approval/review criteria. 39 RequiredPullRequestReviews *ReviewPolicy `json:"required_pull_request_reviews,omitempty"` 40 } 41 42 func (p Policy) defined() bool { 43 return p.Protect != nil || p.RequiredStatusChecks != nil || p.Admins != nil || p.Restrictions != nil || p.RequiredPullRequestReviews != nil 44 } 45 46 // ContextPolicy configures required github contexts. 47 // When merging policies, contexts are appended to context list from parent. 48 // Strict determines whether merging to the branch invalidates existing contexts. 49 type ContextPolicy struct { 50 // Contexts appends required contexts that must be green to merge 51 Contexts []string `json:"contexts,omitempty"` 52 // Strict overrides whether new commits in the base branch require updating the PR if set 53 Strict *bool `json:"strict,omitempty"` 54 } 55 56 // ReviewPolicy specifies github approval/review criteria. 57 // Any nil values inherit the policy from the parent, otherwise bool/ints are overridden. 58 // Non-empty lists are appended to parent lists. 59 type ReviewPolicy struct { 60 // Restrictions appends users/teams that are allowed to merge 61 DismissalRestrictions *Restrictions `json:"dismissal_restrictions,omitempty"` 62 // DismissStale overrides whether new commits automatically dismiss old reviews if set 63 DismissStale *bool `json:"dismiss_stale_reviews,omitempty"` 64 // RequireOwners overrides whether CODEOWNERS must approve PRs if set 65 RequireOwners *bool `json:"require_code_owner_reviews,omitempty"` 66 // Approvals overrides the number of approvals required if set (set to 0 to disable) 67 Approvals *int `json:"required_approving_review_count,omitempty"` 68 } 69 70 // Restrictions limits who can merge 71 // Users and Teams items are appended to parent lists. 72 type Restrictions struct { 73 Users []string `json:"users"` 74 Teams []string `json:"teams"` 75 } 76 77 // selectInt returns the child if set, else parent 78 func selectInt(parent, child *int) *int { 79 if child != nil { 80 return child 81 } 82 return parent 83 } 84 85 // selectBool returns the child argument if set, otherwise the parent 86 func selectBool(parent, child *bool) *bool { 87 if child != nil { 88 return child 89 } 90 return parent 91 } 92 93 // unionStrings merges the parent and child items together 94 func unionStrings(parent, child []string) []string { 95 if child == nil { 96 return parent 97 } 98 if parent == nil { 99 return child 100 } 101 s := sets.NewString(parent...) 102 s.Insert(child...) 103 return s.List() 104 } 105 106 func mergeContextPolicy(parent, child *ContextPolicy) *ContextPolicy { 107 if child == nil { 108 return parent 109 } 110 if parent == nil { 111 return child 112 } 113 return &ContextPolicy{ 114 Contexts: unionStrings(parent.Contexts, child.Contexts), 115 Strict: selectBool(parent.Strict, child.Strict), 116 } 117 } 118 119 func mergeReviewPolicy(parent, child *ReviewPolicy) *ReviewPolicy { 120 if child == nil { 121 return parent 122 } 123 if parent == nil { 124 return child 125 } 126 return &ReviewPolicy{ 127 DismissalRestrictions: mergeRestrictions(parent.DismissalRestrictions, child.DismissalRestrictions), 128 DismissStale: selectBool(parent.DismissStale, child.DismissStale), 129 RequireOwners: selectBool(parent.RequireOwners, child.RequireOwners), 130 Approvals: selectInt(parent.Approvals, child.Approvals), 131 } 132 } 133 134 func mergeRestrictions(parent, child *Restrictions) *Restrictions { 135 if child == nil { 136 return parent 137 } 138 if parent == nil { 139 return child 140 } 141 return &Restrictions{ 142 Users: unionStrings(parent.Users, child.Users), 143 Teams: unionStrings(parent.Teams, child.Teams), 144 } 145 } 146 147 // Apply returns a policy that merges the child into the parent 148 func (p Policy) Apply(child Policy) (Policy, error) { 149 return Policy{ 150 Protect: selectBool(p.Protect, child.Protect), 151 RequiredStatusChecks: mergeContextPolicy(p.RequiredStatusChecks, child.RequiredStatusChecks), 152 Admins: selectBool(p.Admins, child.Admins), 153 Restrictions: mergeRestrictions(p.Restrictions, child.Restrictions), 154 RequiredPullRequestReviews: mergeReviewPolicy(p.RequiredPullRequestReviews, child.RequiredPullRequestReviews), 155 }, nil 156 } 157 158 // BranchProtection specifies the global branch protection policy 159 type BranchProtection struct { 160 Policy 161 ProtectTested bool `json:"protect-tested-repos,omitempty"` 162 Orgs map[string]Org `json:"orgs,omitempty"` 163 AllowDisabledPolicies bool `json:"allow_disabled_policies,omitempty"` 164 } 165 166 // GetOrg returns the org config after merging in any global policies. 167 func (bp BranchProtection) GetOrg(name string) (*Org, error) { 168 o, ok := bp.Orgs[name] 169 if ok { 170 var err error 171 if o.Policy, err = bp.Apply(o.Policy); err != nil { 172 return nil, err 173 } 174 } else { 175 o.Policy = bp.Policy 176 } 177 return &o, nil 178 } 179 180 // Org holds the default protection policy for an entire org, as well as any repo overrides. 181 type Org struct { 182 Policy 183 Repos map[string]Repo `json:"repos,omitempty"` 184 } 185 186 // GetRepo returns the repo config after merging in any org policies. 187 func (o Org) GetRepo(name string) (*Repo, error) { 188 r, ok := o.Repos[name] 189 if ok { 190 var err error 191 if r.Policy, err = o.Apply(r.Policy); err != nil { 192 return nil, err 193 } 194 } else { 195 r.Policy = o.Policy 196 } 197 return &r, nil 198 } 199 200 // Repo holds protection policy overrides for all branches in a repo, as well as specific branch overrides. 201 type Repo struct { 202 Policy 203 Branches map[string]Branch `json:"branches,omitempty"` 204 } 205 206 // GetBranch returns the branch config after merging in any repo policies. 207 func (r Repo) GetBranch(name string) (*Branch, error) { 208 b, ok := r.Branches[name] 209 if ok { 210 var err error 211 if b.Policy, err = r.Apply(b.Policy); err != nil { 212 return nil, err 213 } 214 if b.Protect == nil { 215 return nil, errors.New("defined branch policies must set protect") 216 } 217 } else { 218 b.Policy = r.Policy 219 } 220 return &b, nil 221 } 222 223 // Branch holds protection policy overrides for a particular branch. 224 type Branch struct { 225 Policy 226 } 227 228 // GetBranchProtection returns the policy for a given branch. 229 // 230 // Handles merging any policies defined at repo/org/global levels into the branch policy. 231 func (c *Config) GetBranchProtection(org, repo, branch string) (*Policy, error) { 232 if _, present := c.BranchProtection.Orgs[org]; !present { 233 return nil, nil // only consider branches in configured orgs 234 } 235 var b *Branch 236 if o, err := c.BranchProtection.GetOrg(org); err != nil { 237 return nil, fmt.Errorf("org: %v", err) 238 } else if r, err := o.GetRepo(repo); err != nil { 239 return nil, fmt.Errorf("repo: %v", err) 240 } else if b, err = r.GetBranch(branch); err != nil { 241 return nil, fmt.Errorf("branch: %v", err) 242 } 243 244 return c.GetPolicy(org, repo, branch, *b) 245 } 246 247 // GetPolicy returns the protection policy for the branch, after merging in presubmits. 248 func (c *Config) GetPolicy(org, repo, branch string, b Branch) (*Policy, error) { 249 policy := b.Policy 250 251 // Automatically require any required prow jobs 252 if prowContexts, _ := BranchRequirements(org, repo, branch, c.Presubmits); len(prowContexts) > 0 { 253 // Error if protection is disabled 254 if policy.Protect != nil && !*policy.Protect { 255 return nil, fmt.Errorf("required prow jobs require branch protection") 256 } 257 ps := Policy{ 258 RequiredStatusChecks: &ContextPolicy{ 259 Contexts: prowContexts, 260 }, 261 } 262 // Require protection by default if ProtectTested is true 263 if c.BranchProtection.ProtectTested { 264 yes := true 265 ps.Protect = &yes 266 } 267 var err error 268 if policy, err = policy.Apply(ps); err != nil { 269 return nil, err 270 } 271 } 272 273 if policy.Protect != nil && !*policy.Protect { 274 // Ensure that protection is false => no protection settings 275 var old *bool 276 old, policy.Protect = policy.Protect, old 277 switch { 278 case policy.defined() && c.BranchProtection.AllowDisabledPolicies: 279 logrus.Warnf("%s/%s=%s defines a policy but has protect: false", org, repo, branch) 280 policy = Policy{ 281 Protect: policy.Protect, 282 } 283 case policy.defined(): 284 return nil, fmt.Errorf("%s/%s=%s defines a policy, which requires protect: true", org, repo, branch) 285 } 286 policy.Protect = old 287 } 288 289 if !policy.defined() { 290 return nil, nil 291 } 292 return &policy, nil 293 } 294 295 func jobRequirements(jobs []Presubmit, branch string, after bool) ([]string, []string) { 296 var required, optional []string 297 for _, j := range jobs { 298 if !j.Brancher.RunsAgainstBranch(branch) { 299 continue 300 } 301 // Does this job require a context or have kids that might need one? 302 if !after && !j.AlwaysRun && j.RunIfChanged == "" { 303 continue // No 304 } 305 if j.ContextRequired() { // This job needs a context 306 required = append(required, j.Context) 307 } else { 308 optional = append(optional, j.Context) 309 } 310 // Check which children require contexts 311 r, o := jobRequirements(j.RunAfterSuccess, branch, true) 312 required = append(required, r...) 313 optional = append(optional, o...) 314 } 315 return required, optional 316 } 317 318 // BranchRequirements returns required and optional presubmits prow jobs for a given org, repo branch. 319 func BranchRequirements(org, repo, branch string, presubmits map[string][]Presubmit) ([]string, []string) { 320 p, ok := presubmits[org+"/"+repo] 321 if !ok { 322 return nil, nil 323 } 324 return jobRequirements(p, branch, false) 325 }