github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/gerrit/adapter/adapter.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 adapter implements a controller that interacts with gerrit instances
    18  package adapter
    19  
    20  import (
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/url"
    26  	"os"
    27  	"path/filepath"
    28  	"strconv"
    29  	"time"
    30  
    31  	"github.com/sirupsen/logrus"
    32  
    33  	"k8s.io/test-infra/prow/config"
    34  	"k8s.io/test-infra/prow/gerrit/client"
    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 gerritClient interface {
    44  	QueryChanges(lastUpdate time.Time, rateLimit int) map[string][]client.ChangeInfo
    45  	GetBranchRevision(instance, project, branch string) (string, error)
    46  	SetReview(instance, id, revision, message string, labels map[string]string) error
    47  }
    48  
    49  type configAgent interface {
    50  	Config() *config.Config
    51  }
    52  
    53  // Controller manages gerrit changes.
    54  type Controller struct {
    55  	ca configAgent
    56  	kc kubeClient
    57  	gc gerritClient
    58  
    59  	lastSyncFallback string
    60  
    61  	lastUpdate time.Time
    62  }
    63  
    64  // NewController returns a new gerrit controller client
    65  func NewController(lastSyncFallback, cookiefilePath string, projects map[string][]string, kc *kube.Client, ca *config.Agent) (*Controller, error) {
    66  	if lastSyncFallback == "" {
    67  		return nil, errors.New("empty lastSyncFallback")
    68  	}
    69  
    70  	var lastUpdate time.Time
    71  	if buf, err := ioutil.ReadFile(lastSyncFallback); err == nil {
    72  		unix, err := strconv.ParseInt(string(buf), 10, 64)
    73  		if err != nil {
    74  			return nil, err
    75  		}
    76  		lastUpdate = time.Unix(unix, 0)
    77  	} else if err != nil && !os.IsNotExist(err) {
    78  		return nil, fmt.Errorf("failed to read lastSyncFallback: %v", err)
    79  	} else {
    80  		logrus.Warnf("lastSyncFallback not found: %s", lastSyncFallback)
    81  		lastUpdate = time.Now()
    82  	}
    83  
    84  	c, err := client.NewClient(projects)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	c.Start(cookiefilePath)
    89  
    90  	return &Controller{
    91  		kc:               kc,
    92  		ca:               ca,
    93  		gc:               c,
    94  		lastUpdate:       lastUpdate,
    95  		lastSyncFallback: lastSyncFallback,
    96  	}, nil
    97  }
    98  
    99  func copyFile(srcPath, destPath string) error {
   100  	// fallback to copying the file instead
   101  	src, err := os.Open(srcPath)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	dst, err := os.OpenFile(destPath, os.O_WRONLY, 0666)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	_, err = io.Copy(dst, src)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	dst.Sync()
   114  	dst.Close()
   115  	src.Close()
   116  	return nil
   117  }
   118  
   119  // SaveLastSync saves last sync time in Unix to a volume
   120  func (c *Controller) SaveLastSync(lastSync time.Time) error {
   121  	if c.lastSyncFallback == "" {
   122  		return nil
   123  	}
   124  
   125  	lastSyncUnix := strconv.FormatInt(lastSync.Unix(), 10)
   126  	logrus.Infof("Writing last sync: %s", lastSyncUnix)
   127  
   128  	tempFile, err := ioutil.TempFile(filepath.Dir(c.lastSyncFallback), "temp")
   129  	if err != nil {
   130  		return err
   131  	}
   132  	defer os.Remove(tempFile.Name())
   133  
   134  	err = ioutil.WriteFile(tempFile.Name(), []byte(lastSyncUnix), 0644)
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	err = os.Rename(tempFile.Name(), c.lastSyncFallback)
   140  	if err != nil {
   141  		logrus.WithError(err).Info("Rename failed, fallback to copyfile")
   142  		return copyFile(tempFile.Name(), c.lastSyncFallback)
   143  	}
   144  	return nil
   145  }
   146  
   147  // Sync looks for newly made gerrit changes
   148  // and creates prowjobs according to specs
   149  func (c *Controller) Sync() error {
   150  	// gerrit timestamp only has second precision
   151  	syncTime := time.Now().Truncate(time.Second)
   152  
   153  	for instance, changes := range c.gc.QueryChanges(c.lastUpdate, c.ca.Config().Gerrit.RateLimit) {
   154  		for _, change := range changes {
   155  			if err := c.ProcessChange(instance, change); err != nil {
   156  				logrus.WithError(err).Errorf("Failed process change %v", change.CurrentRevision)
   157  			}
   158  		}
   159  
   160  		logrus.Infof("Processed %d changes for instance %s", len(changes), instance)
   161  	}
   162  
   163  	c.lastUpdate = syncTime
   164  	if err := c.SaveLastSync(syncTime); err != nil {
   165  		logrus.WithError(err).Errorf("last sync %v, cannot save to path %v", syncTime, c.lastSyncFallback)
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  func makeCloneURI(instance, project string) (*url.URL, error) {
   172  	u, err := url.Parse(instance)
   173  	if err != nil {
   174  		return nil, fmt.Errorf("instance %s is not a url: %v", instance, err)
   175  	}
   176  	if u.Host == "" {
   177  		return nil, errors.New("instance does not set host")
   178  	}
   179  	if u.Path != "" {
   180  		return nil, errors.New("instance cannot set path (this is set by project)")
   181  	}
   182  	u.Path = project
   183  	return u, nil
   184  }
   185  
   186  // listChangedFiles lists (in lexicographic order) the files changed as part of a Gerrit patchset
   187  func listChangedFiles(changeInfo client.ChangeInfo) []string {
   188  	changed := []string{}
   189  	revision := changeInfo.Revisions[changeInfo.CurrentRevision]
   190  	for file := range revision.Files {
   191  		changed = append(changed, file)
   192  	}
   193  	return changed
   194  }
   195  
   196  // ProcessChange creates new presubmit prowjobs base off the gerrit changes
   197  func (c *Controller) ProcessChange(instance string, change client.ChangeInfo) error {
   198  	rev, ok := change.Revisions[change.CurrentRevision]
   199  	if !ok {
   200  		return fmt.Errorf("cannot find current revision for change %v", change.ID)
   201  	}
   202  
   203  	logger := logrus.WithField("gerrit change", change.Number)
   204  
   205  	cloneURI, err := makeCloneURI(instance, change.Project)
   206  	if err != nil {
   207  		return fmt.Errorf("failed to create clone uri: %v", err)
   208  	}
   209  
   210  	baseSHA, err := c.gc.GetBranchRevision(instance, change.Project, change.Branch)
   211  	if err != nil {
   212  		return fmt.Errorf("failed to get SHA from base branch: %v", err)
   213  	}
   214  
   215  	triggeredJobs := []string{}
   216  
   217  	kr := kube.Refs{
   218  		Org:      cloneURI.Host,  // Something like android.googlesource.com
   219  		Repo:     change.Project, // Something like platform/build
   220  		BaseRef:  change.Branch,
   221  		BaseSHA:  baseSHA,
   222  		CloneURI: cloneURI.String(), // Something like https://android.googlesource.com/platform/build
   223  		Pulls: []kube.Pull{
   224  			{
   225  				Number: change.Number,
   226  				Author: rev.Commit.Author.Name,
   227  				SHA:    change.CurrentRevision,
   228  				Ref:    rev.Ref,
   229  			},
   230  		},
   231  	}
   232  
   233  	type jobSpec struct {
   234  		spec   kube.ProwJobSpec
   235  		labels map[string]string
   236  	}
   237  
   238  	var jobSpecs []jobSpec
   239  
   240  	changedFiles := listChangedFiles(change)
   241  
   242  	switch change.Status {
   243  	case client.Merged:
   244  		postsubmits := c.ca.Config().Postsubmits[cloneURI.String()]
   245  		postsubmits = append(postsubmits, c.ca.Config().Postsubmits[cloneURI.Host+"/"+cloneURI.Path]...)
   246  		for _, postsubmit := range postsubmits {
   247  			if postsubmit.RunsAgainstChanges(changedFiles) {
   248  				jobSpecs = append(jobSpecs, jobSpec{
   249  					spec:   pjutil.PostsubmitSpec(postsubmit, kr),
   250  					labels: postsubmit.Labels,
   251  				})
   252  			}
   253  		}
   254  	case client.New:
   255  		presubmits := c.ca.Config().Presubmits[cloneURI.String()]
   256  		presubmits = append(presubmits, c.ca.Config().Presubmits[cloneURI.Host+"/"+cloneURI.Path]...)
   257  		for _, presubmit := range presubmits {
   258  			if presubmit.RunsAgainstChanges(changedFiles) {
   259  				jobSpecs = append(jobSpecs, jobSpec{
   260  					spec:   pjutil.PresubmitSpec(presubmit, kr),
   261  					labels: presubmit.Labels,
   262  				})
   263  			}
   264  		}
   265  	}
   266  
   267  	annotations := map[string]string{
   268  		client.GerritID:       change.ID,
   269  		client.GerritInstance: instance,
   270  	}
   271  
   272  	for _, jSpec := range jobSpecs {
   273  		labels := make(map[string]string)
   274  		for k, v := range jSpec.labels {
   275  			labels[k] = v
   276  		}
   277  		labels[client.GerritRevision] = change.CurrentRevision
   278  
   279  		pj := pjutil.NewProwJobWithAnnotation(jSpec.spec, labels, annotations)
   280  		if _, err := c.kc.CreateProwJob(pj); err != nil {
   281  			logger.WithError(err).Errorf("fail to create prowjob %v", pj)
   282  		} else {
   283  			triggeredJobs = append(triggeredJobs, jSpec.spec.Job)
   284  		}
   285  	}
   286  
   287  	if len(triggeredJobs) > 0 {
   288  		// comment back to gerrit
   289  		message := fmt.Sprintf("Triggered %d prow jobs:", len(triggeredJobs))
   290  		for _, job := range triggeredJobs {
   291  			message += fmt.Sprintf("\n  * Name: %s", job)
   292  		}
   293  
   294  		if err := c.gc.SetReview(instance, change.ID, change.CurrentRevision, message, nil); err != nil {
   295  			return err
   296  		}
   297  	}
   298  
   299  	return nil
   300  }