go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/schedule_build.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 rpc 16 17 import ( 18 "context" 19 "crypto/sha256" 20 "encoding/json" 21 "fmt" 22 "regexp" 23 "sort" 24 "strings" 25 "time" 26 27 "google.golang.org/grpc/codes" 28 "google.golang.org/protobuf/encoding/protojson" 29 "google.golang.org/protobuf/proto" 30 "google.golang.org/protobuf/types/known/durationpb" 31 "google.golang.org/protobuf/types/known/structpb" 32 33 cipdcommon "go.chromium.org/luci/cipd/common" 34 "go.chromium.org/luci/common/data/rand/mathrand" 35 "go.chromium.org/luci/common/data/sortby" 36 "go.chromium.org/luci/common/data/stringset" 37 "go.chromium.org/luci/common/data/strpair" 38 "go.chromium.org/luci/common/errors" 39 "go.chromium.org/luci/common/logging" 40 "go.chromium.org/luci/common/sync/parallel" 41 "go.chromium.org/luci/gae/service/info" 42 "go.chromium.org/luci/grpc/appstatus" 43 "go.chromium.org/luci/grpc/grpcutil" 44 "go.chromium.org/luci/server/caching" 45 46 bb "go.chromium.org/luci/buildbucket" 47 "go.chromium.org/luci/buildbucket/appengine/common" 48 "go.chromium.org/luci/buildbucket/appengine/internal/buildtoken" 49 "go.chromium.org/luci/buildbucket/appengine/internal/clients" 50 "go.chromium.org/luci/buildbucket/appengine/internal/config" 51 "go.chromium.org/luci/buildbucket/appengine/internal/perm" 52 "go.chromium.org/luci/buildbucket/appengine/model" 53 "go.chromium.org/luci/buildbucket/bbperms" 54 pb "go.chromium.org/luci/buildbucket/proto" 55 "go.chromium.org/luci/buildbucket/protoutil" 56 ) 57 58 // Allow hostnames permitted by 59 // https://www.rfc-editor.org/rfc/rfc1123#page-13. (Note that 60 // the 255 character limit must be seperately applied.) 61 var hostnameRE = regexp.MustCompile(`^[a-z0-9][a-z0-9-]+(\.[a-z0-9-]+)*$`) 62 63 func min(i, j int) int { 64 if i < j { 65 return i 66 } 67 return j 68 } 69 70 // validateExpirationDuration validates the given expiration duration. 71 func validateExpirationDuration(d *durationpb.Duration) error { 72 switch { 73 case d.GetNanos() != 0: 74 return errors.Reason("nanos must not be specified").Err() 75 case d.GetSeconds() < 0: 76 return errors.Reason("seconds must not be negative").Err() 77 case d.GetSeconds()%60 != 0: 78 return errors.Reason("seconds must be a multiple of 60").Err() 79 default: 80 return nil 81 } 82 } 83 84 // validateRequestedDimension validates the requested dimension. 85 func validateRequestedDimension(dim *pb.RequestedDimension) error { 86 var err error 87 switch { 88 case teeErr(validateDimension(dim), &err) != nil: 89 return err 90 case dim.Key == "caches": 91 return errors.Annotate(errors.Reason("caches may only be specified in builder configs (cr-buildbucket.cfg)").Err(), "key").Err() 92 case dim.Key == "pool": 93 return errors.Annotate(errors.Reason("pool may only be specified in builder configs (cr-buildbucket.cfg)").Err(), "key").Err() 94 default: 95 return nil 96 } 97 } 98 99 // validateRequestedDimensions validates the requested dimensions. 100 func validateRequestedDimensions(dims []*pb.RequestedDimension) error { 101 // A dim.key set which contains non-empty dim.value 102 nonEmpty := stringset.New(len(dims)) 103 // A dim.key set which contains empty dim.value 104 empty := stringset.New(len(dims)) 105 for i, dim := range dims { 106 if err := validateRequestedDimension(dim); err != nil { 107 return errors.Annotate(err, "[%d]", i).Err() 108 } 109 110 if dim.GetValue() == "" { 111 if nonEmpty.Has(dim.Key) { 112 return errors.Reason("contain both empty and non-empty value for the same key - %q", dim.Key).Err() 113 } 114 empty.Add(dim.Key) 115 } else { 116 if empty.Has(dim.Key) { 117 return errors.Reason("contain both empty and non-empty value for the same key - %q", dim.Key).Err() 118 } 119 nonEmpty.Add(dim.Key) 120 } 121 } 122 return nil 123 } 124 125 // validateExecutable validates the given executable. 126 func validateExecutable(exe *pb.Executable) error { 127 var err error 128 switch { 129 case exe.GetCipdPackage() != "": 130 return errors.Reason("cipd_package must not be specified").Err() 131 case exe.GetCipdVersion() != "" && teeErr(cipdcommon.ValidateInstanceVersion(exe.CipdVersion), &err) != nil: 132 return errors.Annotate(err, "cipd_version").Err() 133 default: 134 return nil 135 } 136 } 137 138 // validateGerritChange validates a given gerrit change. 139 func validateGerritChange(ch *pb.GerritChange) error { 140 switch { 141 case ch.GetChange() == 0: 142 return errors.Reason("change must be specified").Err() 143 case ch.Host == "": 144 return errors.Reason("host must be specified").Err() 145 case !hostnameRE.MatchString(ch.Host): 146 return errors.Reason("host does not match pattern %q", hostnameRE).Err() 147 case len(ch.Host) > 255: 148 return errors.Reason("host must not exceed 255 characters").Err() 149 case ch.Patchset == 0: 150 return errors.Reason("patchset must be specified").Err() 151 case ch.Project == "": 152 return errors.Reason("project must be specified").Err() 153 default: 154 return nil 155 } 156 } 157 158 // validateGerritChanges validates the given gerrit changes. 159 func validateGerritChanges(changes []*pb.GerritChange) error { 160 for i, ch := range changes { 161 if err := validateGerritChange(ch); err != nil { 162 return errors.Annotate(err, "[%d]", i).Err() 163 } 164 } 165 return nil 166 } 167 168 // validateNotificationConfig validates the given notification config. 169 func validateNotificationConfig(ctx context.Context, n *pb.NotificationConfig) error { 170 switch { 171 case n.GetPubsubTopic() == "": 172 return errors.Reason("pubsub_topic must be specified").Err() 173 case len(n.UserData) > 4096: 174 return errors.Reason("user_data cannot exceed 4096 bytes").Err() 175 } 176 177 // Validate the topic exists and Buildbucket has the publishing permission. 178 cloudProj, topicID, err := clients.ValidatePubSubTopicName(n.PubsubTopic) 179 if err != nil { 180 return errors.Annotate(err, "invalid pubsub_topic %s", n.PubsubTopic).Err() 181 } 182 183 // Check the global cache first to reduce calls to the actual IAM api. 184 cache := caching.GlobalCache(ctx, "has_perm_on_pubsub_callback_topic") 185 if cache == nil { 186 logging.Warningf(ctx, "global has_perm_on_pubsub_callback_topic cache is not found") 187 } 188 switch hasPerm, err := cache.Get(ctx, n.PubsubTopic); { 189 case err == caching.ErrCacheMiss: 190 case err != nil: 191 logging.Warningf(ctx, "failed to check %s from the global cache", n.PubsubTopic) 192 case hasPerm != nil: 193 return nil 194 } 195 196 // Check perm via the IAM api and save into the cache iff BB has the access on 197 // that topic. Why not also caching the bad result? Because users will usually 198 // correct the permission once they receive the bad response and retry again. 199 // Caching the bad result means we have to figure out a way to invalidate the 200 // cached item before it expires. 201 client, err := clients.NewPubsubClient(ctx, cloudProj, "") 202 if err != nil { 203 return errors.Annotate(err, "failed to create a pubsub client").Err() 204 } 205 topic := client.Topic(topicID) 206 switch perms, err := topic.IAM().TestPermissions(ctx, []string{"pubsub.topics.publish"}); { 207 case err != nil: 208 return errors.Annotate(err, "failed to check existence for topic %s", topic).Err() 209 case len(perms) < 1: 210 return errors.Reason("%s@appspot.gserviceaccount.com account doesn't have the 'pubsub.topics.publish' or 'pubsub.topics.get' permission for %s", info.AppID(ctx), topic).Err() 211 default: 212 if err := cache.Set(ctx, n.PubsubTopic, []byte{1}, 10*time.Hour); err != nil { 213 logging.Warningf(ctx, "failed to save into has_perm_on_pubsub_callback_topic cache for %s", n.PubsubTopic) 214 } 215 } 216 return nil 217 } 218 219 // prohibitedProperties is used to prohibit properties from being set (see 220 // validateProperties). Contains slices of path components forming a prohibited 221 // path. For example, to prohibit a property "a.b", add an element ["a", "b"]. 222 var prohibitedProperties = [][]string{ 223 {"$recipe_engine/buildbucket"}, 224 {"$recipe_engine/runtime", "is_experimental"}, 225 {"$recipe_engine/runtime", "is_luci"}, 226 {"branch"}, 227 {"buildbucket"}, 228 {"buildername"}, 229 {"repository"}, 230 } 231 232 // structContains returns whether the struct contains a value at the given path. 233 // An empty slice of path components always returns true. 234 func structContains(s *structpb.Struct, path []string) bool { 235 for _, p := range path { 236 v, ok := s.GetFields()[p] 237 if !ok { 238 return false 239 } 240 s = v.GetStructValue() 241 } 242 return true 243 } 244 245 // validateProperties validates the given properties. 246 func validateProperties(p *structpb.Struct) error { 247 for _, path := range prohibitedProperties { 248 if structContains(p, path) { 249 return errors.Reason("%q must not be specified", strings.Join(path, ".")).Err() 250 } 251 } 252 return nil 253 } 254 255 // validateParent validates the given parent build, if the request contains 256 // a BUILD token. 257 // 258 // If there is no token present in `ctx`, returns (nil, nil). 259 // Incorrect tokens, broken tokens, non-BUILD tokens, missing builds, etc. 260 // all return errors. 261 func validateParent(ctx context.Context) (*model.Build, error) { 262 buildTok, err := getBuildbucketToken(ctx, false) 263 if err == errBadTokenAuth { 264 return nil, nil 265 } 266 267 // NOTE: We pass buildid == 0 here because we are relying on the token itself 268 // to tell us what the parent build ID is. Do not do this in other locations 269 // or they will be suceptible to accepting tokens generated for other builds. 270 tok, err := buildtoken.ParseToTokenBody(ctx, buildTok, 0, pb.TokenBody_BUILD) 271 if err != nil { 272 // We don't return `err` here because it will include the Unauthenticated 273 // gRPC tag, which isn't accurate. 274 return nil, errors.New("invalid parent buildbucket token", grpcutil.InvalidArgumentTag) 275 } 276 277 pBld, err := common.GetBuild(ctx, tok.BuildId) 278 if err != nil { 279 return nil, err 280 } 281 282 if protoutil.IsEnded(pBld.Proto.Status) || protoutil.IsEnded(pBld.Proto.Output.GetStatus()) { 283 return nil, errors.Reason("%d has ended, cannot add child to it", pBld.ID).Err() 284 } 285 286 return pBld, nil 287 } 288 289 // validateSchedule validates the given request. 290 func validateSchedule(ctx context.Context, req *pb.ScheduleBuildRequest, wellKnownExperiments stringset.Set, parent *model.Build) error { 291 var err error 292 switch { 293 case strings.Contains(req.GetRequestId(), "/"): 294 return errors.Reason("request_id cannot contain '/'").Err() 295 case req.GetBuilder() == nil && req.GetTemplateBuildId() == 0: 296 return errors.Reason("builder or template_build_id is required").Err() 297 case req.Builder != nil && teeErr(protoutil.ValidateRequiredBuilderID(req.Builder), &err) != nil: 298 return errors.Annotate(err, "builder").Err() 299 case teeErr(validateRequestedDimensions(req.Dimensions), &err) != nil: 300 return errors.Annotate(err, "dimensions").Err() 301 case teeErr(validateExecutable(req.Exe), &err) != nil: 302 return errors.Annotate(err, "exe").Err() 303 case teeErr(validateGerritChanges(req.GerritChanges), &err) != nil: 304 return errors.Annotate(err, "gerrit_changes").Err() 305 case req.GitilesCommit != nil && teeErr(validateCommitWithRef(req.GitilesCommit), &err) != nil: 306 return errors.Annotate(err, "gitiles_commit").Err() 307 case req.Notify != nil && teeErr(validateNotificationConfig(ctx, req.Notify), &err) != nil: 308 return errors.Annotate(err, "notify").Err() 309 case req.Priority < 0 || req.Priority > 255: 310 return errors.Reason("priority must be in [0, 255]").Err() 311 case req.Properties != nil && teeErr(validateProperties(req.Properties), &err) != nil: 312 return errors.Annotate(err, "properties").Err() 313 case parent == nil && req.CanOutliveParent != pb.Trinary_UNSET: 314 return errors.Reason("can_outlive_parent is specified without parent build token").Err() 315 case teeErr(validateTags(req.Tags, TagNew), &err) != nil: 316 return errors.Annotate(err, "tags").Err() 317 } 318 319 for expName := range req.Experiments { 320 if err := config.ValidateExperimentName(expName, wellKnownExperiments); err != nil { 321 return errors.Annotate(err, "experiment %q", expName).Err() 322 } 323 } 324 325 // TODO(crbug/1042991): Validate Properties. 326 return nil 327 } 328 329 // templateBuildMask enumerates properties to read from template builds. See 330 // scheduleRequestFromTemplate. 331 var templateBuildMask = model.HardcodedBuildMask( 332 "builder", 333 "critical", 334 "exe", 335 "infra.buildbucket.requested_dimensions", 336 "infra.swarming.priority", 337 "input.experimental", 338 "input.gerrit_changes", 339 "input.gitiles_commit", 340 "input.properties", 341 "tags", 342 ) 343 344 func scheduleRequestFromBuildID(ctx context.Context, bID int64, isRetry bool) (*pb.ScheduleBuildRequest, error) { 345 bld, err := common.GetBuild(ctx, bID) 346 if err != nil { 347 return nil, err 348 } 349 if err := perm.HasInBuilder(ctx, bbperms.BuildsGet, bld.Proto.Builder); err != nil { 350 return nil, err 351 } 352 353 b := bld.ToSimpleBuildProto(ctx) 354 355 if isRetry && b.Retriable == pb.Trinary_NO { 356 return nil, appstatus.BadRequest(errors.Reason("build %d is not retriable", bld.ID).Err()) 357 } 358 359 if err := model.LoadBuildDetails(ctx, templateBuildMask, nil, b); err != nil { 360 return nil, err 361 } 362 363 ret := &pb.ScheduleBuildRequest{ 364 Builder: b.Builder, 365 Critical: b.Critical, 366 Exe: b.Exe, 367 GerritChanges: b.Input.GerritChanges, 368 GitilesCommit: b.Input.GitilesCommit, 369 Properties: b.Input.Properties, 370 Tags: b.Tags, 371 Dimensions: b.Infra.GetBuildbucket().GetRequestedDimensions(), 372 Priority: b.Infra.GetSwarming().GetPriority(), 373 Retriable: b.Retriable, 374 } 375 376 ret.Experiments = make(map[string]bool, len(bld.Experiments)) 377 bld.IterExperiments(func(enabled bool, exp string) bool { 378 ret.Experiments[exp] = enabled 379 return true 380 }) 381 return ret, nil 382 } 383 384 // scheduleRequestFromTemplate returns a request with fields populated by the 385 // given template_build_id if there is one. Fields set in the request override 386 // fields populated from the template. Does not modify the incoming request. 387 func scheduleRequestFromTemplate(ctx context.Context, req *pb.ScheduleBuildRequest) (*pb.ScheduleBuildRequest, error) { 388 if req.GetTemplateBuildId() == 0 { 389 return req, nil 390 } 391 392 ret, err := scheduleRequestFromBuildID(ctx, req.TemplateBuildId, true) 393 if err != nil { 394 return nil, err 395 } 396 397 // proto.Merge concatenates repeated fields. Here the desired behavior is replacement, 398 // so clear slices from the return value before merging, if specified in the request. 399 if req.Exe != nil { 400 ret.Exe = nil 401 } 402 if len(req.GerritChanges) > 0 { 403 ret.GerritChanges = nil 404 } 405 if req.Properties != nil { 406 ret.Properties = nil 407 } 408 if len(req.Tags) > 0 { 409 ret.Tags = nil 410 } 411 if len(req.Dimensions) > 0 { 412 ret.Dimensions = nil 413 } 414 proto.Merge(ret, req) 415 ret.TemplateBuildId = 0 416 417 return ret, nil 418 } 419 420 // fetchBuilderConfigs returns the Builder configs referenced by the given 421 // requests in a map of Bucket ID -> Builder name -> *pb.BuilderConfig, 422 // a map of buckets to their shadow buckets and a map of Bucket ID -> *pb.Bucket. 423 // 424 // A single returned error means a global error which applies to every request. 425 // Otherwise, it would be a MultiError where len(MultiError) equals to len(builderIDs). 426 func fetchBuilderConfigs(ctx context.Context, builderIDs []*pb.BuilderID) (map[string]map[string]*pb.BuilderConfig, map[string]*pb.Bucket, map[string]string, error) { 427 merr := make(errors.MultiError, len(builderIDs)) 428 var bcks []*model.Bucket 429 430 // bckCfgs and bldrCfgs use a double-pointer because GetIgnoreMissing will 431 // indirectly overwrite the pointer in the model struct when loading from the 432 // datastore (so, populating Proto and Config fields and using those values 433 // won't help). 434 bckCfgs := map[string]**pb.Bucket{} // Bucket ID -> **pb.Bucket 435 var bldrs []*model.Builder 436 bldrCfgs := map[string]map[string]**pb.BuilderConfig{} // Bucket ID -> Builder name -> **pb.BuilderConfig 437 idxMap := map[string]map[string][]int{} // Bucket ID -> Builder name -> a list of index 438 for i, bldr := range builderIDs { 439 bucket := protoutil.FormatBucketID(bldr.Project, bldr.Bucket) 440 if _, ok := bldrCfgs[bucket]; !ok { 441 bldrCfgs[bucket] = make(map[string]**pb.BuilderConfig) 442 idxMap[bucket] = map[string][]int{} 443 } 444 if _, ok := bldrCfgs[bucket][bldr.Builder]; ok { 445 idxMap[bucket][bldr.Builder] = append(idxMap[bucket][bldr.Builder], i) 446 continue 447 } 448 if _, ok := bckCfgs[bucket]; !ok { 449 b := &model.Bucket{ 450 Parent: model.ProjectKey(ctx, bldr.Project), 451 ID: bldr.Bucket, 452 } 453 bckCfgs[bucket] = &b.Proto 454 bcks = append(bcks, b) 455 } 456 b := &model.Builder{ 457 Parent: model.BucketKey(ctx, bldr.Project, bldr.Bucket), 458 ID: bldr.Builder, 459 } 460 bldrCfgs[bucket][bldr.Builder] = &b.Config 461 bldrs = append(bldrs, b) 462 idxMap[bucket][bldr.Builder] = append(idxMap[bucket][bldr.Builder], i) 463 } 464 465 // Note; this will fill in bckCfgs and bldrCfgs. 466 if err := model.GetIgnoreMissing(ctx, bcks, bldrs); err != nil { 467 return nil, nil, nil, errors.Annotate(err, "failed to fetch entities").Err() 468 } 469 470 dynamicBuckets := map[string]*pb.Bucket{} 471 shadowMap := make(map[string]string) 472 // Check buckets to see if they support dynamically scheduling builds for builders which are not pre-defined. 473 for _, b := range bcks { 474 bucket := protoutil.FormatBucketID(b.Parent.StringID(), b.ID) 475 if b.Proto.GetName() == "" { 476 for _, bldrIdx := range idxMap[bucket] { 477 for idx := range bldrIdx { 478 merr[idx] = appstatus.Errorf(codes.NotFound, "bucket not found: %q", b.ID) 479 } 480 } 481 } else { 482 shadowMap[bucket] = b.Proto.GetShadow() 483 } 484 } 485 for _, b := range bldrs { 486 // Since b.Config isn't a pointer type it will always be non-nil. However, since name is validated 487 // as required, it can be used as a proxy for determining whether the builder config was found or 488 // not. If it's unspecified, the builder wasn't found. Builds for builders which aren't pre-configured 489 // can only be scheduled in buckets which support dynamic builders. 490 if b.Config.GetName() == "" { 491 bucket := protoutil.FormatBucketID(b.Parent.Parent().StringID(), b.Parent.StringID()) 492 // TODO(crbug/1042991): Check if bucket is explicitly configured for dynamic builders. 493 // Currently buckets do not require pre-defined builders iff they have no Swarming config. 494 if (*bckCfgs[bucket]).GetSwarming() == nil { 495 delete(bldrCfgs[bucket], b.ID) 496 if (*bckCfgs[bucket]).GetDynamicBuilderTemplate() != nil { 497 dynamicBuckets[bucket] = *bckCfgs[bucket] 498 } 499 continue 500 } 501 for _, idx := range idxMap[bucket][b.ID] { 502 merr[idx] = appstatus.Errorf(codes.NotFound, "builder not found: %q", b.ID) 503 } 504 } 505 } 506 507 // deref all the pointers. 508 ret := make(map[string]map[string]*pb.BuilderConfig, len(bldrCfgs)) 509 for bucket, builders := range bldrCfgs { 510 m := make(map[string]*pb.BuilderConfig, len(builders)) 511 for builderName, builder := range builders { 512 m[builderName] = *builder 513 } 514 ret[bucket] = m 515 } 516 517 // doesn't contain any errors. 518 if merr.First() == nil { 519 return ret, dynamicBuckets, shadowMap, nil 520 } 521 return ret, dynamicBuckets, shadowMap, merr.AsError() 522 } 523 524 // builderMatches returns whether or not the given builder matches the given 525 // predicate. A match occurs if any regex matches and none of the exclusions 526 // rule the builder out. If there are no regexes, a match always occurs unless 527 // an exclusion rules the builder out. The predicate must be validated. 528 func builderMatches(builder string, pred *pb.BuilderPredicate) bool { 529 // TODO(crbug/1042991): Cache compiled regexes (possibly in internal/config package). 530 for _, r := range pred.GetRegexExclude() { 531 if m, err := regexp.MatchString(fmt.Sprintf("^%s$", r), builder); err == nil && m { 532 return false 533 } 534 } 535 536 if len(pred.GetRegex()) == 0 { 537 return true 538 } 539 for _, r := range pred.Regex { 540 if m, err := regexp.MatchString(fmt.Sprintf("^%s$", r), builder); err == nil && m { 541 return true 542 } 543 } 544 return false 545 } 546 547 // experimentsMatch returns whether or not the given experimentSet matches the 548 // given includeOnExperiment or omitOnExperiment. 549 func experimentsMatch(experimentSet stringset.Set, includeOnExperiment, omitOnExperiment []string) bool { 550 for _, e := range omitOnExperiment { 551 if experimentSet.Has(e) { 552 return false 553 } 554 } 555 556 if len(includeOnExperiment) > 0 { 557 include := false 558 559 for _, e := range includeOnExperiment { 560 if experimentSet.Has(e) { 561 include = true 562 break 563 } 564 } 565 566 if !include { 567 return false 568 } 569 570 } 571 572 return true 573 } 574 575 // setDimensions computes the dimensions from the given request and builder 576 // config, setting them in the proto. Mutates the given *pb.Build. 577 // build.Infra.Swarming must be set (see setInfra). 578 func setDimensions(req *pb.ScheduleBuildRequest, cfg *pb.BuilderConfig, build *pb.Build, isTaskBackend bool) { 579 // Requested dimensions override dimensions specified in the builder config by wiping out all 580 // same-key dimensions (regardless of expiration time) in the builder config. 581 // 582 // For example: 583 // Case 1: 584 // Request contains: ("key", "value 1", 60), ("key", "value 2", 120) 585 // Config contains: ("key", "value 3", 180), ("key", "value 2", 240) 586 // 587 // Then the result is: 588 // ("key", "value 1", 60), ("key", "value 2", 120) 589 // Even though the expiration times didn't conflict and theoretically could have been merged. 590 // 591 // Case 2: 592 // Request contains: ("key", "") 593 // Config contains: ("key", "value 3", 180), ("key", "value 2", 240) 594 // 595 // Then all dimensions(Key == "key") are excluded. 596 597 // If the config contains any reference to the builder dimension, ignore its auto builder dimension setting. 598 seenBuilder := false 599 600 // key -> slice of dimensions (key, value, expiration) with matching keys. 601 dims := make(map[string][]*pb.RequestedDimension) 602 603 // cfg.Dimensions is a slice of strings. Each string has already been validated to match either 604 // <key>:<value> or <exp>:<key>:<value>, where <exp> is an int64 expiration time, <key> is a 605 // non-empty string which can't be parsed as int64, and <value> is a string which may be empty. 606 // <key>:<value> is shorthand for 0:<key>:<value>. An empty <value> means the dimension should be excluded. 607 for _, d := range cfg.GetDimensions() { 608 exp, k, v := config.ParseDimension(d) 609 if k == "builder" { 610 seenBuilder = true 611 } 612 if v == "" { 613 // Omit empty <value>. 614 continue 615 } 616 dim := &pb.RequestedDimension{ 617 Key: k, 618 Value: v, 619 } 620 if exp > 0 { 621 dim.Expiration = &durationpb.Duration{ 622 Seconds: exp, 623 } 624 } 625 dims[k] = append(dims[k], dim) 626 } 627 628 if cfg.GetAutoBuilderDimension() == pb.Toggle_YES && !seenBuilder { 629 dims["builder"] = []*pb.RequestedDimension{ 630 { 631 Key: "builder", 632 Value: cfg.GetName(), 633 }, 634 } 635 } 636 637 // key -> slice of dimensions (key, value, expiration) with matching keys. 638 reqDims := make(map[string][]*pb.RequestedDimension, len(cfg.GetDimensions())) 639 for _, d := range req.GetDimensions() { 640 if d.GetValue() == "" { 641 // Exclude same-key dimensions in the builder config if the dimension 642 // value in the request is empty. 643 delete(dims, d.Key) 644 continue 645 } 646 reqDims[d.Key] = append(reqDims[d.Key], d) 647 } 648 for k, d := range reqDims { 649 dims[k] = d 650 } 651 652 taskDims := make([]*pb.RequestedDimension, 0, len(reqDims)) 653 for _, d := range dims { 654 taskDims = append(taskDims, d...) 655 } 656 sortRequestedDimension(taskDims) 657 if isTaskBackend { 658 build.Infra.Backend.TaskDimensions = taskDims 659 return 660 } 661 build.Infra.Swarming.TaskDimensions = taskDims 662 } 663 664 func sortRequestedDimension(dims []*pb.RequestedDimension) { 665 sort.Slice(dims, sortby.Chain{ 666 // Sort by key then expiration. 667 func(i, j int) bool { return dims[i].Key < dims[j].Key }, 668 func(i, j int) bool { return dims[i].Expiration.GetSeconds() < dims[j].Expiration.GetSeconds() }, 669 }.Use) 670 } 671 672 // setExecutable computes the executable from the given request and builder 673 // config, setting it in the proto. Mutates the given *pb.Build. 674 func setExecutable(req *pb.ScheduleBuildRequest, cfg *pb.BuilderConfig, build *pb.Build) { 675 build.Exe = cfg.GetExe() 676 if build.Exe == nil { 677 build.Exe = &pb.Executable{} 678 } 679 680 if cfg.GetRecipe() != nil { 681 build.Exe.CipdPackage = cfg.Recipe.CipdPackage 682 build.Exe.CipdVersion = cfg.Recipe.CipdVersion 683 if build.Exe.CipdVersion == "" { 684 build.Exe.CipdVersion = "refs/heads/master" 685 } 686 } 687 688 // The request has highest precedence, but may only override CIPD version. 689 if req.GetExe().GetCipdVersion() != "" { 690 build.Exe.CipdVersion = req.Exe.CipdVersion 691 } 692 } 693 694 // activeGlobalExpsForBuilder filters the global experiments, returning the 695 // experiments that apply to this builder, as well as experiments which are 696 // ignored. 697 // 698 // If experiments are known, but don't apply to the builder, then they're 699 // returned in a form where their DefaultValue and MinimumValue are 0. 700 // 701 // Ignored experiments are global experiments which no longer do anything, 702 // and should be removed from the build (even if specified via 703 // ScheduleBuildRequest). 704 func activeGlobalExpsForBuilder(build *pb.Build, globalCfg *pb.SettingsCfg) (active []*pb.ExperimentSettings_Experiment, ignored stringset.Set) { 705 exps := globalCfg.GetExperiment().GetExperiments() 706 if len(exps) == 0 { 707 return nil, nil 708 } 709 710 active = make([]*pb.ExperimentSettings_Experiment, 0, len(exps)) 711 ignored = stringset.New(0) 712 713 bid := protoutil.FormatBuilderID(build.Builder) 714 for _, exp := range exps { 715 if exp.Inactive { 716 ignored.Add(exp.Name) 717 continue 718 } 719 if !builderMatches(bid, exp.Builders) { 720 exp = proto.Clone(exp).(*pb.ExperimentSettings_Experiment) 721 exp.DefaultValue = 0 722 exp.MinimumValue = 0 723 } 724 active = append(active, exp) 725 } 726 727 return 728 } 729 730 // setExperiments computes the experiments from the given request, builder and 731 // global config, setting them in the proto. Mutates the given *pb.Build. 732 // build.Infra.Buildbucket, build.Input and build.Exe must not be nil (see 733 // setInfra, setInput and setExecutable respectively). The request must not set 734 // legacy experiment values (see normalizeSchedule). 735 func setExperiments(ctx context.Context, req *pb.ScheduleBuildRequest, cfg *pb.BuilderConfig, globalCfg *pb.SettingsCfg, build *pb.Build) { 736 globalExps, ignoredExps := activeGlobalExpsForBuilder(build, globalCfg) 737 738 // Set up the dice-rolling apparatus 739 exps := make(map[string]int32, len(cfg.GetExperiments())+len(globalExps)) 740 er := make(map[string]pb.BuildInfra_Buildbucket_ExperimentReason, len(exps)) 741 742 // 1. Populate with defaults 743 for _, exp := range globalExps { 744 exps[exp.Name] = exp.DefaultValue 745 er[exp.Name] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_GLOBAL_DEFAULT 746 } 747 // 2. Overwrite with builder config 748 for name, value := range cfg.GetExperiments() { 749 er[name] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_BUILDER_CONFIG 750 exps[name] = value 751 } 752 // 3. Overwrite with minimum global experiment values 753 for _, exp := range globalExps { 754 if exp.MinimumValue > exps[exp.Name] { 755 er[exp.Name] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_GLOBAL_MINIMUM 756 exps[exp.Name] = exp.MinimumValue 757 } 758 } 759 // 4. Explicit requests have highest precedence 760 for name, enabled := range req.GetExperiments() { 761 er[name] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED 762 if enabled { 763 exps[name] = 100 764 } else { 765 exps[name] = 0 766 } 767 } 768 // 5. Remove all inactive global expirements 769 ignoredExps.Iter(func(expName string) bool { 770 if _, ok := exps[expName]; ok { 771 er[expName] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_GLOBAL_INACTIVE 772 delete(exps, expName) 773 } 774 return true 775 }) 776 777 selections := make(map[string]bool, len(exps)) 778 779 // Finally, roll the dice. We order `exps` here for test determinisim. 780 expNames := make([]string, 0, len(exps)) 781 for exp := range exps { 782 expNames = append(expNames, exp) 783 } 784 sort.Strings(expNames) 785 for _, exp := range expNames { 786 pct := exps[exp] 787 switch { 788 case pct >= 100: 789 selections[exp] = true 790 case pct <= 0: 791 selections[exp] = false 792 default: 793 selections[exp] = mathrand.Int31n(ctx, 100) < pct 794 } 795 } 796 797 // For now, continue to set legacy field values from the experiments. 798 build.Canary = selections[bb.ExperimentBBCanarySoftware] 799 build.Input.Experimental = selections[bb.ExperimentNonProduction] 800 801 // Set experimental values. 802 if len(build.Exe.Cmd) > 0 { 803 // If the user explicitly set Exe, that counts as a builder 804 // configuration. 805 er[bb.ExperimentBBAgent] = pb.BuildInfra_Buildbucket_EXPERIMENT_REASON_BUILDER_CONFIG 806 807 // If they explicitly picked recipes, this experiment is false. 808 // If they explicitly picked luciexe, this experiment is true 809 selections[bb.ExperimentBBAgent] = build.Exe.Cmd[0] != "recipes" 810 } else if selections[bb.ExperimentBBAgent] { 811 // User didn't explicitly set Exe, bbagent was selected 812 build.Exe.Cmd = []string{"luciexe"} 813 } else { 814 // User didn't explicitly set Exe, bbagent was not selected 815 build.Exe.Cmd = []string{"recipes"} 816 } 817 818 for exp, en := range selections { 819 if !en { 820 continue 821 } 822 build.Input.Experiments = append(build.Input.Experiments, exp) 823 } 824 sort.Strings(build.Input.Experiments) 825 826 if len(er) > 0 { 827 build.Infra.Buildbucket.ExperimentReasons = er 828 } 829 830 return 831 } 832 833 // defBuilderCacheTimeout is the default value for WaitForWarmCache in the 834 // pb.BuildInfra_Swarming_CacheEntry whose Name is "builder" (see setInfra). 835 var defBuilderCacheTimeout = durationpb.New(4 * time.Minute) 836 837 // commonCacheToSwarmingCache returns the equivalent 838 // []*pb.BuildInfra_Swarming_CacheEntry for the given []*pb.CacheEntry. 839 func commonCacheToSwarmingCache(cache []*pb.CacheEntry) []*pb.BuildInfra_Swarming_CacheEntry { 840 var swarmingCache []*pb.BuildInfra_Swarming_CacheEntry 841 for _, c := range cache { 842 cacheEntry := &pb.BuildInfra_Swarming_CacheEntry{ 843 EnvVar: c.GetEnvVar(), 844 Name: c.GetName(), 845 Path: c.GetPath(), 846 WaitForWarmCache: c.GetWaitForWarmCache(), 847 } 848 swarmingCache = append(swarmingCache, cacheEntry) 849 } 850 return swarmingCache 851 } 852 853 // builderCacheToCommonCache returns the equivalent 854 // *pb.CacheEntry for the given *pb.BuilderConfig_CacheEntry. 855 func builderCacheToCommonCache(cache *pb.BuilderConfig_CacheEntry) *pb.CacheEntry { 856 if cache == nil { 857 return nil 858 } 859 commonCache := &pb.CacheEntry{ 860 EnvVar: cache.GetEnvVar(), 861 Name: cache.GetName(), 862 Path: cache.GetPath(), 863 } 864 if commonCache.Name == "" { 865 commonCache.Name = commonCache.Path 866 } 867 if cache.WaitForWarmCacheSecs > 0 { 868 commonCache.WaitForWarmCache = &durationpb.Duration{ 869 Seconds: int64(cache.WaitForWarmCacheSecs), 870 } 871 } 872 return commonCache 873 } 874 875 // setInfra computes the infra values from the given request and builder config, 876 // setting them in the proto. Mutates the given *pb.Build. build.Builder must be 877 // set. Does not set build.Infra.Logdog.Prefix, which can only be determined at 878 // creation time. 879 func setInfra(ctx context.Context, req *pb.ScheduleBuildRequest, cfg *pb.BuilderConfig, build *pb.Build, globalCfg *pb.SettingsCfg) { 880 appID := info.AppID(ctx) // e.g. cr-buildbucket 881 build.Infra = &pb.BuildInfra{ 882 Bbagent: &pb.BuildInfra_BBAgent{ 883 CacheDir: "cache", 884 PayloadPath: "kitchen-checkout", 885 }, 886 Buildbucket: &pb.BuildInfra_Buildbucket{ 887 Hostname: fmt.Sprintf("%s.appspot.com", appID), 888 RequestedDimensions: req.GetDimensions(), 889 RequestedProperties: req.GetProperties(), 890 KnownPublicGerritHosts: globalCfg.GetKnownPublicGerritHosts(), 891 BuildNumber: cfg.GetBuildNumbers() == pb.Toggle_YES, 892 }, 893 Logdog: &pb.BuildInfra_LogDog{ 894 Hostname: globalCfg.GetLogdog().GetHostname(), 895 Project: build.Builder.GetProject(), 896 }, 897 Resultdb: &pb.BuildInfra_ResultDB{ 898 Hostname: globalCfg.GetResultdb().GetHostname(), 899 Enable: cfg.GetResultdb().GetEnable(), 900 BqExports: cfg.GetResultdb().GetBqExports(), 901 }, 902 } 903 if cfg.GetRecipe() != nil { 904 build.Infra.Recipe = &pb.BuildInfra_Recipe{ 905 CipdPackage: cfg.Recipe.CipdPackage, 906 Name: cfg.Recipe.Name, 907 } 908 } 909 } 910 911 func setSwarmingOrBackend(ctx context.Context, req *pb.ScheduleBuildRequest, cfg *pb.BuilderConfig, build *pb.Build, globalCfg *pb.SettingsCfg) { 912 experiments := stringset.NewFromSlice(build.GetInput().GetExperiments()...) 913 // constructing common TaskBackend/Swarming task fields 914 priority := int32(cfg.GetPriority()) 915 if priority == 0 { 916 priority = 30 917 } 918 if req.GetPriority() > 0 { 919 priority = req.Priority 920 } 921 922 // Request > experimental > proto precedence. 923 if experiments.Has(bb.ExperimentNonProduction) && req.GetPriority() == 0 { 924 priority = 255 925 } 926 taskServiceAccount := cfg.GetServiceAccount() 927 928 globalCaches := globalCfg.GetSwarming().GetGlobalCaches() 929 taskCaches := make([]*pb.CacheEntry, len(cfg.GetCaches()), len(cfg.GetCaches())+len(globalCaches)) 930 names := stringset.New(len(cfg.GetCaches())) 931 paths := stringset.New(len(cfg.GetCaches())) 932 for i, c := range cfg.GetCaches() { 933 taskCaches[i] = builderCacheToCommonCache(c) 934 names.Add(taskCaches[i].Name) 935 paths.Add(taskCaches[i].Path) 936 } 937 // Requested caches have precedence over global caches. 938 // Apply global caches whose names and paths weren't overridden. 939 for _, c := range globalCaches { 940 if !names.Has(c.GetName()) && !paths.Has(c.GetPath()) { 941 taskCaches = append(taskCaches, builderCacheToCommonCache(c)) 942 } 943 } 944 945 if !paths.Has("builder") { 946 taskCaches = append(taskCaches, &pb.CacheEntry{ 947 Name: fmt.Sprintf("builder_%x_v2", sha256.Sum256([]byte(protoutil.FormatBuilderID(build.Builder)))), 948 Path: "builder", 949 WaitForWarmCache: defBuilderCacheTimeout, 950 }) 951 } 952 953 sort.Slice(taskCaches, func(i, j int) bool { 954 return taskCaches[i].Path < taskCaches[j].Path 955 }) 956 // Need to configure build.Infra for a backend or swarming. 957 isTaskBackend := false 958 backendAltExpIsTrue := experiments.Has(bb.ExperimentBackendAlt) 959 switch { 960 case backendAltExpIsTrue && (cfg.GetBackendAlt() != nil || cfg.GetBackend() != nil): 961 cfgToPass := cfg.GetBackend() 962 if cfg.GetBackendAlt() != nil { 963 cfgToPass = cfg.BackendAlt 964 } 965 setInfraBackend(ctx, globalCfg, build, cfgToPass, taskCaches, taskServiceAccount, priority, req.GetPriority()) 966 isTaskBackend = true 967 case backendAltExpIsTrue: 968 // Derive backend settings using swarming info. 969 // This is a temporary solution for raw swarming -> task backend migration, 970 // which allows Buildbucket to do the migration behind the scene without 971 // any change on builder configs. 972 // TODO(crbug.com/1448926): Remove this after the migration is completed and 973 // all builder configs are updated with backend/backend_alt configs. 974 derivedBackendCfg := deriveBackendCfgFromSwarming(cfg, globalCfg) 975 if derivedBackendCfg != nil { 976 setInfraBackend(ctx, globalCfg, build, derivedBackendCfg, taskCaches, taskServiceAccount, priority, req.GetPriority()) 977 isTaskBackend = true 978 } 979 } 980 if !isTaskBackend { 981 build.Infra.Swarming = &pb.BuildInfra_Swarming{ 982 Caches: commonCacheToSwarmingCache(taskCaches), 983 Hostname: cfg.GetSwarmingHost(), 984 ParentRunId: req.GetSwarming().GetParentRunId(), 985 Priority: priority, 986 TaskServiceAccount: taskServiceAccount, 987 } 988 } 989 990 setDimensions(req, cfg, build, isTaskBackend) 991 } 992 993 func deriveBackendCfgFromSwarming(cfg *pb.BuilderConfig, globalCfg *pb.SettingsCfg) *pb.BuilderConfig_Backend { 994 var target string 995 for host, backend := range globalCfg.SwarmingBackends { 996 if host == cfg.GetSwarmingHost() { 997 target = backend 998 break 999 } 1000 } 1001 if target == "" { 1002 return nil 1003 } 1004 1005 return &pb.BuilderConfig_Backend{ 1006 Target: target, 1007 } 1008 } 1009 1010 // setInput computes the input values from the given request and builder config, 1011 // setting them in the proto. Mutates the given *pb.Build. May panic if the 1012 // builder config is invalid. 1013 func setInput(ctx context.Context, req *pb.ScheduleBuildRequest, cfg *pb.BuilderConfig, build *pb.Build) { 1014 build.Input = &pb.Build_Input{ 1015 Properties: &structpb.Struct{}, 1016 } 1017 1018 if cfg.GetRecipe() != nil { 1019 // TODO(crbug/1042991): Deduplicate property parsing logic with config validation for properties. 1020 build.Input.Properties.Fields = make(map[string]*structpb.Value, len(cfg.Recipe.Properties)+len(cfg.Recipe.PropertiesJ)+1) 1021 for _, prop := range cfg.Recipe.Properties { 1022 k, v := strpair.Parse(prop) 1023 build.Input.Properties.Fields[k] = &structpb.Value{ 1024 Kind: &structpb.Value_StringValue{ 1025 StringValue: v, 1026 }, 1027 } 1028 } 1029 1030 // Values are JSON-encoded strings which need to be unmarshalled to structpb.Struct. 1031 // jsonpb unmarshals dicts to structpb.Struct, but cannot unmarshal directly to 1032 // structpb.Value, so create a dummy dict in order to get the structpb.Value. 1033 // TODO(crbug/1042991): Deduplicate legacy property parsing with buildbucket/cli. 1034 for _, prop := range cfg.Recipe.PropertiesJ { 1035 k, v := strpair.Parse(prop) 1036 s := &structpb.Struct{} 1037 v = fmt.Sprintf("{\"%s\": %s}", k, v) 1038 if err := protojson.Unmarshal([]byte(v), s); err != nil { 1039 // Builder config should have been validated already. 1040 panic(errors.Annotate(err, "error parsing %q", v).Err()) 1041 } 1042 build.Input.Properties.Fields[k] = s.Fields[k] 1043 } 1044 build.Input.Properties.Fields["recipe"] = &structpb.Value{ 1045 Kind: &structpb.Value_StringValue{ 1046 StringValue: cfg.Recipe.Name, 1047 }, 1048 } 1049 } else if cfg.GetProperties() != "" { 1050 if err := protojson.Unmarshal([]byte(cfg.Properties), build.Input.Properties); err != nil { 1051 // Builder config should have been validated already. 1052 panic(errors.Annotate(err, "error unmarshaling builder properties for %q", cfg.GetName()).Err()) 1053 } 1054 } 1055 1056 if build.Input.Properties.Fields == nil { 1057 build.Input.Properties.Fields = make(map[string]*structpb.Value, len(req.GetProperties().GetFields())) 1058 } 1059 1060 allowedOverrides := stringset.NewFromSlice(cfg.GetAllowedPropertyOverrides()...) 1061 anyOverride := allowedOverrides.Has("*") 1062 for k, v := range req.GetProperties().GetFields() { 1063 if build.Input.Properties.Fields[k] != nil && !anyOverride && !allowedOverrides.Has(k) { 1064 logging.Warningf(ctx, "ScheduleBuild: Unpermitted Override for property %q for builder %q (ignored)", k, protoutil.FormatBuilderID(build.Builder)) 1065 } 1066 build.Input.Properties.Fields[k] = v 1067 } 1068 1069 build.Input.GitilesCommit = req.GetGitilesCommit() 1070 build.Input.GerritChanges = req.GetGerritChanges() 1071 } 1072 1073 // setTags computes the tags from the given request, setting them in the proto. 1074 // Mutates the given *pb.Build. 1075 func setTags(req *pb.ScheduleBuildRequest, build *pb.Build, pRunID string) { 1076 tags := protoutil.StringPairMap(req.GetTags()) 1077 if req.GetBuilder() != nil { 1078 tags.Add("builder", req.Builder.Builder) 1079 } 1080 if gc := req.GetGitilesCommit(); gc != nil { 1081 if buildset := protoutil.GitilesBuildSet(gc); buildset != "" { 1082 tags.Add("buildset", buildset) 1083 } 1084 tags.Add("gitiles_ref", gc.Ref) 1085 } 1086 for _, ch := range req.GetGerritChanges() { 1087 tags.Add("buildset", protoutil.GerritBuildSet(ch)) 1088 } 1089 // Make `parent_task_id` a tag if buildbucket tracks the build's parent/child 1090 // relationship. 1091 if len(build.AncestorIds) > 0 { 1092 // TODO(crbug.com/1031205): Remove this to always use the parent build's 1093 // task_id to populate the tag. 1094 if req.GetSwarming().GetParentRunId() != "" { 1095 tags.Add("parent_task_id", req.Swarming.ParentRunId) 1096 } else if pRunID != "" { 1097 tags.Add("parent_task_id", pRunID) 1098 } 1099 } 1100 build.Tags = protoutil.StringPairs(tags) 1101 } 1102 1103 // defGracePeriod is the default value for pb.Build.GracePeriod. 1104 // See setTimeouts. 1105 var defGracePeriod = durationpb.New(30 * time.Second) 1106 1107 // setTimeouts computes the timeouts from the given request and builder config, 1108 // setting them in the proto. Mutates the given *pb.Build. 1109 func setTimeouts(req *pb.ScheduleBuildRequest, cfg *pb.BuilderConfig, build *pb.Build) { 1110 // Timeouts in the request have highest precedence, followed by 1111 // values in the builder config, followed by default values. 1112 switch { 1113 case req.GetExecutionTimeout() != nil: 1114 build.ExecutionTimeout = req.ExecutionTimeout 1115 case cfg.GetExecutionTimeoutSecs() > 0: 1116 build.ExecutionTimeout = &durationpb.Duration{ 1117 Seconds: int64(cfg.ExecutionTimeoutSecs), 1118 } 1119 default: 1120 build.ExecutionTimeout = durationpb.New(config.DefExecutionTimeout) 1121 } 1122 1123 switch { 1124 case req.GetGracePeriod() != nil: 1125 build.GracePeriod = req.GracePeriod 1126 case cfg.GetGracePeriod() != nil: 1127 build.GracePeriod = cfg.GracePeriod 1128 default: 1129 build.GracePeriod = defGracePeriod 1130 } 1131 1132 switch { 1133 case req.GetSchedulingTimeout() != nil: 1134 build.SchedulingTimeout = req.SchedulingTimeout 1135 case cfg.GetExpirationSecs() > 0: 1136 build.SchedulingTimeout = &durationpb.Duration{ 1137 Seconds: int64(cfg.ExpirationSecs), 1138 } 1139 default: 1140 build.SchedulingTimeout = durationpb.New(config.DefSchedulingTimeout) 1141 } 1142 } 1143 1144 // buildFromScheduleRequest returns a build proto created from the given 1145 // request and builder config. Sets fields except those which can only be 1146 // determined at creation time. 1147 func buildFromScheduleRequest(ctx context.Context, req *pb.ScheduleBuildRequest, ancestors []int64, pRunID string, cfg *pb.BuilderConfig, globalCfg *pb.SettingsCfg) (b *pb.Build) { 1148 b = &pb.Build{ 1149 Builder: req.Builder, 1150 Critical: cfg.GetCritical(), 1151 WaitForCapacity: cfg.GetWaitForCapacity() == pb.Trinary_YES, 1152 Retriable: cfg.GetRetriable(), 1153 } 1154 1155 if cfg.GetDescriptionHtml() != "" { 1156 b.BuilderInfo = &pb.Build_BuilderInfo{ 1157 Description: cfg.GetDescriptionHtml(), 1158 } 1159 } 1160 1161 if req.Critical != pb.Trinary_UNSET { 1162 b.Critical = req.Critical 1163 } 1164 1165 if req.Retriable != pb.Trinary_UNSET { 1166 b.Retriable = req.Retriable 1167 } 1168 1169 if len(ancestors) > 0 { 1170 b.AncestorIds = ancestors 1171 // Temporarily accept req.CanOutliveParent to be unset, and treat it 1172 // the same as pb.Trinary_YES. 1173 // This is to prevent breakage due to unmatched timelines of deployments 1174 // (for example recipes rolls and bb CLI rolls). 1175 // TODO(crbug.com/1031205): after the parent tracking feature is stabled, 1176 // we should require req.CanOutliveParent to be set. 1177 b.CanOutliveParent = req.GetCanOutliveParent() != pb.Trinary_NO 1178 } 1179 1180 setExecutable(req, cfg, b) 1181 setInfra(ctx, req, cfg, b, globalCfg) // Requires setExecutable. 1182 setInput(ctx, req, cfg, b) 1183 setTags(req, b, pRunID) 1184 setTimeouts(req, cfg, b) 1185 setExperiments(ctx, req, cfg, globalCfg, b) // Requires setExecutable, setInfra, setInput. 1186 setSwarmingOrBackend(ctx, req, cfg, b, globalCfg) // Requires setExecutable, setInfra, setInput, setExperiments. 1187 if err := setInfraAgent(b, globalCfg); err != nil { // Requires setExecutable, setInfra, setExperiments, setSwarmingOrBackend. 1188 // TODO(crbug.com/1266060) bubble up the error after TaskBackend workflow is ready. 1189 // The current ScheduleBuild doesn't need this info. Swallow it to not interrupt the normal workflow. 1190 logging.Warningf(ctx, "Failed to set build.Infra.Buildbucket.Agent for build %d: %s", b.Id, err) 1191 } 1192 // Sets the Backend.Config CIPD agent related fields only for swarming task backends 1193 if b.Infra.Backend != nil && strings.Contains(b.Infra.Backend.Task.Id.Target, "swarming") { 1194 setInfraBackendConfigAgent(b) // Requires setInfra, setInfraAgent 1195 } 1196 return 1197 } 1198 1199 // setInfraAgent populate the agent info from the given settings. 1200 // Mutates the given *pb.Build. 1201 // The build.Builder, build.Canary, build.Exe build.Infra.Buildbucket 1202 // and one of build.Infra.Swarming or build.Infra.Backend must be set. 1203 func setInfraAgent(build *pb.Build, globalCfg *pb.SettingsCfg) error { 1204 build.Infra.Buildbucket.Agent = &pb.BuildInfra_Buildbucket_Agent{} 1205 experiments := stringset.NewFromSlice(build.GetInput().GetExperiments()...) 1206 builderID := protoutil.FormatBuilderID(build.Builder) 1207 1208 // TODO(crbug.com/1345722) In the future, bbagent will entirely manage the 1209 // user executable payload, which means Buildbucket should not specify the 1210 // payload path. 1211 // We should change the purpose field and use symbolic paths in the input 1212 // like "$exe" and "$agentUtils". 1213 // Reference: https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/3792330/comments/734e18f7_b7f4726d 1214 build.Infra.Buildbucket.Agent.Purposes = map[string]pb.BuildInfra_Buildbucket_Agent_Purpose{ 1215 "kitchen-checkout": pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, 1216 } 1217 1218 setInfraAgentInputData(build, globalCfg, experiments, builderID) 1219 if len(build.Infra.Buildbucket.Agent.Input.Data) > 0 { 1220 setCipdPackagesCache(build) 1221 } 1222 1223 return setInfraAgentSource(build, globalCfg, experiments, builderID) 1224 } 1225 1226 func addInfraAgentInputData(build *pb.Build, builderID, cipdServer, basePath string, experiments stringset.Set, packages []*pb.SwarmingSettings_Package) { 1227 inputData := build.Infra.Buildbucket.Agent.Input.Data 1228 purposes := build.Infra.Buildbucket.Agent.Purposes 1229 for _, p := range packages { 1230 if !builderMatches(builderID, p.Builders) { 1231 continue 1232 } 1233 1234 if !experimentsMatch(experiments, p.GetIncludeOnExperiment(), p.GetOmitOnExperiment()) { 1235 continue 1236 } 1237 1238 path := basePath 1239 if p.Subdir != "" { 1240 path = fmt.Sprintf("%s/%s", path, p.Subdir) 1241 } 1242 if _, ok := inputData[path]; !ok { 1243 inputData[path] = &pb.InputDataRef{ 1244 DataType: &pb.InputDataRef_Cipd{ 1245 Cipd: &pb.InputDataRef_CIPD{ 1246 Server: cipdServer, 1247 }, 1248 }, 1249 OnPath: []string{path, fmt.Sprintf("%s/%s", path, "bin")}, 1250 } 1251 if basePath == BbagentUtilPkgDir { 1252 purposes[path] = pb.BuildInfra_Buildbucket_Agent_PURPOSE_BBAGENT_UTILITY 1253 } 1254 } 1255 1256 inputData[path].GetCipd().Specs = append(inputData[path].GetCipd().Specs, &pb.InputDataRef_CIPD_PkgSpec{ 1257 Package: p.PackageName, 1258 Version: extractCipdVersion(p, build), 1259 }) 1260 } 1261 } 1262 1263 // setInfraAgentInputData populate input cipd info from the given settings. 1264 // In the future, they can be also from per-builder-level or per-request-level. 1265 // Mutates the given *pb.Build. 1266 // The build.Builder, build.Canary, build.Exe, and build.Infra.Buildbucket.Agent must be set 1267 func setInfraAgentInputData(build *pb.Build, globalCfg *pb.SettingsCfg, experiments stringset.Set, builderID string) { 1268 inputData := make(map[string]*pb.InputDataRef) 1269 build.Infra.Buildbucket.Agent.Input = &pb.BuildInfra_Buildbucket_Agent_Input{ 1270 Data: inputData, 1271 } 1272 1273 // add cipd client. 1274 cipdServer := globalCfg.GetCipd().GetServer() 1275 version := globalCfg.GetCipd().GetSource().GetVersion() 1276 if build.Canary && globalCfg.GetCipd().GetSource().GetVersionCanary() != "" { 1277 version = globalCfg.GetCipd().GetSource().GetVersionCanary() 1278 } 1279 if version != "" { 1280 build.Infra.Buildbucket.Agent.Input.CipdSource = map[string]*pb.InputDataRef{ 1281 CipdClientDir: { 1282 DataType: &pb.InputDataRef_Cipd{ 1283 Cipd: &pb.InputDataRef_CIPD{ 1284 Server: cipdServer, 1285 Specs: []*pb.InputDataRef_CIPD_PkgSpec{ 1286 { 1287 Package: globalCfg.GetCipd().GetSource().GetPackageName(), 1288 Version: version, 1289 }, 1290 }, 1291 }, 1292 }, 1293 OnPath: []string{CipdClientDir, fmt.Sprintf("%s/%s", CipdClientDir, "bin")}, 1294 }, 1295 } 1296 build.Infra.Buildbucket.Agent.CipdClientCache = &pb.CacheEntry{ 1297 // Sha the version to make sure the cache name matches 1298 // "^[a-z0-9_]{1,4096}$". 1299 Name: fmt.Sprintf("cipd_client_%x", sha256.Sum256([]byte(version))), 1300 Path: "cipd_client", 1301 } 1302 } 1303 1304 // add user packages. 1305 addInfraAgentInputData(build, builderID, cipdServer, UserPackageDir, experiments, globalCfg.GetSwarming().GetUserPackages()) 1306 1307 // add bbagent utility packages. 1308 addInfraAgentInputData(build, builderID, cipdServer, BbagentUtilPkgDir, experiments, globalCfg.GetSwarming().GetBbagentUtilityPackages()) 1309 1310 if build.Exe.GetCipdPackage() != "" || build.Exe.GetCipdVersion() != "" { 1311 inputData["kitchen-checkout"] = &pb.InputDataRef{ 1312 DataType: &pb.InputDataRef_Cipd{ 1313 Cipd: &pb.InputDataRef_CIPD{ 1314 Server: cipdServer, 1315 Specs: []*pb.InputDataRef_CIPD_PkgSpec{ 1316 { 1317 Package: build.Exe.GetCipdPackage(), 1318 Version: build.Exe.GetCipdVersion(), 1319 }, 1320 }, 1321 }, 1322 }, 1323 } 1324 } 1325 } 1326 1327 // setInfraAgentSource extracts bbagent source info from the given settings. 1328 // In the future, they can be also from per-builder-level or per-request-level. 1329 // Mutates the given *pb.Build. 1330 // The build.Canary, build.Infra.Buildbucket.Agent must be set 1331 func setInfraAgentSource(build *pb.Build, globalCfg *pb.SettingsCfg, experiments stringset.Set, builderID string) error { 1332 bbagent := globalCfg.GetSwarming().GetBbagentPackage() 1333 bbagentAlternatives := make([]*pb.SwarmingSettings_Package, 0, len(globalCfg.GetSwarming().GetAlternativeAgentPackages())) 1334 for _, p := range globalCfg.GetSwarming().GetAlternativeAgentPackages() { 1335 if !builderMatches(builderID, p.Builders) { 1336 continue 1337 } 1338 1339 if !experimentsMatch(experiments, p.GetIncludeOnExperiment(), p.GetOmitOnExperiment()) { 1340 continue 1341 } 1342 1343 bbagentAlternatives = append(bbagentAlternatives, p) 1344 } 1345 if len(bbagentAlternatives) > 1 { 1346 return errors.Reason("cannot decide buildbucket agent source").Err() 1347 } 1348 if len(bbagentAlternatives) == 1 { 1349 bbagent = bbagentAlternatives[0] 1350 } 1351 if bbagent == nil { 1352 return nil 1353 } 1354 1355 if !strings.HasSuffix(bbagent.PackageName, "/${platform}") { 1356 return errors.New("bad settings: bbagent package name must end with '/${platform}'") 1357 } 1358 cipdHost := globalCfg.GetCipd().GetServer() 1359 build.Infra.Buildbucket.Agent.Source = &pb.BuildInfra_Buildbucket_Agent_Source{ 1360 DataType: &pb.BuildInfra_Buildbucket_Agent_Source_Cipd{ 1361 Cipd: &pb.BuildInfra_Buildbucket_Agent_Source_CIPD{ 1362 Package: bbagent.PackageName, 1363 Version: extractCipdVersion(bbagent, build), 1364 Server: cipdHost, 1365 }, 1366 }, 1367 } 1368 return nil 1369 } 1370 1371 // setInfraBackendConfigAgent extracts bbagent source info from the build proto. 1372 // Mutates the given *pb.Build. 1373 // The build.Infra.Buildbucket.Agent must be set 1374 func setInfraBackendConfigAgent(b *pb.Build) { 1375 agentSource := b.Infra.Buildbucket.GetAgent().GetSource() 1376 b.Infra.Backend.Config.Fields["agent_binary_cipd_pkg"] = structpb.NewStringValue(agentSource.GetCipd().Package) 1377 b.Infra.Backend.Config.Fields["agent_binary_cipd_vers"] = structpb.NewStringValue(agentSource.GetCipd().Version) 1378 b.Infra.Backend.Config.Fields["agent_binary_cipd_server"] = structpb.NewStringValue(agentSource.GetCipd().Server) 1379 // TODO(crbug.com/1420443): Remove this harcoding and use 1380 // globalCfg.GetSwarming().GetBbagentPackage().binary_agent_name. 1381 b.Infra.Backend.Config.Fields["agent_binary_cipd_filename"] = structpb.NewStringValue("bbagent${EXECUTABLE_SUFFIX}") 1382 } 1383 1384 func setInfraBackend(ctx context.Context, globalCfg *pb.SettingsCfg, build *pb.Build, backend *pb.BuilderConfig_Backend, taskCaches []*pb.CacheEntry, taskServiceAccount string, priority, reqPriority int32) { 1385 config := &structpb.Struct{} 1386 if backend.GetConfigJson() != "" { // bypass empty config_json 1387 err := json.Unmarshal([]byte(backend.ConfigJson), config) 1388 if err != nil { 1389 logging.Warningf(ctx, err.Error()) 1390 } 1391 } 1392 if config.GetFields() == nil { 1393 config.Fields = make(map[string]*structpb.Value) 1394 } 1395 1396 if config.Fields["service_account"].GetStringValue() == "" && taskServiceAccount != "" { 1397 config.Fields["service_account"] = structpb.NewStringValue(taskServiceAccount) 1398 } 1399 1400 // If request has a priority, use that 1401 // else if backend config_json did not have a priority 1402 // we use the builder one (or value 30 if builder was not set) 1403 if config.Fields["priority"].GetNumberValue() == 0 || reqPriority > 0 { 1404 config.Fields["priority"] = structpb.NewNumberValue(float64(priority)) 1405 } 1406 hostname, err := clients.ComputeHostnameFromTarget(backend.GetTarget(), globalCfg) 1407 if err != nil { 1408 logging.Warningf(ctx, err.Error()) 1409 } 1410 1411 build.Infra.Backend = &pb.BuildInfra_Backend{ 1412 Caches: taskCaches, 1413 Config: config, 1414 Task: &pb.Task{ 1415 Id: &pb.TaskID{ 1416 Target: backend.GetTarget(), 1417 }, 1418 UpdateId: 0, 1419 }, 1420 Hostname: hostname, 1421 } 1422 } 1423 1424 // setExperimentsFromProto sets experiments in the model (see model/build.go). 1425 // build.Proto.Input.Experiments and 1426 // build.Proto.Infra.Buildbucket.ExperimentReasons must be set (see setExperiments). 1427 func setExperimentsFromProto(build *model.Build) { 1428 setExps := stringset.NewFromSlice(build.Proto.Input.Experiments...) 1429 for exp := range build.Proto.Infra.Buildbucket.ExperimentReasons { 1430 if !setExps.Has(exp) { 1431 build.Experiments = append(build.Experiments, fmt.Sprintf("-%s", exp)) 1432 } 1433 } 1434 for _, exp := range build.Proto.Input.Experiments { 1435 build.Experiments = append(build.Experiments, fmt.Sprintf("+%s", exp)) 1436 } 1437 sort.Strings(build.Experiments) 1438 1439 build.Canary = build.Proto.Canary 1440 build.Experimental = build.Proto.Input.Experimental 1441 } 1442 1443 func getParentInfo(ctx context.Context, pBld *model.Build) (ancestors []int64, pRunID string, err error) { 1444 switch { 1445 case pBld == nil: 1446 ancestors = make([]int64, 0) 1447 case len(pBld.AncestorIds) > 0: 1448 ancestors = append(pBld.AncestorIds, pBld.ID) 1449 default: 1450 ancestors = append(ancestors, pBld.ID) 1451 } 1452 1453 if pBld != nil { 1454 parentBuildMask := model.HardcodedBuildMask("infra.swarming.task_id") 1455 pBuild := pBld.ToSimpleBuildProto(ctx) 1456 if err = model.LoadBuildDetails(ctx, parentBuildMask, nil, pBuild); err != nil { 1457 return 1458 } 1459 1460 pRunID = pBuild.GetInfra().GetSwarming().GetTaskId() 1461 if pRunID != "" { 1462 pRunID = pRunID[:len(pRunID)-1] + "1" 1463 } 1464 } 1465 return 1466 } 1467 1468 // getShadowBuckets gets the shadow buckets. 1469 // 1470 // For the requests with `ShadowInput`, the build should be scheduled in the 1471 // shadow bucket of the requested bucket. So we need to get the shadow buckets 1472 // for validation. 1473 func getShadowBuckets(ctx context.Context, reqs []*pb.ScheduleBuildRequest) (map[string]string, error) { 1474 bcksWithShadow := stringset.New(0) 1475 var buckets []*model.Bucket 1476 for _, req := range reqs { 1477 if req.GetShadowInput() == nil { 1478 continue 1479 } 1480 k := protoutil.FormatBucketID(req.Builder.Project, req.Builder.Bucket) 1481 if bcksWithShadow.Add(k) { 1482 buckets = append(buckets, &model.Bucket{ 1483 Parent: model.ProjectKey(ctx, req.Builder.Project), 1484 ID: req.Builder.Bucket, 1485 }) 1486 } 1487 } 1488 if len(bcksWithShadow) == 0 { 1489 return nil, nil 1490 } 1491 1492 if err := model.GetIgnoreMissing(ctx, buckets); err != nil { 1493 return nil, errors.Annotate(err, "failed to fetch bucket entities").Err() 1494 } 1495 1496 shadows := make(map[string]string) 1497 for _, b := range buckets { 1498 if b == nil { 1499 continue 1500 } 1501 k := protoutil.FormatBucketID(b.Parent.StringID(), b.ID) 1502 shadows[k] = b.Proto.GetShadow() 1503 } 1504 return shadows, nil 1505 } 1506 1507 // scheduleBuilds handles requests to schedule builds. Requests must be validated and authorized. 1508 // The length of returned builds always equal to len(reqs). 1509 // A single returned error means a global error which applies to every request. 1510 // Otherwise, it would be a MultiError where len(MultiError) equals to len(reqs). 1511 func scheduleBuilds(ctx context.Context, globalCfg *pb.SettingsCfg, reqs ...*pb.ScheduleBuildRequest) ([]*model.Build, error) { 1512 if len(reqs) == 0 { 1513 return []*model.Build{}, nil 1514 } 1515 1516 dryRun := reqs[0].DryRun 1517 for _, req := range reqs { 1518 if req.DryRun != dryRun { 1519 return nil, appstatus.BadRequest(errors.Reason("all requests must have the same dry_run value").Err()) 1520 } 1521 } 1522 1523 merr := make(errors.MultiError, len(reqs)) 1524 // Bucket -> Builder -> *pb.BuilderConfig. 1525 bldrIDs := make([]*pb.BuilderID, 0, len(reqs)) 1526 for _, req := range reqs { 1527 bldrIDs = append(bldrIDs, req.Builder) 1528 } 1529 cfgs, dynamicBuckets, shadowMap, err := fetchBuilderConfigs(ctx, bldrIDs) 1530 if me, ok := err.(errors.MultiError); ok { 1531 merr = mergeErrs(merr, me, "error fetching builders", func(i int) int { return i }) 1532 } else if err != nil { 1533 return nil, err 1534 } 1535 1536 validReq, idxMapBlds := getValidReqs(reqs, merr) 1537 blds := make([]*model.Build, len(validReq)) 1538 1539 pBld, err := validateParent(ctx) 1540 if err != nil { 1541 return nil, err 1542 } 1543 1544 ancestors, pRunID, err := getParentInfo(ctx, pBld) 1545 if err != nil { 1546 return nil, err 1547 } 1548 1549 var pInfra *model.BuildInfra 1550 for i := range blds { 1551 origI := idxMapBlds[i] 1552 bucket := fmt.Sprintf("%s/%s", validReq[i].Builder.Project, validReq[i].Builder.Bucket) 1553 cfg := cfgs[bucket][validReq[i].Builder.Builder] 1554 inDynamicBucket := false 1555 if bkt, ok := dynamicBuckets[bucket]; ok { 1556 inDynamicBucket = true 1557 cfg = bkt.GetDynamicBuilderTemplate().GetTemplate() 1558 } 1559 1560 var build *pb.Build 1561 if reqs[origI].ShadowInput != nil { 1562 // Schedule a build with shadow info. 1563 if shadowMap[bucket] == "" || shadowMap[bucket] == validReq[i].Builder.Bucket { 1564 // Scheduling a shadow build in the original bucket is prohibited. 1565 // In theory this part of code should not be reached, since validateScheduleBuild 1566 // has checked. 1567 // But still check here just in case a builder config happened to be 1568 // updated between validateScheduleBuild and here. 1569 merr[origI] = errors.Reason("scheduling a shadow build in the original bucket is not allowed").Err() 1570 blds[i] = nil 1571 continue 1572 } 1573 // Schedule a build with shadow info. 1574 build = scheduleShadowBuild(ctx, reqs[origI], ancestors, shadowMap[bucket], globalCfg, cfg) 1575 if pBld != nil { 1576 if pInfra == nil { 1577 entities, err := common.GetBuildEntities(ctx, pBld.ID, model.BuildInfraKind) 1578 if err != nil { 1579 merr[origI] = errors.Reason("failed to get BuildInfra for build %d", pBld.ID).Err() 1580 blds[i] = nil 1581 continue 1582 } 1583 pInfra = entities[0].(*model.BuildInfra) 1584 } 1585 // Inherit agent input and agent source from the parent build. 1586 build.Infra.Buildbucket.Agent.Input = pInfra.Proto.Buildbucket.Agent.Input 1587 build.Infra.Buildbucket.Agent.Source = pInfra.Proto.Buildbucket.Agent.Source 1588 build.Exe = pBld.Proto.Exe 1589 if len(build.Infra.Buildbucket.Agent.Input.Data) > 0 { 1590 setCipdPackagesCache(build) 1591 } 1592 } 1593 } else { 1594 // TODO(crbug.com/1042991): Parallelize build creation from requests if necessary. 1595 build = buildFromScheduleRequest(ctx, reqs[origI], ancestors, pRunID, cfg, globalCfg) 1596 } 1597 1598 blds[i] = &model.Build{ 1599 Proto: build, 1600 } 1601 1602 setExperimentsFromProto(blds[i]) 1603 blds[i].IsLuci = cfg != nil || inDynamicBucket 1604 blds[i].PubSubCallback.Topic = validReq[i].GetNotify().GetPubsubTopic() 1605 blds[i].PubSubCallback.UserData = validReq[i].GetNotify().GetUserData() 1606 // Tags are stored in the outer struct (see model/build.go). 1607 tags := protoutil.StringPairMap(blds[i].Proto.Tags).Format() 1608 tags = stringset.NewFromSlice(tags...).ToSlice() // Deduplicate tags. 1609 sort.Strings(tags) 1610 blds[i].Tags = tags 1611 1612 exp := make(map[int64]struct{}) 1613 for _, d := range blds[i].Proto.Infra.GetSwarming().GetTaskDimensions() { 1614 exp[d.Expiration.GetSeconds()] = struct{}{} 1615 } 1616 if len(exp) > 6 { 1617 merr[origI] = appstatus.BadRequest(errors.Reason("build %d contains more than 6 unique expirations", i).Err()) 1618 continue 1619 } 1620 } 1621 if dryRun { 1622 if merr.First() == nil { 1623 return blds, nil 1624 } 1625 return blds, merr 1626 } 1627 1628 reqIDs := make([]string, 0, len(reqs)) 1629 for _, req := range reqs { 1630 reqIDs = append(reqIDs, req.RequestId) 1631 } 1632 bc := &buildCreator{ 1633 blds: blds, 1634 idxMapBldToReq: idxMapBlds, 1635 reqIDs: reqIDs, 1636 merr: merr, 1637 } 1638 return bc.createBuilds(ctx) 1639 } 1640 1641 // normalizeSchedule converts deprecated fields to non-deprecated ones. 1642 // 1643 // In particular, this currently converts the Canary and Experimental fields to 1644 // the non-deprecated Experiments field. 1645 func normalizeSchedule(req *pb.ScheduleBuildRequest) { 1646 if req.Experiments == nil { 1647 req.Experiments = map[string]bool{} 1648 } 1649 1650 if _, has := req.Experiments[bb.ExperimentBBCanarySoftware]; !has { 1651 if req.Canary == pb.Trinary_YES { 1652 req.Experiments[bb.ExperimentBBCanarySoftware] = true 1653 } else if req.Canary == pb.Trinary_NO { 1654 req.Experiments[bb.ExperimentBBCanarySoftware] = false 1655 } 1656 req.Canary = pb.Trinary_UNSET 1657 } 1658 1659 if _, has := req.Experiments[bb.ExperimentNonProduction]; !has { 1660 if req.Experimental == pb.Trinary_YES { 1661 req.Experiments[bb.ExperimentNonProduction] = true 1662 } else if req.Experimental == pb.Trinary_NO { 1663 req.Experiments[bb.ExperimentNonProduction] = false 1664 } 1665 req.Experimental = pb.Trinary_UNSET 1666 } 1667 } 1668 1669 // validateScheduleBuild validates and authorizes the given request, returning 1670 // a normalized version of the request and field mask. 1671 func validateScheduleBuild(ctx context.Context, wellKnownExperiments stringset.Set, req *pb.ScheduleBuildRequest, parent *model.Build, shadowBuckets map[string]string) (*pb.ScheduleBuildRequest, *model.BuildMask, error) { 1672 var err error 1673 if err = validateSchedule(ctx, req, wellKnownExperiments, parent); err != nil { 1674 return nil, nil, appstatus.BadRequest(err) 1675 } 1676 normalizeSchedule(req) 1677 1678 m, err := model.NewBuildMask("", req.Fields, req.Mask) 1679 if err != nil { 1680 return nil, nil, appstatus.BadRequest(errors.Annotate(err, "invalid mask").Err()) 1681 } 1682 1683 if req, err = scheduleRequestFromTemplate(ctx, req); err != nil { 1684 return nil, nil, err 1685 } 1686 1687 bkt := req.Builder.Bucket 1688 if req.GetShadowInput() != nil { 1689 k := protoutil.FormatBucketID(req.Builder.Project, req.Builder.Bucket) 1690 shadow := shadowBuckets[k] 1691 if shadow == "" || shadow == req.Builder.Bucket { 1692 return nil, nil, appstatus.BadRequest(errors.Reason("scheduling a shadow build in the original bucket is not allowed").Err()) 1693 } 1694 bkt = shadow 1695 } 1696 1697 if err = perm.HasInBucket(ctx, bbperms.BuildsAdd, req.Builder.Project, bkt); err != nil { 1698 return nil, nil, err 1699 } 1700 return req, m, nil 1701 } 1702 1703 // ScheduleBuild handles a request to schedule a build. Implements pb.BuildsServer. 1704 func (*Builds) ScheduleBuild(ctx context.Context, req *pb.ScheduleBuildRequest) (*pb.Build, error) { 1705 globalCfg, err := config.GetSettingsCfg(ctx) 1706 if err != nil { 1707 return nil, errors.Annotate(err, "error fetching service config").Err() 1708 } 1709 wellKnownExperiments := protoutil.WellKnownExperiments(globalCfg) 1710 1711 pBld, err := validateParent(ctx) 1712 if err != nil { 1713 return nil, err 1714 } 1715 1716 // get shadow buckets. 1717 shadowBuckets, err := getShadowBuckets(ctx, []*pb.ScheduleBuildRequest{req}) 1718 if err != nil { 1719 return nil, errors.Annotate(err, "error in getting shadow buckets").Err() 1720 } 1721 1722 req, m, err := validateScheduleBuild(ctx, wellKnownExperiments, req, pBld, shadowBuckets) 1723 if err != nil { 1724 return nil, err 1725 } 1726 1727 blds, err := scheduleBuilds(ctx, globalCfg, req) 1728 if err != nil { 1729 if merr, ok := err.(errors.MultiError); ok { 1730 return nil, merr.First() 1731 } 1732 return nil, err 1733 } 1734 if req.DryRun { 1735 // Dry run build is not saved in datastore, return the proto right away. 1736 return blds[0].Proto, nil 1737 } 1738 1739 // No need to redact the response here, because we're effectively just sending 1740 // the caller's inputs back to them. 1741 return blds[0].ToProto(ctx, m, nil) 1742 } 1743 1744 // scheduleBuilds handles requests to schedule builds. 1745 // The length of returned builds and errors (if any) always equal to the len(reqs). 1746 // The returned error type is always MultiError. 1747 func (*Builds) scheduleBuilds(ctx context.Context, globalCfg *pb.SettingsCfg, reqs []*pb.ScheduleBuildRequest) ([]*pb.Build, errors.MultiError) { 1748 // The ith error is the error associated with the ith request. 1749 merr := make(errors.MultiError, len(reqs)) 1750 // The ith mask is the field mask derived from the ith request. 1751 masks := make([]*model.BuildMask, len(reqs)) 1752 wellKnownExperiments := protoutil.WellKnownExperiments(globalCfg) 1753 1754 errorInBatch := func(err error, attach func(error) error) errors.MultiError { 1755 for i, e := range merr { 1756 if e == nil { 1757 merr[i] = attach(err) 1758 } 1759 } 1760 return merr 1761 } 1762 1763 // Validate parent. 1764 pBld, err := validateParent(ctx) 1765 if err != nil { 1766 return nil, errorInBatch(err, func(err error) error { 1767 return appstatus.BadRequest(errors.Annotate(err, "error in schedule batch").Err()) 1768 }) 1769 } 1770 1771 // get shadow buckets. 1772 shadowBuckets, err := getShadowBuckets(ctx, reqs) 1773 if err != nil { 1774 return nil, errorInBatch(err, func(err error) error { 1775 return appstatus.BadRequest(errors.Annotate(err, "error in schedule batch").Err()) 1776 }) 1777 } 1778 1779 // Validate requests. 1780 _ = parallel.WorkPool(min(64, len(reqs)), func(work chan<- func() error) { 1781 for i, req := range reqs { 1782 i := i 1783 req := req 1784 work <- func() error { 1785 reqs[i], masks[i], merr[i] = validateScheduleBuild(ctx, wellKnownExperiments, req, pBld, shadowBuckets) 1786 return nil 1787 } 1788 } 1789 }) 1790 1791 validReqs, idxMapValidReqs := getValidReqs(reqs, merr) 1792 // Non-MultiError error should apply to every item and fail all requests. 1793 blds, err := scheduleBuilds(ctx, globalCfg, validReqs...) 1794 if err != nil { 1795 if me, ok := err.(errors.MultiError); ok { 1796 merr = mergeErrs(merr, me, "", func(i int) int { return idxMapValidReqs[i] }) 1797 } else { 1798 return nil, errorInBatch(err, func(err error) error { 1799 if _, isAppStatusErr := appstatus.Get(err); isAppStatusErr { 1800 return err 1801 } else { 1802 return appstatus.Errorf(codes.Internal, "error in schedule batch: %s", err) 1803 } 1804 }) 1805 } 1806 } 1807 1808 ret := make([]*pb.Build, len(blds)) 1809 _ = parallel.WorkPool(min(64, len(blds)), func(work chan<- func() error) { 1810 for i, bld := range blds { 1811 if bld == nil { 1812 continue 1813 } 1814 i := i 1815 origI := idxMapValidReqs[i] 1816 bld := bld 1817 work <- func() error { 1818 // Note: We don't redact the Build response here because we expect any user with 1819 // BuildsAdd permission should also have BuildsGet. 1820 // TODO(crbug/1042991): Don't re-read freshly written entities (see ToProto). 1821 ret[i], merr[origI] = bld.ToProto(ctx, masks[origI], nil) 1822 return nil 1823 } 1824 } 1825 }) 1826 1827 if merr.First() == nil { 1828 return ret, nil 1829 } 1830 origRet := make([]*pb.Build, len(reqs)) 1831 for i, origI := range idxMapValidReqs { 1832 if merr[origI] == nil { 1833 origRet[origI] = ret[i] 1834 } 1835 } 1836 return origRet, merr 1837 } 1838 1839 // mergeErrs merges errs into origErrs according to the idxMapper. 1840 func mergeErrs(origErrs, errs errors.MultiError, reason string, idxMapper func(int) int) errors.MultiError { 1841 for i, err := range errs { 1842 if err != nil { 1843 origErrs[idxMapper(i)] = errors.Annotate(err, reason).Err() 1844 } 1845 } 1846 return origErrs 1847 } 1848 1849 // getValidReqs returns a list of valid ScheduleBuildRequest where its corresponding error is nil, 1850 // as well as an index map where idxMap[returnedIndex] == originalIndex. 1851 func getValidReqs(reqs []*pb.ScheduleBuildRequest, errs errors.MultiError) ([]*pb.ScheduleBuildRequest, []int) { 1852 if len(reqs) != len(errs) { 1853 panic("The length of reqs and the length of errs must be the same.") 1854 } 1855 var validReqs []*pb.ScheduleBuildRequest 1856 var idxMap []int 1857 for i, req := range reqs { 1858 if errs[i] == nil { 1859 idxMap = append(idxMap, i) 1860 validReqs = append(validReqs, req) 1861 } 1862 } 1863 return validReqs, idxMap 1864 } 1865 1866 func extractCipdVersion(p *pb.SwarmingSettings_Package, b *pb.Build) string { 1867 if b.Canary && p.VersionCanary != "" { 1868 return p.VersionCanary 1869 } 1870 return p.Version 1871 } 1872 1873 // setCipdPackagesCache sets the named cache for bbagent downloaded cipd packages. 1874 // One of build.Infra.Swarming and build.Infra.Backend must be set. 1875 func setCipdPackagesCache(build *pb.Build) { 1876 var taskServiceAccount string 1877 if build.Infra.Swarming != nil { 1878 taskServiceAccount = build.Infra.Swarming.TaskServiceAccount 1879 } else if build.Infra.Backend.GetConfig() != nil { 1880 taskServiceAccount = build.Infra.Backend.Config.Fields["service_account"].GetStringValue() 1881 } 1882 build.Infra.Buildbucket.Agent.CipdPackagesCache = &pb.CacheEntry{ 1883 Name: fmt.Sprintf("cipd_cache_%x", sha256.Sum256([]byte(taskServiceAccount))), 1884 Path: "cipd_cache", 1885 } 1886 }