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