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  }