github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/velodrome/fetcher/client.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 main
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"strings"
    24  	"time"
    25  
    26  	"golang.org/x/oauth2"
    27  
    28  	"github.com/golang/glog"
    29  	"github.com/google/go-github/github"
    30  	"github.com/spf13/cobra"
    31  )
    32  
    33  // Client can be used to run commands again Github API
    34  type Client struct {
    35  	Token     string
    36  	TokenFile string
    37  	Org       string
    38  	Project   string
    39  
    40  	githubClient *github.Client
    41  }
    42  
    43  const (
    44  	tokenLimit = 50 // We try to stop that far from the API limit
    45  )
    46  
    47  // AddFlags parses options for github client
    48  func (client *Client) AddFlags(cmd *cobra.Command) {
    49  	cmd.PersistentFlags().StringVar(&client.Token, "token", "",
    50  		"The OAuth Token to use for requests.")
    51  	cmd.PersistentFlags().StringVar(&client.TokenFile, "token-file", "",
    52  		"The file containing the OAuth Token to use for requests.")
    53  	cmd.PersistentFlags().StringVar(&client.Org, "organization", "",
    54  		"The github organization to scan")
    55  	cmd.PersistentFlags().StringVar(&client.Project, "project", "",
    56  		"The github project to scan")
    57  }
    58  
    59  // CheckFlags looks for organization and project flags to configure the client
    60  func (client *Client) CheckFlags() error {
    61  	if client.Org == "" {
    62  		return fmt.Errorf("organization flag must be set")
    63  	}
    64  	client.Org = strings.ToLower(client.Org)
    65  
    66  	if client.Project == "" {
    67  		return fmt.Errorf("project flag must be set")
    68  	}
    69  	client.Project = strings.ToLower(client.Project)
    70  
    71  	return nil
    72  }
    73  
    74  // getGithubClient create the github client that we use to communicate with github
    75  func (client *Client) getGithubClient() (*github.Client, error) {
    76  	if client.githubClient != nil {
    77  		return client.githubClient, nil
    78  	}
    79  	token := client.Token
    80  	if len(token) == 0 && len(client.TokenFile) != 0 {
    81  		data, err := ioutil.ReadFile(client.TokenFile)
    82  		if err != nil {
    83  			return nil, err
    84  		}
    85  		token = strings.TrimSpace(string(data))
    86  	}
    87  
    88  	if len(token) > 0 {
    89  		ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
    90  		tc := oauth2.NewClient(oauth2.NoContext, ts)
    91  		client.githubClient = github.NewClient(tc)
    92  	} else {
    93  		client.githubClient = github.NewClient(nil)
    94  	}
    95  	return client.githubClient, nil
    96  }
    97  
    98  // limitsCheckAndWait make sure we have not reached the limit or wait
    99  func (client *Client) limitsCheckAndWait() {
   100  	var sleep time.Duration
   101  	githubClient, err := client.getGithubClient()
   102  	if err != nil {
   103  		glog.Error("Failed to get RateLimits: ", err)
   104  		sleep = time.Minute
   105  	} else {
   106  		limits, _, err := githubClient.RateLimits(context.Background())
   107  		if err != nil {
   108  			glog.Error("Failed to get RateLimits:", err)
   109  			sleep = time.Minute
   110  		}
   111  		if limits != nil && limits.Core != nil && limits.Core.Remaining < tokenLimit {
   112  			sleep = limits.Core.Reset.Sub(time.Now())
   113  			glog.Warning("RateLimits: reached. Sleeping for ", sleep)
   114  		}
   115  	}
   116  
   117  	time.Sleep(sleep)
   118  }
   119  
   120  // ClientInterface describes what a client should be able to do
   121  type ClientInterface interface {
   122  	RepositoryName() string
   123  	FetchIssues(last time.Time, c chan *github.Issue)
   124  	FetchIssueEvents(issueID int, last *int, c chan *github.IssueEvent)
   125  	FetchIssueComments(issueID int, last time.Time, c chan *github.IssueComment)
   126  	FetchPullComments(issueID int, last time.Time, c chan *github.PullRequestComment)
   127  }
   128  
   129  // RepositoryName returns github's repository name in the form of org/project
   130  func (client *Client) RepositoryName() string {
   131  	return fmt.Sprintf("%s/%s", client.Org, client.Project)
   132  }
   133  
   134  // FetchIssues from Github, until 'latest' time
   135  func (client *Client) FetchIssues(latest time.Time, c chan *github.Issue) {
   136  	opt := &github.IssueListByRepoOptions{Since: latest, Sort: "updated", State: "all", Direction: "asc"}
   137  
   138  	githubClient, err := client.getGithubClient()
   139  	if err != nil {
   140  		close(c)
   141  		glog.Error(err)
   142  		return
   143  	}
   144  
   145  	count := 0
   146  	for {
   147  		client.limitsCheckAndWait()
   148  
   149  		issues, resp, err := githubClient.Issues.ListByRepo(
   150  			context.Background(),
   151  			client.Org,
   152  			client.Project,
   153  			opt,
   154  		)
   155  		if err != nil {
   156  			close(c)
   157  			glog.Error(err)
   158  			return
   159  		}
   160  
   161  		for _, issue := range issues {
   162  			c <- issue
   163  			count++
   164  		}
   165  
   166  		if resp.NextPage == 0 {
   167  			break
   168  		}
   169  		opt.ListOptions.Page = resp.NextPage
   170  	}
   171  
   172  	glog.Infof("Fetched %d issues updated issue since %v.", count, latest)
   173  	close(c)
   174  }
   175  
   176  // hasID look for a specific Id in a list of events
   177  func hasID(events []*github.IssueEvent, ID int) bool {
   178  	for _, event := range events {
   179  		if *event.ID == ID {
   180  			return true
   181  		}
   182  	}
   183  	return false
   184  }
   185  
   186  // FetchIssueEvents from github and return the full list, until it matches 'latest'
   187  // The entire last page will be included so you can have redundancy.
   188  func (client *Client) FetchIssueEvents(issueID int, latest *int, c chan *github.IssueEvent) {
   189  	opt := &github.ListOptions{PerPage: 100}
   190  
   191  	githubClient, err := client.getGithubClient()
   192  	if err != nil {
   193  		close(c)
   194  		glog.Error(err)
   195  		return
   196  	}
   197  
   198  	count := 0
   199  	for {
   200  		client.limitsCheckAndWait()
   201  
   202  		events, resp, err := githubClient.Issues.ListIssueEvents(
   203  			context.Background(),
   204  			client.Org,
   205  			client.Project,
   206  			issueID,
   207  			opt,
   208  		)
   209  		if err != nil {
   210  			glog.Errorf("ListIssueEvents failed: %s. Retrying...", err)
   211  			time.Sleep(time.Second)
   212  			continue
   213  		}
   214  
   215  		for _, event := range events {
   216  			c <- event
   217  			count++
   218  		}
   219  		if resp.NextPage == 0 || (latest != nil && hasID(events, *latest)) {
   220  			break
   221  		}
   222  		opt.Page = resp.NextPage
   223  	}
   224  
   225  	glog.Infof("Fetched %d events.", count)
   226  	close(c)
   227  }
   228  
   229  // FetchIssueComments fetches comments associated to given Issue (since latest)
   230  func (client *Client) FetchIssueComments(issueID int, latest time.Time, c chan *github.IssueComment) {
   231  	opt := &github.IssueListCommentsOptions{Since: latest, Sort: "updated", Direction: "asc"}
   232  
   233  	githubClient, err := client.getGithubClient()
   234  	if err != nil {
   235  		close(c)
   236  		glog.Error(err)
   237  		return
   238  	}
   239  
   240  	count := 0
   241  	for {
   242  		client.limitsCheckAndWait()
   243  
   244  		comments, resp, err := githubClient.Issues.ListComments(
   245  			context.Background(),
   246  			client.Org,
   247  			client.Project,
   248  			issueID,
   249  			opt,
   250  		)
   251  		if err != nil {
   252  			close(c)
   253  			glog.Error(err)
   254  			return
   255  		}
   256  
   257  		for _, comment := range comments {
   258  			c <- comment
   259  			count++
   260  		}
   261  		if resp.NextPage == 0 {
   262  			break
   263  		}
   264  		opt.ListOptions.Page = resp.NextPage
   265  	}
   266  
   267  	glog.Infof("Fetched %d issue comments updated since %v for issue #%d.", count, latest, issueID)
   268  	close(c)
   269  }
   270  
   271  // FetchPullComments fetches comments associated to given PullRequest (since latest)
   272  func (client *Client) FetchPullComments(issueID int, latest time.Time, c chan *github.PullRequestComment) {
   273  	opt := &github.PullRequestListCommentsOptions{Since: latest, Sort: "updated", Direction: "asc"}
   274  
   275  	githubClient, err := client.getGithubClient()
   276  	if err != nil {
   277  		close(c)
   278  		glog.Error(err)
   279  		return
   280  	}
   281  
   282  	count := 0
   283  	for {
   284  		client.limitsCheckAndWait()
   285  
   286  		comments, resp, err := githubClient.PullRequests.ListComments(
   287  			context.Background(),
   288  			client.Org,
   289  			client.Project,
   290  			issueID,
   291  			opt,
   292  		)
   293  		if err != nil {
   294  			close(c)
   295  			glog.Error(err)
   296  			return
   297  		}
   298  
   299  		for _, comment := range comments {
   300  			c <- comment
   301  			count++
   302  		}
   303  		if resp.NextPage == 0 {
   304  			break
   305  		}
   306  		opt.ListOptions.Page = resp.NextPage
   307  	}
   308  
   309  	glog.Infof("Fetched %d review comments updated since %v for issue #%d.", count, latest, issueID)
   310  	close(c)
   311  }