github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/cmd/branchprotector/protect.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 main
    18  
    19  import (
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"os"
    24  	"strings"
    25  	"sync"
    26  
    27  	"github.com/sirupsen/logrus"
    28  
    29  	"k8s.io/test-infra/prow/config"
    30  	"k8s.io/test-infra/prow/config/secret"
    31  	"k8s.io/test-infra/prow/flagutil"
    32  	"k8s.io/test-infra/prow/github"
    33  	"k8s.io/test-infra/prow/logrusutil"
    34  )
    35  
    36  type options struct {
    37  	config    string
    38  	jobConfig string
    39  	confirm   bool
    40  	github    flagutil.GitHubOptions
    41  }
    42  
    43  func (o *options) Validate() error {
    44  	if err := o.github.Validate(!o.confirm); err != nil {
    45  		return err
    46  	}
    47  
    48  	if o.config == "" {
    49  		return errors.New("empty --config-path")
    50  	}
    51  
    52  	return nil
    53  }
    54  
    55  func gatherOptions() options {
    56  	o := options{}
    57  	fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
    58  	fs.StringVar(&o.config, "config-path", "", "Path to prow config.yaml")
    59  	fs.StringVar(&o.jobConfig, "job-config-path", "", "Path to prow job configs.")
    60  	fs.BoolVar(&o.confirm, "confirm", false, "Mutate github if set")
    61  	o.github.AddFlags(fs)
    62  	fs.Parse(os.Args[1:])
    63  	return o
    64  }
    65  
    66  type requirements struct {
    67  	Org     string
    68  	Repo    string
    69  	Branch  string
    70  	Request *github.BranchProtectionRequest
    71  }
    72  
    73  // Errors holds a list of errors, including a method to concurrently append.
    74  type Errors struct {
    75  	lock sync.Mutex
    76  	errs []error
    77  }
    78  
    79  func (e *Errors) add(err error) {
    80  	e.lock.Lock()
    81  	logrus.Info(err)
    82  	defer e.lock.Unlock()
    83  	e.errs = append(e.errs, err)
    84  }
    85  
    86  func main() {
    87  	logrus.SetFormatter(
    88  		logrusutil.NewDefaultFieldsFormatter(nil, logrus.Fields{"component": "branchprotector"}),
    89  	)
    90  
    91  	o := gatherOptions()
    92  	if err := o.Validate(); err != nil {
    93  		logrus.Fatal(err)
    94  	}
    95  
    96  	cfg, err := config.Load(o.config, o.jobConfig)
    97  	if err != nil {
    98  		logrus.WithError(err).Fatalf("Failed to load --config-path=%s", o.config)
    99  	}
   100  
   101  	secretAgent := &secret.Agent{}
   102  	if err := secretAgent.Start([]string{o.github.TokenPath}); err != nil {
   103  		logrus.WithError(err).Fatal("Error starting secrets agent.")
   104  	}
   105  
   106  	githubClient, err := o.github.GitHubClient(secretAgent, !o.confirm)
   107  	if err != nil {
   108  		logrus.WithError(err).Fatal("Error getting GitHub client.")
   109  	}
   110  	githubClient.Throttle(300, 100) // 300 hourly tokens, bursts of 100
   111  
   112  	p := protector{
   113  		client:         githubClient,
   114  		cfg:            cfg,
   115  		updates:        make(chan requirements),
   116  		errors:         Errors{},
   117  		completedRepos: make(map[string]bool),
   118  		done:           make(chan []error),
   119  	}
   120  
   121  	go p.configureBranches()
   122  	p.protect()
   123  	close(p.updates)
   124  	errors := <-p.done
   125  	if n := len(errors); n > 0 {
   126  		for i, err := range errors {
   127  			logrus.WithError(err).Error(i)
   128  		}
   129  		logrus.Fatalf("Encountered %d errors protecting branches", n)
   130  	}
   131  }
   132  
   133  type client interface {
   134  	RemoveBranchProtection(org, repo, branch string) error
   135  	UpdateBranchProtection(org, repo, branch string, config github.BranchProtectionRequest) error
   136  	GetBranches(org, repo string, onlyProtected bool) ([]github.Branch, error)
   137  	GetRepo(owner, name string) (github.Repo, error)
   138  	GetRepos(org string, user bool) ([]github.Repo, error)
   139  }
   140  
   141  type protector struct {
   142  	client         client
   143  	cfg            *config.Config
   144  	updates        chan requirements
   145  	errors         Errors
   146  	completedRepos map[string]bool
   147  	done           chan []error
   148  }
   149  
   150  func (p *protector) configureBranches() {
   151  	for u := range p.updates {
   152  		if u.Request == nil {
   153  			if err := p.client.RemoveBranchProtection(u.Org, u.Repo, u.Branch); err != nil {
   154  				p.errors.add(fmt.Errorf("remove %s/%s=%s protection failed: %v", u.Org, u.Repo, u.Branch, err))
   155  			}
   156  			continue
   157  		}
   158  
   159  		if err := p.client.UpdateBranchProtection(u.Org, u.Repo, u.Branch, *u.Request); err != nil {
   160  			p.errors.add(fmt.Errorf("update %s/%s=%s protection to %v failed: %v", u.Org, u.Repo, u.Branch, *u.Request, err))
   161  		}
   162  	}
   163  	p.done <- p.errors.errs
   164  }
   165  
   166  // protect protects branches specified in the presubmit and branch-protection config sections.
   167  func (p *protector) protect() {
   168  	bp := p.cfg.BranchProtection
   169  
   170  	// Scan the branch-protection configuration
   171  	for orgName := range bp.Orgs {
   172  		if org, err := bp.GetOrg(orgName); err != nil {
   173  			p.errors.add(fmt.Errorf("get %s: %v", orgName, err))
   174  		} else if err = p.UpdateOrg(orgName, *org); err != nil {
   175  			p.errors.add(fmt.Errorf("update %s: %v", orgName, err))
   176  		}
   177  	}
   178  
   179  	// Do not automatically protect tested repositories
   180  	if !bp.ProtectTested {
   181  		return
   182  	}
   183  
   184  	// Some repos with presubmits might not be listed in the branch-protection
   185  	for repo := range p.cfg.Presubmits {
   186  		if p.completedRepos[repo] == true {
   187  			continue
   188  		}
   189  		parts := strings.Split(repo, "/")
   190  		if len(parts) != 2 { // TODO(fejta): use a strong type here instead
   191  			p.errors.add(fmt.Errorf("bad presubmit repo: %s", repo))
   192  			continue
   193  		}
   194  		orgName := parts[0]
   195  		repoName := parts[1]
   196  		if org, err := bp.GetOrg(orgName); err != nil {
   197  			p.errors.add(fmt.Errorf("get %s: %v", orgName, err))
   198  		} else if repo, err := org.GetRepo(repoName); err != nil {
   199  			p.errors.add(fmt.Errorf("get %s/%s: %v", orgName, repoName, err))
   200  		} else if err = p.UpdateRepo(orgName, repoName, *repo); err != nil {
   201  			p.errors.add(fmt.Errorf("update %s/%s: %v", orgName, repoName, err))
   202  		}
   203  	}
   204  }
   205  
   206  // UpdateOrg updates all repos in the org with the specified defaults
   207  func (p *protector) UpdateOrg(orgName string, org config.Org) error {
   208  	var repos []string
   209  	if org.Protect != nil {
   210  		// Strongly opinionated org, configure every repo in the org.
   211  		rs, err := p.client.GetRepos(orgName, false)
   212  		if err != nil {
   213  			return fmt.Errorf("list repos: %v", err)
   214  		}
   215  		for _, r := range rs {
   216  			if !r.Archived {
   217  				repos = append(repos, r.Name)
   218  			}
   219  		}
   220  	} else {
   221  		// Unopinionated org, just set explicitly defined repos
   222  		for r := range org.Repos {
   223  			repos = append(repos, r)
   224  		}
   225  	}
   226  
   227  	for _, repoName := range repos {
   228  		if repo, err := org.GetRepo(repoName); err != nil {
   229  			return fmt.Errorf("get %s: %v", repoName, err)
   230  		} else if err = p.UpdateRepo(orgName, repoName, *repo); err != nil {
   231  			return fmt.Errorf("update %s: %v", repoName, err)
   232  		}
   233  	}
   234  	return nil
   235  }
   236  
   237  // UpdateRepo updates all branches in the repo with the specified defaults
   238  func (p *protector) UpdateRepo(orgName string, repoName string, repo config.Repo) error {
   239  	p.completedRepos[orgName+"/"+repoName] = true
   240  
   241  	githubRepo, err := p.client.GetRepo(orgName, repoName)
   242  	if err != nil {
   243  		return fmt.Errorf("could not get repo to check for archival: %v", err)
   244  	}
   245  	if githubRepo.Archived {
   246  		// nothing to do
   247  		return nil
   248  	}
   249  
   250  	branches := map[string]github.Branch{}
   251  	for _, onlyProtected := range []bool{false, true} { // put true second so b.Protected is set correctly
   252  		bs, err := p.client.GetBranches(orgName, repoName, onlyProtected)
   253  		if err != nil {
   254  			return fmt.Errorf("list branches: %v", err)
   255  		}
   256  		for _, b := range bs {
   257  			branches[b.Name] = b
   258  		}
   259  	}
   260  
   261  	for bn, githubBranch := range branches {
   262  		if branch, err := repo.GetBranch(bn); err != nil {
   263  			return fmt.Errorf("get %s: %v", bn, err)
   264  		} else if err = p.UpdateBranch(orgName, repoName, bn, *branch, githubBranch.Protected); err != nil {
   265  			return fmt.Errorf("update %s from protected=%t: %v", bn, githubBranch.Protected, err)
   266  		}
   267  	}
   268  	return nil
   269  }
   270  
   271  // UpdateBranch updates the branch with the specified configuration
   272  func (p *protector) UpdateBranch(orgName, repo string, branchName string, branch config.Branch, protected bool) error {
   273  	bp, err := p.cfg.GetPolicy(orgName, repo, branchName, branch)
   274  	if err != nil {
   275  		return fmt.Errorf("get policy: %v", err)
   276  	}
   277  	if bp == nil || bp.Protect == nil {
   278  		return nil
   279  	}
   280  	if !protected && !*bp.Protect {
   281  		logrus.Infof("%s/%s=%s: already unprotected", orgName, repo, branchName)
   282  		return nil
   283  	}
   284  	var req *github.BranchProtectionRequest
   285  	if *bp.Protect {
   286  		r := makeRequest(*bp)
   287  		req = &r
   288  	}
   289  	p.updates <- requirements{
   290  		Org:     orgName,
   291  		Repo:    repo,
   292  		Branch:  branchName,
   293  		Request: req,
   294  	}
   295  	return nil
   296  }