github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/gerrit/client/client.go (about)

     1  /*
     2  Copyright 2018 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 client implements a client that can handle multiple gerrit instances
    18  // derived from https://github.com/andygrunwald/go-gerrit
    19  package client
    20  
    21  import (
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net/url"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/andygrunwald/go-gerrit"
    29  	"github.com/sirupsen/logrus"
    30  )
    31  
    32  const (
    33  	// LGTM means all presubmits passed, but need someone else to approve before merge.
    34  	LGTM = "+1"
    35  	// LBTM means some presubmits failed, perfer not merge.
    36  	LBTM = "-1"
    37  	// CodeReview is the default gerrit code review label
    38  	CodeReview = "Code-Review"
    39  
    40  	// GerritID identifies a gerrit change
    41  	GerritID = "prow.k8s.io/gerrit-id"
    42  	// GerritInstance is the gerrit host url
    43  	GerritInstance = "prow.k8s.io/gerrit-instance"
    44  	// GerritRevision is the SHA of current patchset from a gerrit change
    45  	GerritRevision = "prow.k8s.io/gerrit-revision"
    46  	// GerritReportLabel is the gerrit label prow will cast vote on, fallback to CodeReview label if unset
    47  	GerritReportLabel = "prow.k8s.io/gerrit-report-label"
    48  
    49  	// Merged status indicates a Gerrit change has been merged
    50  	Merged = "MERGED"
    51  	// New status indicates a Gerrit change is new (ie pending)
    52  	New = "NEW"
    53  )
    54  
    55  // ProjectsFlag is the flag type for gerrit projects when initializing a gerrit client
    56  type ProjectsFlag map[string][]string
    57  
    58  func (p ProjectsFlag) String() string {
    59  	var hosts []string
    60  	for host, repos := range p {
    61  		hosts = append(hosts, host+"="+strings.Join(repos, ","))
    62  	}
    63  	return strings.Join(hosts, " ")
    64  }
    65  
    66  // Set populates ProjectsFlag upon flag.Parse()
    67  func (p ProjectsFlag) Set(value string) error {
    68  	parts := strings.SplitN(value, "=", 2)
    69  	if len(parts) != 2 {
    70  		return fmt.Errorf("%s not in the form of host=repo-a,repo-b,etc", value)
    71  	}
    72  	host := parts[0]
    73  	if _, ok := p[host]; ok {
    74  		return fmt.Errorf("duplicate host: %s", host)
    75  	}
    76  	repos := strings.Split(parts[1], ",")
    77  	p[host] = repos
    78  	return nil
    79  }
    80  
    81  type gerritAuthentication interface {
    82  	SetCookieAuth(name, value string)
    83  }
    84  
    85  type gerritAccount interface {
    86  	GetAccount(name string) (*gerrit.AccountInfo, *gerrit.Response, error)
    87  	SetUsername(accountID string, input *gerrit.UsernameInput) (*string, *gerrit.Response, error)
    88  }
    89  
    90  type gerritChange interface {
    91  	QueryChanges(opt *gerrit.QueryChangeOptions) (*[]gerrit.ChangeInfo, *gerrit.Response, error)
    92  	SetReview(changeID, revisionID string, input *gerrit.ReviewInput) (*gerrit.ReviewResult, *gerrit.Response, error)
    93  }
    94  
    95  type gerritProjects interface {
    96  	GetBranch(projectName, branchID string) (*gerrit.BranchInfo, *gerrit.Response, error)
    97  }
    98  
    99  // gerritInstanceHandler holds all actual gerrit handlers
   100  type gerritInstanceHandler struct {
   101  	instance string
   102  	projects []string
   103  
   104  	authService    gerritAuthentication
   105  	accountService gerritAccount
   106  	changeService  gerritChange
   107  	projectService gerritProjects
   108  }
   109  
   110  // Client holds a instance:handler map
   111  type Client struct {
   112  	handlers map[string]*gerritInstanceHandler
   113  }
   114  
   115  // ChangeInfo is a gerrit.ChangeInfo
   116  type ChangeInfo = gerrit.ChangeInfo
   117  
   118  // RevisionInfo is a gerrit.RevisionInfo
   119  type RevisionInfo = gerrit.RevisionInfo
   120  
   121  // FileInfo is a gerrit.FileInfo
   122  type FileInfo = gerrit.FileInfo
   123  
   124  // NewClient returns a new gerrit client
   125  func NewClient(instances map[string][]string) (*Client, error) {
   126  	c := &Client{
   127  		handlers: map[string]*gerritInstanceHandler{},
   128  	}
   129  	for instance := range instances {
   130  		gc, err := gerrit.NewClient(instance, nil)
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  
   135  		c.handlers[instance] = &gerritInstanceHandler{
   136  			instance:       instance,
   137  			projects:       instances[instance],
   138  			authService:    gc.Authentication,
   139  			accountService: gc.Accounts,
   140  			changeService:  gc.Changes,
   141  			projectService: gc.Projects,
   142  		}
   143  	}
   144  
   145  	return c, nil
   146  }
   147  
   148  func auth(c *Client, cookiefilePath string) {
   149  	logrus.Info("Starting auth loop...")
   150  	var previousToken string
   151  	wait := 10 * time.Minute
   152  	for {
   153  		raw, err := ioutil.ReadFile(cookiefilePath)
   154  		if err != nil {
   155  			logrus.WithError(err).Error("Failed to read auth cookie")
   156  		}
   157  		fields := strings.Fields(string(raw))
   158  		token := fields[len(fields)-1]
   159  
   160  		if token == previousToken {
   161  			time.Sleep(wait)
   162  			continue
   163  		}
   164  
   165  		logrus.Info("New token, updating handlers...")
   166  
   167  		// update auth token for each instance
   168  		for _, handler := range c.handlers {
   169  			handler.authService.SetCookieAuth("o", token)
   170  
   171  			self, _, err := handler.accountService.GetAccount("self")
   172  			if err != nil {
   173  				logrus.WithError(err).Error("Failed to auth with token")
   174  				continue
   175  			}
   176  
   177  			logrus.Infof("Authentication to %s successful, Username: %s", handler.instance, self.Name)
   178  		}
   179  		previousToken = token
   180  		time.Sleep(wait)
   181  	}
   182  }
   183  
   184  // Start will authenticate the client with gerrit periodically
   185  // Start must be called before user calls any client functions.
   186  func (c *Client) Start(cookiefilePath string) {
   187  	if cookiefilePath != "" {
   188  		go auth(c, cookiefilePath)
   189  	}
   190  }
   191  
   192  // QueryChanges queries for all changes from all projects after lastUpdate time
   193  // returns an instance:changes map
   194  func (c *Client) QueryChanges(lastUpdate time.Time, rateLimit int) map[string][]ChangeInfo {
   195  	result := map[string][]ChangeInfo{}
   196  	for _, h := range c.handlers {
   197  		changes := h.queryAllChanges(lastUpdate, rateLimit)
   198  		if len(changes) > 0 {
   199  			result[h.instance] = []ChangeInfo{}
   200  			for _, change := range changes {
   201  				result[h.instance] = append(result[h.instance], change)
   202  			}
   203  		}
   204  	}
   205  	return result
   206  }
   207  
   208  // SetReview writes a review comment base on the change id + revision
   209  func (c *Client) SetReview(instance, id, revision, message string, labels map[string]string) error {
   210  	h, ok := c.handlers[instance]
   211  	if !ok {
   212  		return fmt.Errorf("not activated gerrit instance: %s", instance)
   213  	}
   214  
   215  	if _, _, err := h.changeService.SetReview(id, revision, &gerrit.ReviewInput{
   216  		Message: message,
   217  		Labels:  labels,
   218  	}); err != nil {
   219  		return fmt.Errorf("cannot comment to gerrit: %v", err)
   220  	}
   221  
   222  	return nil
   223  }
   224  
   225  // GetBranchRevision returns SHA of HEAD of a branch
   226  func (c *Client) GetBranchRevision(instance, project, branch string) (string, error) {
   227  	h, ok := c.handlers[instance]
   228  	if !ok {
   229  		return "", fmt.Errorf("not activated gerrit instance: %s", instance)
   230  	}
   231  
   232  	res, _, err := h.projectService.GetBranch(project, url.QueryEscape(branch))
   233  	if err != nil {
   234  		return "", err
   235  	}
   236  
   237  	return res.Revision, nil
   238  }
   239  
   240  // private handler implementation details
   241  
   242  func (h *gerritInstanceHandler) queryAllChanges(lastUpdate time.Time, rateLimit int) []gerrit.ChangeInfo {
   243  	result := []gerrit.ChangeInfo{}
   244  	for _, project := range h.projects {
   245  		changes, err := h.queryChangesForProject(project, lastUpdate, rateLimit)
   246  		if err != nil {
   247  			// don't halt on error from one project, log & continue
   248  			logrus.WithError(err).Errorf("fail to query changes for project %s", project)
   249  			continue
   250  		}
   251  		result = append(result, changes...)
   252  	}
   253  
   254  	return result
   255  }
   256  
   257  func (h *gerritInstanceHandler) queryChangesForProject(project string, lastUpdate time.Time, rateLimit int) ([]gerrit.ChangeInfo, error) {
   258  	pending := []gerrit.ChangeInfo{}
   259  
   260  	opt := &gerrit.QueryChangeOptions{}
   261  	opt.Query = append(opt.Query, "project:"+project)
   262  	opt.AdditionalFields = []string{"CURRENT_REVISION", "CURRENT_COMMIT", "CURRENT_FILES"}
   263  
   264  	start := 0
   265  
   266  	for {
   267  		opt.Limit = rateLimit
   268  		opt.Start = start
   269  
   270  		// The change output is sorted by the last update time, most recently updated to oldest updated.
   271  		// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
   272  		changes, _, err := h.changeService.QueryChanges(opt)
   273  		if err != nil {
   274  			// should not happen? Let next sync loop catch up
   275  			return nil, fmt.Errorf("failed to query gerrit changes: %v", err)
   276  		}
   277  
   278  		if changes == nil || len(*changes) == 0 {
   279  			logrus.Infof("no more changes from query, returning...")
   280  			return pending, nil
   281  		}
   282  
   283  		logrus.Infof("Find %d changes from query %v", len(*changes), opt.Query)
   284  
   285  		start += len(*changes)
   286  
   287  		for _, change := range *changes {
   288  			// if we already processed this change, then we stop the current sync loop
   289  			const layout = "2006-01-02 15:04:05"
   290  			updated, err := time.Parse(layout, change.Updated)
   291  			if err != nil {
   292  				logrus.WithError(err).Errorf("Parse time %v failed", change.Updated)
   293  				continue
   294  			}
   295  
   296  			logrus.Infof("Change %d, last updated %s, status %s", change.Number, change.Updated, change.Status)
   297  
   298  			// process if updated later than last updated
   299  			// stop if update was stale
   300  			if !updated.Before(lastUpdate) {
   301  				switch change.Status {
   302  				case Merged:
   303  					submitted, err := time.Parse(layout, change.Submitted)
   304  					if err != nil {
   305  						logrus.WithError(err).Errorf("Parse time %v failed", change.Submitted)
   306  						continue
   307  					}
   308  					if submitted.Before(lastUpdate) {
   309  						logrus.Infof("Change %d, submitted %s before lastUpdate %s, skipping this patchset", change.Number, submitted, lastUpdate)
   310  						continue
   311  					}
   312  					pending = append(pending, change)
   313  				case New:
   314  					// we need to make sure the change update is from a fresh commit change
   315  					rev, ok := change.Revisions[change.CurrentRevision]
   316  					if !ok {
   317  						logrus.WithError(err).Errorf("(should not happen?)cannot find current revision for change %v", change.ID)
   318  						continue
   319  					}
   320  
   321  					created, err := time.Parse(layout, rev.Created)
   322  					if err != nil {
   323  						logrus.WithError(err).Errorf("Parse time %v failed", rev.Created)
   324  						continue
   325  					}
   326  
   327  					if created.Before(lastUpdate) {
   328  						// stale commit
   329  						logrus.Infof("Change %d, latest revision updated %s before lastUpdate %s, skipping this patchset", change.Number, created, lastUpdate)
   330  						continue
   331  					}
   332  
   333  					pending = append(pending, change)
   334  				default:
   335  					// change has been abandoned, do nothing
   336  				}
   337  			} else {
   338  				logrus.Infof("Change %d, updated %s before lastUpdate %s, return", change.Number, change.Updated, lastUpdate)
   339  				return pending, nil
   340  			}
   341  		}
   342  	}
   343  }