go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/job/jobcreate/create.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 jobcreate
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"path"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"google.golang.org/protobuf/types/known/fieldmaskpb"
    26  	"google.golang.org/protobuf/types/known/structpb"
    27  
    28  	"go.chromium.org/luci/buildbucket"
    29  	"go.chromium.org/luci/buildbucket/cmd/bbagent/bbinput"
    30  	bbpb "go.chromium.org/luci/buildbucket/proto"
    31  	"go.chromium.org/luci/common/data/stringset"
    32  	"go.chromium.org/luci/common/data/strpair"
    33  	"go.chromium.org/luci/common/errors"
    34  	"go.chromium.org/luci/grpc/prpc"
    35  	"go.chromium.org/luci/led/job"
    36  	swarmingpb "go.chromium.org/luci/swarming/proto/api_v2"
    37  )
    38  
    39  // Returns "bbagent", "kitchen" or "raw" depending on the type of task detected.
    40  func detectMode(r *swarmingpb.NewTaskRequest) string {
    41  	arg0, ts := "", r.TaskSlices[0]
    42  	if ts.Properties != nil {
    43  		if len(ts.Properties.Command) > 0 {
    44  			arg0 = ts.Properties.Command[0]
    45  		}
    46  	}
    47  	switch arg0 {
    48  	case "bbagent${EXECUTABLE_SUFFIX}":
    49  		return "bbagent"
    50  	case "kitchen${EXECUTABLE_SUFFIX}":
    51  		return "kitchen"
    52  	}
    53  	return "raw"
    54  }
    55  
    56  // setPriority mutates the provided build to set the priority of its underlying
    57  // swarming task.
    58  //
    59  // The priority for buildbucket type tasks is between 20 to 255.
    60  func setPriority(build *bbpb.Build, priorityDiff int) {
    61  	calPriority := func(originalPriority int32) int32 {
    62  		switch priority := originalPriority + int32(priorityDiff); {
    63  		case priority < 20:
    64  			return 20
    65  		case priority > 255:
    66  			return 255
    67  		default:
    68  			return priority
    69  		}
    70  	}
    71  
    72  	if build.Infra.Swarming != nil {
    73  		build.Infra.Swarming.Priority = calPriority(build.Infra.Swarming.Priority)
    74  	} else {
    75  		config := build.Infra.Backend.GetConfig().GetFields()
    76  		newPriority := calPriority(int32(config["priority"].GetNumberValue()))
    77  		build.Infra.Backend.Config.Fields["priority"] = structpb.NewNumberValue(float64(newPriority))
    78  	}
    79  }
    80  
    81  // FromNewTaskRequest generates a new job.Definition by parsing the
    82  // given NewTaskRequest.
    83  //
    84  // If the task's first slice looks like either a bbagent or kitchen-based
    85  // Buildbucket task, the returned Definition will have the `buildbucket`
    86  // field populated, otherwise the `swarming` field will be populated.
    87  func FromNewTaskRequest(ctx context.Context, r *swarmingpb.NewTaskRequest, name, swarmingHost string, ks job.KitchenSupport, priorityDiff int, bld *bbpb.Build, extraTags []string, authClient *http.Client) (ret *job.Definition, err error) {
    88  	if len(r.TaskSlices) == 0 {
    89  		return nil, errors.New("swarming tasks without task slices are not supported")
    90  	}
    91  
    92  	ret = &job.Definition{}
    93  	name = "led: " + name
    94  
    95  	switch detectMode(r) {
    96  	case "bbagent":
    97  		bb := &job.Buildbucket{}
    98  		ret.JobType = &job.Definition_Buildbucket{Buildbucket: bb}
    99  		// TODO(crbug.com/1219018): use bbCommonFromTaskRequest only in the long
   100  		// bbagent arg case.
   101  		// Discussion: https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/3511002/comments/0daf496b_2c8ba5a2
   102  		bbCommonFromTaskRequest(bb, r)
   103  		cmd := r.TaskSlices[0].Properties.Command
   104  		switch {
   105  		case len(cmd) == 2:
   106  			bb.BbagentArgs, err = bbinput.Parse(cmd[len(cmd)-1])
   107  			bb.UpdateBuildFromBbagentArgs()
   108  		case bld != nil:
   109  			bb.BbagentArgs = bbagentArgsFromBuild(bld)
   110  		default:
   111  			bb.BbagentArgs, err = getBbagentArgsFromCMD(ctx, cmd, authClient)
   112  			bb.UpdateBuildFromBbagentArgs()
   113  		}
   114  
   115  		// This check is only here because of bbCommonFromTaskRequest.
   116  		// TODO(crbug.com/1219018): remove this check after bbCommonFromTaskRequest
   117  		// is only used for long bbagent arg case.
   118  		if bb.BbagentDownloadCIPDPkgs() {
   119  			bb.CipdPackages = nil
   120  		}
   121  
   122  	case "kitchen":
   123  		bb := &job.Buildbucket{LegacyKitchen: true}
   124  		ret.JobType = &job.Definition_Buildbucket{Buildbucket: bb}
   125  		bbCommonFromTaskRequest(bb, r)
   126  		err = ks.FromSwarmingV2(ctx, r, bb)
   127  
   128  	case "raw":
   129  		// non-Buildbucket Swarming task
   130  		sw := &job.Swarming{Hostname: swarmingHost}
   131  		ret.JobType = &job.Definition_Swarming{Swarming: sw}
   132  		jobDefinitionFromSwarming(sw, r)
   133  		sw.Task.Name = name
   134  
   135  	default:
   136  		panic("impossible")
   137  	}
   138  
   139  	if bb := ret.GetBuildbucket(); err == nil && bb != nil {
   140  		bb.Name = name
   141  		bb.FinalBuildProtoPath = "build.proto.json"
   142  
   143  		// set all buildbucket type tasks to experimental by default.
   144  		bb.BbagentArgs.Build.Input.Experimental = true
   145  
   146  		setPriority(bb.BbagentArgs.Build, priorityDiff)
   147  
   148  		// clear fields which don't make sense
   149  		bb.BbagentArgs.Build.CanceledBy = ""
   150  		bb.BbagentArgs.Build.CreatedBy = ""
   151  		bb.BbagentArgs.Build.CreateTime = nil
   152  		bb.BbagentArgs.Build.Id = 0
   153  		bb.BbagentArgs.Build.Infra.Buildbucket.Hostname = ""
   154  		bb.BbagentArgs.Build.Infra.Buildbucket.RequestedProperties = nil
   155  		bb.BbagentArgs.Build.Infra.Logdog.Prefix = ""
   156  		bb.BbagentArgs.Build.Infra.Swarming.TaskId = ""
   157  		bb.BbagentArgs.Build.Number = 0
   158  		bb.BbagentArgs.Build.Status = 0
   159  		bb.BbagentArgs.Build.UpdateTime = nil
   160  
   161  		bb.BbagentArgs.Build.Tags = nil
   162  		if len(extraTags) > 0 {
   163  			tags := make([]*bbpb.StringPair, 0, len(extraTags))
   164  			for _, tag := range extraTags {
   165  				k, v := strpair.Parse(tag)
   166  				tags = append(tags, &bbpb.StringPair{
   167  					Key:   k,
   168  					Value: v,
   169  				})
   170  			}
   171  			sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key })
   172  			bb.BbagentArgs.Build.Tags = tags
   173  		}
   174  
   175  		// drop the executable path; it's canonically represented by
   176  		// out.BBAgentArgs.PayloadPath and out.BBAgentArgs.Build.Exe.
   177  		if exePath := bb.BbagentArgs.ExecutablePath; exePath != "" {
   178  			// convert to new mode
   179  			payload, arg := path.Split(exePath)
   180  			bb.BbagentArgs.ExecutablePath = ""
   181  			bb.UpdatePayloadPath(strings.TrimSuffix(payload, "/"))
   182  			bb.BbagentArgs.Build.Exe.Cmd = []string{arg}
   183  		}
   184  
   185  		if !bb.BbagentDownloadCIPDPkgs() {
   186  			dropRecipePackage(&bb.CipdPackages, bb.PayloadPath())
   187  		}
   188  
   189  		props := bb.BbagentArgs.GetBuild().GetInput().GetProperties()
   190  		// everything in here is reflected elsewhere in the Build and will be
   191  		// re-synthesized by kitchen support or the recipe engine itself, depending
   192  		// on the final kitchen/bbagent execution mode.
   193  		delete(props.GetFields(), "$recipe_engine/runtime")
   194  
   195  		// drop legacy recipe fields
   196  		if recipe := bb.BbagentArgs.Build.Infra.Recipe; recipe != nil {
   197  			bb.BbagentArgs.Build.Infra.Recipe = nil
   198  		}
   199  	}
   200  
   201  	// ensure isolate/rbe-cas source consistency
   202  	casUserPayload := &swarmingpb.CASReference{
   203  		Digest: &swarmingpb.Digest{},
   204  	}
   205  	for i, slice := range r.TaskSlices {
   206  		if cir := slice.Properties.CasInputRoot; cir != nil {
   207  			if err := populateCasPayload(casUserPayload, cir); err != nil {
   208  				return nil, errors.Annotate(err, "task slice %d", i).Err()
   209  			}
   210  		}
   211  	}
   212  	if casUserPayload.Digest.GetHash() == "" {
   213  		return ret, err
   214  	}
   215  
   216  	if ret.GetSwarming() != nil {
   217  		ret.GetSwarming().CasUserPayload = casUserPayload
   218  	}
   219  	if ret.GetBuildbucket() != nil {
   220  		// `led get-builder` is still using swarmingbucket.get_task_def, so
   221  		// we need to fill in the data to ret.GetBuildbucket() for its builds.
   222  		// TODO(crbug.com/1345722): remove this after we migrate away from
   223  		// swarmingbucket.get_task_def.
   224  		payloadPath := ret.GetBuildbucket().BbagentArgs.PayloadPath
   225  		updates := &bbpb.BuildInfra_Buildbucket_Agent{
   226  			Input: &bbpb.BuildInfra_Buildbucket_Agent_Input{
   227  				Data: map[string]*bbpb.InputDataRef{
   228  					payloadPath: {
   229  						DataType: &bbpb.InputDataRef_Cas{
   230  							Cas: &bbpb.InputDataRef_CAS{
   231  								CasInstance: casUserPayload.GetCasInstance(),
   232  								Digest: &bbpb.InputDataRef_CAS_Digest{
   233  									Hash:      casUserPayload.GetDigest().GetHash(),
   234  									SizeBytes: casUserPayload.GetDigest().GetSizeBytes(),
   235  								},
   236  							},
   237  						},
   238  					},
   239  				},
   240  			},
   241  			Purposes: map[string]bbpb.BuildInfra_Buildbucket_Agent_Purpose{
   242  				payloadPath: bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD,
   243  			},
   244  		}
   245  		ret.GetBuildbucket().UpdateBuildbucketAgent(updates)
   246  	}
   247  
   248  	return ret, err
   249  }
   250  
   251  func populateCasPayload(cas *swarmingpb.CASReference, cir *swarmingpb.CASReference) error {
   252  	if cas.CasInstance == "" {
   253  		cas.CasInstance = cir.CasInstance
   254  	} else if cas.CasInstance != cir.CasInstance {
   255  		return errors.Reason("RBE-CAS instance inconsistency: %q != %q", cas.CasInstance, cir.CasInstance).Err()
   256  	}
   257  
   258  	if cas.Digest.Hash != "" && (cir.Digest == nil || cir.Digest.Hash != cas.Digest.Hash) {
   259  		return errors.Reason("RBE-CAS digest hash inconsistency: %+v != %+v", cas.Digest, cir.Digest).Err()
   260  	} else if cir.Digest != nil {
   261  		cas.Digest.Hash = cir.Digest.Hash
   262  	}
   263  
   264  	if cas.Digest.SizeBytes != 0 && (cir.Digest == nil || cir.Digest.SizeBytes != cas.Digest.SizeBytes) {
   265  		return errors.Reason("RBE-CAS digest size bytes inconsistency: %+v != %+v", cas.Digest, cir.Digest).Err()
   266  	} else if cir.Digest != nil {
   267  		cas.Digest.SizeBytes = cir.Digest.SizeBytes
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func getBbagentArgsFromCMD(ctx context.Context, cmd []string, authClient *http.Client) (*bbpb.BBAgentArgs, error) {
   274  	var hostname string
   275  	var bID int64
   276  	for i, s := range cmd {
   277  		switch {
   278  		case s == "-host" && i < len(cmd)-1:
   279  			hostname = cmd[i+1]
   280  		case s == "-build-id" && i < len(cmd)-1:
   281  			var err error
   282  			if bID, err = strconv.ParseInt(cmd[i+1], 10, 64); err != nil {
   283  				return nil, errors.Annotate(err, "cmd -build-id").Err()
   284  			}
   285  		}
   286  	}
   287  	if hostname == "" && bID == 0 {
   288  		// This could happen if the cmd was for a led build like
   289  		// `bbagent${EXECUTABLE_SUFFIX} --output ${ISOLATED_OUTDIR}/build.proto.json <encoded bbinput>`
   290  		return bbinput.Parse(cmd[len(cmd)-1])
   291  	}
   292  	if hostname == "" {
   293  		return nil, errors.New("host is required in cmd")
   294  	}
   295  	if bID == 0 {
   296  		return nil, errors.New("build-id is required in cmd")
   297  	}
   298  	bbclient := bbpb.NewBuildsPRPCClient(&prpc.Client{
   299  		C:    authClient,
   300  		Host: hostname,
   301  	})
   302  	bld, err := bbclient.GetBuild(ctx, &bbpb.GetBuildRequest{
   303  		Id: bID,
   304  		Mask: &bbpb.BuildMask{
   305  			Fields: &fieldmaskpb.FieldMask{
   306  				Paths: []string{
   307  					"builder",
   308  					"infra",
   309  					"input",
   310  					"scheduling_timeout",
   311  					"execution_timeout",
   312  					"grace_period",
   313  					"exe",
   314  					"tags",
   315  				},
   316  			},
   317  		},
   318  	})
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	return bbagentArgsFromBuild(bld), nil
   323  }
   324  
   325  // TODO(crbug.com/1098551): Invert this and make led use the build proto directly.
   326  func bbagentArgsFromBuild(bld *bbpb.Build) *bbpb.BBAgentArgs {
   327  	return &bbpb.BBAgentArgs{
   328  		PayloadPath:            bld.Infra.Bbagent.PayloadPath,
   329  		CacheDir:               bld.Infra.Bbagent.CacheDir,
   330  		KnownPublicGerritHosts: bld.Infra.Buildbucket.KnownPublicGerritHosts,
   331  		Build:                  bld,
   332  	}
   333  }
   334  
   335  // FromBuild generates a new job.Definition using the provided Build.
   336  func FromBuild(build *bbpb.Build, hostname, name string, priorityDiff int, extraTags []string) *job.Definition {
   337  	ret := &job.Definition{}
   338  
   339  	setPriority(build, priorityDiff)
   340  
   341  	// Attach tags.
   342  	tags := build.Tags
   343  	tags = append(tags, &bbpb.StringPair{
   344  		Key:   "led-job-name",
   345  		Value: name,
   346  	})
   347  	tags = append(tags, &bbpb.StringPair{
   348  		Key:   "user_agent",
   349  		Value: "led",
   350  	})
   351  	for _, tag := range extraTags {
   352  		k, v := strpair.Parse(tag)
   353  		tags = append(tags, &bbpb.StringPair{
   354  			Key:   k,
   355  			Value: v,
   356  		})
   357  	}
   358  	sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key })
   359  	build.Tags = tags
   360  
   361  	// Set buildbucket hostname.
   362  	if build.Infra.Buildbucket.Hostname == "" {
   363  		build.Infra.Buildbucket.Hostname = hostname
   364  	}
   365  
   366  	// Set build to be experimental.
   367  	build.Input.Experimental = true // Legacy field, set it for now.
   368  	enabled := stringset.NewFromSlice(build.Input.Experiments...)
   369  	enabled.Add(buildbucket.ExperimentNonProduction)
   370  	build.Input.Experiments = enabled.ToSortedSlice()
   371  
   372  	build.Infra.Buildbucket.ExperimentReasons[buildbucket.ExperimentNonProduction] = bbpb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED
   373  	ret.JobType = &job.Definition_Buildbucket{
   374  		Buildbucket: &job.Buildbucket{
   375  			Name:                name,
   376  			FinalBuildProtoPath: "build.proto.json",
   377  			BbagentArgs: &bbpb.BBAgentArgs{
   378  				Build: build,
   379  			},
   380  			RealBuild: true,
   381  		},
   382  	}
   383  
   384  	return ret
   385  }