blob: 7d2cd1cb9aa963683afd2aeaad3827dde5b17b17 [file] [log] [blame]
package repo_manager
/*
RepoManager which rolls based on files in GCS.
*/
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"path"
"sort"
"strings"
"cloud.google.com/go/storage"
"go.skia.org/infra/autoroll/go/codereview"
"go.skia.org/infra/autoroll/go/revision"
"go.skia.org/infra/go/gcs"
"go.skia.org/infra/go/gcs/gcsclient"
"go.skia.org/infra/go/gerrit"
"go.skia.org/infra/go/gitiles"
"go.skia.org/infra/go/sklog"
"google.golang.org/api/option"
)
var (
errInvalidGCSVersion = errors.New("Invalid GCS version.")
)
type GCSRepoManagerConfig struct {
NoCheckoutRepoManagerConfig
// GCS bucket used for finding child revisions.
GCSBucket string
// Path within the GCS bucket which contains child revisions.
GCSPath string
// File to update in the parent repo.
VersionFile string
}
func (c *GCSRepoManagerConfig) Validate() error {
if err := c.NoCheckoutRepoManagerConfig.Validate(); err != nil {
return err
}
if c.GCSBucket == "" {
return errors.New("GCSBucket is required.")
}
if c.GCSPath == "" {
return errors.New("GCSPath is required.")
}
if c.VersionFile == "" {
return errors.New("VersionFile is required.")
}
return nil
}
// gcsVersion represents a version of a file in GCS. It can be compared to other
// gcsVersion instances of the same type.
type gcsVersion interface {
// Compare returns 0 if the given gcsVersion is equal to this one, >0 if
// this gcsVersion comes before the given gcsVersion, and <0 if this
// gcsVersion comes after the given gcsVersion.
Compare(gcsVersion) int
// Id returns the ID of this gcsVersion.
Id() string
}
type gcsVersionSlice []gcsVersion
func (s gcsVersionSlice) Len() int {
return len(s)
}
func (s gcsVersionSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// We sort newest to oldest.
func (s gcsVersionSlice) Less(i, j int) bool {
return s[i].Compare(s[j]) < 0
}
// getGCSVersionFunc is a function which returns a gcsVersion based on the given
// object in GCS. If the given object does not represent a valid gcsVersion, the
// func should return errInvalidGCSVersion, and the object will be ignored. Any
// other error will be logged and the object still ignored.
type getGCSVersionFunc func(*revision.Revision) (gcsVersion, error)
// shortRevFunc is a function which returns a shortened revision ID.
type shortRevFunc func(string) string
// gcsRepoManager is a RepoManager which creates rolls based on files in GCS.
type gcsRepoManager struct {
*noCheckoutRepoManager
gcs gcs.GCSClient
gcsBucket string
gcsPath string
getGCSVersion getGCSVersionFunc
shortRev shortRevFunc
versionFile string
}
// Return a gcsRepoManager instance.
func newGCSRepoManager(ctx context.Context, c *GCSRepoManagerConfig, workdir string, g gerrit.GerritInterface, serverURL string, client *http.Client, cr codereview.CodeReview, local bool, getGCSVersion getGCSVersionFunc, shortRev shortRevFunc) (RepoManager, error) {
if err := c.Validate(); err != nil {
return nil, err
}
storageClient, err := storage.NewClient(ctx, option.WithHTTPClient(client))
if err != nil {
return nil, err
}
gcsClient := gcsclient.New(storageClient, c.GCSBucket)
rv := &gcsRepoManager{
gcs: gcsClient,
gcsBucket: c.GCSBucket,
gcsPath: c.GCSPath,
getGCSVersion: getGCSVersion,
shortRev: shortRev,
versionFile: c.VersionFile,
}
ncrm, err := newNoCheckoutRepoManager(ctx, c.NoCheckoutRepoManagerConfig, workdir, g, serverURL, client, cr, rv.createRoll, rv.updateHelper, local)
if err != nil {
return nil, err
}
rv.noCheckoutRepoManager = ncrm
return rv, nil
}
// See documentation for noCheckoutRepoManagerCreateRollHelperFunc.
func (rm *gcsRepoManager) createRoll(ctx context.Context, from, to *revision.Revision, rolling []*revision.Revision, serverURL, cqExtraTrybots string, emails []string) (string, map[string]string, error) {
commitMsg, err := rm.buildCommitMsg(&CommitMsgVars{
CqExtraTrybots: cqExtraTrybots,
Reviewers: emails,
RollingFrom: from,
RollingTo: to,
ServerURL: serverURL,
})
if err != nil {
return "", nil, err
}
return commitMsg, map[string]string{rm.versionFile: to.Id}, nil
}
// See documentation for noCheckoutRepoManagerUpdateHelperFunc.
func (rm *gcsRepoManager) updateHelper(ctx context.Context, parentRepo *gitiles.Repo, baseCommit string) (*revision.Revision, *revision.Revision, []*revision.Revision, error) {
// Read the version file to determine the last roll rev.
buf := bytes.NewBuffer([]byte{})
if err := parentRepo.ReadFileAtRef(ctx, rm.versionFile, baseCommit, buf); err != nil {
return nil, nil, nil, err
}
lastRollRevId := strings.TrimSpace(buf.String())
// Find the available versions, sorted newest to oldest.
versions := []gcsVersion{}
revisions := map[string]*revision.Revision{}
if err := rm.gcs.AllFilesInDirectory(ctx, rm.gcsPath, func(item *storage.ObjectAttrs) {
rev := rm.objectAttrsToRevision(item)
ver, err := rm.getGCSVersion(rev)
if err == nil {
versions = append(versions, ver)
revisions[rev.Id] = rev
} else if err == errInvalidGCSVersion {
// There are files we don't care about in this bucket. Just ignore.
} else {
sklog.Error(err)
}
}); err != nil {
return nil, nil, nil, err
}
if len(versions) == 0 {
return nil, nil, nil, fmt.Errorf("No valid files found in GCS.")
}
sort.Sort(gcsVersionSlice(versions))
lastIdx := -1
var lastRollRev *revision.Revision
for idx, v := range versions {
rev := revisions[v.Id()]
if rev.Id == lastRollRevId {
lastIdx = idx
lastRollRev = rev
break
}
}
if lastIdx == -1 {
sklog.Errorf("Last roll rev %q not found in available versions. This is acceptable for some rollers which allow outside versions to be rolled manually (eg. AFDO roller). A human should verify that this is indeed caused by a manual roll. Using the single most recent available version for the not-yet-rolled revisions list.", lastRollRevId)
lastIdx = 1
lastRollRev = revisions[versions[lastIdx].Id()]
}
// Get the list of not-yet-rolled revisions.
notRolledRevs := make([]*revision.Revision, 0, lastIdx)
for i := 0; i < lastIdx; i++ {
notRolledRevs = append(notRolledRevs, revisions[versions[i].Id()])
}
tipRev := lastRollRev
if len(notRolledRevs) > 0 {
tipRev = notRolledRevs[0]
}
return lastRollRev, tipRev, notRolledRevs, nil
}
// See documentation for RepoManager interface.
func (r *gcsRepoManager) GetRevision(ctx context.Context, id string) (*revision.Revision, error) {
item, err := r.gcs.GetFileObjectAttrs(ctx, path.Join(r.gcsPath, id))
if err != nil {
return nil, err
}
return r.objectAttrsToRevision(item), nil
}
// objectAttrsToRevision returns a revision.Revision based on the given
// storage.ObjectAttrs. It is intended to be used by structs which embed
// gcsRepoManager as a helper for creating revision.Revisions.
func (r *gcsRepoManager) objectAttrsToRevision(item *storage.ObjectAttrs) *revision.Revision {
id := path.Base(item.Name)
return &revision.Revision{
Id: id,
Display: r.shortRev(id),
Author: item.Owner,
Timestamp: item.Updated,
URL: item.MediaLink,
}
}