go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/create_build.go (about) 1 // Copyright 2022 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 "fmt" 20 "regexp" 21 "sort" 22 "strings" 23 "time" 24 25 "github.com/google/uuid" 26 "google.golang.org/genproto/googleapis/api/annotations" 27 "google.golang.org/grpc/codes" 28 "google.golang.org/protobuf/proto" 29 "google.golang.org/protobuf/reflect/protoreflect" 30 "google.golang.org/protobuf/types/descriptorpb" 31 "google.golang.org/protobuf/types/known/structpb" 32 "google.golang.org/protobuf/types/known/timestamppb" 33 34 cipdCommon "go.chromium.org/luci/cipd/common" 35 "go.chromium.org/luci/common/clock" 36 "go.chromium.org/luci/common/data/stringset" 37 "go.chromium.org/luci/common/errors" 38 "go.chromium.org/luci/common/logging" 39 "go.chromium.org/luci/common/proto/protowalk" 40 "go.chromium.org/luci/common/sync/parallel" 41 "go.chromium.org/luci/gae/service/datastore" 42 "go.chromium.org/luci/gae/service/info" 43 "go.chromium.org/luci/grpc/appstatus" 44 "go.chromium.org/luci/server/auth" 45 46 bb "go.chromium.org/luci/buildbucket" 47 "go.chromium.org/luci/buildbucket/appengine/internal/buildid" 48 "go.chromium.org/luci/buildbucket/appengine/internal/config" 49 "go.chromium.org/luci/buildbucket/appengine/internal/metrics" 50 "go.chromium.org/luci/buildbucket/appengine/internal/perm" 51 "go.chromium.org/luci/buildbucket/appengine/internal/resultdb" 52 "go.chromium.org/luci/buildbucket/appengine/internal/search" 53 "go.chromium.org/luci/buildbucket/appengine/model" 54 "go.chromium.org/luci/buildbucket/appengine/tasks" 55 taskdefs "go.chromium.org/luci/buildbucket/appengine/tasks/defs" 56 "go.chromium.org/luci/buildbucket/bbperms" 57 pb "go.chromium.org/luci/buildbucket/proto" 58 "go.chromium.org/luci/buildbucket/protoutil" 59 ) 60 61 var casInstanceRe = regexp.MustCompile(`^projects/[^/]*/instances/[^/]*$`) 62 63 type CreateBuildChecker struct{} 64 65 var _ protowalk.FieldProcessor = (*CreateBuildChecker)(nil) 66 67 func (*CreateBuildChecker) Process(field protoreflect.FieldDescriptor, msg protoreflect.Message) (data protowalk.ResultData, applied bool) { 68 cbfb := proto.GetExtension(field.Options().(*descriptorpb.FieldOptions), pb.E_CreateBuildFieldOption).(*pb.CreateBuildFieldOption) 69 switch cbfb.FieldBehavior { 70 case annotations.FieldBehavior_OUTPUT_ONLY: 71 msg.Clear(field) 72 return protowalk.ResultData{Message: "cleared OUTPUT_ONLY field"}, true 73 case annotations.FieldBehavior_REQUIRED: 74 return protowalk.ResultData{Message: "required", IsErr: true}, true 75 default: 76 panic("unsupported field behavior") 77 } 78 } 79 80 func init() { 81 protowalk.RegisterFieldProcessor(&CreateBuildChecker{}, func(field protoreflect.FieldDescriptor) protowalk.ProcessAttr { 82 if fo := field.Options().(*descriptorpb.FieldOptions); fo != nil { 83 if cbfb := proto.GetExtension(fo, pb.E_CreateBuildFieldOption).(*pb.CreateBuildFieldOption); cbfb != nil { 84 switch cbfb.FieldBehavior { 85 case annotations.FieldBehavior_OUTPUT_ONLY: 86 return protowalk.ProcessIfSet 87 case annotations.FieldBehavior_REQUIRED: 88 return protowalk.ProcessIfUnset 89 default: 90 panic("unsupported field behavior") 91 } 92 } 93 } 94 return protowalk.ProcessNever 95 }) 96 } 97 98 func validateBucketConstraints(ctx context.Context, b *pb.Build) error { 99 bck := &model.Bucket{ 100 Parent: model.ProjectKey(ctx, b.Builder.Project), 101 ID: b.Builder.Bucket, 102 } 103 bckStr := fmt.Sprintf("%s:%s", b.Builder.Project, b.Builder.Bucket) 104 if err := datastore.Get(ctx, bck); err != nil { 105 return errors.Annotate(err, "failed to fetch bucket config %s", bckStr).Err() 106 } 107 108 constraints := bck.Proto.GetConstraints() 109 if constraints == nil { 110 return errors.Reason("constraints for %s not found", bckStr).Err() 111 } 112 113 // want to return early if swarming is not set and backend is set. 114 if b.GetInfra().GetSwarming() == nil { 115 return nil 116 } 117 allowedPools := stringset.NewFromSlice(constraints.GetPools()...) 118 allowedSAs := stringset.NewFromSlice(constraints.GetServiceAccounts()...) 119 poolAllowed := false 120 var pool string 121 for _, dim := range b.GetInfra().GetSwarming().GetTaskDimensions() { 122 if dim.Key != "pool" { 123 continue 124 } 125 pool = dim.Value 126 if allowedPools.Has(dim.Value) { 127 poolAllowed = true 128 break 129 } 130 } 131 if !poolAllowed { 132 return errors.Reason("build.infra.swarming.dimension['pool']: %s not allowed", pool).Err() 133 } 134 135 sa := b.GetInfra().GetSwarming().GetTaskServiceAccount() 136 if sa == "" || !allowedSAs.Has(sa) { 137 return errors.Reason("build.infra.swarming.task_service_account: %s not allowed", sa).Err() 138 } 139 return nil 140 } 141 142 func validateHostName(host string) error { 143 if strings.Contains(host, "://") { 144 return errors.Reason(`must not contain "://"`).Err() 145 } 146 return nil 147 } 148 149 func validateCipdPackage(pkg string, mustWithSuffix bool) error { 150 pkgSuffix := "/${platform}" 151 if mustWithSuffix && !strings.HasSuffix(pkg, pkgSuffix) { 152 return errors.Reason("expected to end with %s", pkgSuffix).Err() 153 } 154 return cipdCommon.ValidatePackageName(strings.TrimSuffix(pkg, pkgSuffix)) 155 } 156 157 func validateAgentInput(in *pb.BuildInfra_Buildbucket_Agent_Input) error { 158 for path, ref := range in.GetData() { 159 for i, spec := range ref.GetCipd().GetSpecs() { 160 if err := validateCipdPackage(spec.GetPackage(), false); err != nil { 161 return errors.Annotate(err, "[%s]: [%d]: cipd.package", path, i).Err() 162 } 163 if err := cipdCommon.ValidateInstanceVersion(spec.GetVersion()); err != nil { 164 return errors.Annotate(err, "[%s]: [%d]: cipd.version", path, i).Err() 165 } 166 } 167 168 cas := ref.GetCas() 169 if cas != nil { 170 switch { 171 case !casInstanceRe.MatchString(cas.GetCasInstance()): 172 return errors.Reason("[%s]: cas.cas_instance: does not match %s", path, casInstanceRe).Err() 173 case cas.GetDigest() == nil: 174 return errors.Reason("[%s]: cas.digest: not specified", path).Err() 175 case cas.Digest.GetSizeBytes() < 0: 176 return errors.Reason("[%s]: cas.digest.size_bytes: must be greater or equal to 0", path).Err() 177 } 178 } 179 } 180 return nil 181 } 182 183 func validateAgentSource(src *pb.BuildInfra_Buildbucket_Agent_Source) error { 184 cipd := src.GetCipd() 185 if err := validateCipdPackage(cipd.GetPackage(), true); err != nil { 186 return errors.Annotate(err, "cipd.package:").Err() 187 } 188 if err := cipdCommon.ValidateInstanceVersion(cipd.GetVersion()); err != nil { 189 return errors.Annotate(err, "cipd.version").Err() 190 } 191 return nil 192 } 193 194 func validateAgentPurposes(purposes map[string]pb.BuildInfra_Buildbucket_Agent_Purpose, in *pb.BuildInfra_Buildbucket_Agent_Input) error { 195 if len(purposes) == 0 { 196 return nil 197 } 198 199 for path := range purposes { 200 if _, ok := in.GetData()[path]; !ok { 201 return errors.Reason("Invalid path %s - not in input dataRef", path).Err() 202 } 203 } 204 return nil 205 } 206 207 func validateAgent(agent *pb.BuildInfra_Buildbucket_Agent) error { 208 var err error 209 switch { 210 case teeErr(validateAgentInput(agent.GetInput()), &err) != nil: 211 return errors.Annotate(err, "input").Err() 212 case teeErr(validateAgentSource(agent.GetSource()), &err) != nil: 213 return errors.Annotate(err, "source").Err() 214 case teeErr(validateAgentPurposes(agent.GetPurposes(), agent.GetInput()), &err) != nil: 215 return errors.Annotate(err, "purposes").Err() 216 default: 217 return nil 218 } 219 } 220 221 func validateInfraBuildbucket(ctx context.Context, ib *pb.BuildInfra_Buildbucket) error { 222 var err error 223 bbHost := fmt.Sprintf("%s.appspot.com", info.AppID(ctx)) 224 switch { 225 case teeErr(validateHostName(ib.GetHostname()), &err) != nil: 226 return errors.Annotate(err, "hostname").Err() 227 case ib.GetHostname() != "" && ib.Hostname != bbHost: 228 return errors.Reason("incorrect hostname, want: %s, got: %s", bbHost, ib.Hostname).Err() 229 case teeErr(validateAgent(ib.GetAgent()), &err) != nil: 230 return errors.Annotate(err, "agent").Err() 231 case teeErr(validateRequestedDimensions(ib.RequestedDimensions), &err) != nil: 232 return errors.Annotate(err, "requested_dimensions").Err() 233 case teeErr(validateProperties(ib.RequestedProperties), &err) != nil: 234 return errors.Annotate(err, "requested_properties").Err() 235 } 236 for _, host := range ib.GetKnownPublicGerritHosts() { 237 if err = validateHostName(host); err != nil { 238 return errors.Annotate(err, "known_public_gerrit_hosts").Err() 239 } 240 } 241 return nil 242 } 243 244 func convertSwarmingCaches(swarmingCaches []*pb.BuildInfra_Swarming_CacheEntry) []*pb.CacheEntry { 245 caches := make([]*pb.CacheEntry, len(swarmingCaches)) 246 for i, c := range swarmingCaches { 247 caches[i] = &pb.CacheEntry{ 248 Name: c.Name, 249 Path: c.Path, 250 WaitForWarmCache: c.WaitForWarmCache, 251 EnvVar: c.EnvVar, 252 } 253 } 254 return caches 255 } 256 257 func validateCaches(caches []*pb.CacheEntry) error { 258 names := stringset.New(len(caches)) 259 paths := stringset.New(len(caches)) 260 for i, cache := range caches { 261 switch { 262 case cache.Name == "": 263 return errors.Reason(fmt.Sprintf("%dth cache: name unspecified", i)).Err() 264 case len(cache.Name) > 128: 265 return errors.Reason(fmt.Sprintf("%dth cache: name too long (limit is 128)", i)).Err() 266 case !names.Add(cache.Name): 267 return errors.Reason(fmt.Sprintf("duplicated cache name: %s", cache.Name)).Err() 268 case cache.Path == "": 269 return errors.Reason(fmt.Sprintf("%dth cache: path unspecified", i)).Err() 270 case strings.Contains(cache.Path, "\\"): 271 return errors.Reason(fmt.Sprintf("%dth cache: path must use POSIX format", i)).Err() 272 case !paths.Add(cache.Path): 273 return errors.Reason(fmt.Sprintf("duplicated cache path: %s", cache.Path)).Err() 274 case cache.WaitForWarmCache.AsDuration()%(60*time.Second) != 0: 275 return errors.Reason(fmt.Sprintf("%dth cache: wait_for_warm_cache must be multiples of 60 seconds.", i)).Err() 276 } 277 } 278 return nil 279 } 280 281 // validateDimensions validates the task dimension. 282 func validateDimension(dim *pb.RequestedDimension) error { 283 var err error 284 switch { 285 case teeErr(validateExpirationDuration(dim.GetExpiration()), &err) != nil: 286 return errors.Annotate(err, "expiration").Err() 287 case dim.GetKey() == "": 288 return errors.Reason("key must be specified").Err() 289 default: 290 return nil 291 } 292 } 293 294 // validateDimensions validates the task dimensions. 295 func validateDimensions(dims []*pb.RequestedDimension) error { 296 for i, dim := range dims { 297 switch err := validateDimension(dim); { 298 case err != nil: 299 return errors.Annotate(err, "[%d]", i).Err() 300 case dim.Value == "": 301 return errors.Reason("[%d]: value must be specified", i).Err() 302 } 303 } 304 return nil 305 } 306 307 func validateBackendConfig(config *structpb.Struct) error { 308 if config == nil { 309 return nil 310 } 311 312 var priority float64 313 if p := config.GetFields()["priority"]; p != nil { 314 if _, ok := p.GetKind().(*structpb.Value_NumberValue); !ok { 315 return errors.Reason("priority must be a number").Err() 316 } 317 priority = p.GetNumberValue() 318 } 319 // Currently apply the same rule as swarming priority rule for backend priority. 320 // This may change when we have other backends in the future. 321 if priority < 0 || priority > 255 { 322 return errors.Reason("priority must be in [0, 255]").Err() 323 } 324 return nil 325 } 326 327 func validateInfraBackend(ctx context.Context, ib *pb.BuildInfra_Backend) error { 328 if ib == nil { 329 return nil 330 } 331 332 globalCfg, err := config.GetSettingsCfg(ctx) 333 if err != nil { 334 return errors.Annotate(err, "error fetching service config").Err() 335 } 336 337 switch { 338 case teeErr(config.ValidateTaskBackendTarget(globalCfg, ib.GetTask().GetId().GetTarget()), &err) != nil: 339 return err 340 case teeErr(validateBackendConfig(ib.GetConfig()), &err) != nil: 341 return errors.Annotate(err, "config").Err() 342 case teeErr(validateDimensions(ib.GetTaskDimensions()), &err) != nil: 343 return errors.Annotate(err, "task_dimensions").Err() 344 case teeErr(validateCaches(ib.GetCaches()), &err) != nil: 345 return errors.Annotate(err, "caches").Err() 346 default: 347 return nil 348 } 349 } 350 351 func validateInfraSwarming(is *pb.BuildInfra_Swarming) error { 352 var err error 353 if is == nil { 354 return nil 355 } 356 switch { 357 case teeErr(validateHostName(is.GetHostname()), &err) != nil: 358 return errors.Annotate(err, "hostname").Err() 359 case is.GetPriority() < 0 || is.GetPriority() > 255: 360 return errors.Reason("priority must be in [0, 255]").Err() 361 case teeErr(validateDimensions(is.GetTaskDimensions()), &err) != nil: 362 return errors.Annotate(err, "task_dimensions").Err() 363 case teeErr(validateCaches(convertSwarmingCaches(is.GetCaches())), &err) != nil: 364 return errors.Annotate(err, "caches").Err() 365 default: 366 return nil 367 } 368 } 369 370 func validateInfraLogDog(il *pb.BuildInfra_LogDog) error { 371 var err error 372 switch { 373 case teeErr(validateHostName(il.GetHostname()), &err) != nil: 374 return errors.Annotate(err, "hostname").Err() 375 default: 376 return nil 377 } 378 } 379 380 func validateInfraResultDB(irdb *pb.BuildInfra_ResultDB) error { 381 var err error 382 switch { 383 case irdb == nil: 384 return nil 385 case teeErr(validateHostName(irdb.GetHostname()), &err) != nil: 386 return errors.Annotate(err, "hostname").Err() 387 default: 388 return nil 389 } 390 } 391 392 func validateInfra(ctx context.Context, infra *pb.BuildInfra) error { 393 var err error 394 switch { 395 case infra.GetBackend() == nil && infra.GetSwarming() == nil: 396 return errors.Reason("backend or swarming is needed in build infra").Err() 397 case infra.GetBackend() != nil && infra.GetSwarming() != nil: 398 return errors.Reason("can only have one of backend or swarming in build infra. both were provided").Err() 399 case teeErr(validateInfraBackend(ctx, infra.GetBackend()), &err) != nil: 400 return errors.Annotate(err, "backend").Err() 401 case teeErr(validateInfraSwarming(infra.GetSwarming()), &err) != nil: 402 return errors.Annotate(err, "swarming").Err() 403 case teeErr(validateInfraBuildbucket(ctx, infra.GetBuildbucket()), &err) != nil: 404 return errors.Annotate(err, "buildbucket").Err() 405 case teeErr(validateInfraLogDog(infra.GetLogdog()), &err) != nil: 406 return errors.Annotate(err, "logdog").Err() 407 case teeErr(validateInfraResultDB(infra.GetResultdb()), &err) != nil: 408 return errors.Annotate(err, "resultdb").Err() 409 default: 410 return nil 411 } 412 } 413 414 func validateInput(wellKnownExperiments stringset.Set, in *pb.Build_Input) error { 415 var err error 416 switch { 417 case teeErr(validateGerritChanges(in.GerritChanges), &err) != nil: 418 return errors.Annotate(err, "gerrit_changes").Err() 419 case in.GetGitilesCommit() != nil && teeErr(validateCommitWithRef(in.GitilesCommit), &err) != nil: 420 return errors.Annotate(err, "gitiles_commit").Err() 421 case in.Properties != nil && teeErr(validateProperties(in.Properties), &err) != nil: 422 return errors.Annotate(err, "properties").Err() 423 } 424 for _, expName := range in.Experiments { 425 if err := config.ValidateExperimentName(expName, wellKnownExperiments); err != nil { 426 return errors.Annotate(err, "experiment %q", expName).Err() 427 } 428 } 429 return nil 430 } 431 432 func validateExe(exe *pb.Executable, agent *pb.BuildInfra_Buildbucket_Agent) error { 433 var err error 434 switch { 435 case exe.GetCipdPackage() == "": 436 return nil 437 case teeErr(validateCipdPackage(exe.CipdPackage, false), &err) != nil: 438 return errors.Annotate(err, "cipd_package").Err() 439 case exe.GetCipdVersion() != "" && teeErr(cipdCommon.ValidateInstanceVersion(exe.CipdVersion), &err) != nil: 440 return errors.Annotate(err, "cipd_version").Err() 441 } 442 443 // Validate exe matches with agent. 444 var payloadPath string 445 for dir, purpose := range agent.GetPurposes() { 446 if purpose == pb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD { 447 payloadPath = dir 448 break 449 } 450 } 451 if payloadPath == "" { 452 return nil 453 } 454 455 if pkgs, ok := agent.GetInput().GetData()[payloadPath]; ok { 456 cipdPkgs := pkgs.GetCipd() 457 if cipdPkgs == nil { 458 return errors.Reason("not match build.infra.buildbucket.agent").Err() 459 } 460 461 packageMatches := false 462 for _, spec := range cipdPkgs.Specs { 463 if spec.Package != exe.CipdPackage { 464 continue 465 } 466 packageMatches = true 467 if spec.Version != exe.CipdVersion { 468 return errors.Reason("cipd_version does not match build.infra.buildbucket.agent").Err() 469 } 470 break 471 } 472 if !packageMatches { 473 return errors.Reason("cipd_package does not match build.infra.buildbucket.agent").Err() 474 } 475 } 476 return nil 477 } 478 479 func validateBuild(ctx context.Context, wellKnownExperiments stringset.Set, b *pb.Build) error { 480 var err error 481 switch { 482 case teeErr(protoutil.ValidateRequiredBuilderID(b.Builder), &err) != nil: 483 return errors.Annotate(err, "builder").Err() 484 case teeErr(validateExe(b.Exe, b.GetInfra().GetBuildbucket().GetAgent()), &err) != nil: 485 return errors.Annotate(err, "exe").Err() 486 case teeErr(validateInput(wellKnownExperiments, b.Input), &err) != nil: 487 return errors.Annotate(err, "input").Err() 488 case teeErr(validateInfra(ctx, b.Infra), &err) != nil: 489 return errors.Annotate(err, "infra").Err() 490 case teeErr(validateBucketConstraints(ctx, b), &err) != nil: 491 return err 492 case teeErr(validateTags(b.Tags, TagNew), &err) != nil: 493 return errors.Annotate(err, "tags").Err() 494 default: 495 return nil 496 } 497 } 498 499 func validateCreateBuildRequest(ctx context.Context, wellKnownExperiments stringset.Set, req *pb.CreateBuildRequest) (*model.BuildMask, error) { 500 if procRes := protowalk.Fields(req, &protowalk.DeprecatedProcessor{}, &protowalk.OutputOnlyProcessor{}, &protowalk.RequiredProcessor{}, &CreateBuildChecker{}); procRes != nil { 501 if resStrs := procRes.Strings(); len(resStrs) > 0 { 502 logging.Infof(ctx, strings.Join(resStrs, ". ")) 503 } 504 if err := procRes.Err(); err != nil { 505 return nil, err 506 } 507 } 508 509 if err := validateBuild(ctx, wellKnownExperiments, req.GetBuild()); err != nil { 510 return nil, errors.Annotate(err, "build").Err() 511 } 512 513 if strings.Contains(req.GetRequestId(), "/") { 514 return nil, errors.Reason("request_id cannot contain '/'").Err() 515 } 516 517 m, err := model.NewBuildMask("", nil, req.Mask) 518 if err != nil { 519 return nil, errors.Annotate(err, "invalid mask").Err() 520 } 521 522 return m, nil 523 } 524 525 type buildCreator struct { 526 // Valid builds to be saved in datastore. The len(blds) <= len(reqIDs) 527 blds []*model.Build 528 // idxMapBldToReq is an index map of index of blds -> index of reqIDs. 529 idxMapBldToReq []int 530 // RequestIDs of each request. 531 reqIDs []string 532 // errors when creating the builds. 533 merr errors.MultiError 534 } 535 536 // createBuilds saves the builds to datastore and triggers swarming task creation 537 // tasks for each saved build. 538 // A single returned error means a top-level error. 539 // Otherwise, it would be a MultiError where len(MultiError) equals to len(bc.reqIDs). 540 func (bc *buildCreator) createBuilds(ctx context.Context) ([]*model.Build, error) { 541 now := clock.Now(ctx).UTC() 542 user := auth.CurrentIdentity(ctx) 543 appID := info.AppID(ctx) // e.g. cr-buildbucket 544 ids := buildid.NewBuildIDs(ctx, now, len(bc.blds)) 545 nums := make([]*model.Build, 0, len(bc.blds)) 546 var idxMapNums []int 547 548 for i := range bc.blds { 549 if bc.blds[i] == nil { 550 continue 551 } 552 bc.blds[i].ID = ids[i] 553 bc.blds[i].CreatedBy = user 554 bc.blds[i].CreateTime = now 555 556 // Set proto field values which can only be determined at creation-time. 557 bc.blds[i].Proto.CreatedBy = string(user) 558 bc.blds[i].Proto.CreateTime = timestamppb.New(now) 559 bc.blds[i].Proto.Id = ids[i] 560 if bc.blds[i].Proto.Infra.Buildbucket.Hostname == "" { 561 bc.blds[i].Proto.Infra.Buildbucket.Hostname = fmt.Sprintf("%s.appspot.com", appID) 562 } 563 bc.blds[i].Proto.Infra.Logdog.Prefix = fmt.Sprintf("buildbucket/%s/%d", appID, bc.blds[i].Proto.Id) 564 protoutil.SetStatus(now, bc.blds[i].Proto, pb.Status_SCHEDULED) 565 566 if bc.blds[i].Proto.GetInfra().GetBuildbucket().GetBuildNumber() { 567 idxMapNums = append(idxMapNums, bc.idxMapBldToReq[i]) 568 nums = append(nums, bc.blds[i]) 569 } 570 } 571 572 if err := generateBuildNumbers(ctx, nums); err != nil { 573 me := err.(errors.MultiError) 574 bc.merr = mergeErrs(bc.merr, me, "error generating build numbers", func(idx int) int { return idxMapNums[idx] }) 575 } 576 577 validBlds, idxMapValidBlds := getValidBlds(bc.blds, bc.merr, bc.idxMapBldToReq) 578 err := parallel.FanOutIn(func(work chan<- func() error) { 579 work <- func() error { return model.UpdateBuilderStat(ctx, validBlds, now) } 580 work <- func() error { return resultdb.CreateInvocations(ctx, validBlds) } 581 work <- func() error { return search.UpdateTagIndex(ctx, validBlds) } 582 }) 583 if err != nil { 584 errs := err.(errors.MultiError) 585 for _, e := range errs { 586 if me, ok := e.(errors.MultiError); ok { 587 bc.merr = mergeErrs(bc.merr, me, "", func(idx int) int { return idxMapValidBlds[idx] }) 588 } else { 589 return nil, e // top-level error 590 } 591 } 592 } 593 594 // This parallel work isn't combined with the above parallel work to ensure build entities and Swarming (or Backend) 595 // task creation tasks are only created if everything else has succeeded (since everything can't be done 596 // in one transaction). 597 _ = parallel.WorkPool(min(64, len(validBlds)), func(work chan<- func() error) { 598 for i, b := range validBlds { 599 i := i 600 b := b 601 origI := idxMapValidBlds[i] 602 if bc.merr[origI] != nil { 603 validBlds[i] = nil 604 continue 605 } 606 607 reqID := bc.reqIDs[origI] 608 work <- func() error { 609 bldr := b.Proto.Builder 610 bs := &model.BuildStatus{ 611 Build: datastore.KeyForObj(ctx, b), 612 Status: pb.Status_SCHEDULED, 613 BuildAddress: fmt.Sprintf("%s/%s/%s/b%d", bldr.Project, bldr.Bucket, bldr.Builder, b.ID), 614 } 615 if b.Proto.Number > 0 { 616 bs.BuildAddress = fmt.Sprintf("%s/%s/%s/%d", bldr.Project, bldr.Bucket, bldr.Builder, b.Proto.Number) 617 } 618 toPut := []any{ 619 b, 620 bs, 621 &model.BuildInfra{ 622 Build: datastore.KeyForObj(ctx, b), 623 Proto: b.Proto.Infra, 624 }, 625 &model.BuildInputProperties{ 626 Build: datastore.KeyForObj(ctx, b), 627 Proto: b.Proto.Input.Properties, 628 }, 629 } 630 r := model.NewRequestID(ctx, b.ID, now, reqID) 631 632 // Write the entities and trigger a task queue task to create the Swarming task. 633 err := datastore.RunInTransaction(ctx, func(ctx context.Context) error { 634 // Deduplicate by request ID. 635 if reqID != "" { 636 switch err := datastore.Get(ctx, r); { 637 case err == datastore.ErrNoSuchEntity: 638 toPut = append(toPut, r) 639 case err != nil: 640 return errors.Annotate(err, "failed to deduplicate request ID: %d", b.ID).Err() 641 default: 642 b.ID = r.BuildID 643 if err := datastore.Get(ctx, b); err != nil { 644 return errors.Annotate(err, "failed to fetch deduplicated build: %d", b.ID).Err() 645 } 646 return nil 647 } 648 } 649 650 // Request was not a duplicate. 651 switch err := datastore.Get(ctx, &model.Build{ID: b.ID}); { 652 case err == nil: 653 return appstatus.Errorf(codes.AlreadyExists, "build already exists: %d", b.ID) 654 case err != datastore.ErrNoSuchEntity: 655 return errors.Annotate(err, "failed to fetch build: %d", b.ID).Err() 656 } 657 658 // Drop the infra, input.properties when storing into Build entity, as 659 // they are stored in separate datastore entities. 660 infra := b.Proto.Infra 661 inProp := b.Proto.Input.Properties 662 b.Proto.Infra = nil 663 b.Proto.Input.Properties = nil 664 defer func() { 665 b.Proto.Infra = infra 666 b.Proto.Input.Properties = inProp 667 }() 668 if err := datastore.Put(ctx, toPut...); err != nil { 669 return errors.Annotate(err, "failed to store build: %d", b.ID).Err() 670 } 671 672 // If a backend is set, create a backend task. Otherwise, create a swarming task. 673 switch { 674 case infra.GetBackend() != nil: 675 if err := tasks.CreateBackendBuildTask(ctx, &taskdefs.CreateBackendBuildTask{ 676 BuildId: b.ID, 677 RequestId: uuid.New().String(), 678 }); err != nil { 679 return errors.Annotate(err, "failed to enqueue CreateBackendTask").Err() 680 } 681 case infra.GetSwarming().GetHostname() == "": 682 logging.Debugf(ctx, "skipped creating swarming task for build %d", b.ID) 683 return nil 684 default: 685 if stringset.NewFromSlice(b.Proto.Input.Experiments...).Has(bb.ExperimentBackendGo) { 686 if err := tasks.CreateSwarmingBuildTask(ctx, &taskdefs.CreateSwarmingBuildTask{ 687 BuildId: b.ID, 688 }); err != nil { 689 return errors.Annotate(err, "failed to enqueue CreateSwarmingBuildTask: %d", b.ID).Err() 690 } 691 } else { 692 if err := tasks.CreateSwarmingTask(ctx, &taskdefs.CreateSwarmingTask{ 693 BuildId: b.ID, 694 }); err != nil { 695 return errors.Annotate(err, "failed to enqueue CreateSwarmingTask: %d", b.ID).Err() 696 } 697 } 698 } 699 700 if err := tasks.NotifyPubSub(ctx, b); err != nil { 701 // Don't fail the entire creation. Just log the error since the 702 // status notification for unspecified -> scheduled is a 703 // nice-to-have not a must-to-have. 704 logging.Warningf(ctx, "failed to enqueue the notification when Build(%d) is scheduled: %s", b.ID, err) 705 } 706 return nil 707 }, nil) 708 709 // Record any error happened in the above transaction. 710 if err != nil { 711 validBlds[i] = nil 712 bc.merr[origI] = err 713 return nil 714 } 715 metrics.BuildCreated(ctx, b) 716 return nil 717 } 718 } 719 }) 720 721 if bc.merr.First() == nil { 722 return validBlds, nil 723 } 724 // Map back to final results to make sure len(resBlds) always equal to len(reqs). 725 resBlds := make([]*model.Build, len(bc.reqIDs)) 726 for i, bld := range validBlds { 727 origI := idxMapValidBlds[i] 728 if bc.merr[origI] == nil { 729 resBlds[origI] = bld 730 } 731 } 732 return resBlds, bc.merr 733 } 734 735 // getValidBlds returns a list of valid builds where its corresponding error is nil. 736 // as well as an index map where idxMap[returnedIndex] == originalIndex. 737 func getValidBlds(blds []*model.Build, origErrs errors.MultiError, idxMapBldToReq []int) ([]*model.Build, []int) { 738 if len(blds) != len(idxMapBldToReq) { 739 panic("The length of blds and the length of idxMapBldToReq must be the same.") 740 } 741 var validBlds []*model.Build 742 var idxMap []int 743 for i, bld := range blds { 744 origI := idxMapBldToReq[i] 745 if origErrs[origI] == nil { 746 idxMap = append(idxMap, origI) 747 validBlds = append(validBlds, bld) 748 } 749 } 750 return validBlds, idxMap 751 } 752 753 // generateBuildNumbers mutates the given builds, setting build numbers and 754 // build address tags. 755 // 756 // It would return a MultiError (if any) where len(MultiError) equals to len(reqs). 757 func generateBuildNumbers(ctx context.Context, builds []*model.Build) error { 758 merr := make(errors.MultiError, len(builds)) 759 seq := make(map[string][]*model.Build) 760 idxMap := make(map[string][]int) // BuilderID -> a list of index 761 for i, b := range builds { 762 name := protoutil.FormatBuilderID(b.Proto.Builder) 763 seq[name] = append(seq[name], b) 764 idxMap[name] = append(idxMap[name], i) 765 } 766 _ = parallel.WorkPool(min(64, len(builds)), func(work chan<- func() error) { 767 for name, blds := range seq { 768 name := name 769 blds := blds 770 work <- func() error { 771 n, err := model.GenerateSequenceNumbers(ctx, name, len(blds)) 772 if err != nil { 773 for _, idx := range idxMap[name] { 774 merr[idx] = err 775 } 776 return nil 777 } 778 for i, b := range blds { 779 b.Proto.Number = n + int32(i) 780 addr := fmt.Sprintf("build_address:luci.%s.%s/%s/%d", b.Proto.Builder.Project, b.Proto.Builder.Bucket, b.Proto.Builder.Builder, b.Proto.Number) 781 b.Tags = append(b.Tags, addr) 782 sort.Strings(b.Tags) 783 } 784 return nil 785 } 786 } 787 }) 788 789 if merr.First() == nil { 790 return nil 791 } 792 return merr.AsError() 793 } 794 795 // CreateBuild handles a request to create a build. Implements pb.BuildsServer. 796 func (*Builds) CreateBuild(ctx context.Context, req *pb.CreateBuildRequest) (*pb.Build, error) { 797 if err := perm.HasInBucket(ctx, bbperms.BuildsCreate, req.Build.Builder.Project, req.Build.Builder.Bucket); err != nil { 798 return nil, err 799 } 800 801 globalCfg, err := config.GetSettingsCfg(ctx) 802 if err != nil { 803 return nil, errors.Annotate(err, "error fetching service config").Err() 804 } 805 wellKnownExperiments := protoutil.WellKnownExperiments(globalCfg) 806 807 m, err := validateCreateBuildRequest(ctx, wellKnownExperiments, req) 808 if err != nil { 809 return nil, appstatus.BadRequest(err) 810 } 811 812 bld := &model.Build{ 813 Proto: req.Build, 814 } 815 816 // Update ancestors info. 817 pBld, err := validateParent(ctx) 818 if err != nil { 819 return nil, errors.Annotate(err, "build parent").Err() 820 } 821 ancestors, pRunID, err := getParentInfo(ctx, pBld) 822 if err != nil { 823 return nil, errors.Annotate(err, "build ancestors").Err() 824 } 825 if len(ancestors) > 0 { 826 bld.Proto.AncestorIds = ancestors 827 } 828 829 setExperimentsFromProto(bld) 830 // Tags are stored in the outer struct (see model/build.go). 831 tagMap := protoutil.StringPairMap(bld.Proto.Tags) 832 if pRunID != "" { 833 tagMap.Add("parent_task_id", pRunID) 834 } 835 tags := tagMap.Format() 836 tags = stringset.NewFromSlice(tags...).ToSlice() // Deduplicate tags. 837 sort.Strings(tags) 838 bld.Tags = tags 839 840 bc := &buildCreator{ 841 blds: []*model.Build{bld}, 842 idxMapBldToReq: []int{0}, 843 reqIDs: []string{req.RequestId}, 844 merr: make(errors.MultiError, 1), 845 } 846 blds, err := bc.createBuilds(ctx) 847 if err != nil { 848 return nil, errors.Annotate(err, "error creating build").Err() 849 } 850 851 return blds[0].ToProto(ctx, m, nil) 852 }