github.com/abayer/test-infra@v0.0.5/prow/cmd/splice/main.go (about) 1 /* 2 Copyright 2016 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 "encoding/json" 21 "errors" 22 "flag" 23 "fmt" 24 "io/ioutil" 25 "net/http" 26 "os" 27 "os/exec" 28 "strings" 29 "time" 30 31 "github.com/sirupsen/logrus" 32 33 "k8s.io/apimachinery/pkg/util/sets" 34 35 "k8s.io/test-infra/prow/config" 36 "k8s.io/test-infra/prow/kube" 37 "k8s.io/test-infra/prow/logrusutil" 38 "k8s.io/test-infra/prow/pjutil" 39 ) 40 41 type options struct { 42 submitQueueURL string 43 remoteURL string 44 orgName string 45 repoName string 46 configPath string 47 jobConfigPath string 48 maxBatchSize int 49 alwaysRun string 50 } 51 52 func gatherOptions() options { 53 o := options{} 54 flag.StringVar(&o.submitQueueURL, "submit-queue-endpoint", "http://submit-queue.k8s.io/github-e2e-queue", "Submit Queue status URL") 55 flag.StringVar(&o.remoteURL, "remote-url", "https://github.com/kubernetes/kubernetes", "Remote Git URL") 56 flag.StringVar(&o.orgName, "org", "kubernetes", "Org name") 57 flag.StringVar(&o.repoName, "repo", "kubernetes", "Repo name") 58 flag.StringVar(&o.configPath, "config-path", "/etc/config/config.yaml", "Path to config.yaml.") 59 flag.StringVar(&o.jobConfigPath, "job-config-path", "", "Path to prow job configs.") 60 flag.IntVar(&o.maxBatchSize, "batch-size", 5, "Maximum batch size") 61 flag.StringVar(&o.alwaysRun, "always-run", "", "Job names that should be treated as always_run: true in Splice") 62 flag.Parse() 63 return o 64 } 65 66 func (o *options) Validate() error { 67 if o.maxBatchSize < 1 { 68 return errors.New("batch size cannot be less that one") 69 } 70 71 return nil 72 } 73 74 // Call a binary and return its output and success status. 75 func call(binary string, args ...string) (string, error) { 76 cmdout := "+ " + binary + " " 77 for _, arg := range args { 78 cmdout += arg + " " 79 } 80 logrus.Info(cmdout) 81 82 cmd := exec.Command(binary, args...) 83 output, err := cmd.CombinedOutput() 84 return string(output), err 85 } 86 87 // getQueuedPRs reads the list of queued PRs from the Submit Queue. 88 func getQueuedPRs(url string) ([]int, error) { 89 resp, err := http.Get(url) 90 if err != nil { 91 return nil, err 92 } 93 defer resp.Body.Close() 94 body, err := ioutil.ReadAll(resp.Body) 95 if err != nil { 96 return nil, err 97 } 98 99 queue := struct { 100 E2EQueue []struct { 101 Number int 102 BaseRef string 103 } 104 }{} 105 err = json.Unmarshal(body, &queue) 106 if err != nil { 107 return nil, err 108 } 109 110 ret := []int{} 111 for _, e := range queue.E2EQueue { 112 if e.BaseRef == "" || e.BaseRef == "master" { 113 ret = append(ret, e.Number) 114 } 115 } 116 return ret, nil 117 } 118 119 // Splicer manages a git repo in specific directory. 120 type splicer struct { 121 dir string // The repository location. 122 } 123 124 // makeSplicer returns a splicer in a new temporary directory, 125 // with an initial .git dir created. 126 func makeSplicer() (*splicer, error) { 127 dir, err := ioutil.TempDir("", "splice_") 128 if err != nil { 129 return nil, err 130 } 131 s := &splicer{dir} 132 err = s.gitCalls([][]string{ 133 {"init"}, 134 {"config", "--local", "user.name", "K8S Prow Splice"}, 135 {"config", "--local", "user.email", "splice@localhost"}, 136 {"config", "--local", "commit.gpgsign", "false"}, 137 }) 138 if err != nil { 139 s.cleanup() 140 return nil, err 141 } 142 logrus.Infof("Splicer created in %s.", dir) 143 return s, nil 144 } 145 146 // cleanup recurisvely deletes the repository 147 func (s *splicer) cleanup() { 148 os.RemoveAll(s.dir) 149 } 150 151 // gitCall is a helper to call `git -C $path $args`. 152 func (s *splicer) gitCall(args ...string) error { 153 fullArgs := append([]string{"-C", s.dir}, args...) 154 output, err := call("git", fullArgs...) 155 if len(output) > 0 { 156 logrus.Info(output) 157 } 158 return err 159 } 160 161 // gitCalls is a helper to chain repeated gitCall invocations, 162 // returning the first failure, or nil if they all succeeded. 163 func (s *splicer) gitCalls(argsList [][]string) error { 164 for _, args := range argsList { 165 err := s.gitCall(args...) 166 if err != nil { 167 return err 168 } 169 } 170 return nil 171 } 172 173 // findMergeable fetches given PRs from upstream, merges them locally, 174 // and finally returns a list of PRs that can be merged without conflicts. 175 func (s *splicer) findMergeable(remote string, prs []int) ([]int, error) { 176 args := []string{"fetch", "-f", remote, "master:master"} 177 for _, pr := range prs { 178 args = append(args, fmt.Sprintf("pull/%d/head:pr/%d", pr, pr)) 179 } 180 181 err := s.gitCalls([][]string{ 182 {"reset", "--hard"}, 183 {"checkout", "--orphan", "blank"}, 184 {"reset", "--hard"}, 185 {"clean", "-fdx"}, 186 args, 187 {"checkout", "-B", "batch", "master"}, 188 }) 189 if err != nil { 190 return nil, err 191 } 192 193 out := []int{} 194 for _, pr := range prs { 195 err := s.gitCall("merge", "--no-ff", "--no-stat", 196 "-m", fmt.Sprintf("merge #%d", pr), 197 fmt.Sprintf("pr/%d", pr)) 198 if err != nil { 199 // merge conflict: cleanup and move on 200 err = s.gitCall("merge", "--abort") 201 if err != nil { 202 return nil, err 203 } 204 continue 205 } 206 out = append(out, pr) 207 } 208 return out, nil 209 } 210 211 // gitRef returns the SHA for the given git object-- a branch, generally. 212 func (s *splicer) gitRef(ref string) string { 213 output, err := call("git", "-C", s.dir, "rev-parse", ref) 214 if err != nil { 215 return "" 216 } 217 return strings.TrimSpace(output) 218 } 219 220 // Produce a kube.Refs for the given pull requests. This involves computing the 221 // git ref for master and the PRs. 222 func (s *splicer) makeBuildRefs(org, repo string, prs []int) kube.Refs { 223 refs := kube.Refs{ 224 Org: org, 225 Repo: repo, 226 BaseRef: "master", 227 BaseSHA: s.gitRef("master"), 228 } 229 for _, pr := range prs { 230 branch := fmt.Sprintf("pr/%d", pr) 231 refs.Pulls = append(refs.Pulls, kube.Pull{Number: pr, SHA: s.gitRef(branch)}) 232 } 233 return refs 234 } 235 236 // Filters to the list of jobs which already passed this commit 237 func completedJobs(currentJobs []kube.ProwJob, refs kube.Refs) []kube.ProwJob { 238 var skippable []kube.ProwJob 239 rs := refs.String() 240 241 for _, job := range currentJobs { 242 if job.Spec.Type != kube.BatchJob { 243 continue 244 } 245 if !job.Complete() { 246 continue 247 } 248 if job.Status.State != kube.SuccessState { 249 continue 250 } 251 if job.Spec.Refs.String() != rs { 252 continue 253 } 254 skippable = append(skippable, job) 255 } 256 return skippable 257 } 258 259 // Filters to the list of required presubmits that report 260 func requiredPresubmits(presubmits []config.Presubmit, alwaysRunOverride sets.String) []config.Presubmit { 261 var out []config.Presubmit 262 for _, job := range presubmits { 263 if !job.AlwaysRun && !alwaysRunOverride.Has(job.Name) { // Ignore manual jobs as these do not block 264 continue 265 } 266 if job.SkipReport { // Ignore silent jobs as these do not block 267 continue 268 } 269 if !job.RunsAgainstBranch("master") { // Ignore jobs that don't run on master 270 continue 271 } 272 out = append(out, job) 273 } 274 return out 275 } 276 277 // Filters to the list of required presubmit which have not already passed this commit 278 func neededPresubmits(presubmits []config.Presubmit, currentJobs []kube.ProwJob, refs kube.Refs, alwaysRunOverride sets.String) []config.Presubmit { 279 skippable := make(map[string]bool) 280 for _, job := range completedJobs(currentJobs, refs) { 281 skippable[job.Spec.Context] = true 282 } 283 284 var needed []config.Presubmit 285 for _, job := range requiredPresubmits(presubmits, alwaysRunOverride) { 286 if skippable[job.Context] { 287 continue 288 } 289 needed = append(needed, job) 290 } 291 return needed 292 } 293 294 func main() { 295 o := gatherOptions() 296 if err := o.Validate(); err != nil { 297 logrus.Fatalf("Invalid options: %v", err) 298 } 299 300 logrus.SetFormatter( 301 logrusutil.NewDefaultFieldsFormatter(nil, logrus.Fields{"component": "splice"}), 302 ) 303 304 splicer, err := makeSplicer() 305 if err != nil { 306 logrus.WithError(err).Fatal("Could not make splicer.") 307 } 308 defer splicer.cleanup() 309 310 configAgent := &config.Agent{} 311 if err := configAgent.Start(o.configPath, o.jobConfigPath); err != nil { 312 logrus.WithError(err).Fatal("Error starting config agent.") 313 } 314 315 kc, err := kube.NewClientInCluster(configAgent.Config().ProwJobNamespace) 316 if err != nil { 317 logrus.WithError(err).Fatal("Error getting kube client.") 318 } 319 320 // get overridden always_run jobs 321 alwaysRunOverride := sets.NewString(strings.Split(o.alwaysRun, ",")...) 322 323 cooldown := 0 324 // Loop endlessly, sleeping a minute between iterations 325 for range time.Tick(1 * time.Minute) { 326 start := time.Now() 327 // List batch jobs, only start a new one if none are active. 328 currentJobs, err := kc.ListProwJobs(kube.EmptySelector) 329 if err != nil { 330 logrus.WithError(err).Error("Error listing prow jobs.") 331 continue 332 } 333 334 running := []string{} 335 for _, job := range currentJobs { 336 if job.Spec.Type != kube.BatchJob { 337 continue 338 } 339 if !job.Complete() { 340 running = append(running, job.Spec.Job) 341 } 342 } 343 if len(running) > 0 { 344 logrus.Infof("Waiting on %d jobs: %v", len(running), running) 345 continue 346 } 347 348 // Start a new batch if the cooldown is 0, otherwise wait. This gives 349 // the SQ some time to merge before we start a new batch. 350 if cooldown > 0 { 351 cooldown-- 352 continue 353 } 354 355 queue, err := getQueuedPRs(o.submitQueueURL) 356 if err != nil { 357 logrus.WithError(err).Warning("Error getting queued PRs. Is the submit queue down?") 358 continue 359 } 360 // No need to check for mergeable PRs if none is in the queue. 361 if len(queue) == 0 { 362 continue 363 } 364 logrus.Infof("PRs in queue: %v", queue) 365 batchPRs, err := splicer.findMergeable(o.remoteURL, queue) 366 if err != nil { 367 logrus.WithError(err).Error("Error computing mergeable PRs.") 368 continue 369 } 370 // No need to start batches for single PRs 371 if len(batchPRs) <= 1 { 372 continue 373 } 374 // Trim down to the desired batch size. 375 if len(batchPRs) > o.maxBatchSize { 376 batchPRs = batchPRs[:o.maxBatchSize] 377 } 378 logrus.Infof("Starting a batch for the following PRs: %v", batchPRs) 379 refs := splicer.makeBuildRefs(o.orgName, o.repoName, batchPRs) 380 presubmits := configAgent.Config().Presubmits[fmt.Sprintf("%s/%s", o.orgName, o.repoName)] 381 for _, job := range neededPresubmits(presubmits, currentJobs, refs, alwaysRunOverride) { 382 if _, err := kc.CreateProwJob(pjutil.NewProwJob(pjutil.BatchSpec(job, refs), job.Labels)); err != nil { 383 logrus.WithError(err).WithField("job", job.Name).Error("Error starting batch job.") 384 } 385 } 386 cooldown = 5 387 logrus.Infof("Sync time: %v", time.Since(start)) 388 } 389 }