github.com/abayer/test-infra@v0.0.5/mungegithub/mungers/cherrypick-queue.go (about)

     1  /*
     2  Copyright 2015 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  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"sort"
    24  	"sync"
    25  	"time"
    26  
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"k8s.io/test-infra/mungegithub/features"
    29  	"k8s.io/test-infra/mungegithub/github"
    30  	"k8s.io/test-infra/mungegithub/options"
    31  
    32  	"github.com/golang/glog"
    33  )
    34  
    35  const (
    36  	cpCandidateLabel = "cherrypick-candidate"
    37  	cpApprovedLabel  = "cherrypick-approved"
    38  )
    39  
    40  var (
    41  	_       = fmt.Print
    42  	maxTime = time.Unix(1<<63-62135596801, 999999999) // http://stackoverflow.com/questions/25065055/what-is-the-maximum-time-time-in-go
    43  )
    44  
    45  type rawReadyInfo struct {
    46  	Number int
    47  	Title  string
    48  	SHA    string
    49  }
    50  
    51  type cherrypickStatus struct {
    52  	statusPullRequest
    53  	ExtraInfo []string
    54  }
    55  
    56  type queueData struct {
    57  	MergedAndApproved []cherrypickStatus
    58  	Merged            []cherrypickStatus
    59  	Unmerged          []cherrypickStatus
    60  }
    61  
    62  // CherrypickQueue will merge PR which meet a set of requirements.
    63  type CherrypickQueue struct {
    64  	sync.Mutex
    65  	mergedAndApproved map[int]*github.MungeObject
    66  	merged            map[int]*github.MungeObject
    67  	unmerged          map[int]*github.MungeObject
    68  }
    69  
    70  func init() {
    71  	RegisterMungerOrDie(&CherrypickQueue{})
    72  }
    73  
    74  // Name is the name usable in --pr-mungers
    75  func (c *CherrypickQueue) Name() string { return "cherrypick-queue" }
    76  
    77  // RequiredFeatures is a slice of 'features' that must be provided
    78  func (c *CherrypickQueue) RequiredFeatures() []string { return []string{features.ServerFeatureName} }
    79  
    80  // Initialize will initialize the munger
    81  func (c *CherrypickQueue) Initialize(config *github.Config, features *features.Features) error {
    82  	c.Lock()
    83  	defer c.Unlock()
    84  
    85  	c.mergedAndApproved = map[int]*github.MungeObject{}
    86  	c.merged = map[int]*github.MungeObject{}
    87  	c.unmerged = map[int]*github.MungeObject{}
    88  
    89  	if features.Server.Enabled {
    90  		features.Server.HandleFunc("/queue", c.serveQueue)
    91  		features.Server.HandleFunc("/raw", c.serveRaw)
    92  		features.Server.HandleFunc("/queue-info", c.serveQueueInfo)
    93  	}
    94  	return nil
    95  }
    96  
    97  // EachLoop is called at the start of every munge loop
    98  func (c *CherrypickQueue) EachLoop() error { return nil }
    99  
   100  // RegisterOptions registers options for this munger; returns any that require a restart when changed.
   101  func (c *CherrypickQueue) RegisterOptions(opts *options.Options) sets.String { return nil }
   102  
   103  // Munge is the workhorse the will actually make updates to the PR
   104  func (c *CherrypickQueue) Munge(obj *github.MungeObject) {
   105  	num := *obj.Issue.Number
   106  
   107  	if !obj.HasLabel(cpCandidateLabel) {
   108  		c.Lock()
   109  		// Make sure we don't track PR that don't have the flag
   110  		delete(c.mergedAndApproved, num)
   111  		delete(c.merged, num)
   112  		delete(c.unmerged, num)
   113  		c.Unlock()
   114  	}
   115  	if !obj.IsPR() {
   116  		return
   117  	}
   118  	// This will cache the PR and events so when we try to view the queue we don't
   119  	// hit github while trying to load the page
   120  	obj.GetPR()
   121  
   122  	c.Lock()
   123  	// Delete the PR before we re-add it where it should
   124  	delete(c.mergedAndApproved, num)
   125  	delete(c.merged, num)
   126  	delete(c.unmerged, num)
   127  	merged, _ := obj.IsMerged()
   128  	if merged {
   129  		if obj.HasLabel(cpApprovedLabel) {
   130  			c.mergedAndApproved[num] = obj
   131  		} else {
   132  			c.merged[num] = obj
   133  		}
   134  	} else {
   135  		c.unmerged[num] = obj
   136  	}
   137  	c.Unlock()
   138  	return
   139  }
   140  
   141  func mergeTime(obj *github.MungeObject) time.Time {
   142  	t, ok := obj.MergedAt()
   143  	if !ok || t == nil {
   144  		t = &maxTime
   145  	}
   146  	return *t
   147  }
   148  
   149  type cpQueueSorter []*github.MungeObject
   150  
   151  func (s cpQueueSorter) Len() int      { return len(s) }
   152  func (s cpQueueSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   153  
   154  //  PLEASE PLEASE PLEASE update serveQueueInfo() if you update this.
   155  func (s cpQueueSorter) Less(i, j int) bool {
   156  	a := s[i]
   157  	b := s[j]
   158  
   159  	// Sort first based on release milestone
   160  	// ignore errors because ReleaseMilestoneDue returns a time WAY in the future on errors
   161  	aDue, _ := a.ReleaseMilestoneDue()
   162  	bDue, _ := b.ReleaseMilestoneDue()
   163  	if aDue.Before(bDue) {
   164  		return true
   165  	} else if aDue.After(bDue) {
   166  		return false
   167  	}
   168  
   169  	// Then sort by the order in which they were merged
   170  	aTime := mergeTime(a)
   171  	bTime := mergeTime(b)
   172  	if aTime.Before(bTime) {
   173  		return true
   174  	} else if aTime.After(bTime) {
   175  		return false
   176  	}
   177  
   178  	// Then show those which have been approved
   179  	aApproved := a.HasLabel(cpApprovedLabel)
   180  	bApproved := b.HasLabel(cpApprovedLabel)
   181  	if aApproved && !bApproved {
   182  		return true
   183  	} else if !aApproved && bApproved {
   184  		return false
   185  	}
   186  
   187  	// Sort by LGTM as humans are likely to want to approve
   188  	// those first. After it merges the above check will win
   189  	// and LGTM won't matter
   190  	aLGTM := a.HasLabel(lgtmLabel)
   191  	bLGTM := b.HasLabel(lgtmLabel)
   192  	if aLGTM && !bLGTM {
   193  		return true
   194  	} else if !aLGTM && bLGTM {
   195  		return false
   196  	}
   197  
   198  	// And finally by issue number, just so there is some consistency
   199  	return *a.Issue.Number < *b.Issue.Number
   200  }
   201  
   202  func orderedQueue(queue map[int]*github.MungeObject) []int {
   203  	objs := []*github.MungeObject{}
   204  	for _, obj := range queue {
   205  		objs = append(objs, obj)
   206  	}
   207  	sort.Sort(cpQueueSorter(objs))
   208  
   209  	var ordered []int
   210  	for _, obj := range objs {
   211  		ordered = append(ordered, *obj.Issue.Number)
   212  	}
   213  	return ordered
   214  }
   215  
   216  // copyQueue returns a copy of the queue.
   217  func (c *CherrypickQueue) copyQueue(queue map[int]*github.MungeObject) map[int]*github.MungeObject {
   218  	c.Lock()
   219  	defer c.Unlock()
   220  
   221  	out := map[int]*github.MungeObject{}
   222  	for i, v := range queue {
   223  		out[i] = v
   224  	}
   225  	return out
   226  }
   227  
   228  func (c *CherrypickQueue) serveRaw(res http.ResponseWriter, req *http.Request) {
   229  	c.Lock()
   230  	queue := c.copyQueue(c.mergedAndApproved)
   231  	c.Unlock()
   232  	keyOrder := orderedQueue(queue)
   233  	sortedQueue := []rawReadyInfo{}
   234  	for _, key := range keyOrder {
   235  		obj := queue[key]
   236  		sha, ok := obj.MergeCommit()
   237  		if !ok || sha == nil {
   238  			empty := "UnknownSHA"
   239  			sha = &empty
   240  		}
   241  		rri := rawReadyInfo{
   242  			Number: *obj.Issue.Number,
   243  			Title:  *obj.Issue.Title,
   244  			SHA:    *sha,
   245  		}
   246  		sortedQueue = append(sortedQueue, rri)
   247  	}
   248  	data, err := json.Marshal(sortedQueue)
   249  	if err != nil {
   250  		res.Header().Set("Content-type", "text/plain")
   251  		res.WriteHeader(http.StatusInternalServerError)
   252  		glog.Errorf("Unable to Marshal Status: %v: %v", data, err)
   253  		return
   254  	}
   255  	res.Header().Set("Content-type", "application/json")
   256  	res.WriteHeader(http.StatusOK)
   257  	res.Write(data)
   258  }
   259  
   260  func (c *CherrypickQueue) getQueueData(queue map[int]*github.MungeObject) []cherrypickStatus {
   261  	out := []cherrypickStatus{}
   262  	keyOrder := orderedQueue(queue)
   263  	for _, key := range keyOrder {
   264  		obj := queue[key]
   265  		cps := cherrypickStatus{
   266  			statusPullRequest: *objToStatusPullRequest(obj),
   267  		}
   268  		if obj.HasLabel(cpApprovedLabel) {
   269  			cps.ExtraInfo = append(cps.ExtraInfo, cpApprovedLabel)
   270  		}
   271  		milestone, ok := obj.ReleaseMilestone()
   272  		if ok && milestone != "" {
   273  			cps.ExtraInfo = append(cps.ExtraInfo, milestone)
   274  		}
   275  		merged, _ := obj.IsMerged()
   276  		if !merged && obj.HasLabel(lgtmLabel) {
   277  			// Don't bother showing LGTM for merged things
   278  			// it's just a distraction at that point
   279  			cps.ExtraInfo = append(cps.ExtraInfo, lgtmLabel)
   280  		}
   281  		out = append(out, cps)
   282  	}
   283  
   284  	return out
   285  }
   286  
   287  func (c *CherrypickQueue) serveQueue(res http.ResponseWriter, req *http.Request) {
   288  	outData := queueData{}
   289  
   290  	c.Lock()
   291  	outData.MergedAndApproved = c.getQueueData(c.mergedAndApproved)
   292  	outData.Merged = c.getQueueData(c.merged)
   293  	outData.Unmerged = c.getQueueData(c.unmerged)
   294  	c.Unlock()
   295  
   296  	data, err := json.Marshal(outData)
   297  	if err != nil {
   298  		res.Header().Set("Content-type", "text/plain")
   299  		res.WriteHeader(http.StatusInternalServerError)
   300  		glog.Errorf("Unable to Marshal Status: %v: %v", data, err)
   301  		return
   302  	}
   303  	res.Header().Set("Content-type", "application/json")
   304  	res.WriteHeader(http.StatusOK)
   305  	res.Write(data)
   306  }
   307  
   308  func (c *CherrypickQueue) serveQueueInfo(res http.ResponseWriter, req *http.Request) {
   309  	res.Header().Set("Content-type", "text/plain")
   310  	res.WriteHeader(http.StatusOK)
   311  	res.Write([]byte(`The cherrypick queue is sorted by the following. If there is a tie in any test the next test will be used.
   312  <ol>
   313    <li>Milestone Due Date
   314      <ul>
   315        <li>Release milestones must be of the form vX.Y</li>
   316        <li>PRs without a milestone are considered after PRs with a milestone</li>
   317      </ul>
   318    </li>
   319    <li>Labeld with "` + cpApprovedLabel + `"
   320      <ul>
   321        <li>PRs with the "` + cpApprovedLabel + `" label come before those without</li>
   322      </ul>
   323    </li>
   324    <li>Merge Time
   325      <ul>
   326        <li>The earlier a PR was merged the earlier it is in the list</li>
   327        <li>PRs which have not merged are considered 'after' any merged PR</li>
   328      </ul>
   329    </li>
   330    <li>Labeld with "` + lgtmLabel + `"
   331      <ul>
   332        <li>PRs with the "` + lgtmLabel + `" label come before those without</li>
   333      </ul>
   334    <li>PR number</li>
   335  </ol> `))
   336  }