go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/job/edit_buildbucket.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package job 16 17 import ( 18 "encoding/json" 19 "sort" 20 "time" 21 22 "github.com/golang/protobuf/proto" 23 "google.golang.org/protobuf/types/known/durationpb" 24 "google.golang.org/protobuf/types/known/structpb" 25 26 "go.chromium.org/luci/buildbucket" 27 bbpb "go.chromium.org/luci/buildbucket/proto" 28 "go.chromium.org/luci/common/data/stringset" 29 "go.chromium.org/luci/common/data/strpair" 30 "go.chromium.org/luci/common/errors" 31 swarmingpb "go.chromium.org/luci/swarming/proto/api_v2" 32 ) 33 34 // RecipeDirectory is a very unfortunate constant which is here for 35 // a combination of reasons: 36 // 1. swarming doesn't allow you to 'checkout' an isolate relative to any 37 // path in the task (other than the task root). This means that 38 // whatever value we pick for EditRecipeBundle must be used EVERYWHERE 39 // the isolated hash is used. 40 // 2. Currently the 'recipe_engine/led' module will blindly take the 41 // isolated input and 'inject' it into further uses of led. This module 42 // currently doesn't specify the checkout dir, relying on kitchen's 43 // default value of (you guessed it) "kitchen-checkout". 44 // 45 // In order to fix this (and it will need to be fixed for bbagent support): 46 // - The 'recipe_engine/led' module needs to accept 'checkout-dir' as 47 // a parameter in its input properties. 48 // - led needs to start passing the checkout dir to the led module's input 49 // properties. 50 // - `led edit` needs a way to manipulate the checkout directory in a job 51 // - The 'recipe_engine/led' module needs to set this in the job 52 // alongside the isolate hash when it's doing the injection. 53 // 54 // For now, we just hard-code it. 55 // 56 // TODO(crbug.com/1072117): Fix this, it's weird. 57 const RecipeDirectory = "kitchen-checkout" 58 59 // LEDBuilderIsBootstrappedProperty should be set to a boolean value. If true, 60 // edit-recipe-bundle will set the "led_cas_recipe_bundle" property 61 // instead of overwriting the build's payload. 62 const LEDBuilderIsBootstrappedProperty = "led_builder_is_bootstrapped" 63 64 type buildbucketEditor struct { 65 jd *Definition 66 bb *Buildbucket 67 68 err error 69 } 70 71 var _ HighLevelEditor = (*buildbucketEditor)(nil) 72 73 func newBuildbucketEditor(jd *Definition) *buildbucketEditor { 74 bb := jd.GetBuildbucket() 75 if bb == nil { 76 panic(errors.New("impossible: only supported for Buildbucket builds")) 77 } 78 bb.EnsureBasics() 79 80 return &buildbucketEditor{jd, bb, nil} 81 } 82 83 func (bbe *buildbucketEditor) Close() error { 84 return bbe.err 85 } 86 87 func (bbe *buildbucketEditor) tweak(fn func() error) { 88 if bbe.err == nil { 89 bbe.err = fn() 90 } 91 } 92 93 func (bbe *buildbucketEditor) Tags(values []string) { 94 if len(values) == 0 { 95 return 96 } 97 98 bbe.tweak(func() (err error) { 99 if err = validateTags(values); err == nil { 100 tags := bbe.bb.BbagentArgs.Build.Tags 101 for _, tag := range values { 102 k, v := strpair.Parse(tag) 103 tags = append(tags, &bbpb.StringPair{ 104 Key: k, 105 Value: v, 106 }) 107 } 108 sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key }) 109 bbe.bb.BbagentArgs.Build.Tags = tags 110 } 111 return nil 112 }) 113 } 114 115 func (bbe *buildbucketEditor) TaskPayloadSource(cipdPkg, cipdVers string) { 116 bbe.tweak(func() error { 117 usedCipdVers := cipdVers 118 if cipdVers == "" { 119 usedCipdVers = "latest" 120 } 121 // Update exe. 122 exe := bbe.bb.BbagentArgs.Build.Exe 123 if cipdPkg != "" { 124 exe.CipdPackage = cipdPkg 125 exe.CipdVersion = usedCipdVers 126 } else if cipdPkg == "" && cipdVers == "" { 127 exe.CipdPackage = "" 128 exe.CipdVersion = "" 129 } else { 130 return errors.Reason( 131 "cipdPkg and cipdVers must both be set or both be empty: cipdPkg=%q cipdVers=%q", 132 cipdPkg, cipdVers).Err() 133 } 134 135 // Update infra.Buildbucket.Agent.Input 136 if cipdPkg == "" && cipdVers == "" { 137 return nil 138 } 139 bbe.TaskPayloadPath(RecipeDirectory) 140 input := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input 141 if input == nil { 142 input = &bbpb.BuildInfra_Buildbucket_Agent_Input{} 143 bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input = input 144 } 145 inputData := input.GetData() 146 if len(inputData) == 0 { 147 inputData = make(map[string]*bbpb.InputDataRef) 148 input.Data = inputData 149 } 150 if ref, ok := inputData[RecipeDirectory]; ok && ref.GetCipd() != nil { 151 if len(ref.GetCipd().Specs) > 1 { 152 return errors.Reason("can only have one user payload under %s", RecipeDirectory).Err() 153 } 154 ref.GetCipd().Specs[0] = &bbpb.InputDataRef_CIPD_PkgSpec{ 155 Package: cipdPkg, 156 Version: usedCipdVers, 157 } 158 return nil 159 } 160 inputData[RecipeDirectory] = &bbpb.InputDataRef{ 161 DataType: &bbpb.InputDataRef_Cipd{ 162 Cipd: &bbpb.InputDataRef_CIPD{ 163 Specs: []*bbpb.InputDataRef_CIPD_PkgSpec{ 164 &bbpb.InputDataRef_CIPD_PkgSpec{ 165 Package: cipdPkg, 166 Version: usedCipdVers, 167 }, 168 }, 169 }, 170 }, 171 } 172 173 return nil 174 }) 175 } 176 177 func (bbe *buildbucketEditor) TaskPayloadPath(path string) { 178 bbe.tweak(func() error { 179 bbe.bb.UpdatePayloadPath(path) 180 return nil 181 }) 182 } 183 184 func (bbe *buildbucketEditor) CASTaskPayload(path string, casRef *swarmingpb.CASReference) { 185 bbe.tweak(func() error { 186 if path != "" { 187 bbe.TaskPayloadPath(path) 188 } else { 189 purposes := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.GetAgent().GetPurposes() 190 for dir, pur := range purposes { 191 if pur == bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD { 192 path = dir 193 break 194 } 195 } 196 } 197 198 if path == "" { 199 return errors.Reason("failed to get exe payload path").Err() 200 } 201 202 input := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input 203 if input == nil { 204 input = &bbpb.BuildInfra_Buildbucket_Agent_Input{} 205 bbe.bb.BbagentArgs.Build.Infra.Buildbucket.Agent.Input = input 206 } 207 inputData := input.GetData() 208 if len(inputData) == 0 { 209 inputData = make(map[string]*bbpb.InputDataRef) 210 input.Data = inputData 211 } 212 213 if ref, ok := inputData[path]; ok && ref.GetCas() != nil { 214 if casRef.CasInstance != "" { 215 ref.GetCas().CasInstance = casRef.CasInstance 216 } 217 ref.GetCas().Digest = &bbpb.InputDataRef_CAS_Digest{ 218 Hash: casRef.GetDigest().GetHash(), 219 SizeBytes: casRef.GetDigest().GetSizeBytes(), 220 } 221 } else { 222 casInstance := casRef.CasInstance 223 if casInstance == "" { 224 var err error 225 casInstance, err = bbe.jd.CasInstance() 226 if err != nil { 227 return err 228 } 229 } 230 inputData[path] = &bbpb.InputDataRef{ 231 DataType: &bbpb.InputDataRef_Cas{ 232 Cas: &bbpb.InputDataRef_CAS{ 233 CasInstance: casInstance, 234 Digest: &bbpb.InputDataRef_CAS_Digest{ 235 Hash: casRef.GetDigest().GetHash(), 236 SizeBytes: casRef.GetDigest().GetSizeBytes(), 237 }, 238 }, 239 }, 240 } 241 } 242 return nil 243 }) 244 } 245 246 func (bbe *buildbucketEditor) TaskPayloadCmd(args []string) { 247 bbe.tweak(func() error { 248 if len(args) == 0 { 249 args = []string{"luciexe"} 250 } 251 bbe.bb.BbagentArgs.Build.Exe.Cmd = args 252 return nil 253 }) 254 } 255 256 func (bbe *buildbucketEditor) ClearCurrentIsolated() { 257 bbe.tweak(func() error { 258 agent := bbe.bb.BbagentArgs.Build.GetInfra().GetBuildbucket().GetAgent() 259 if agent == nil { 260 return nil 261 } 262 263 payloadPath := "" 264 for p, purpose := range agent.GetPurposes() { 265 if purpose == bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD { 266 payloadPath = p 267 break 268 } 269 } 270 if payloadPath == "" { 271 return nil 272 } 273 inputData := agent.GetInput().GetData() 274 if ref, ok := inputData[payloadPath]; ok { 275 if ref.GetCas() != nil { 276 delete(inputData, payloadPath) 277 } 278 } 279 return nil 280 }) 281 } 282 283 func (bbe *buildbucketEditor) ClearDimensions() { 284 bbe.tweak(func() error { 285 infra := bbe.bb.BbagentArgs.Build.Infra 286 if infra.Swarming != nil { 287 bbe.bb.BbagentArgs.Build.Infra.Swarming.TaskDimensions = nil 288 } else { 289 bbe.bb.BbagentArgs.Build.Infra.Backend.TaskDimensions = nil 290 } 291 292 return nil 293 }) 294 } 295 296 func (bbe *buildbucketEditor) SetDimensions(dims ExpiringDimensions) { 297 bbe.ClearDimensions() 298 dec := DimensionEditCommands{} 299 for key, vals := range dims { 300 dec[key] = &DimensionEditCommand{SetValues: vals} 301 } 302 bbe.EditDimensions(dec) 303 } 304 305 func (bbe *buildbucketEditor) EditDimensions(dimEdits DimensionEditCommands) { 306 if len(dimEdits) == 0 { 307 return 308 } 309 310 bbe.tweak(func() error { 311 dims, err := bbe.jd.Info().Dimensions() 312 if err != nil { 313 return err 314 } 315 316 dimMap := dims.toLogical() 317 dimEdits.apply(dimMap, 0) 318 319 build := bbe.bb.BbagentArgs.Build 320 var curTimeout time.Duration 321 if build.SchedulingTimeout != nil { 322 if err := build.SchedulingTimeout.CheckValid(); err != nil { 323 return err 324 } 325 curTimeout = build.SchedulingTimeout.AsDuration() 326 } 327 var maxExp time.Duration 328 var newDimLen int 329 if build.Infra.Swarming != nil { 330 newDimLen = len(build.Infra.Swarming.TaskDimensions) + len(dimEdits) 331 } else { 332 newDimLen = len(build.Infra.Backend.TaskDimensions) + len(dimEdits) 333 } 334 newDims := make([]*bbpb.RequestedDimension, 0, newDimLen) 335 for _, key := range keysOf(dimMap) { 336 valueExp := dimMap[key] 337 for _, value := range keysOf(valueExp) { 338 exp := valueExp[value] 339 if exp > maxExp { 340 maxExp = exp 341 } 342 343 toAdd := &bbpb.RequestedDimension{ 344 Key: key, 345 Value: value, 346 } 347 if exp > 0 && exp != curTimeout { 348 toAdd.Expiration = durationpb.New(exp) 349 } 350 newDims = append(newDims, toAdd) 351 } 352 } 353 if build.Infra.Swarming != nil { 354 build.Infra.Swarming.TaskDimensions = newDims 355 } else { 356 build.Infra.Backend.TaskDimensions = newDims 357 } 358 359 if maxExp > curTimeout { 360 build.SchedulingTimeout = durationpb.New(maxExp) 361 } 362 return nil 363 }) 364 } 365 366 func (bbe *buildbucketEditor) Env(env map[string]string) { 367 if len(env) == 0 { 368 return 369 } 370 371 bbe.tweak(func() error { 372 updateStringPairList(&bbe.bb.EnvVars, env) 373 return nil 374 }) 375 } 376 377 func (bbe *buildbucketEditor) Priority(priority int32) { 378 bbe.tweak(func() error { 379 if priority < 0 { 380 return errors.Reason("negative Priority argument: %d", priority).Err() 381 } 382 383 infra := bbe.bb.BbagentArgs.Build.Infra 384 if infra.Swarming != nil { 385 infra.Swarming.Priority = priority 386 } else { 387 infra.Backend.Config.Fields["priority"] = structpb.NewNumberValue(float64(priority)) 388 } 389 return nil 390 }) 391 } 392 393 func (bbe *buildbucketEditor) Properties(props map[string]string, auto bool) { 394 if len(props) == 0 { 395 return 396 } 397 bbe.tweak(func() error { 398 toWrite := map[string]any{} 399 removed := make([]string, 0, len(props)) 400 401 for k, v := range props { 402 if v == "" { 403 toWrite[k] = nil 404 removed = append(removed, k) 405 } else { 406 var obj any 407 if err := json.Unmarshal([]byte(v), &obj); err != nil { 408 if !auto { 409 return err 410 } 411 obj = v 412 } 413 toWrite[k] = obj 414 } 415 } 416 417 if bbe.bb.BbagentArgs.Build.Input.Properties.GetFields()[LEDBuilderIsBootstrappedProperty].GetBoolValue() { 418 propCopy := map[string]any{} 419 for k, v := range toWrite { 420 if v == nil { 421 // removed properties are tracked by `removed`. 422 continue 423 } 424 propCopy[k] = v 425 } 426 toWrite["led_edited_properties"] = propCopy 427 toWrite["led_removed_properties"] = removed 428 } 429 430 bbe.bb.WriteProperties(toWrite) 431 return nil 432 }) 433 } 434 435 func (bbe *buildbucketEditor) CIPDPkgs(cipdPkgs CIPDPkgs) { 436 if len(cipdPkgs) == 0 { 437 return 438 } 439 440 bbe.tweak(func() error { 441 if !bbe.bb.BbagentDownloadCIPDPkgs() { 442 cipdPkgs.updateCipdPkgs(&bbe.bb.CipdPackages) 443 return nil 444 } 445 return errors.Reason("not supported for Buildbucket v2 builds").Err() 446 }) 447 } 448 449 func (bbe *buildbucketEditor) SwarmingHostname(host string) { 450 bbe.tweak(func() (err error) { 451 if host == "" { 452 return errors.New("empty SwarmingHostname") 453 } 454 455 infra := bbe.bb.BbagentArgs.Build.Infra 456 if infra.Swarming != nil { 457 infra.Swarming.Hostname = host 458 } else { 459 return errors.New("the build does not run on swarming directly.") 460 } 461 return 462 }) 463 } 464 465 func (bbe *buildbucketEditor) TaskName(name string) { 466 bbe.tweak(func() (err error) { 467 bbe.bb.Name = name 468 return 469 }) 470 } 471 472 func (bbe *buildbucketEditor) Experimental(isExperimental bool) { 473 bbe.tweak(func() error { 474 bbe.Experiments(map[string]bool{buildbucket.ExperimentNonProduction: isExperimental}) 475 return nil 476 }) 477 } 478 479 func (bbe *buildbucketEditor) Experiments(exps map[string]bool) { 480 bbe.tweak(func() error { 481 if len(exps) == 0 { 482 return nil 483 } 484 485 er := bbe.bb.BbagentArgs.Build.Infra.Buildbucket.ExperimentReasons 486 if er == nil { 487 er = make(map[string]bbpb.BuildInfra_Buildbucket_ExperimentReason) 488 bbe.bb.BbagentArgs.Build.Infra.Buildbucket.ExperimentReasons = er 489 } 490 enabled := stringset.NewFromSlice(bbe.bb.BbagentArgs.Build.Input.Experiments...) 491 for k, v := range exps { 492 if k == buildbucket.ExperimentNonProduction { 493 bbe.bb.BbagentArgs.Build.Input.Experimental = v 494 } 495 if v { 496 enabled.Add(k) 497 er[k] = bbpb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED 498 } else { 499 enabled.Del(k) 500 delete(er, k) 501 } 502 } 503 bbe.bb.BbagentArgs.Build.Input.Experiments = enabled.ToSortedSlice() 504 return nil 505 }) 506 } 507 508 func (bbe *buildbucketEditor) PrefixPathEnv(values []string) { 509 if len(values) == 0 { 510 return 511 } 512 513 bbe.tweak(func() error { 514 updatePrefixPathEnv(values, &bbe.bb.EnvPrefixes) 515 return nil 516 }) 517 } 518 519 func (bbe *buildbucketEditor) ClearGerritChanges() { 520 bbe.tweak(func() error { 521 bbe.bb.BbagentArgs.Build.Input.GerritChanges = nil 522 return nil 523 }) 524 } 525 526 func (bbe *buildbucketEditor) AddGerritChange(cl *bbpb.GerritChange) { 527 if cl == nil { 528 return 529 } 530 531 bbe.tweak(func() error { 532 gc := &bbe.bb.BbagentArgs.Build.Input.GerritChanges 533 for _, change := range *gc { 534 if proto.Equal(change, cl) { 535 return nil 536 } 537 } 538 *gc = append(*gc, cl) 539 return nil 540 }) 541 } 542 543 func (bbe *buildbucketEditor) RemoveGerritChange(cl *bbpb.GerritChange) { 544 if cl == nil { 545 return 546 } 547 548 bbe.tweak(func() error { 549 gc := &bbe.bb.BbagentArgs.Build.Input.GerritChanges 550 for idx, change := range *gc { 551 if proto.Equal(change, cl) { 552 *gc = append((*gc)[:idx], (*gc)[idx+1:]...) 553 return nil 554 } 555 } 556 return nil 557 }) 558 } 559 560 func (bbe *buildbucketEditor) GitilesCommit(commit *bbpb.GitilesCommit) { 561 bbe.tweak(func() error { 562 bbe.bb.BbagentArgs.Build.Input.GitilesCommit = commit 563 return nil 564 }) 565 }