• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4package gen_tasks_logic
5
6import (
7	"fmt"
8	"log"
9	"reflect"
10	"strings"
11	"time"
12
13	"go.skia.org/infra/go/cipd"
14	"go.skia.org/infra/task_scheduler/go/specs"
15)
16
17// taskBuilder is a helper for creating a task.
18type taskBuilder struct {
19	*jobBuilder
20	parts
21	Name             string
22	Spec             *specs.TaskSpec
23	recipeProperties map[string]string
24}
25
26// newTaskBuilder returns a taskBuilder instance.
27func newTaskBuilder(b *jobBuilder, name string) *taskBuilder {
28	parts, err := b.jobNameSchema.ParseJobName(name)
29	if err != nil {
30		log.Fatal(err)
31	}
32	return &taskBuilder{
33		jobBuilder:       b,
34		parts:            parts,
35		Name:             name,
36		Spec:             &specs.TaskSpec{},
37		recipeProperties: map[string]string{},
38	}
39}
40
41// attempts sets the desired MaxAttempts for this task.
42func (b *taskBuilder) attempts(a int) {
43	b.Spec.MaxAttempts = a
44}
45
46// cache adds the given caches to the task.
47func (b *taskBuilder) cache(caches ...*specs.Cache) {
48	for _, c := range caches {
49		alreadyHave := false
50		for _, exist := range b.Spec.Caches {
51			if c.Name == exist.Name {
52				if !reflect.DeepEqual(c, exist) {
53					log.Fatalf("Already have cache %s with a different definition!", c.Name)
54				}
55				alreadyHave = true
56				break
57			}
58		}
59		if !alreadyHave {
60			b.Spec.Caches = append(b.Spec.Caches, c)
61		}
62	}
63}
64
65// cmd sets the command for the task.
66func (b *taskBuilder) cmd(c ...string) {
67	b.Spec.Command = c
68}
69
70// dimension adds the given dimensions to the task.
71func (b *taskBuilder) dimension(dims ...string) {
72	for _, dim := range dims {
73		if !In(dim, b.Spec.Dimensions) {
74			b.Spec.Dimensions = append(b.Spec.Dimensions, dim)
75		}
76	}
77}
78
79// expiration sets the expiration of the task.
80func (b *taskBuilder) expiration(e time.Duration) {
81	b.Spec.Expiration = e
82}
83
84// idempotent marks the task as idempotent.
85func (b *taskBuilder) idempotent() {
86	b.Spec.Idempotent = true
87}
88
89// cas sets the CasSpec used by the task.
90func (b *taskBuilder) cas(casSpec string) {
91	b.Spec.CasSpec = casSpec
92}
93
94// env sets the value for the given environment variable for the task.
95func (b *taskBuilder) env(key, value string) {
96	if b.Spec.Environment == nil {
97		b.Spec.Environment = map[string]string{}
98	}
99	b.Spec.Environment[key] = value
100}
101
102// envPrefixes appends the given values to the given environment variable for
103// the task.
104func (b *taskBuilder) envPrefixes(key string, values ...string) {
105	if b.Spec.EnvPrefixes == nil {
106		b.Spec.EnvPrefixes = map[string][]string{}
107	}
108	for _, value := range values {
109		if !In(value, b.Spec.EnvPrefixes[key]) {
110			b.Spec.EnvPrefixes[key] = append(b.Spec.EnvPrefixes[key], value)
111		}
112	}
113}
114
115// addToPATH adds the given locations to PATH for the task.
116func (b *taskBuilder) addToPATH(loc ...string) {
117	b.envPrefixes("PATH", loc...)
118}
119
120// output adds the given paths as outputs to the task, which results in their
121// contents being uploaded to the isolate server.
122func (b *taskBuilder) output(paths ...string) {
123	for _, path := range paths {
124		if !In(path, b.Spec.Outputs) {
125			b.Spec.Outputs = append(b.Spec.Outputs, path)
126		}
127	}
128}
129
130// serviceAccount sets the service account for this task.
131func (b *taskBuilder) serviceAccount(sa string) {
132	b.Spec.ServiceAccount = sa
133}
134
135// timeout sets the timeout(s) for this task.
136func (b *taskBuilder) timeout(timeout time.Duration) {
137	b.Spec.ExecutionTimeout = timeout
138	b.Spec.IoTimeout = timeout // With kitchen, step logs don't count toward IoTimeout.
139}
140
141// dep adds the given tasks as dependencies of this task.
142func (b *taskBuilder) dep(tasks ...string) {
143	for _, task := range tasks {
144		if !In(task, b.Spec.Dependencies) {
145			b.Spec.Dependencies = append(b.Spec.Dependencies, task)
146		}
147	}
148}
149
150// cipd adds the given CIPD packages to the task.
151func (b *taskBuilder) cipd(pkgs ...*specs.CipdPackage) {
152	for _, pkg := range pkgs {
153		alreadyHave := false
154		for _, exist := range b.Spec.CipdPackages {
155			if pkg.Name == exist.Name {
156				if !reflect.DeepEqual(pkg, exist) {
157					log.Fatalf("Already have package %s with a different definition!", pkg.Name)
158				}
159				alreadyHave = true
160				break
161			}
162		}
163		if !alreadyHave {
164			b.Spec.CipdPackages = append(b.Spec.CipdPackages, pkg)
165		}
166	}
167}
168
169// useIsolatedAssets returns true if this task should use assets which are
170// isolated rather than downloading directly from CIPD.
171func (b *taskBuilder) useIsolatedAssets() bool {
172	// Only do this on the RPIs for now. Other, faster machines shouldn't
173	// see much benefit and we don't need the extra complexity, for now.
174	if b.os("ChromeOS", "iOS") || b.matchOs("Android") {
175		return true
176	}
177	return false
178}
179
180// uploadAssetCASCfg represents a task which copies a CIPD package into
181// isolate.
182type uploadAssetCASCfg struct {
183	alwaysIsolate  bool
184	uploadTaskName string
185	path           string
186}
187
188// assetWithVersion adds the given asset with the given version number to the
189// task as a CIPD package.
190func (b *taskBuilder) assetWithVersion(assetName string, version int) {
191	pkg := &specs.CipdPackage{
192		Name:    fmt.Sprintf("skia/bots/%s", assetName),
193		Path:    assetName,
194		Version: fmt.Sprintf("version:%d", version),
195	}
196	b.cipd(pkg)
197}
198
199// asset adds the given assets to the task as CIPD packages.
200func (b *taskBuilder) asset(assets ...string) {
201	shouldIsolate := b.useIsolatedAssets()
202	pkgs := make([]*specs.CipdPackage, 0, len(assets))
203	for _, asset := range assets {
204		if cfg, ok := ISOLATE_ASSET_MAPPING[asset]; ok && (cfg.alwaysIsolate || shouldIsolate) {
205			b.dep(b.uploadCIPDAssetToCAS(asset))
206		} else {
207			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset(asset))
208		}
209	}
210	b.cipd(pkgs...)
211}
212
213// usesCCache adds attributes to tasks which need bazel (via bazelisk).
214func (b *taskBuilder) usesBazel(hostOSArch string) {
215	archToPkg := map[string]string{
216		"linux_x64": "bazelisk_linux_amd64",
217		"mac_x64":   "bazelisk_mac_amd64",
218	}
219	pkg, ok := archToPkg[hostOSArch]
220	if !ok {
221		panic("Unsupported osAndArch for bazelisk: " + hostOSArch)
222	}
223	b.cipd(b.MustGetCipdPackageFromAsset(pkg))
224	b.addToPATH(pkg)
225}
226
227// usesCCache adds attributes to tasks which use ccache.
228func (b *taskBuilder) usesCCache() {
229	b.cache(CACHES_CCACHE...)
230}
231
232// usesGit adds attributes to tasks which use git.
233func (b *taskBuilder) usesGit() {
234	b.cache(CACHES_GIT...)
235	if b.matchOs("Win") || b.matchExtraConfig("Win") {
236		b.cipd(specs.CIPD_PKGS_GIT_WINDOWS_AMD64...)
237	} else if b.matchOs("Mac") || b.matchExtraConfig("Mac") {
238		b.cipd(specs.CIPD_PKGS_GIT_MAC_AMD64...)
239	} else {
240		b.cipd(specs.CIPD_PKGS_GIT_LINUX_AMD64...)
241	}
242}
243
244// usesGo adds attributes to tasks which use go. Recipes should use
245// "with api.context(env=api.infra.go_env)".
246func (b *taskBuilder) usesGo() {
247	b.usesGit() // Go requires Git.
248	b.cache(CACHES_GO...)
249	pkg := b.MustGetCipdPackageFromAsset("go")
250	if b.matchOs("Win") || b.matchExtraConfig("Win") {
251		pkg = b.MustGetCipdPackageFromAsset("go_win")
252		pkg.Path = "go"
253	}
254	b.cipd(pkg)
255	b.addToPATH(pkg.Path + "/go/bin")
256	b.envPrefixes("GOROOT", pkg.Path+"/go")
257}
258
259// usesDocker adds attributes to tasks which use docker.
260func (b *taskBuilder) usesDocker() {
261	b.dimension("docker_installed:true")
262
263	// The "docker" binary reads its config from $HOME/.docker/config.json which, after running
264	// "gcloud auth configure-docker", typically looks like this:
265	//
266	//     {
267	//       "credHelpers": {
268	//         "gcr.io": "gcloud",
269	//         "us.gcr.io": "gcloud",
270	//         "eu.gcr.io": "gcloud",
271	//         "asia.gcr.io": "gcloud",
272	//         "staging-k8s.gcr.io": "gcloud",
273	//         "marketplace.gcr.io": "gcloud"
274	//       }
275	//     }
276	//
277	// This instructs "docker" to get its GCR credentials from a credential helper [1] program
278	// named "docker-credential-gcloud" [2], which is part of the Google Cloud SDK. This program is
279	// a shell script that invokes the "gcloud" command, which is itself a shell script that probes
280	// the environment to find a viable Python interpreter, and then invokes
281	// /usr/lib/google-cloud-sdk/lib/gcloud.py. For some unknown reason, sometimes "gcloud" decides
282	// to use "/b/s/w/ir/cache/vpython/875f1a/bin/python" as the Python interpreter (exact path may
283	// vary), which causes gcloud.py to fail with the following error:
284	//
285	//     ModuleNotFoundError: No module named 'contextlib'
286	//
287	// Fortunately, "gcloud" supports specifying a Python interpreter via the GCLOUDSDK_PYTHON
288	// environment variable.
289	//
290	// [1] https://docs.docker.com/engine/reference/commandline/login/#credential-helpers
291	// [2] See /usr/bin/docker-credential-gcloud on your gLinux system, which is provided by the
292	//     google-cloud-sdk package.
293	b.envPrefixes("CLOUDSDK_PYTHON", "cipd_bin_packages/cpython3/bin/python3")
294
295	// As mentioned, Docker uses gcloud for authentication against GCR, and gcloud requires Python.
296	b.usesPython()
297}
298
299// usesGSUtil adds the gsutil dependency from CIPD and puts it on PATH.
300func (b *taskBuilder) usesGSUtil() {
301	b.asset("gsutil")
302	b.addToPATH("gsutil/gsutil")
303}
304
305// needsFontsForParagraphTests downloads the skparagraph CIPD package to
306// a subdirectory of the Skia checkout: resources/extra_fonts
307func (b *taskBuilder) needsFontsForParagraphTests() {
308	pkg := b.MustGetCipdPackageFromAsset("skparagraph")
309	pkg.Path = "skia/resources/extra_fonts"
310	b.cipd(pkg)
311}
312
313// recipeProp adds the given recipe property key/value pair. Panics if
314// getRecipeProps() was already called.
315func (b *taskBuilder) recipeProp(key, value string) {
316	if b.recipeProperties == nil {
317		log.Fatal("taskBuilder.recipeProp() cannot be called after taskBuilder.getRecipeProps()!")
318	}
319	b.recipeProperties[key] = value
320}
321
322// recipeProps calls recipeProp for every key/value pair in the given map.
323// Panics if getRecipeProps() was already called.
324func (b *taskBuilder) recipeProps(props map[string]string) {
325	for k, v := range props {
326		b.recipeProp(k, v)
327	}
328}
329
330// getRecipeProps returns JSON-encoded recipe properties. Subsequent calls to
331// recipeProp[s] will panic, to prevent accidentally adding recipe properties
332// after they have been added to the task.
333func (b *taskBuilder) getRecipeProps() string {
334	props := make(map[string]interface{}, len(b.recipeProperties)+2)
335	// TODO(borenet): I'm not sure why we supply the original task name
336	// and not the upload task name.  We should investigate whether this is
337	// needed.
338	buildername := b.Name
339	if b.role("Upload") {
340		buildername = strings.TrimPrefix(buildername, "Upload-")
341	}
342	props["buildername"] = buildername
343	props["$kitchen"] = struct {
344		DevShell bool `json:"devshell"`
345		GitAuth  bool `json:"git_auth"`
346	}{
347		DevShell: true,
348		GitAuth:  true,
349	}
350	for k, v := range b.recipeProperties {
351		props[k] = v
352	}
353	b.recipeProperties = nil
354	return marshalJson(props)
355}
356
357// cipdPlatform returns the CIPD platform for this task.
358func (b *taskBuilder) cipdPlatform() string {
359	if b.role("Upload") {
360		return cipd.PlatformLinuxAmd64
361	} else if b.matchOs("Win") || b.matchExtraConfig("Win") {
362		return cipd.PlatformWindowsAmd64
363	} else if b.matchOs("Mac") {
364		return cipd.PlatformMacAmd64
365	} else if b.matchArch("Arm64") {
366		return cipd.PlatformLinuxArm64
367	} else if b.matchOs("Android", "ChromeOS") {
368		return cipd.PlatformLinuxArm64
369	} else if b.matchOs("iOS") {
370		return cipd.PlatformLinuxArm64
371	} else {
372		return cipd.PlatformLinuxAmd64
373	}
374}
375
376// usesPython adds attributes to tasks which use python.
377func (b *taskBuilder) usesPython() {
378	pythonPkgs := cipd.PkgsPython[b.cipdPlatform()]
379	b.cipd(pythonPkgs...)
380	b.addToPATH(
381		"cipd_bin_packages/cpython3",
382		"cipd_bin_packages/cpython3/bin",
383	)
384	b.cache(&specs.Cache{
385		Name: "vpython3",
386		Path: "cache/vpython3",
387	})
388	b.envPrefixes("VPYTHON_VIRTUALENV_ROOT", "cache/vpython3")
389	b.env("VPYTHON_LOG_TRACE", "1")
390}
391
392func (b *taskBuilder) usesNode() {
393	// It is very important when including node via CIPD to also add it to the PATH of the
394	// taskdriver or mysterious things can happen when subprocesses try to resolve node/npm.
395	b.asset("node")
396	b.addToPATH("node/node/bin")
397}
398
399func (b *taskBuilder) needsLottiesWithAssets() {
400	// This CIPD package was made by hand with the following invocation:
401	//   cipd create -name skia/internal/lotties_with_assets -in ./lotties/ -tag version:2
402	//   cipd acl-edit skia/internal/lotties_with_assets -reader group:project-skia-external-task-accounts
403	//   cipd acl-edit skia/internal/lotties_with_assets -reader user:pool-skia@chromium-swarm.iam.gserviceaccount.com
404	// Where lotties is a hand-selected set of lottie animations and (optionally) assets used in
405	// them (e.g. fonts, images).
406	// Each test case is in its own folder, with a data.json file and an optional images/ subfolder
407	// with any images/fonts/etc loaded by the animation.
408	// Note: If you are downloading the existing package to update them, remove the CIPD-generated
409	// .cipdpkg subfolder before trying to re-upload it.
410	// Note: It is important that the folder names do not special characters like . (), &, as
411	// the Android filesystem does not support folders with those names well.
412	b.cipd(&specs.CipdPackage{
413		Name:    "skia/internal/lotties_with_assets",
414		Path:    "lotties_with_assets",
415		Version: "version:4",
416	})
417}
418