blob: baa41c13b8104efead4fe1663c0c82b98a5c48b3 [file] [log] [blame]
package child
import (
"context"
"fmt"
"net/http"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
cipd_api "go.chromium.org/luci/cipd/client/cipd"
"go.chromium.org/luci/cipd/common"
"go.skia.org/infra/autoroll/go/config"
"go.skia.org/infra/autoroll/go/revision"
"go.skia.org/infra/go/cipd"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"go.skia.org/infra/go/vfs"
)
const (
cipdPackageUrlTmpl = "%s/p/%s/+/%s"
cipdBuganizerPrefix = "b/"
)
var (
cipdDetailsRegex = regexp.MustCompile(`details(\d+)`)
)
// NewCIPD returns an implementation of Child which deals with a CIPD package.
// If the caller calls CIPDChild.Download, the destination must be a descendant of
// the provided workdir.
func NewCIPD(ctx context.Context, c *config.CIPDChildConfig, client *http.Client, workdir string) (*CIPDChild, error) {
if err := c.Validate(); err != nil {
return nil, skerr.Wrap(err)
}
cipdClient, err := cipd.NewClient(client, workdir)
if err != nil {
return nil, skerr.Wrap(err)
}
return &CIPDChild{
client: cipdClient,
name: c.Name,
root: workdir,
tag: c.Tag,
}, nil
}
// CIPDChild is an implementation of Child which deals with a CIPD package.
type CIPDChild struct {
client cipd.CIPDClient
name string
root string
tag string
}
// GetRevision implements Child.
func (c *CIPDChild) GetRevision(ctx context.Context, id string) (*revision.Revision, error) {
instance, err := c.client.Describe(ctx, c.name, id)
if err != nil {
return nil, err
}
return CIPDInstanceToRevision(c.name, instance), nil
}
// Update implements Child.
// Note: that this just finds the newest version of the CIPD package.
func (c *CIPDChild) Update(ctx context.Context, lastRollRev *revision.Revision) (*revision.Revision, []*revision.Revision, error) {
head, err := c.client.ResolveVersion(ctx, c.name, c.tag)
if err != nil {
return nil, nil, skerr.Wrap(err)
}
tipRev, err := c.GetRevision(ctx, head.InstanceID)
if err != nil {
return nil, nil, skerr.Wrap(err)
}
notRolledRevs := []*revision.Revision{}
if lastRollRev.Id != tipRev.Id {
notRolledRevs = append(notRolledRevs, tipRev)
}
return tipRev, notRolledRevs, nil
}
// VFS implements the Child interface.
func (c *CIPDChild) VFS(ctx context.Context, rev *revision.Revision) (vfs.FS, error) {
fs, err := vfs.TempDir(ctx, c.root, "tmp")
if err != nil {
return nil, skerr.Wrap(err)
}
pin := common.Pin{
PackageName: c.name,
InstanceID: rev.Id,
}
dest, err := filepath.Rel(c.root, fs.Dir())
if err := c.client.FetchAndDeployInstance(ctx, dest, pin, 0); err != nil {
return nil, skerr.Wrap(err)
}
return fs, nil
}
// SetClientForTesting sets the CIPDClient used by the CIPDChild so that it can
// be overridden for testing.
func (c *CIPDChild) SetClientForTesting(client cipd.CIPDClient) {
c.client = client
}
type cipdDetailsLine struct {
index int
line string
}
// CIPDInstanceToRevision creates a revision.Revision based on the given
// InstanceInfo.
func CIPDInstanceToRevision(name string, instance *cipd_api.InstanceDescription) *revision.Revision {
rev := &revision.Revision{
Id: instance.Pin.InstanceID,
Author: instance.RegisteredBy,
Display: util.Truncate(instance.Pin.InstanceID, 12),
Description: instance.Pin.String(),
Timestamp: time.Time(instance.RegisteredTs),
URL: fmt.Sprintf(cipdPackageUrlTmpl, cipd.ServiceUrl, name, instance.Pin.InstanceID),
}
detailsLines := []*cipdDetailsLine{}
for _, tag := range instance.Tags {
split := strings.SplitN(tag.Tag, ":", 2)
if len(split) != 2 {
sklog.Errorf("Invalid CIPD tag %q; expected <key>:<value>", tag.Tag)
continue
}
key := split[0]
val := split[1]
if key == "bug" {
// For bugs, we expect either eg. "chromium:1234" or "b/1234".
split := strings.SplitN(val, ":", 2)
if rev.Bugs == nil {
rev.Bugs = map[string][]string{}
}
if len(split) == 2 {
rev.Bugs[split[0]] = append(rev.Bugs[split[0]], split[1])
} else if strings.HasPrefix(val, cipdBuganizerPrefix) {
rev.Bugs[util.BUG_PROJECT_BUGANIZER] = append(rev.Bugs[util.BUG_PROJECT_BUGANIZER], val[len(cipdBuganizerPrefix):])
} else {
sklog.Errorf("Invalid format for \"bug\" tag: %s", tag.Tag)
}
} else if m := cipdDetailsRegex.FindStringSubmatch(key); len(m) == 2 {
// For details, the tag value becomes one line. The tag key includes
// an int which is used to determine the ordering of the lines.
index, err := strconv.Atoi(m[1])
if err != nil {
// This shouldn't happen thanks to the regex.
sklog.Errorf("Failed to parse int from details tag %q: %s", tag.Tag, err)
continue
}
detailsLines = append(detailsLines, &cipdDetailsLine{
index: index,
line: val,
})
}
}
// Concatenate the details lines.
if len(detailsLines) > 0 {
sort.Slice(detailsLines, func(i, j int) bool {
if detailsLines[i].index == detailsLines[j].index {
return detailsLines[i].line < detailsLines[j].line
}
return detailsLines[i].index < detailsLines[j].index
})
for idx, line := range detailsLines {
rev.Details += line.line
if idx < len(detailsLines)-1 {
rev.Details += "\n"
}
}
}
return rev
}
var _ Child = &CIPDChild{}