github.com/abayer/test-infra@v0.0.5/mungegithub/mungers/submit-queue-batch.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 mungers
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"strconv"
    23  	"strings"
    24  	"sync/atomic"
    25  	"time"
    26  
    27  	"github.com/golang/glog"
    28  	githubapi "github.com/google/go-github/github"
    29  	"k8s.io/test-infra/mungegithub/github"
    30  	"k8s.io/test-infra/mungegithub/mungeopts"
    31  )
    32  
    33  type batchPull struct {
    34  	Number int
    35  	Sha    string
    36  }
    37  
    38  // Batch represents a specific merge state:
    39  // a base branch and SHA, and the SHAs of each PR merged into it.
    40  type Batch struct {
    41  	BaseName string
    42  	BaseSha  string
    43  	Pulls    []batchPull
    44  }
    45  
    46  func (b *Batch) String() string {
    47  	out := b.BaseName + ":" + b.BaseSha
    48  	for _, pull := range b.Pulls {
    49  		out += "," + strconv.Itoa(pull.Number) + ":" + pull.Sha
    50  	}
    51  	return out
    52  }
    53  
    54  // batchRefToBatch parses a string into a Batch.
    55  // The input is a comma-separated list of colon-separated ref/sha pairs,
    56  // like "master:abcdef0,123:f00d,456:f00f".
    57  func batchRefToBatch(batchRef string) (Batch, error) {
    58  	batch := Batch{}
    59  	for i, ref := range strings.Split(batchRef, ",") {
    60  		parts := strings.Split(ref, ":")
    61  		if len(parts) != 2 {
    62  			return Batch{}, errors.New("bad batchref: " + batchRef)
    63  		}
    64  		if i == 0 {
    65  			batch.BaseName = parts[0]
    66  			batch.BaseSha = parts[1]
    67  		} else {
    68  			num, err := strconv.ParseInt(parts[0], 10, 32)
    69  			if err != nil {
    70  				return Batch{}, fmt.Errorf("bad batchref: %s (%v)", batchRef, err)
    71  			}
    72  			batch.Pulls = append(batch.Pulls, batchPull{int(num), parts[1]})
    73  		}
    74  	}
    75  	return batch, nil
    76  }
    77  
    78  // getCompleteBatches returns a list of Batches that passed all
    79  // required tests.
    80  func (sq *SubmitQueue) getCompleteBatches(jobs prowJobs) []Batch {
    81  	// for each batch specifier, a set of successful contexts
    82  	batchContexts := make(map[string]map[string]interface{})
    83  	for _, job := range jobs {
    84  		if batchContexts[job.Refs] == nil {
    85  			batchContexts[job.Refs] = make(map[string]interface{})
    86  		}
    87  		batchContexts[job.Refs][job.Context] = nil
    88  	}
    89  	batches := []Batch{}
    90  	for batchRef, contexts := range batchContexts {
    91  		match := true
    92  		// Did this succeed in all the contexts we want?
    93  		sq.opts.Lock()
    94  		mergeContexts := mungeopts.RequiredContexts.Merge
    95  		retestContexts := mungeopts.RequiredContexts.Retest
    96  		sq.opts.Unlock()
    97  		for _, ctx := range mergeContexts {
    98  			if _, ok := contexts[ctx]; !ok {
    99  				match = false
   100  			}
   101  		}
   102  		for _, ctx := range retestContexts {
   103  			if _, ok := contexts[ctx]; !ok {
   104  				match = false
   105  			}
   106  		}
   107  		if match {
   108  			batch, err := batchRefToBatch(batchRef)
   109  			if err != nil {
   110  				continue
   111  			}
   112  			batches = append(batches, batch)
   113  		}
   114  	}
   115  	return batches
   116  }
   117  
   118  // batchIntersectsQueue returns whether at least one PR in the batch is queued.
   119  func (sq *SubmitQueue) batchIntersectsQueue(batch Batch) bool {
   120  	sq.Lock()
   121  	defer sq.Unlock()
   122  	for _, pull := range batch.Pulls {
   123  		if _, ok := sq.githubE2EQueue[pull.Number]; ok {
   124  			return true
   125  		}
   126  	}
   127  	return false
   128  }
   129  
   130  // matchesCommit determines if the batch can be merged given some commits.
   131  // That is, does it contain exactly:
   132  // 1) the batch's BaseSha
   133  // 2) (optional) merge commits for PRs in the batch
   134  // 3) any merged PRs in the batch are sequential from the beginning
   135  // The return value is the number of PRs already merged, and any errors.
   136  func (b *Batch) matchesCommits(commits []*githubapi.RepositoryCommit) (int, error) {
   137  	if len(commits) == 0 {
   138  		return 0, errors.New("no commits")
   139  	}
   140  
   141  	shaToPR := make(map[string]int)
   142  
   143  	for _, pull := range b.Pulls {
   144  		shaToPR[pull.Sha] = pull.Number
   145  	}
   146  
   147  	matchedPRs := []int{}
   148  
   149  	// convert the list of commits into a DAG for easy following
   150  	dag := make(map[string]*githubapi.RepositoryCommit)
   151  	for _, commit := range commits {
   152  		dag[*commit.SHA] = commit
   153  	}
   154  
   155  	ref := *commits[0].SHA
   156  	for {
   157  		if ref == b.BaseSha {
   158  			break // found the base ref (condition #1)
   159  		}
   160  		commit, ok := dag[ref]
   161  		if !ok {
   162  			return 0, errors.New("ran out of commits (missing ref " + ref + ")")
   163  		}
   164  		message := ""
   165  		if commit.Commit != nil && commit.Commit.Message != nil {
   166  			// The actual commit message is buried a little oddly.
   167  			message = *commit.Commit.Message
   168  		}
   169  		if len(commit.Parents) == 2 && strings.HasPrefix(message, "Merge") {
   170  			// looks like a merge commit!
   171  
   172  			// first parent is the normal branch
   173  			ref = *commit.Parents[0].SHA
   174  			// second parent is the PR
   175  			pr, ok := shaToPR[*commit.Parents[1].SHA]
   176  			if !ok {
   177  				return 0, errors.New("Merge of something not in batch")
   178  			}
   179  			matchedPRs = append(matchedPRs, pr)
   180  		} else {
   181  			return 0, errors.New("Unknown non-merge commit " + ref)
   182  		}
   183  	}
   184  
   185  	// Now, ensure that the merged PRs are ordered correctly.
   186  	for i, pr := range matchedPRs {
   187  		if b.Pulls[len(matchedPRs)-1-i].Number != pr {
   188  			return 0, errors.New("Batch PRs merged out-of-order")
   189  		}
   190  	}
   191  	return len(matchedPRs), nil
   192  }
   193  
   194  // batchIsApplicable returns whether a successful batch result can be used--
   195  // 1) some of the batch is still unmerged and in the queue.
   196  // 2) the recent commits are the batch head ref or merges of batch PRs.
   197  // 3) all unmerged PRs in the batch are still in the queue.
   198  // The return value is the number of PRs already merged, and any errors.
   199  func (sq *SubmitQueue) batchIsApplicable(batch Batch) (int, error) {
   200  	// batch must intersect the queue
   201  	if !sq.batchIntersectsQueue(batch) {
   202  		return 0, errors.New("batch has no PRs in Queue")
   203  	}
   204  	commits, err := sq.githubConfig.GetBranchCommits(batch.BaseName, 100)
   205  	if err != nil {
   206  		glog.Errorf("Error getting commits for batchIsApplicable: %v", err)
   207  		return 0, errors.New("failed to get branch commits: " + err.Error())
   208  	}
   209  	return batch.matchesCommits(commits)
   210  }
   211  
   212  func (sq *SubmitQueue) handleGithubE2EBatchMerge() {
   213  	repo := sq.githubConfig.Org + "/" + sq.githubConfig.Project
   214  	for range time.Tick(1 * time.Minute) {
   215  		sq.opts.Lock()
   216  		url := sq.ProwURL + "/data.js"
   217  		sq.opts.Unlock()
   218  		allJobs, err := getJobs(url)
   219  		if err != nil {
   220  			glog.Errorf("Error reading batch jobs from Prow URL %v: %v", url, err)
   221  			continue
   222  		}
   223  		batchJobs := allJobs.batch().repo(repo)
   224  		jobs := batchJobs.successful()
   225  		batches := sq.getCompleteBatches(jobs)
   226  		batchErrors := make(map[string]string)
   227  		for _, batch := range batches {
   228  			_, err := sq.batchIsApplicable(batch)
   229  			if err != nil {
   230  				batchErrors[batch.String()] = err.Error()
   231  				continue
   232  			}
   233  			sq.doBatchMerge(batch)
   234  		}
   235  		sq.batchStatus.Error = batchErrors
   236  		sq.batchStatus.Running = batchJobs.firstUnfinished()
   237  	}
   238  }
   239  
   240  // doBatchMerge iteratively merges PRs in the batch if possible.
   241  // If you modify this, consider modifying doGithubE2EAndMerge too.
   242  func (sq *SubmitQueue) doBatchMerge(batch Batch) {
   243  	sq.mergeLock.Lock()
   244  	defer sq.mergeLock.Unlock()
   245  
   246  	// Test again inside the merge lock, in case some other merge snuck in.
   247  	match, err := sq.batchIsApplicable(batch)
   248  	if err != nil {
   249  		glog.Errorf("unexpected! batchIsApplicable failed after success %v", err)
   250  		return
   251  	}
   252  	if !sq.e2eStable(true) {
   253  		return
   254  	}
   255  
   256  	glog.Infof("merging batch: %s", batch)
   257  	prs := []*github.MungeObject{}
   258  	// Check entire batch's preconditions first.
   259  	for _, pull := range batch.Pulls[match:] {
   260  		obj, err := sq.githubConfig.GetObject(pull.Number)
   261  		if err != nil {
   262  			glog.Errorf("error getting object for pr #%d: %v", pull.Number, err)
   263  			return
   264  		}
   265  		if sha, _, ok := obj.GetHeadAndBase(); !ok {
   266  			glog.Errorf("error getting pr #%d sha: %v", pull.Number, err)
   267  			return
   268  		} else if sha != pull.Sha {
   269  			glog.Errorf("error: batch PR #%d HEAD changed: %s instead of %s",
   270  				pull.Number, sha, pull.Sha)
   271  			return
   272  		}
   273  		if !sq.validForMergeExt(obj, false) {
   274  			return
   275  		}
   276  		prs = append(prs, obj)
   277  	}
   278  
   279  	// Make the merge less confusing: describe the overall batch.
   280  	prStrings := []string{}
   281  	for _, pull := range batch.Pulls {
   282  		prStrings = append(prStrings, strconv.Itoa(pull.Number))
   283  	}
   284  	extra := fmt.Sprintf(" (batch tested with PRs %s)", strings.Join(prStrings, ", "))
   285  
   286  	// then merge each
   287  	for _, pr := range prs {
   288  		ok := sq.mergePullRequest(pr, mergedBatch, extra)
   289  		if !ok {
   290  			return
   291  		}
   292  		atomic.AddInt32(&sq.batchMerges, 1)
   293  	}
   294  }