github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/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 deprecatedPolicy 31 deprecatedWarning bool // true if a warning message was sent 32 // Protect overrides whether branch protection is enabled if set. 33 Protect *bool `json:"protect,omitempty"` 34 // RequiredStatusChecks configures github contexts 35 RequiredStatusChecks *ContextPolicy `json:"required_status_checks,omitempty"` 36 // Admins overrides whether protections apply to admins if set. 37 Admins *bool `json:"enforce_admins,omitempty"` 38 // Restrictions limits who can merge 39 Restrictions *Restrictions `json:"restrictions,omitempty"` 40 // RequiredPullRequestReviews specifies github approval/review criteria. 41 RequiredPullRequestReviews *ReviewPolicy `json:"required_pull_request_reviews,omitempty"` 42 } 43 44 // deprecatedPolicy deserializes fields that are no longer in use 45 type deprecatedPolicy struct { 46 DeprecatedProtect *bool `json:"protect-by-default,omitempty"` 47 DeprecatedContexts []string `json:"require-contexts,omitempty"` 48 DeprecatedPushers []string `json:"allow-push,omitempty"` 49 } 50 51 func (d deprecatedPolicy) defined() bool { 52 return d.DeprecatedProtect != nil || d.DeprecatedContexts != nil || d.DeprecatedPushers != nil 53 } 54 55 func (p Policy) defined() bool { 56 return p.Protect != nil || p.RequiredStatusChecks != nil || p.Admins != nil || p.Restrictions != nil || p.RequiredPullRequestReviews != nil 57 } 58 59 // HasProtect returns true if the policy or deprecated policy defines protection 60 func (p Policy) HasProtect() bool { 61 return p.Protect != nil || p.deprecatedPolicy.DeprecatedProtect != nil 62 } 63 64 // ContextPolicy configures required github contexts. 65 // When merging policies, contexts are appended to context list from parent. 66 // Strict determines whether merging to the branch invalidates existing contexts. 67 type ContextPolicy struct { 68 // Contexts appends required contexts that must be green to merge 69 Contexts []string `json:"contexts,omitempty"` 70 // Strict overrides whether new commits in the base branch require updating the PR if set 71 Strict *bool `json:"strict,omitempty"` 72 } 73 74 // ReviewPolicy specifies github approval/review criteria. 75 // Any nil values inherit the policy from the parent, otherwise bool/ints are overridden. 76 // Non-empty lists are appended to parent lists. 77 type ReviewPolicy struct { 78 // Restrictions appends users/teams that are allowed to merge 79 DismissalRestrictions *Restrictions `json:"dismissal_restrictions,omitempty"` 80 // DismissStale overrides whether new commits automatically dismiss old reviews if set 81 DismissStale *bool `json:"dismiss_stale_reviews,omitempty"` 82 // RequireOwners overrides whether CODEOWNERS must approve PRs if set 83 RequireOwners *bool `json:"require_code_owner_reviews,omitempty"` 84 // Approvals overrides the number of approvals required if set (set to 0 to disable) 85 Approvals *int `json:"required_approving_review_count,omitempty"` 86 } 87 88 // Restrictions limits who can merge 89 // Users and Teams items are appended to parent lists. 90 type Restrictions struct { 91 Users []string `json:"users"` 92 Teams []string `json:"teams"` 93 } 94 95 // selectInt returns the child if set, else parent 96 func selectInt(parent, child *int) *int { 97 if child != nil { 98 return child 99 } 100 return parent 101 } 102 103 // selectBool returns the child argument if set, otherwise the parent 104 func selectBool(parent, child *bool) *bool { 105 if child != nil { 106 return child 107 } 108 return parent 109 } 110 111 // unionStrings merges the parent and child items together 112 func unionStrings(parent, child []string) []string { 113 if child == nil { 114 return parent 115 } 116 if parent == nil { 117 return child 118 } 119 s := sets.NewString(parent...) 120 s.Insert(child...) 121 return s.List() 122 } 123 124 func mergeContextPolicy(parent, child *ContextPolicy) *ContextPolicy { 125 if child == nil { 126 return parent 127 } 128 if parent == nil { 129 return child 130 } 131 return &ContextPolicy{ 132 Contexts: unionStrings(parent.Contexts, child.Contexts), 133 Strict: selectBool(parent.Strict, child.Strict), 134 } 135 } 136 137 func mergeReviewPolicy(parent, child *ReviewPolicy) *ReviewPolicy { 138 if child == nil { 139 return parent 140 } 141 if parent == nil { 142 return child 143 } 144 return &ReviewPolicy{ 145 DismissalRestrictions: mergeRestrictions(parent.DismissalRestrictions, child.DismissalRestrictions), 146 DismissStale: selectBool(parent.DismissStale, child.DismissStale), 147 RequireOwners: selectBool(parent.RequireOwners, child.RequireOwners), 148 Approvals: selectInt(parent.Approvals, child.Approvals), 149 } 150 } 151 152 func mergeRestrictions(parent, child *Restrictions) *Restrictions { 153 if child == nil { 154 return parent 155 } 156 if parent == nil { 157 return child 158 } 159 return &Restrictions{ 160 Users: unionStrings(parent.Users, child.Users), 161 Teams: unionStrings(parent.Teams, child.Teams), 162 } 163 } 164 165 // Apply returns a policy that merges the child into the parent 166 func (p Policy) Apply(child Policy) (Policy, error) { 167 if old := child.deprecatedPolicy.defined(); old && child.defined() { 168 return p, errors.New("cannot mix Policy and deprecatedPolicy branch protection fields") 169 } else if old { 170 if !p.deprecatedWarning { 171 p.deprecatedWarning = true 172 logrus.Warn("WARNING: protect-by-default, require-contexts, allow-push are deprecated. Please replace them before July 2018") 173 } 174 d := child.deprecatedPolicy 175 child = Policy{ 176 Protect: d.DeprecatedProtect, 177 } 178 if d.DeprecatedContexts != nil { 179 child.RequiredStatusChecks = &ContextPolicy{ 180 Contexts: d.DeprecatedContexts, 181 } 182 } 183 if d.DeprecatedPushers != nil { 184 child.Restrictions = &Restrictions{ 185 Teams: d.DeprecatedPushers, 186 } 187 } 188 } 189 190 return Policy{ 191 Protect: selectBool(p.Protect, child.Protect), 192 RequiredStatusChecks: mergeContextPolicy(p.RequiredStatusChecks, child.RequiredStatusChecks), 193 Admins: selectBool(p.Admins, child.Admins), 194 Restrictions: mergeRestrictions(p.Restrictions, child.Restrictions), 195 RequiredPullRequestReviews: mergeReviewPolicy(p.RequiredPullRequestReviews, child.RequiredPullRequestReviews), 196 deprecatedWarning: p.deprecatedWarning, 197 }, nil 198 } 199 200 // BranchProtection specifies the global branch protection policy 201 type BranchProtection struct { 202 Policy 203 ProtectTested bool `json:"protect-tested-repos,omitempty"` 204 Orgs map[string]Org `json:"orgs,omitempty"` 205 AllowDisabledPolicies bool `json:"allow_disabled_policies,omitempty"` 206 207 warned bool // warn if deprecated fields are use 208 } 209 210 // Org holds the default protection policy for an entire org, as well as any repo overrides. 211 type Org struct { 212 Policy 213 Repos map[string]Repo `json:"repos,omitempty"` 214 } 215 216 // Repo holds protection policy overrides for all branches in a repo, as well as specific branch overrides. 217 type Repo struct { 218 Policy 219 Branches map[string]Branch `json:"branches,omitempty"` 220 } 221 222 // Branch holds protection policy overrides for a particular branch. 223 type Branch struct { 224 Policy 225 } 226 227 // GetBranchProtection returns the policy for a given branch. 228 // 229 // Handles merging any policies defined at repo/org/global levels into the branch policy. 230 func (c *Config) GetBranchProtection(org, repo, branch string) (*Policy, error) { 231 bp := c.BranchProtection 232 var policy Policy 233 policy, err := policy.Apply(bp.Policy) 234 if err != nil { 235 return nil, err 236 } 237 238 if o, ok := bp.Orgs[org]; ok { 239 policy, err = policy.Apply(o.Policy) 240 if err != nil { 241 return nil, err 242 } 243 if r, ok := o.Repos[repo]; ok { 244 policy, err = policy.Apply(r.Policy) 245 if err != nil { 246 return nil, err 247 } 248 if b, ok := r.Branches[branch]; ok { 249 policy, err = policy.Apply(b.Policy) 250 if err != nil { 251 return nil, err 252 } 253 if policy.Protect == nil { 254 return nil, errors.New("defined branch policies must set protect") 255 } 256 } 257 } 258 } else { 259 return nil, nil 260 } 261 262 // Automatically require any required prow jobs 263 if prowContexts, _ := BranchRequirements(org, repo, branch, c.Presubmits); len(prowContexts) > 0 { 264 // Error if protection is disabled 265 if policy.Protect != nil && !*policy.Protect { 266 return nil, fmt.Errorf("required prow jobs require branch protection") 267 } 268 ps := Policy{ 269 RequiredStatusChecks: &ContextPolicy{ 270 Contexts: prowContexts, 271 }, 272 } 273 // Require protection by default if ProtectTested is true 274 if bp.ProtectTested { 275 yes := true 276 ps.Protect = &yes 277 } 278 policy, err = policy.Apply(ps) 279 if err != nil { 280 return nil, err 281 } 282 } 283 284 if policy.Protect != nil && !*policy.Protect { 285 // Ensure that protection is false => no protection settings 286 var old *bool 287 old, policy.Protect = policy.Protect, old 288 switch { 289 case policy.defined() && bp.AllowDisabledPolicies: 290 logrus.Warnf("%s/%s=%s defines a policy but has protect: false", org, repo, branch) 291 policy = Policy{ 292 Protect: policy.Protect, 293 } 294 case policy.defined(): 295 return nil, fmt.Errorf("%s/%s=%s defines a policy, which requires protect: true", org, repo, branch) 296 } 297 policy.Protect = old 298 } 299 300 if !policy.defined() { 301 return nil, nil 302 } 303 return &policy, nil 304 } 305 306 func jobRequirements(jobs []Presubmit, branch string, after bool) ([]string, []string) { 307 var required, optional []string 308 for _, j := range jobs { 309 if !j.Brancher.RunsAgainstBranch(branch) { 310 continue 311 } 312 // Does this job require a context or have kids that might need one? 313 if !after && !j.AlwaysRun && j.RunIfChanged == "" { 314 continue // No 315 } 316 if j.ContextRequired() { // This job needs a context 317 required = append(required, j.Context) 318 } else { 319 optional = append(optional, j.Context) 320 } 321 // Check which children require contexts 322 r, o := jobRequirements(j.RunAfterSuccess, branch, true) 323 required = append(required, r...) 324 optional = append(optional, o...) 325 } 326 return required, optional 327 } 328 329 // BranchRequirements returns required and optional presubmits prow jobs for a given org, repo branch. 330 func BranchRequirements(org, repo, branch string, presubmits map[string][]Presubmit) ([]string, []string) { 331 p, ok := presubmits[org+"/"+repo] 332 if !ok { 333 return nil, nil 334 } 335 return jobRequirements(p, branch, false) 336 }