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  }