blob: 8e23001c62b6fee48d1e6a883019c9b73cbce9c9 [file] [log] [blame]
package specs
/*
Helper functions for client repos.
*/
import (
"bytes"
"flag"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/pmezard/go-difflib/difflib"
"go.skia.org/infra/go/cas/rbe"
"go.skia.org/infra/go/common"
"go.skia.org/infra/go/sklog"
)
var (
// Flags.
test = flag.Bool("test", false, "Run in test mode: verify that the output hasn't changed.")
// EmptyCasSpec is a CasSpec with no contents.
EmptyCasSpec = &CasSpec{
Digest: rbe.EmptyDigest,
}
)
// GetCheckoutRoot returns the path of the root of the checkout.
func GetCheckoutRoot() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(cwd); err != nil {
return "", err
}
// TODO(borenet): Should we verify that this is the
// correct checkout and not something else?
// Check for infra/bots dir.
s, err := os.Stat(filepath.Join(cwd, "infra", "bots"))
if err == nil && s.IsDir() {
return cwd, nil
}
// Check for .git dir.
s, err = os.Stat(filepath.Join(cwd, ".git"))
if err == nil && s.IsDir() {
return cwd, nil
}
// Move up a level.
cwd = filepath.Clean(filepath.Join(cwd, ".."))
// Stop if we're at the filesystem root.
// Per filepath.Clean docs, cwd will end in a slash only if it
// represents a root directory.
if strings.HasSuffix(cwd, string(filepath.Separator)) {
return "", fmt.Errorf("Unable to find repository root.")
}
}
}
// TasksCfgBuilder is a helper struct used for building a TasksCfg.
type TasksCfgBuilder struct {
assetsDir string
cfg *TasksCfg
cipdPackages map[string]*CipdPackage
root string
}
// NewTasksCfgBuilder returns a TasksCfgBuilder instance.
func NewTasksCfgBuilder() (*TasksCfgBuilder, error) {
common.Init()
// Create the config.
cfg := &TasksCfg{
CasSpecs: map[string]*CasSpec{},
Jobs: map[string]*JobSpec{},
Tasks: map[string]*TaskSpec{},
CommitQueue: map[string]*CommitQueueJobConfig{},
}
root, err := GetCheckoutRoot()
if err != nil {
return nil, err
}
return &TasksCfgBuilder{
cfg: cfg,
cipdPackages: map[string]*CipdPackage{},
root: root,
}, nil
}
// MustNewTasksCfgBuilder returns a TasksCfgBuilder instance. Panics on error.
func MustNewTasksCfgBuilder() *TasksCfgBuilder {
b, err := NewTasksCfgBuilder()
if err != nil {
sklog.Fatal(err)
}
return b
}
// CheckoutRoot returns the path to the root of the client checkout.
func (b *TasksCfgBuilder) CheckoutRoot() string {
return b.root
}
// SetAssetsDir sets the directory path used for assets.
func (b *TasksCfgBuilder) SetAssetsDir(assetsDir string) {
b.assetsDir = assetsDir
}
// AddTask adds a TaskSpec to the TasksCfgBuilder. Returns an error if the
// config already contains a Task with the same name and a different
// implementation.
func (b *TasksCfgBuilder) AddTask(name string, t *TaskSpec) error {
// Return an error if the task contains duplicate dimensions, which will
// cause it to be rejected by Swarming.
dims := make(map[string]bool, len(t.Dimensions))
for _, dim := range t.Dimensions {
if _, ok := dims[dim]; ok {
return fmt.Errorf("Dimension %q is duplicated for task %s", dim, name)
}
dims[dim] = true
}
// Ensure that we don't already have a different definition for this task
// name.
if old, ok := b.cfg.Tasks[name]; ok {
if !reflect.DeepEqual(old, t) {
return fmt.Errorf("Config already contains a Task named %q with a different implementation!\nHave:\n%v\n\nGot:\n%v", name, old, t)
}
return nil
}
b.cfg.Tasks[name] = t
return nil
}
// MustAddTask adds a TaskSpec to the TasksCfgBuilder and panics on failure.
func (b *TasksCfgBuilder) MustAddTask(name string, t *TaskSpec) {
if err := b.AddTask(name, t); err != nil {
sklog.Fatal(err)
}
}
// AddJob adds a JobSpec to the TasksCfgBuilder.
func (b *TasksCfgBuilder) AddJob(name string, j *JobSpec) error {
if _, ok := b.cfg.Jobs[name]; ok {
return fmt.Errorf("Config already contains a Job named %q", name)
}
b.cfg.Jobs[name] = j
return nil
}
// MustAddJob adds a JobSpec to the TasksCfgBuilder and panics on failure.
func (b *TasksCfgBuilder) MustAddJob(name string, j *JobSpec) {
if err := b.AddJob(name, j); err != nil {
sklog.Fatal(err)
}
}
// AddCQJob adds a CommitQueueJobConfig to the TasksCfgBuilder.
func (b *TasksCfgBuilder) AddCQJob(name string, c *CommitQueueJobConfig) error {
if _, ok := b.cfg.CommitQueue[name]; ok {
return fmt.Errorf("CommitQueue already contains a CQJob named %q", name)
}
b.cfg.CommitQueue[name] = c
return nil
}
// MustAddCQJob adds a CommitQueueJobConfig to the TasksCfgBuilder and panics
// on failure.
func (b *TasksCfgBuilder) MustAddCQJob(name string, c *CommitQueueJobConfig) {
if err := b.AddCQJob(name, c); err != nil {
sklog.Fatal(err)
}
}
// GetCipdPackageFromAsset reads the version information for the given asset
// and returns a CipdPackage instance.
func (b *TasksCfgBuilder) GetCipdPackageFromAsset(assetName string) (*CipdPackage, error) {
if pkg, ok := b.cipdPackages[assetName]; ok {
return pkg, nil
}
assetsDir := b.assetsDir
if assetsDir == "" {
assetsDir = filepath.Join(b.root, "infra", "bots", "assets")
}
versionFile := filepath.Join(assetsDir, assetName, "VERSION")
contents, err := os.ReadFile(versionFile)
if err != nil {
return nil, err
}
version := strings.TrimSpace(string(contents))
pkg := &CipdPackage{
Name: fmt.Sprintf("skia/bots/%s", assetName),
Path: assetName,
Version: fmt.Sprintf("version:%s", version),
}
b.cipdPackages[assetName] = pkg
return pkg, nil
}
// MustGetCipdPackageFromAsset reads the version information for the given asset
// and returns a CipdPackage instance. Panics on failure.
func (b *TasksCfgBuilder) MustGetCipdPackageFromAsset(assetName string) *CipdPackage {
pkg, err := b.GetCipdPackageFromAsset(assetName)
if err != nil {
sklog.Fatal(err)
}
return pkg
}
// AddCasSpec adds a CasSpec to the TasksCfgBuilder.
func (b *TasksCfgBuilder) AddCasSpec(name string, c *CasSpec) error {
if _, ok := b.cfg.CasSpecs[name]; ok {
return fmt.Errorf("Config already contains a CasSpec named %q", name)
}
b.cfg.CasSpecs[name] = c
return nil
}
// MustAddCasSpec adds a CasSpec to the TasksCfgBuilder and panics on failure.
func (b *TasksCfgBuilder) MustAddCasSpec(name string, c *CasSpec) {
if err := b.AddCasSpec(name, c); err != nil {
sklog.Fatal(err)
}
}
// Finish validates and writes out the TasksCfg, or, if the --test flag is
// provided, verifies that the contents have not changed.
func (b *TasksCfgBuilder) Finish() error {
// Sort the elements whose order is not important to maintain
// consistency.
for _, t := range b.cfg.Tasks {
sort.Slice(t.Caches, func(i, j int) bool {
return t.Caches[i].Name < t.Caches[j].Name
})
sort.Slice(t.CipdPackages, func(i, j int) bool {
return t.CipdPackages[i].Name < t.CipdPackages[j].Name
})
sort.Strings(t.Dependencies)
sort.Strings(t.Outputs)
}
for _, j := range b.cfg.Jobs {
sort.Strings(j.TaskSpecs)
}
// Validate the config.
if err := b.cfg.Validate(); err != nil {
return err
}
enc, err := EncodeTasksCfg(b.cfg)
if err != nil {
return err
}
// Write the tasks.json file.
outFile := filepath.Join(b.root, TASKS_CFG_FILE)
if *test {
// Don't write the file; read it and compare.
expect, err := os.ReadFile(outFile)
if err != nil {
return err
}
if !bytes.Equal(expect, enc) {
diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
A: difflib.SplitLines(string(expect)),
B: difflib.SplitLines(string(enc)),
FromFile: "Expected",
ToFile: "Actual",
Context: 3,
Eol: "\n",
})
if err != nil {
diff = fmt.Sprintf("<failed to obtain diff: %s>", err)
}
return fmt.Errorf(`Expected no changes, but changes were found:
%s
You may need to run:
$ go run infra/bots/gen_tasks.go
`, diff)
}
} else {
if err := os.WriteFile(outFile, enc, os.ModePerm); err != nil {
return err
}
}
return nil
}
// MustFinish validates and writes out the TasksCfg, or, if the --test flag is
// provided, verifies that the contents have not changed. Panics on failure.
func (b *TasksCfgBuilder) MustFinish() {
if err := b.Finish(); err != nil {
sklog.Error(err)
os.Exit(1)
}
}