• 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	"log"
8	"reflect"
9	"strings"
10	"time"
11
12	"go.skia.org/infra/go/cipd"
13	"go.skia.org/infra/task_scheduler/go/specs"
14)
15
16// taskBuilder is a helper for creating a task.
17type taskBuilder struct {
18	*jobBuilder
19	parts
20	Name             string
21	Spec             *specs.TaskSpec
22	recipeProperties map[string]string
23}
24
25// newTaskBuilder returns a taskBuilder instance.
26func newTaskBuilder(b *jobBuilder, name string) *taskBuilder {
27	parts, err := b.jobNameSchema.ParseJobName(name)
28	if err != nil {
29		log.Fatal(err)
30	}
31	return &taskBuilder{
32		jobBuilder:       b,
33		parts:            parts,
34		Name:             name,
35		Spec:             &specs.TaskSpec{},
36		recipeProperties: map[string]string{},
37	}
38}
39
40// attempts sets the desired MaxAttempts for this task.
41func (b *taskBuilder) attempts(a int) {
42	b.Spec.MaxAttempts = a
43}
44
45// cache adds the given caches to the task.
46func (b *taskBuilder) cache(caches ...*specs.Cache) {
47	for _, c := range caches {
48		alreadyHave := false
49		for _, exist := range b.Spec.Caches {
50			if c.Name == exist.Name {
51				if !reflect.DeepEqual(c, exist) {
52					log.Fatalf("Already have cache %s with a different definition!", c.Name)
53				}
54				alreadyHave = true
55				break
56			}
57		}
58		if !alreadyHave {
59			b.Spec.Caches = append(b.Spec.Caches, c)
60		}
61	}
62}
63
64// cmd sets the command for the task.
65func (b *taskBuilder) cmd(c ...string) {
66	b.Spec.Command = c
67}
68
69// dimension adds the given dimensions to the task.
70func (b *taskBuilder) dimension(dims ...string) {
71	for _, dim := range dims {
72		if !In(dim, b.Spec.Dimensions) {
73			b.Spec.Dimensions = append(b.Spec.Dimensions, dim)
74		}
75	}
76}
77
78// expiration sets the expiration of the task.
79func (b *taskBuilder) expiration(e time.Duration) {
80	b.Spec.Expiration = e
81}
82
83// idempotent marks the task as idempotent.
84func (b *taskBuilder) idempotent() {
85	b.Spec.Idempotent = true
86}
87
88// cas sets the CasSpec used by the task.
89func (b *taskBuilder) cas(casSpec string) {
90	b.Spec.CasSpec = casSpec
91}
92
93// env sets the value for the given environment variable for the task.
94func (b *taskBuilder) env(key, value string) {
95	if b.Spec.Environment == nil {
96		b.Spec.Environment = map[string]string{}
97	}
98	b.Spec.Environment[key] = value
99}
100
101// envPrefixes appends the given values to the given environment variable for
102// the task.
103func (b *taskBuilder) envPrefixes(key string, values ...string) {
104	if b.Spec.EnvPrefixes == nil {
105		b.Spec.EnvPrefixes = map[string][]string{}
106	}
107	for _, value := range values {
108		if !In(value, b.Spec.EnvPrefixes[key]) {
109			b.Spec.EnvPrefixes[key] = append(b.Spec.EnvPrefixes[key], value)
110		}
111	}
112}
113
114// addToPATH adds the given locations to PATH for the task.
115func (b *taskBuilder) addToPATH(loc ...string) {
116	b.envPrefixes("PATH", loc...)
117}
118
119// output adds the given paths as outputs to the task, which results in their
120// contents being uploaded to the isolate server.
121func (b *taskBuilder) output(paths ...string) {
122	for _, path := range paths {
123		if !In(path, b.Spec.Outputs) {
124			b.Spec.Outputs = append(b.Spec.Outputs, path)
125		}
126	}
127}
128
129// serviceAccount sets the service account for this task.
130func (b *taskBuilder) serviceAccount(sa string) {
131	b.Spec.ServiceAccount = sa
132}
133
134// timeout sets the timeout(s) for this task.
135func (b *taskBuilder) timeout(timeout time.Duration) {
136	b.Spec.ExecutionTimeout = timeout
137	b.Spec.IoTimeout = timeout // With kitchen, step logs don't count toward IoTimeout.
138}
139
140// dep adds the given tasks as dependencies of this task.
141func (b *taskBuilder) dep(tasks ...string) {
142	for _, task := range tasks {
143		if !In(task, b.Spec.Dependencies) {
144			b.Spec.Dependencies = append(b.Spec.Dependencies, task)
145		}
146	}
147}
148
149// cipd adds the given CIPD packages to the task.
150func (b *taskBuilder) cipd(pkgs ...*specs.CipdPackage) {
151	for _, pkg := range pkgs {
152		alreadyHave := false
153		for _, exist := range b.Spec.CipdPackages {
154			if pkg.Name == exist.Name {
155				if !reflect.DeepEqual(pkg, exist) {
156					log.Fatalf("Already have package %s with a different definition!", pkg.Name)
157				}
158				alreadyHave = true
159				break
160			}
161		}
162		if !alreadyHave {
163			b.Spec.CipdPackages = append(b.Spec.CipdPackages, pkg)
164		}
165	}
166}
167
168// useIsolatedAssets returns true if this task should use assets which are
169// isolated rather than downloading directly from CIPD.
170func (b *taskBuilder) useIsolatedAssets() bool {
171	// Only do this on the RPIs for now. Other, faster machines shouldn't
172	// see much benefit and we don't need the extra complexity, for now.
173	if b.os("Android", "ChromeOS", "iOS") {
174		return true
175	}
176	return false
177}
178
179// uploadAssetCASCfg represents a task which copies a CIPD package into
180// isolate.
181type uploadAssetCASCfg struct {
182	alwaysIsolate  bool
183	uploadTaskName string
184	path           string
185}
186
187// asset adds the given assets to the task as CIPD packages.
188func (b *taskBuilder) asset(assets ...string) {
189	shouldIsolate := b.useIsolatedAssets()
190	pkgs := make([]*specs.CipdPackage, 0, len(assets))
191	for _, asset := range assets {
192		if cfg, ok := ISOLATE_ASSET_MAPPING[asset]; ok && (cfg.alwaysIsolate || shouldIsolate) {
193			b.dep(b.uploadCIPDAssetToCAS(asset))
194		} else {
195			pkgs = append(pkgs, b.MustGetCipdPackageFromAsset(asset))
196		}
197	}
198	b.cipd(pkgs...)
199}
200
201// usesCCache adds attributes to tasks which use ccache.
202func (b *taskBuilder) usesCCache() {
203	b.cache(CACHES_CCACHE...)
204}
205
206// usesGit adds attributes to tasks which use git.
207func (b *taskBuilder) usesGit() {
208	b.cache(CACHES_GIT...)
209	if b.matchOs("Win") || b.matchExtraConfig("Win") {
210		b.cipd(specs.CIPD_PKGS_GIT_WINDOWS_AMD64...)
211	} else if b.matchOs("Mac") || b.matchExtraConfig("Mac") {
212		b.cipd(specs.CIPD_PKGS_GIT_MAC_AMD64...)
213	} else {
214		b.cipd(specs.CIPD_PKGS_GIT_LINUX_AMD64...)
215	}
216}
217
218// usesGo adds attributes to tasks which use go. Recipes should use
219// "with api.context(env=api.infra.go_env)".
220func (b *taskBuilder) usesGo() {
221	b.usesGit() // Go requires Git.
222	b.cache(CACHES_GO...)
223	pkg := b.MustGetCipdPackageFromAsset("go")
224	if b.matchOs("Win") || b.matchExtraConfig("Win") {
225		pkg = b.MustGetCipdPackageFromAsset("go_win")
226		pkg.Path = "go"
227	}
228	b.cipd(pkg)
229	b.addToPATH(pkg.Path + "/go/bin")
230	b.envPrefixes("GOROOT", pkg.Path+"/go")
231}
232
233// usesDocker adds attributes to tasks which use docker.
234func (b *taskBuilder) usesDocker() {
235	b.dimension("docker_installed:true")
236}
237
238// recipeProp adds the given recipe property key/value pair. Panics if
239// getRecipeProps() was already called.
240func (b *taskBuilder) recipeProp(key, value string) {
241	if b.recipeProperties == nil {
242		log.Fatal("taskBuilder.recipeProp() cannot be called after taskBuilder.getRecipeProps()!")
243	}
244	b.recipeProperties[key] = value
245}
246
247// recipeProps calls recipeProp for every key/value pair in the given map.
248// Panics if getRecipeProps() was already called.
249func (b *taskBuilder) recipeProps(props map[string]string) {
250	for k, v := range props {
251		b.recipeProp(k, v)
252	}
253}
254
255// getRecipeProps returns JSON-encoded recipe properties. Subsequent calls to
256// recipeProp[s] will panic, to prevent accidentally adding recipe properties
257// after they have been added to the task.
258func (b *taskBuilder) getRecipeProps() string {
259	props := make(map[string]interface{}, len(b.recipeProperties)+2)
260	// TODO(borenet): I'm not sure why we supply the original task name
261	// and not the upload task name.  We should investigate whether this is
262	// needed.
263	buildername := b.Name
264	if b.role("Upload") {
265		buildername = strings.TrimPrefix(buildername, "Upload-")
266	}
267	props["buildername"] = buildername
268	props["$kitchen"] = struct {
269		DevShell bool `json:"devshell"`
270		GitAuth  bool `json:"git_auth"`
271	}{
272		DevShell: true,
273		GitAuth:  true,
274	}
275	for k, v := range b.recipeProperties {
276		props[k] = v
277	}
278	b.recipeProperties = nil
279	return marshalJson(props)
280}
281
282// cipdPlatform returns the CIPD platform for this task.
283func (b *taskBuilder) cipdPlatform() string {
284	if b.role("Upload") {
285		return cipd.PlatformLinuxAmd64
286	} else if b.matchOs("Win") || b.matchExtraConfig("Win") {
287		return cipd.PlatformWindowsAmd64
288	} else if b.matchOs("Mac") {
289		return cipd.PlatformMacAmd64
290	} else if b.matchArch("Arm64") {
291		return cipd.PlatformLinuxArm64
292	} else if b.matchOs("Android", "ChromeOS") {
293		return cipd.PlatformLinuxArm64
294	} else if b.matchOs("iOS") {
295		return cipd.PlatformLinuxArm64
296	} else {
297		return cipd.PlatformLinuxAmd64
298	}
299}
300
301// usesPython adds attributes to tasks which use python.
302func (b *taskBuilder) usesPython() {
303	pythonPkgs := cipd.PkgsPython[b.cipdPlatform()]
304	b.cipd(pythonPkgs...)
305	b.addToPATH(
306		"cipd_bin_packages/cpython",
307		"cipd_bin_packages/cpython/bin",
308		"cipd_bin_packages/cpython3",
309		"cipd_bin_packages/cpython3/bin",
310	)
311	b.cache(&specs.Cache{
312		Name: "vpython",
313		Path: "cache/vpython",
314	})
315	b.envPrefixes("VPYTHON_VIRTUALENV_ROOT", "cache/vpython")
316	b.env("VPYTHON_LOG_TRACE", "1")
317}
318
319func (b *taskBuilder) usesNode() {
320	// It is very important when including node via CIPD to also add it to the PATH of the
321	// taskdriver or mysterious things can happen when subprocesses try to resolve node/npm.
322	b.asset("node")
323	b.addToPATH("node/node/bin")
324}
325