mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 12:18:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			284 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			284 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2015 The Kubernetes Authors All rights reserved.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package github
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
 | 
						|
 | 
						|
	"github.com/golang/glog"
 | 
						|
	"github.com/google/go-github/github"
 | 
						|
	"golang.org/x/oauth2"
 | 
						|
)
 | 
						|
 | 
						|
func MakeClient(token string) *github.Client {
 | 
						|
	if len(token) > 0 {
 | 
						|
		ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
 | 
						|
		tc := oauth2.NewClient(oauth2.NoContext, ts)
 | 
						|
		return github.NewClient(tc)
 | 
						|
	}
 | 
						|
	return github.NewClient(nil)
 | 
						|
}
 | 
						|
 | 
						|
func hasLabel(labels []github.Label, name string) bool {
 | 
						|
	for i := range labels {
 | 
						|
		label := &labels[i]
 | 
						|
		if label.Name != nil && *label.Name == name {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
func hasLabels(labels []github.Label, names []string) bool {
 | 
						|
	for i := range names {
 | 
						|
		if !hasLabel(labels, names[i]) {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
func fetchAllPRs(client *github.Client, user, project string) ([]github.PullRequest, error) {
 | 
						|
	page := 1
 | 
						|
	var result []github.PullRequest
 | 
						|
	for {
 | 
						|
		glog.V(4).Infof("Fetching page %d", page)
 | 
						|
		listOpts := &github.PullRequestListOptions{
 | 
						|
			Sort:        "desc",
 | 
						|
			ListOptions: github.ListOptions{PerPage: 100, Page: page},
 | 
						|
		}
 | 
						|
		prs, response, err := client.PullRequests.List(user, project, listOpts)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		result = append(result, prs...)
 | 
						|
		if response.LastPage == 0 || response.LastPage == page {
 | 
						|
			break
 | 
						|
		}
 | 
						|
		page++
 | 
						|
	}
 | 
						|
	return result, nil
 | 
						|
}
 | 
						|
 | 
						|
type PRFunction func(*github.Client, *github.PullRequest, *github.Issue) error
 | 
						|
 | 
						|
type FilterConfig struct {
 | 
						|
	MinPRNumber            int
 | 
						|
	UserWhitelist          []string
 | 
						|
	WhitelistOverride      string
 | 
						|
	RequiredStatusContexts []string
 | 
						|
}
 | 
						|
 | 
						|
// For each PR in the project that matches:
 | 
						|
//   * pr.Number > minPRNumber
 | 
						|
//   * is mergeable
 | 
						|
//   * has labels "cla: yes", "lgtm"
 | 
						|
//   * combinedStatus = 'success' (e.g. all hooks have finished success in github)
 | 
						|
// Run the specified function
 | 
						|
func ForEachCandidatePRDo(client *github.Client, user, project string, fn PRFunction, once bool, config *FilterConfig) error {
 | 
						|
	// Get all PRs
 | 
						|
	prs, err := fetchAllPRs(client, user, project)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	userSet := util.StringSet{}
 | 
						|
	userSet.Insert(config.UserWhitelist...)
 | 
						|
 | 
						|
	for ix := range prs {
 | 
						|
		if prs[ix].User == nil || prs[ix].User.Login == nil {
 | 
						|
			glog.V(2).Infof("Skipping PR %d with no user info %v.", *prs[ix].Number, *prs[ix].User)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if *prs[ix].Number < config.MinPRNumber {
 | 
						|
			glog.V(6).Infof("Dropping %d < %d", *prs[ix].Number, config.MinPRNumber)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		pr, _, err := client.PullRequests.Get(user, project, *prs[ix].Number)
 | 
						|
		if err != nil {
 | 
						|
			glog.Errorf("Error getting pull request: %v", err)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		glog.V(2).Infof("----==== %d ====----", *pr.Number)
 | 
						|
 | 
						|
		// Labels are actually stored in the Issues API, not the Pull Request API
 | 
						|
		issue, _, err := client.Issues.Get(user, project, *pr.Number)
 | 
						|
		if err != nil {
 | 
						|
			glog.Errorf("Failed to get issue for PR: %v", err)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		glog.V(8).Infof("%v", issue.Labels)
 | 
						|
		if !hasLabels(issue.Labels, []string{"lgtm", "cla: yes"}) {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if !hasLabel(issue.Labels, config.WhitelistOverride) && !userSet.Has(*prs[ix].User.Login) {
 | 
						|
			glog.V(4).Infof("Dropping %d since %s isn't in whitelist and %s isn't present", *prs[ix].Number, *prs[ix].User.Login, config.WhitelistOverride)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// This is annoying, github appears to only temporarily cache mergeability, if it is nil, wait
 | 
						|
		// for an async refresh and retry.
 | 
						|
		if pr.Mergeable == nil {
 | 
						|
			glog.Infof("Waiting for mergeability on %s %d", *pr.Title, *pr.Number)
 | 
						|
			// TODO: determine what a good empirical setting for this is.
 | 
						|
			time.Sleep(10 * time.Second)
 | 
						|
			pr, _, err = client.PullRequests.Get(user, project, *prs[ix].Number)
 | 
						|
		}
 | 
						|
		if pr.Mergeable == nil {
 | 
						|
			glog.Errorf("No mergeability information for %s %d, Skipping.", *pr.Title, *pr.Number)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if !*pr.Mergeable {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// Validate the status information for this PR
 | 
						|
		ok, err := ValidateStatus(client, user, project, *pr.Number, config.RequiredStatusContexts, false)
 | 
						|
		if err != nil {
 | 
						|
			glog.Errorf("Error validating PR status: %v", err)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if !ok {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if err := fn(client, pr, issue); err != nil {
 | 
						|
			glog.Errorf("Failed to run user function: %v", err)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if once {
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func getCommitStatus(client *github.Client, user, project string, prNumber int) ([]*github.CombinedStatus, error) {
 | 
						|
	commits, _, err := client.PullRequests.ListCommits(user, project, prNumber, &github.ListOptions{})
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	commitStatus := make([]*github.CombinedStatus, len(commits))
 | 
						|
	for ix := range commits {
 | 
						|
		commit := &commits[ix]
 | 
						|
		statusList, _, err := client.Repositories.GetCombinedStatus(user, project, *commit.SHA, &github.ListOptions{})
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		commitStatus[ix] = statusList
 | 
						|
	}
 | 
						|
	return commitStatus, nil
 | 
						|
}
 | 
						|
 | 
						|
// Gets the current status of a PR by introspecting the status of the commits in the PR.
 | 
						|
// The rules are:
 | 
						|
//    * If any member of the 'requiredContexts' list is missing, it is 'incomplete'
 | 
						|
//    * If any commit is 'pending', the PR is 'pending'
 | 
						|
//    * If any commit is 'error', the PR is in 'error'
 | 
						|
//    * If any commit is 'failure', the PR is 'failure'
 | 
						|
//    * Otherwise the PR is 'success'
 | 
						|
func GetStatus(client *github.Client, user, project string, prNumber int, requiredContexts []string) (string, error) {
 | 
						|
	statusList, err := getCommitStatus(client, user, project, prNumber)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	return computeStatus(statusList, requiredContexts), nil
 | 
						|
}
 | 
						|
 | 
						|
func computeStatus(statusList []*github.CombinedStatus, requiredContexts []string) string {
 | 
						|
	states := util.StringSet{}
 | 
						|
	providers := util.StringSet{}
 | 
						|
	for ix := range statusList {
 | 
						|
		status := statusList[ix]
 | 
						|
		glog.V(8).Infof("Checking commit: %s", *status.SHA)
 | 
						|
		glog.V(8).Infof("Checking commit: %v", status)
 | 
						|
		states.Insert(*status.State)
 | 
						|
 | 
						|
		for _, subStatus := range status.Statuses {
 | 
						|
			glog.V(8).Infof("Found status from: %v", subStatus)
 | 
						|
			providers.Insert(*subStatus.Context)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	for _, provider := range requiredContexts {
 | 
						|
		if !providers.Has(provider) {
 | 
						|
			glog.V(8).Infof("Failed to find %s in %v", provider, providers)
 | 
						|
			return "incomplete"
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	switch {
 | 
						|
	case states.Has("pending"):
 | 
						|
		return "pending"
 | 
						|
	case states.Has("error"):
 | 
						|
		return "error"
 | 
						|
	case states.Has("failure"):
 | 
						|
		return "failure"
 | 
						|
	default:
 | 
						|
		return "success"
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Make sure that the combined status for all commits in a PR is 'success'
 | 
						|
// if 'waitForPending' is true, this function will wait until the PR is no longer pending (all checks have run)
 | 
						|
func ValidateStatus(client *github.Client, user, project string, prNumber int, requiredContexts []string, waitOnPending bool) (bool, error) {
 | 
						|
	pending := true
 | 
						|
	for pending {
 | 
						|
		status, err := GetStatus(client, user, project, prNumber, requiredContexts)
 | 
						|
		if err != nil {
 | 
						|
			return false, err
 | 
						|
		}
 | 
						|
		switch status {
 | 
						|
		case "error", "failure":
 | 
						|
			return false, nil
 | 
						|
		case "pending":
 | 
						|
			if !waitOnPending {
 | 
						|
				return false, nil
 | 
						|
			}
 | 
						|
			pending = true
 | 
						|
			glog.V(4).Info("PR is pending, waiting for 30 seconds")
 | 
						|
			time.Sleep(30 * time.Second)
 | 
						|
		case "success":
 | 
						|
			return true, nil
 | 
						|
		case "incomplete":
 | 
						|
			return false, nil
 | 
						|
		default:
 | 
						|
			return false, fmt.Errorf("unknown status: %s", status)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return true, nil
 | 
						|
}
 | 
						|
 | 
						|
// Wait for a PR to move into Pending.  This is useful because the request to test a PR again
 | 
						|
// is asynchronous with the PR actually moving into a pending state
 | 
						|
// TODO: add a timeout
 | 
						|
func WaitForPending(client *github.Client, user, project string, prNumber int) error {
 | 
						|
	for {
 | 
						|
		status, err := GetStatus(client, user, project, prNumber, []string{})
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if status == "pending" {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		glog.V(4).Info("PR is not pending, waiting for 30 seconds")
 | 
						|
		time.Sleep(30 * time.Second)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 |