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