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  }