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