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