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  }