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 }