github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/gerrit/gerrit.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 gerrit implements a gerrit-fetcher using https://github.com/andygrunwald/go-gerrit
    18  package gerrit
    19  
    20  import (
    21  	"bytes"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/andygrunwald/go-gerrit"
    32  	"github.com/sirupsen/logrus"
    33  
    34  	"k8s.io/test-infra/prow/config"
    35  	"k8s.io/test-infra/prow/kube"
    36  	"k8s.io/test-infra/prow/pjutil"
    37  )
    38  
    39  type kubeClient interface {
    40  	CreateProwJob(kube.ProwJob) (kube.ProwJob, error)
    41  }
    42  
    43  type gerritAuthentication interface {
    44  	SetCookieAuth(name, value string)
    45  }
    46  
    47  type gerritAccount interface {
    48  	GetAccount(name string) (*gerrit.AccountInfo, *gerrit.Response, error)
    49  	SetUsername(accountID string, input *gerrit.UsernameInput) (*string, *gerrit.Response, error)
    50  }
    51  
    52  type gerritChange interface {
    53  	QueryChanges(opt *gerrit.QueryChangeOptions) (*[]gerrit.ChangeInfo, *gerrit.Response, error)
    54  	SetReview(changeID, revisionID string, input *gerrit.ReviewInput) (*gerrit.ReviewResult, *gerrit.Response, error)
    55  }
    56  
    57  type configAgent interface {
    58  	Config() *config.Config
    59  }
    60  
    61  // Controller manages gerrit changes.
    62  type Controller struct {
    63  	ca configAgent
    64  
    65  	// go-gerrit change endpoint client
    66  	auth     gerritAuthentication
    67  	account  gerritAccount
    68  	gc       gerritChange
    69  	instance string
    70  	storage  string
    71  	projects []string
    72  
    73  	kc kubeClient
    74  
    75  	lastUpdate time.Time
    76  }
    77  
    78  // NewController returns a new gerrit controller client
    79  func NewController(instance, storage string, projects []string, kc *kube.Client, ca *config.Agent) (*Controller, error) {
    80  	lastUpdate := time.Now()
    81  	if storage != "" {
    82  		buf, err := ioutil.ReadFile(storage)
    83  		if err == nil {
    84  			unix, err := strconv.ParseInt(string(buf), 10, 64)
    85  			if err != nil {
    86  				return nil, err
    87  			} else {
    88  				lastUpdate = time.Unix(unix, 0)
    89  			}
    90  		} else if !os.IsNotExist(err) {
    91  			return nil, err
    92  		}
    93  		// fallback to time.Now() if file does not exist yet
    94  	}
    95  
    96  	c, err := gerrit.NewClient(instance, nil)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	return &Controller{
   102  		instance:   instance,
   103  		projects:   projects,
   104  		kc:         kc,
   105  		ca:         ca,
   106  		auth:       c.Authentication,
   107  		account:    c.Accounts,
   108  		gc:         c.Changes,
   109  		lastUpdate: lastUpdate,
   110  		storage:    storage,
   111  	}, nil
   112  }
   113  
   114  // Auth authenticates to gerrit server
   115  // Token will expire, so we need to regenerate it once so often
   116  func (c *Controller) Auth() error {
   117  	cmd := exec.Command("python", "./git-cookie-authdaemon")
   118  	if err := cmd.Run(); err != nil {
   119  		return fmt.Errorf("Fail to authenticate to gerrit using git-cookie-authdaemon : %v", err)
   120  	}
   121  
   122  	raw, err := ioutil.ReadFile(filepath.Join(os.Getenv("HOME"), ".git-credential-cache/cookie"))
   123  	if err != nil {
   124  		return err
   125  	}
   126  	fields := strings.Fields(string(raw))
   127  	token := fields[len(fields)-1]
   128  
   129  	c.auth.SetCookieAuth("o", token)
   130  
   131  	self, _, err := c.account.GetAccount("self")
   132  	if err != nil {
   133  		logrus.WithError(err).Errorf("Fail to auth with token: %s", token)
   134  		return err
   135  	}
   136  
   137  	logrus.Infof("Authentication successful, Username: %s", self.Name)
   138  
   139  	return nil
   140  }
   141  
   142  // SaveLastSync saves last sync time in Unix to a volume
   143  func (c *Controller) SaveLastSync(lastSync time.Time) error {
   144  	if c.storage == "" {
   145  		return nil
   146  	}
   147  
   148  	lastSyncUnix := strconv.FormatInt(lastSync.Unix(), 10)
   149  	logrus.Infof("Writing last sync: %s", lastSyncUnix)
   150  
   151  	err := ioutil.WriteFile(c.storage+".tmp", []byte(lastSyncUnix), 0644)
   152  	if err != nil {
   153  		return err
   154  	}
   155  	return os.Rename(c.storage+".tmp", c.storage)
   156  }
   157  
   158  // Sync looks for newly made gerrit changes
   159  // and creates prowjobs according to presubmit specs
   160  func (c *Controller) Sync() error {
   161  	syncTime := time.Now()
   162  	changes := c.QueryChanges()
   163  
   164  	for _, change := range changes {
   165  		if err := c.ProcessChange(change); err != nil {
   166  			logrus.WithError(err).Errorf("Failed process change %v", change.CurrentRevision)
   167  		}
   168  	}
   169  
   170  	c.lastUpdate = syncTime
   171  	if err := c.SaveLastSync(syncTime); err != nil {
   172  		logrus.WithError(err).Errorf("last sync %v, cannot save to path %v", syncTime, c.storage)
   173  	}
   174  	logrus.Infof("Processed %d changes", len(changes))
   175  	return nil
   176  }
   177  
   178  func (c *Controller) queryProjectChanges(proj string) ([]gerrit.ChangeInfo, error) {
   179  	pending := []gerrit.ChangeInfo{}
   180  
   181  	opt := &gerrit.QueryChangeOptions{}
   182  	opt.Query = append(opt.Query, "project:"+proj+"+status:open")
   183  	opt.AdditionalFields = []string{"CURRENT_REVISION", "CURRENT_COMMIT"}
   184  
   185  	start := 0
   186  
   187  	for {
   188  		opt.Limit = c.ca.Config().Gerrit.RateLimit
   189  		opt.Start = start
   190  
   191  		// The change output is sorted by the last update time, most recently updated to oldest updated.
   192  		// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes
   193  		changes, _, err := c.gc.QueryChanges(opt)
   194  		if err != nil {
   195  			// should not happen? Let next sync loop catch up
   196  			return pending, fmt.Errorf("failed to query gerrit changes: %v", err)
   197  		}
   198  
   199  		logrus.Infof("Find %d changes from query %v", len(*changes), opt.Query)
   200  
   201  		if len(*changes) == 0 {
   202  			return pending, nil
   203  		}
   204  		start += len(*changes)
   205  
   206  		for _, change := range *changes {
   207  			// if we already processed this change, then we stop the current sync loop
   208  			const layout = "2006-01-02 15:04:05"
   209  			updated, err := time.Parse(layout, change.Updated)
   210  			if err != nil {
   211  				logrus.WithError(err).Errorf("Parse time %v failed", change.Updated)
   212  				continue
   213  			}
   214  
   215  			// process if updated later than last updated
   216  			// stop if update was stale
   217  			if updated.After(c.lastUpdate) {
   218  				// we need to make sure the change update is from a new commit change
   219  				rev, ok := change.Revisions[change.CurrentRevision]
   220  				if !ok {
   221  					logrus.WithError(err).Errorf("(should not happen?)cannot find current revision for change %v", change.ID)
   222  					continue
   223  				}
   224  
   225  				created, err := time.Parse(layout, rev.Created)
   226  				if err != nil {
   227  					logrus.WithError(err).Errorf("Parse time %v failed", rev.Created)
   228  					continue
   229  				}
   230  
   231  				if !created.After(c.lastUpdate) {
   232  					// stale commit
   233  					continue
   234  				}
   235  
   236  				pending = append(pending, change)
   237  			} else {
   238  				return pending, nil
   239  			}
   240  		}
   241  	}
   242  }
   243  
   244  // QueryChanges will query all valid gerrit changes since controller's last sync loop
   245  func (c *Controller) QueryChanges() []gerrit.ChangeInfo {
   246  	// store a map of changeID:change
   247  	pending := []gerrit.ChangeInfo{}
   248  
   249  	// can only query against one project at a time :-(
   250  	for _, proj := range c.projects {
   251  		if res, err := c.queryProjectChanges(proj); err != nil {
   252  			logrus.WithError(err).Errorf("fail to query changes for project %s", proj)
   253  		} else {
   254  			pending = append(pending, res...)
   255  		}
   256  	}
   257  
   258  	return pending
   259  }
   260  
   261  // ProcessChange creates new presubmit prowjobs base off the gerrit changes
   262  func (c *Controller) ProcessChange(change gerrit.ChangeInfo) error {
   263  	rev, ok := change.Revisions[change.CurrentRevision]
   264  	if !ok {
   265  		return fmt.Errorf("cannot find current revision for change %v", change.ID)
   266  	}
   267  
   268  	parentSHA := ""
   269  	if len(rev.Commit.Parents) > 0 {
   270  		parentSHA = rev.Commit.Parents[0].Commit
   271  	}
   272  
   273  	logger := logrus.WithField("gerrit change", change.Number)
   274  
   275  	type triggeredJob struct {
   276  		Name, URL string
   277  	}
   278  	triggeredJobs := []triggeredJob{}
   279  
   280  	for _, spec := range c.ca.Config().Presubmits[c.instance+"/"+change.Project] {
   281  		kr := kube.Refs{
   282  			Org:     c.instance,
   283  			Repo:    change.Project,
   284  			BaseRef: change.Branch,
   285  			BaseSHA: parentSHA,
   286  			Pulls: []kube.Pull{
   287  				{
   288  					Number: change.Number,
   289  					Author: rev.Commit.Author.Name,
   290  					SHA:    change.CurrentRevision,
   291  					Ref:    rev.Ref,
   292  				},
   293  			},
   294  		}
   295  
   296  		// TODO(krzyzacy): Support AlwaysRun and RunIfChanged
   297  		pj := pjutil.NewProwJob(pjutil.PresubmitSpec(spec, kr), map[string]string{})
   298  		logger.WithFields(pjutil.ProwJobFields(&pj)).Infof("Creating a new prowjob for change %s.", change.Number)
   299  		if _, err := c.kc.CreateProwJob(pj); err != nil {
   300  			logger.WithError(err).Errorf("fail to create prowjob %v", pj)
   301  		} else {
   302  			var b bytes.Buffer
   303  			url := ""
   304  			template := c.ca.Config().Plank.JobURLTemplate
   305  			if template != nil {
   306  				if err := template.Execute(&b, &pj); err != nil {
   307  					logger.WithFields(pjutil.ProwJobFields(&pj)).Errorf("error executing URL template: %v", err)
   308  				}
   309  				// TODO(krzyzacy): We doesn't have buildID here yet - do a hack to get a proper URL to the PR
   310  				// Remove this once we have proper report interface.
   311  
   312  				// mangle
   313  				// https://gubernator.k8s.io/build/gob-prow/pr-logs/pull/some/repo/8940/pull-test-infra-presubmit//
   314  				// to
   315  				// https://gubernator.k8s.io/builds/gob-prow/pr-logs/pull/some_repo/8940/pull-test-infra-presubmit/
   316  				url = b.String()
   317  				url = strings.Replace(url, "build", "builds", 1)
   318  				// TODO(krzyzacy): gerrit path can be foo.googlesource.com/bar/baz, which means we took bar/baz as the repo
   319  				// we are mangling the path in bootstrap.py, we need to handle this better in podutils
   320  				url = strings.Replace(url, change.Project, strings.Replace(change.Project, "/", "_", -1), 1)
   321  				url = strings.TrimSuffix(url, "//")
   322  			}
   323  			triggeredJobs = append(triggeredJobs, triggeredJob{Name: spec.Name, URL: url})
   324  		}
   325  	}
   326  
   327  	if len(triggeredJobs) > 0 {
   328  		// comment back to gerrit
   329  		message := "Triggered presubmit:"
   330  		for _, job := range triggeredJobs {
   331  			if job.URL != "" {
   332  				message += fmt.Sprintf("\n  * Name: %s, URL: %s", job.Name, job.URL)
   333  			} else {
   334  				message += fmt.Sprintf("\n  * Name: %s", job.Name)
   335  			}
   336  		}
   337  
   338  		if _, _, err := c.gc.SetReview(change.ID, change.CurrentRevision, &gerrit.ReviewInput{
   339  			Message: message,
   340  		}); err != nil {
   341  			return fmt.Errorf("cannot comment to gerrit: %v", err)
   342  		}
   343  	}
   344  
   345  	return nil
   346  }