| 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, |
| } |
| } |