github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/tide/tide.go (about) 1 /* 2 Copyright 2017 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 tide contains a controller for managing a tide pool of PRs. 18 package tide 19 20 import ( 21 "context" 22 "fmt" 23 "strings" 24 25 "github.com/shurcooL/githubql" 26 "github.com/sirupsen/logrus" 27 28 "k8s.io/test-infra/prow/config" 29 "k8s.io/test-infra/prow/git" 30 "k8s.io/test-infra/prow/github" 31 "k8s.io/test-infra/prow/kube" 32 "k8s.io/test-infra/prow/pjutil" 33 ) 34 35 type kubeClient interface { 36 ListProwJobs(map[string]string) ([]kube.ProwJob, error) 37 CreateProwJob(kube.ProwJob) (kube.ProwJob, error) 38 } 39 40 type githubClient interface { 41 GetRef(string, string, string) (string, error) 42 Query(context.Context, interface{}, map[string]interface{}) error 43 Merge(string, string, int, github.MergeDetails) error 44 } 45 46 // Controller knows how to sync PRs and PJs. 47 type Controller struct { 48 Logger *logrus.Entry 49 DryRun bool 50 ca *config.Agent 51 ghc githubClient 52 kc kubeClient 53 gc *git.Client 54 } 55 56 // NewController makes a Controller out of the given clients. 57 func NewController(ghc *github.Client, kc *kube.Client, ca *config.Agent, gc *git.Client) *Controller { 58 return &Controller{ 59 ghc: ghc, 60 kc: kc, 61 ca: ca, 62 gc: gc, 63 } 64 } 65 66 // Sync runs one sync iteration. 67 func (c *Controller) Sync() error { 68 ctx := context.Background() 69 c.Logger.Info("Building tide pool.") 70 var pool []pullRequest 71 for _, q := range c.ca.Config().Tide.Queries { 72 prs, err := c.search(ctx, q) 73 if err != nil { 74 return err 75 } 76 pool = append(pool, prs...) 77 } 78 var pjs []kube.ProwJob 79 var err error 80 if len(pool) > 0 { 81 pjs, err = c.kc.ListProwJobs(nil) 82 if err != nil { 83 return err 84 } 85 } 86 sps, err := c.dividePool(pool, pjs) 87 if err != nil { 88 return err 89 } 90 for _, sp := range sps { 91 if err := c.syncSubpool(sp); err != nil { 92 return err 93 } 94 } 95 return nil 96 } 97 98 type simpleState string 99 100 const ( 101 noneState simpleState = "none" 102 pendingState simpleState = "pending" 103 successState simpleState = "success" 104 ) 105 106 func toSimpleState(s kube.ProwJobState) simpleState { 107 if s == kube.TriggeredState || s == kube.PendingState { 108 return pendingState 109 } else if s == kube.SuccessState { 110 return successState 111 } 112 return noneState 113 } 114 115 func pickSmallestPassingNumber(prs []pullRequest) (bool, pullRequest) { 116 smallestNumber := -1 117 var smallestPR pullRequest 118 for _, pr := range prs { 119 if smallestNumber != -1 && int(pr.Number) >= smallestNumber { 120 continue 121 } 122 if len(pr.Commits.Nodes) < 1 { 123 continue 124 } 125 // TODO(spxtr): Check the actual statuses for individual jobs. 126 if string(pr.Commits.Nodes[0].Commit.Status.State) != "SUCCESS" { 127 continue 128 } 129 smallestNumber = int(pr.Number) 130 smallestPR = pr 131 } 132 return smallestNumber > -1, smallestPR 133 } 134 135 // accumulateBatch returns a list of PRs that can be merged after passing batch 136 // testing, if any exist. It also returns whether or not a batch is currently 137 // running. 138 func accumulateBatch(presubmits []string, prs []pullRequest, pjs []kube.ProwJob) ([]pullRequest, bool) { 139 prNums := make(map[int]pullRequest) 140 for _, pr := range prs { 141 prNums[int(pr.Number)] = pr 142 } 143 type accState struct { 144 prs []pullRequest 145 jobStates map[string]simpleState 146 // Are the pull requests in the ref still acceptable? That is, do they 147 // still point to the heads of the PRs? 148 validPulls bool 149 } 150 states := make(map[string]*accState) 151 for _, pj := range pjs { 152 if pj.Spec.Type != kube.BatchJob { 153 continue 154 } 155 // If any batch job is pending, return now. 156 if toSimpleState(pj.Status.State) == pendingState { 157 return nil, true 158 } 159 // Otherwise, accumulate results. 160 ref := pj.Spec.Refs.String() 161 if _, ok := states[ref]; !ok { 162 states[ref] = &accState{ 163 jobStates: make(map[string]simpleState), 164 validPulls: true, 165 } 166 for _, pull := range pj.Spec.Refs.Pulls { 167 if pr, ok := prNums[pull.Number]; ok && string(pr.HeadRef.Target.OID) == pull.SHA { 168 states[ref].prs = append(states[ref].prs, pr) 169 } else { 170 states[ref].validPulls = false 171 break 172 } 173 } 174 } 175 if !states[ref].validPulls { 176 // The batch contains a PR ref that has changed. Skip it. 177 continue 178 } 179 job := pj.Spec.Job 180 if s, ok := states[ref].jobStates[job]; !ok || s == noneState { 181 states[ref].jobStates[job] = toSimpleState(pj.Status.State) 182 } 183 } 184 for _, state := range states { 185 if !state.validPulls { 186 continue 187 } 188 passesAll := true 189 for _, p := range presubmits { 190 if s, ok := state.jobStates[p]; !ok || s != successState { 191 passesAll = false 192 continue 193 } 194 } 195 if !passesAll { 196 continue 197 } 198 return state.prs, false 199 } 200 return nil, false 201 } 202 203 // accumulate returns the supplied PRs sorted into three buckets based on their 204 // accumulated state across the presubmits. 205 func accumulate(presubmits []string, prs []pullRequest, pjs []kube.ProwJob) (successes, pendings, nones []pullRequest) { 206 for _, pr := range prs { 207 // Accumulate the best result for each job. 208 psStates := make(map[string]simpleState) 209 for _, pj := range pjs { 210 if pj.Spec.Type != kube.PresubmitJob { 211 continue 212 } 213 if pj.Spec.Refs.Pulls[0].Number != int(pr.Number) { 214 continue 215 } 216 name := pj.Spec.Job 217 oldState := psStates[name] 218 newState := toSimpleState(pj.Status.State) 219 if oldState == noneState || oldState == "" { 220 psStates[name] = newState 221 } else if oldState == pendingState && newState == successState { 222 psStates[name] = successState 223 } 224 } 225 // The overall result is the worst of the best. 226 overallState := successState 227 for _, ps := range presubmits { 228 if s, ok := psStates[ps]; s == noneState || !ok { 229 overallState = noneState 230 break 231 } else if s == pendingState { 232 overallState = pendingState 233 } 234 } 235 if overallState == successState { 236 successes = append(successes, pr) 237 } else if overallState == pendingState { 238 pendings = append(pendings, pr) 239 } else { 240 nones = append(nones, pr) 241 } 242 } 243 return 244 } 245 246 func prNumbers(prs []pullRequest) []int { 247 var nums []int 248 for _, pr := range prs { 249 nums = append(nums, int(pr.Number)) 250 } 251 return nums 252 } 253 254 func (c *Controller) pickBatch(sp subpool) ([]pullRequest, error) { 255 r, err := c.gc.Clone(sp.org + "/" + sp.repo) 256 if err != nil { 257 return nil, err 258 } 259 defer r.Clean() 260 if err := r.Config("user.name", "prow"); err != nil { 261 return nil, err 262 } 263 if err := r.Config("user.email", "prow@localhost"); err != nil { 264 return nil, err 265 } 266 if err := r.Checkout(sp.sha); err != nil { 267 return nil, err 268 } 269 // TODO(spxtr): Limit batch size. 270 var res []pullRequest 271 for _, pr := range sp.prs { 272 // TODO(spxtr): Check the actual statuses for individual jobs. 273 if string(pr.Commits.Nodes[0].Commit.Status.State) != "SUCCESS" { 274 continue 275 } 276 if ok, err := r.Merge(string(pr.HeadRef.Target.OID)); err != nil { 277 return nil, err 278 } else if ok { 279 res = append(res, pr) 280 } 281 } 282 return res, nil 283 } 284 285 func (c *Controller) mergePRs(sp subpool, prs []pullRequest) error { 286 for _, pr := range prs { 287 if err := c.ghc.Merge(sp.org, sp.repo, int(pr.Number), github.MergeDetails{ 288 SHA: string(pr.HeadRef.Target.OID), 289 }); err != nil { 290 if _, ok := err.(github.ModifiedHeadError); ok { 291 // This is a possible source of incorrect behavior. If someone 292 // modifies their PR as we try to merge it in a batch then we 293 // end up in an untested state. This is unlikely to cause any 294 // real problems. 295 c.Logger.WithError(err).Info("Merge failed: PR was modified.") 296 } else if _, ok = err.(github.UnmergablePRError); ok { 297 c.Logger.WithError(err).Warning("Merge failed: PR is unmergable. How did it pass tests?!") 298 } else { 299 return err 300 } 301 } 302 } 303 return nil 304 } 305 306 func (c *Controller) trigger(sp subpool, prs []pullRequest) error { 307 for _, ps := range c.ca.Config().Presubmits[sp.org+"/"+sp.repo] { 308 if ps.SkipReport || !ps.AlwaysRun || !ps.RunsAgainstBranch(sp.branch) { 309 continue 310 } 311 312 var spec kube.ProwJobSpec 313 refs := kube.Refs{ 314 Org: sp.org, 315 Repo: sp.repo, 316 BaseRef: sp.branch, 317 BaseSHA: sp.sha, 318 } 319 for _, pr := range prs { 320 refs.Pulls = append( 321 refs.Pulls, 322 kube.Pull{ 323 Number: int(pr.Number), 324 Author: string(pr.Author.Login), 325 SHA: string(pr.HeadRef.Target.OID), 326 }, 327 ) 328 } 329 if len(prs) == 1 { 330 spec = pjutil.PresubmitSpec(ps, refs) 331 } else { 332 spec = pjutil.BatchSpec(ps, refs) 333 } 334 pj := pjutil.NewProwJob(spec) 335 if _, err := c.kc.CreateProwJob(pj); err != nil { 336 return err 337 } 338 } 339 return nil 340 } 341 342 func (c *Controller) takeAction(sp subpool, batchPending bool, successes, pendings, nones, batchMerges []pullRequest) error { 343 // Merge the batch! 344 if len(batchMerges) > 0 { 345 c.Logger.Infof("Merge PRs %v.", prNumbers(batchMerges)) 346 if c.DryRun { 347 return nil 348 } 349 return c.mergePRs(sp, batchMerges) 350 } 351 // Do not merge PRs while waiting for a batch to complete. We don't want to 352 // invalidate the old batch result. 353 if len(successes) > 0 && !batchPending { 354 if ok, pr := pickSmallestPassingNumber(successes); ok { 355 c.Logger.Infof("Merge PR #%d.", int(pr.Number)) 356 if c.DryRun { 357 return nil 358 } 359 return c.mergePRs(sp, []pullRequest{pr}) 360 } 361 } 362 // If we have no serial jobs pending or successful, trigger one. 363 if len(nones) > 0 && len(pendings) == 0 && len(successes) == 0 { 364 if ok, pr := pickSmallestPassingNumber(nones); ok { 365 c.Logger.Infof("Trigger tests for PR #%d.", int(pr.Number)) 366 if !c.DryRun { 367 if err := c.trigger(sp, []pullRequest{pr}); err != nil { 368 return err 369 } 370 } 371 } 372 } 373 // If we have no batch, trigger one. 374 if len(sp.prs) > 1 && !batchPending { 375 batch, err := c.pickBatch(sp) 376 if err != nil { 377 return err 378 } 379 if len(batch) > 1 { 380 c.Logger.Infof("Trigger batch for %v", prNumbers(batch)) 381 if !c.DryRun { 382 if err := c.trigger(sp, batch); err != nil { 383 return err 384 } 385 } 386 } 387 } 388 return nil 389 } 390 391 func (c *Controller) syncSubpool(sp subpool) error { 392 c.Logger.Infof("%s/%s %s: %d PRs, %d PJs.", sp.org, sp.repo, sp.branch, len(sp.prs), len(sp.pjs)) 393 var presubmits []string 394 for _, ps := range c.ca.Config().Presubmits[sp.org+"/"+sp.repo] { 395 if ps.SkipReport || !ps.AlwaysRun || !ps.RunsAgainstBranch(sp.branch) { 396 continue 397 } 398 presubmits = append(presubmits, ps.Name) 399 } 400 successes, pendings, nones := accumulate(presubmits, sp.prs, sp.pjs) 401 batchMerge, batchPending := accumulateBatch(presubmits, sp.prs, sp.pjs) 402 c.Logger.Infof("Passing PRs: %v", prNumbers(successes)) 403 c.Logger.Infof("Pending PRs: %v", prNumbers(pendings)) 404 c.Logger.Infof("Missing PRs: %v", prNumbers(nones)) 405 c.Logger.Infof("Passing batch: %v", prNumbers(batchMerge)) 406 c.Logger.Infof("Pending batch: %v", batchPending) 407 return c.takeAction(sp, batchPending, successes, pendings, nones, batchMerge) 408 } 409 410 type subpool struct { 411 org string 412 repo string 413 branch string 414 sha string 415 pjs []kube.ProwJob 416 prs []pullRequest 417 } 418 419 // dividePool splits up the list of pull requests and prow jobs into a group 420 // per repo and branch. It only keeps ProwJobs that match the latest branch. 421 func (c *Controller) dividePool(pool []pullRequest, pjs []kube.ProwJob) ([]subpool, error) { 422 sps := make(map[string]*subpool) 423 for _, pr := range pool { 424 org := string(pr.Repository.Owner.Login) 425 repo := string(pr.Repository.Name) 426 branch := string(pr.BaseRef.Name) 427 branchRef := string(pr.BaseRef.Prefix) + string(pr.BaseRef.Name) 428 fn := fmt.Sprintf("%s/%s %s", org, repo, branch) 429 if sps[fn] == nil { 430 sha, err := c.ghc.GetRef(org, repo, strings.TrimPrefix(branchRef, "refs/")) 431 if err != nil { 432 return nil, err 433 } 434 sps[fn] = &subpool{ 435 org: org, 436 repo: repo, 437 branch: branch, 438 sha: sha, 439 } 440 } 441 sps[fn].prs = append(sps[fn].prs, pr) 442 } 443 for _, pj := range pjs { 444 if pj.Spec.Type != kube.PresubmitJob && pj.Spec.Type != kube.BatchJob { 445 continue 446 } 447 fn := fmt.Sprintf("%s/%s %s", pj.Spec.Refs.Org, pj.Spec.Refs.Repo, pj.Spec.Refs.BaseRef) 448 if sps[fn] == nil || pj.Spec.Refs.BaseSHA != sps[fn].sha { 449 continue 450 } 451 sps[fn].pjs = append(sps[fn].pjs, pj) 452 } 453 var ret []subpool 454 for _, sp := range sps { 455 ret = append(ret, *sp) 456 } 457 return ret, nil 458 } 459 460 func (c *Controller) search(ctx context.Context, q string) ([]pullRequest, error) { 461 var ret []pullRequest 462 vars := map[string]interface{}{ 463 "query": githubql.String(q), 464 "searchCursor": (*githubql.String)(nil), 465 } 466 var totalCost int 467 var remaining int 468 for { 469 sq := searchQuery{} 470 if err := c.ghc.Query(ctx, &sq, vars); err != nil { 471 return nil, err 472 } 473 totalCost += int(sq.RateLimit.Cost) 474 remaining = int(sq.RateLimit.Remaining) 475 for _, n := range sq.Search.Nodes { 476 ret = append(ret, n.PullRequest) 477 } 478 if !sq.Search.PageInfo.HasNextPage { 479 break 480 } 481 vars["searchCursor"] = githubql.NewString(sq.Search.PageInfo.EndCursor) 482 } 483 c.Logger.Infof("Search for query \"%s\" cost %d point(s). %d remaining.", q, totalCost, remaining) 484 return ret, nil 485 } 486 487 type pullRequest struct { 488 Number githubql.Int 489 Author struct { 490 Login githubql.String 491 } 492 BaseRef struct { 493 Name githubql.String 494 Prefix githubql.String 495 } 496 Repository struct { 497 Name githubql.String 498 NameWithOwner githubql.String 499 Owner struct { 500 Login githubql.String 501 } 502 } 503 HeadRef struct { 504 Target struct { 505 OID githubql.String `graphql:"oid"` 506 } 507 } 508 Commits struct { 509 Nodes []struct { 510 Commit struct { 511 Status struct { 512 State githubql.String 513 } 514 } 515 } 516 } `graphql:"commits(last: 1)"` 517 } 518 519 type searchQuery struct { 520 RateLimit struct { 521 Cost githubql.Int 522 Remaining githubql.Int 523 } 524 Search struct { 525 PageInfo struct { 526 HasNextPage githubql.Boolean 527 EndCursor githubql.String 528 } 529 Nodes []struct { 530 PullRequest pullRequest `graphql:"... on PullRequest"` 531 } 532 } `graphql:"search(type: ISSUE, first: 100, after: $searchCursor, query: $query)"` 533 }