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  }