409 lines
13 KiB
Go
409 lines
13 KiB
Go
// Copyright 2020 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
package gen_tasks_logic
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.skia.org/infra/go/cipd"
|
|
"go.skia.org/infra/task_scheduler/go/specs"
|
|
)
|
|
|
|
// taskBuilder is a helper for creating a task.
|
|
type taskBuilder struct {
|
|
*jobBuilder
|
|
parts
|
|
Name string
|
|
Spec *specs.TaskSpec
|
|
recipeProperties map[string]string
|
|
}
|
|
|
|
// newTaskBuilder returns a taskBuilder instance.
|
|
func newTaskBuilder(b *jobBuilder, name string) *taskBuilder {
|
|
parts, err := b.jobNameSchema.ParseJobName(name)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return &taskBuilder{
|
|
jobBuilder: b,
|
|
parts: parts,
|
|
Name: name,
|
|
Spec: &specs.TaskSpec{},
|
|
recipeProperties: map[string]string{},
|
|
}
|
|
}
|
|
|
|
// attempts sets the desired MaxAttempts for this task.
|
|
func (b *taskBuilder) attempts(a int) {
|
|
b.Spec.MaxAttempts = a
|
|
}
|
|
|
|
// cache adds the given caches to the task.
|
|
func (b *taskBuilder) cache(caches ...*specs.Cache) {
|
|
for _, c := range caches {
|
|
alreadyHave := false
|
|
for _, exist := range b.Spec.Caches {
|
|
if c.Name == exist.Name {
|
|
if !reflect.DeepEqual(c, exist) {
|
|
log.Fatalf("Already have cache %s with a different definition!", c.Name)
|
|
}
|
|
alreadyHave = true
|
|
break
|
|
}
|
|
}
|
|
if !alreadyHave {
|
|
b.Spec.Caches = append(b.Spec.Caches, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// cmd sets the command for the task.
|
|
func (b *taskBuilder) cmd(c ...string) {
|
|
b.Spec.Command = c
|
|
}
|
|
|
|
// dimension adds the given dimensions to the task.
|
|
func (b *taskBuilder) dimension(dims ...string) {
|
|
for _, dim := range dims {
|
|
if !In(dim, b.Spec.Dimensions) {
|
|
b.Spec.Dimensions = append(b.Spec.Dimensions, dim)
|
|
}
|
|
}
|
|
}
|
|
|
|
// expiration sets the expiration of the task.
|
|
func (b *taskBuilder) expiration(e time.Duration) {
|
|
b.Spec.Expiration = e
|
|
}
|
|
|
|
// idempotent marks the task as idempotent.
|
|
func (b *taskBuilder) idempotent() {
|
|
b.Spec.Idempotent = true
|
|
}
|
|
|
|
// cas sets the CasSpec used by the task.
|
|
func (b *taskBuilder) cas(casSpec string) {
|
|
b.Spec.CasSpec = casSpec
|
|
}
|
|
|
|
// env sets the value for the given environment variable for the task.
|
|
func (b *taskBuilder) env(key, value string) {
|
|
if b.Spec.Environment == nil {
|
|
b.Spec.Environment = map[string]string{}
|
|
}
|
|
b.Spec.Environment[key] = value
|
|
}
|
|
|
|
// envPrefixes appends the given values to the given environment variable for
|
|
// the task.
|
|
func (b *taskBuilder) envPrefixes(key string, values ...string) {
|
|
if b.Spec.EnvPrefixes == nil {
|
|
b.Spec.EnvPrefixes = map[string][]string{}
|
|
}
|
|
for _, value := range values {
|
|
if !In(value, b.Spec.EnvPrefixes[key]) {
|
|
b.Spec.EnvPrefixes[key] = append(b.Spec.EnvPrefixes[key], value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// addToPATH adds the given locations to PATH for the task.
|
|
func (b *taskBuilder) addToPATH(loc ...string) {
|
|
b.envPrefixes("PATH", loc...)
|
|
}
|
|
|
|
// output adds the given paths as outputs to the task, which results in their
|
|
// contents being uploaded to the isolate server.
|
|
func (b *taskBuilder) output(paths ...string) {
|
|
for _, path := range paths {
|
|
if !In(path, b.Spec.Outputs) {
|
|
b.Spec.Outputs = append(b.Spec.Outputs, path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// serviceAccount sets the service account for this task.
|
|
func (b *taskBuilder) serviceAccount(sa string) {
|
|
b.Spec.ServiceAccount = sa
|
|
}
|
|
|
|
// timeout sets the timeout(s) for this task.
|
|
func (b *taskBuilder) timeout(timeout time.Duration) {
|
|
b.Spec.ExecutionTimeout = timeout
|
|
b.Spec.IoTimeout = timeout // With kitchen, step logs don't count toward IoTimeout.
|
|
}
|
|
|
|
// dep adds the given tasks as dependencies of this task.
|
|
func (b *taskBuilder) dep(tasks ...string) {
|
|
for _, task := range tasks {
|
|
if !In(task, b.Spec.Dependencies) {
|
|
b.Spec.Dependencies = append(b.Spec.Dependencies, task)
|
|
}
|
|
}
|
|
}
|
|
|
|
// cipd adds the given CIPD packages to the task.
|
|
func (b *taskBuilder) cipd(pkgs ...*specs.CipdPackage) {
|
|
for _, pkg := range pkgs {
|
|
alreadyHave := false
|
|
for _, exist := range b.Spec.CipdPackages {
|
|
if pkg.Name == exist.Name {
|
|
if !reflect.DeepEqual(pkg, exist) {
|
|
log.Fatalf("Already have package %s with a different definition!", pkg.Name)
|
|
}
|
|
alreadyHave = true
|
|
break
|
|
}
|
|
}
|
|
if !alreadyHave {
|
|
b.Spec.CipdPackages = append(b.Spec.CipdPackages, pkg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// useIsolatedAssets returns true if this task should use assets which are
|
|
// isolated rather than downloading directly from CIPD.
|
|
func (b *taskBuilder) useIsolatedAssets() bool {
|
|
// Only do this on the RPIs for now. Other, faster machines shouldn't
|
|
// see much benefit and we don't need the extra complexity, for now.
|
|
if b.os("ChromeOS", "iOS") || b.matchOs("Android") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// uploadAssetCASCfg represents a task which copies a CIPD package into
|
|
// isolate.
|
|
type uploadAssetCASCfg struct {
|
|
alwaysIsolate bool
|
|
uploadTaskName string
|
|
path string
|
|
}
|
|
|
|
// assetWithVersion adds the given asset with the given version number to the
|
|
// task as a CIPD package.
|
|
func (b *taskBuilder) assetWithVersion(assetName string, version int) {
|
|
pkg := &specs.CipdPackage{
|
|
Name: fmt.Sprintf("skia/bots/%s", assetName),
|
|
Path: assetName,
|
|
Version: fmt.Sprintf("version:%d", version),
|
|
}
|
|
b.cipd(pkg)
|
|
}
|
|
|
|
// asset adds the given assets to the task as CIPD packages.
|
|
func (b *taskBuilder) asset(assets ...string) {
|
|
shouldIsolate := b.useIsolatedAssets()
|
|
pkgs := make([]*specs.CipdPackage, 0, len(assets))
|
|
for _, asset := range assets {
|
|
if cfg, ok := ISOLATE_ASSET_MAPPING[asset]; ok && (cfg.alwaysIsolate || shouldIsolate) {
|
|
b.dep(b.uploadCIPDAssetToCAS(asset))
|
|
} else {
|
|
pkgs = append(pkgs, b.MustGetCipdPackageFromAsset(asset))
|
|
}
|
|
}
|
|
b.cipd(pkgs...)
|
|
}
|
|
|
|
// usesCCache adds attributes to tasks which use ccache.
|
|
func (b *taskBuilder) usesCCache() {
|
|
b.cache(CACHES_CCACHE...)
|
|
}
|
|
|
|
// usesGit adds attributes to tasks which use git.
|
|
func (b *taskBuilder) usesGit() {
|
|
b.cache(CACHES_GIT...)
|
|
if b.matchOs("Win") || b.matchExtraConfig("Win") {
|
|
b.cipd(specs.CIPD_PKGS_GIT_WINDOWS_AMD64...)
|
|
} else if b.matchOs("Mac") || b.matchExtraConfig("Mac") {
|
|
b.cipd(specs.CIPD_PKGS_GIT_MAC_AMD64...)
|
|
} else {
|
|
b.cipd(specs.CIPD_PKGS_GIT_LINUX_AMD64...)
|
|
}
|
|
}
|
|
|
|
// usesGo adds attributes to tasks which use go. Recipes should use
|
|
// "with api.context(env=api.infra.go_env)".
|
|
func (b *taskBuilder) usesGo() {
|
|
b.usesGit() // Go requires Git.
|
|
b.cache(CACHES_GO...)
|
|
pkg := b.MustGetCipdPackageFromAsset("go")
|
|
if b.matchOs("Win") || b.matchExtraConfig("Win") {
|
|
pkg = b.MustGetCipdPackageFromAsset("go_win")
|
|
pkg.Path = "go"
|
|
}
|
|
b.cipd(pkg)
|
|
b.addToPATH(pkg.Path + "/go/bin")
|
|
b.envPrefixes("GOROOT", pkg.Path+"/go")
|
|
}
|
|
|
|
// usesDocker adds attributes to tasks which use docker.
|
|
func (b *taskBuilder) usesDocker() {
|
|
b.dimension("docker_installed:true")
|
|
|
|
// The "docker" binary reads its config from $HOME/.docker/config.json which, after running
|
|
// "gcloud auth configure-docker", typically looks like this:
|
|
//
|
|
// {
|
|
// "credHelpers": {
|
|
// "gcr.io": "gcloud",
|
|
// "us.gcr.io": "gcloud",
|
|
// "eu.gcr.io": "gcloud",
|
|
// "asia.gcr.io": "gcloud",
|
|
// "staging-k8s.gcr.io": "gcloud",
|
|
// "marketplace.gcr.io": "gcloud"
|
|
// }
|
|
// }
|
|
//
|
|
// This instructs "docker" to get its GCR credentials from a credential helper [1] program
|
|
// named "docker-credential-gcloud" [2], which is part of the Google Cloud SDK. This program is
|
|
// a shell script that invokes the "gcloud" command, which is itself a shell script that probes
|
|
// the environment to find a viable Python interpreter, and then invokes
|
|
// /usr/lib/google-cloud-sdk/lib/gcloud.py. For some unknown reason, sometimes "gcloud" decides
|
|
// to use "/b/s/w/ir/cache/vpython/875f1a/bin/python" as the Python interpreter (exact path may
|
|
// vary), which causes gcloud.py to fail with the following error:
|
|
//
|
|
// ModuleNotFoundError: No module named 'contextlib'
|
|
//
|
|
// Fortunately, "gcloud" supports specifying a Python interpreter via the GCLOUDSDK_PYTHON
|
|
// environment variable.
|
|
//
|
|
// [1] https://docs.docker.com/engine/reference/commandline/login/#credential-helpers
|
|
// [2] See /usr/bin/docker-credential-gcloud on your gLinux system, which is provided by the
|
|
// google-cloud-sdk package.
|
|
b.envPrefixes("CLOUDSDK_PYTHON", "cipd_bin_packages/cpython3/bin/python3")
|
|
|
|
// As mentioned, Docker uses gcloud for authentication against GCR, and gcloud requires Python.
|
|
b.usesPython()
|
|
}
|
|
|
|
// usesGSUtil adds the gsutil dependency from CIPD and puts it on PATH.
|
|
func (b *taskBuilder) usesGSUtil() {
|
|
b.asset("gsutil")
|
|
b.addToPATH("gsutil/gsutil")
|
|
}
|
|
|
|
// recipeProp adds the given recipe property key/value pair. Panics if
|
|
// getRecipeProps() was already called.
|
|
func (b *taskBuilder) recipeProp(key, value string) {
|
|
if b.recipeProperties == nil {
|
|
log.Fatal("taskBuilder.recipeProp() cannot be called after taskBuilder.getRecipeProps()!")
|
|
}
|
|
b.recipeProperties[key] = value
|
|
}
|
|
|
|
// recipeProps calls recipeProp for every key/value pair in the given map.
|
|
// Panics if getRecipeProps() was already called.
|
|
func (b *taskBuilder) recipeProps(props map[string]string) {
|
|
for k, v := range props {
|
|
b.recipeProp(k, v)
|
|
}
|
|
}
|
|
|
|
// getRecipeProps returns JSON-encoded recipe properties. Subsequent calls to
|
|
// recipeProp[s] will panic, to prevent accidentally adding recipe properties
|
|
// after they have been added to the task.
|
|
func (b *taskBuilder) getRecipeProps() string {
|
|
props := make(map[string]interface{}, len(b.recipeProperties)+2)
|
|
// TODO(borenet): I'm not sure why we supply the original task name
|
|
// and not the upload task name. We should investigate whether this is
|
|
// needed.
|
|
buildername := b.Name
|
|
if b.role("Upload") {
|
|
buildername = strings.TrimPrefix(buildername, "Upload-")
|
|
}
|
|
props["buildername"] = buildername
|
|
props["$kitchen"] = struct {
|
|
DevShell bool `json:"devshell"`
|
|
GitAuth bool `json:"git_auth"`
|
|
}{
|
|
DevShell: true,
|
|
GitAuth: true,
|
|
}
|
|
for k, v := range b.recipeProperties {
|
|
props[k] = v
|
|
}
|
|
b.recipeProperties = nil
|
|
return marshalJson(props)
|
|
}
|
|
|
|
// cipdPlatform returns the CIPD platform for this task.
|
|
func (b *taskBuilder) cipdPlatform() string {
|
|
if b.role("Upload") {
|
|
return cipd.PlatformLinuxAmd64
|
|
} else if b.matchOs("Win") || b.matchExtraConfig("Win") {
|
|
return cipd.PlatformWindowsAmd64
|
|
} else if b.matchOs("Mac") {
|
|
return cipd.PlatformMacAmd64
|
|
} else if b.matchArch("Arm64") {
|
|
return cipd.PlatformLinuxArm64
|
|
} else if b.matchOs("Android", "ChromeOS") {
|
|
return cipd.PlatformLinuxArm64
|
|
} else if b.matchOs("iOS") {
|
|
return cipd.PlatformLinuxArm64
|
|
} else {
|
|
return cipd.PlatformLinuxAmd64
|
|
}
|
|
}
|
|
|
|
// usesPython adds attributes to tasks which use python.
|
|
func (b *taskBuilder) usesPython() {
|
|
pythonPkgs := removePython2(cipd.PkgsPython[b.cipdPlatform()])
|
|
b.cipd(pythonPkgs...)
|
|
b.addToPATH(
|
|
"cipd_bin_packages/cpython3",
|
|
"cipd_bin_packages/cpython3/bin",
|
|
)
|
|
b.cache(&specs.Cache{
|
|
Name: "vpython",
|
|
Path: "cache/vpython",
|
|
})
|
|
b.envPrefixes("VPYTHON_VIRTUALENV_ROOT", "cache/vpython")
|
|
b.env("VPYTHON_LOG_TRACE", "1")
|
|
}
|
|
|
|
// removePython2 removes all python2 packages from a list of CIPD packages. This can be used to
|
|
// enforce the lack of Python2 dependencies in our tests.
|
|
func removePython2(pyPackages []*cipd.Package) []*cipd.Package {
|
|
var python3Pkgs []*cipd.Package
|
|
for _, p := range pyPackages {
|
|
if strings.HasPrefix(p.Version, "version:2@2.7") {
|
|
continue
|
|
}
|
|
python3Pkgs = append(python3Pkgs, p)
|
|
}
|
|
return python3Pkgs
|
|
}
|
|
|
|
func (b *taskBuilder) usesNode() {
|
|
// It is very important when including node via CIPD to also add it to the PATH of the
|
|
// taskdriver or mysterious things can happen when subprocesses try to resolve node/npm.
|
|
b.asset("node")
|
|
b.addToPATH("node/node/bin")
|
|
}
|
|
|
|
func (b *taskBuilder) needsLottiesWithAssets() {
|
|
// This CIPD package was made by hand with the following invocation:
|
|
// cipd create -name skia/internal/lotties_with_assets -in ./lotties/ -tag version:2
|
|
// cipd acl-edit skia/internal/lotties_with_assets -reader group:project-skia-external-task-accounts
|
|
// cipd acl-edit skia/internal/lotties_with_assets -reader user:pool-skia@chromium-swarm.iam.gserviceaccount.com
|
|
// Where lotties is a hand-selected set of lottie animations and (optionally) assets used in
|
|
// them (e.g. fonts, images).
|
|
// Each test case is in its own folder, with a data.json file and an optional images/ subfolder
|
|
// with any images/fonts/etc loaded by the animation.
|
|
// Note: If you are downloading the existing package to update them, remove the CIPD-generated
|
|
// .cipdpkg subfolder before trying to re-upload it.
|
|
// Note: It is important that the folder names do not special characters like . (), &, as
|
|
// the Android filesystem does not support folders with those names well.
|
|
b.cipd(&specs.CipdPackage{
|
|
Name: "skia/internal/lotties_with_assets",
|
|
Path: "lotties_with_assets",
|
|
Version: "version:4",
|
|
})
|
|
}
|