| // Copyright 2020 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package jobcreate |
| |
| import ( |
| "context" |
| "net/http" |
| "path" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "google.golang.org/protobuf/types/known/fieldmaskpb" |
| "google.golang.org/protobuf/types/known/structpb" |
| |
| "go.chromium.org/luci/buildbucket" |
| "go.chromium.org/luci/buildbucket/cmd/bbagent/bbinput" |
| bbpb "go.chromium.org/luci/buildbucket/proto" |
| "go.chromium.org/luci/common/data/stringset" |
| "go.chromium.org/luci/common/data/strpair" |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/grpc/prpc" |
| "go.chromium.org/luci/led/job" |
| swarmingpb "go.chromium.org/luci/swarming/proto/api_v2" |
| ) |
| |
| // Returns "bbagent", "kitchen" or "raw" depending on the type of task detected. |
| func detectMode(r *swarmingpb.NewTaskRequest) string { |
| arg0, ts := "", r.TaskSlices[0] |
| if ts.Properties != nil { |
| if len(ts.Properties.Command) > 0 { |
| arg0 = ts.Properties.Command[0] |
| } |
| } |
| switch arg0 { |
| case "bbagent${EXECUTABLE_SUFFIX}": |
| return "bbagent" |
| case "kitchen${EXECUTABLE_SUFFIX}": |
| return "kitchen" |
| } |
| return "raw" |
| } |
| |
| // setPriority mutates the provided build to set the priority of its underlying |
| // swarming task. |
| // |
| // The priority for buildbucket type tasks is between 20 to 255. |
| func setPriority(build *bbpb.Build, priorityDiff int) { |
| calPriority := func(originalPriority int32) int32 { |
| switch priority := originalPriority + int32(priorityDiff); { |
| case priority < 20: |
| return 20 |
| case priority > 255: |
| return 255 |
| default: |
| return priority |
| } |
| } |
| |
| if build.Infra.Swarming != nil { |
| build.Infra.Swarming.Priority = calPriority(build.Infra.Swarming.Priority) |
| } else { |
| config := build.Infra.Backend.GetConfig().GetFields() |
| newPriority := calPriority(int32(config["priority"].GetNumberValue())) |
| build.Infra.Backend.Config.Fields["priority"] = structpb.NewNumberValue(float64(newPriority)) |
| } |
| } |
| |
| // FromNewTaskRequest generates a new job.Definition by parsing the |
| // given NewTaskRequest. |
| // |
| // If the task's first slice looks like either a bbagent or kitchen-based |
| // Buildbucket task, the returned Definition will have the `buildbucket` |
| // field populated, otherwise the `swarming` field will be populated. |
| 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) { |
| if len(r.TaskSlices) == 0 { |
| return nil, errors.New("swarming tasks without task slices are not supported") |
| } |
| |
| ret = &job.Definition{} |
| name = "led: " + name |
| |
| switch detectMode(r) { |
| case "bbagent": |
| bb := &job.Buildbucket{} |
| ret.JobType = &job.Definition_Buildbucket{Buildbucket: bb} |
| // TODO(crbug.com/1219018): use bbCommonFromTaskRequest only in the long |
| // bbagent arg case. |
| // Discussion: https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/3511002/comments/0daf496b_2c8ba5a2 |
| bbCommonFromTaskRequest(bb, r) |
| cmd := r.TaskSlices[0].Properties.Command |
| switch { |
| case len(cmd) == 2: |
| bb.BbagentArgs, err = bbinput.Parse(cmd[len(cmd)-1]) |
| bb.UpdateBuildFromBbagentArgs() |
| case bld != nil: |
| bb.BbagentArgs = bbagentArgsFromBuild(bld) |
| default: |
| bb.BbagentArgs, err = getBbagentArgsFromCMD(ctx, cmd, authClient) |
| bb.UpdateBuildFromBbagentArgs() |
| } |
| |
| // This check is only here because of bbCommonFromTaskRequest. |
| // TODO(crbug.com/1219018): remove this check after bbCommonFromTaskRequest |
| // is only used for long bbagent arg case. |
| if bb.BbagentDownloadCIPDPkgs() { |
| bb.CipdPackages = nil |
| } |
| |
| case "kitchen": |
| bb := &job.Buildbucket{LegacyKitchen: true} |
| ret.JobType = &job.Definition_Buildbucket{Buildbucket: bb} |
| bbCommonFromTaskRequest(bb, r) |
| err = ks.FromSwarmingV2(ctx, r, bb) |
| |
| case "raw": |
| // non-Buildbucket Swarming task |
| sw := &job.Swarming{Hostname: swarmingHost} |
| ret.JobType = &job.Definition_Swarming{Swarming: sw} |
| jobDefinitionFromSwarming(sw, r) |
| sw.Task.Name = name |
| |
| default: |
| panic("impossible") |
| } |
| |
| if bb := ret.GetBuildbucket(); err == nil && bb != nil { |
| bb.Name = name |
| bb.FinalBuildProtoPath = "build.proto.json" |
| |
| // set all buildbucket type tasks to experimental by default. |
| bb.BbagentArgs.Build.Input.Experimental = true |
| |
| setPriority(bb.BbagentArgs.Build, priorityDiff) |
| |
| // clear fields which don't make sense |
| bb.BbagentArgs.Build.CanceledBy = "" |
| bb.BbagentArgs.Build.CreatedBy = "" |
| bb.BbagentArgs.Build.CreateTime = nil |
| bb.BbagentArgs.Build.Id = 0 |
| bb.BbagentArgs.Build.Infra.Buildbucket.Hostname = "" |
| bb.BbagentArgs.Build.Infra.Buildbucket.RequestedProperties = nil |
| bb.BbagentArgs.Build.Infra.Logdog.Prefix = "" |
| bb.BbagentArgs.Build.Infra.Swarming.TaskId = "" |
| bb.BbagentArgs.Build.Number = 0 |
| bb.BbagentArgs.Build.Status = 0 |
| bb.BbagentArgs.Build.UpdateTime = nil |
| |
| bb.BbagentArgs.Build.Tags = nil |
| if len(extraTags) > 0 { |
| tags := make([]*bbpb.StringPair, 0, len(extraTags)) |
| for _, tag := range extraTags { |
| k, v := strpair.Parse(tag) |
| tags = append(tags, &bbpb.StringPair{ |
| Key: k, |
| Value: v, |
| }) |
| } |
| sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key }) |
| bb.BbagentArgs.Build.Tags = tags |
| } |
| |
| // drop the executable path; it's canonically represented by |
| // out.BBAgentArgs.PayloadPath and out.BBAgentArgs.Build.Exe. |
| if exePath := bb.BbagentArgs.ExecutablePath; exePath != "" { |
| // convert to new mode |
| payload, arg := path.Split(exePath) |
| bb.BbagentArgs.ExecutablePath = "" |
| bb.UpdatePayloadPath(strings.TrimSuffix(payload, "/")) |
| bb.BbagentArgs.Build.Exe.Cmd = []string{arg} |
| } |
| |
| if !bb.BbagentDownloadCIPDPkgs() { |
| dropRecipePackage(&bb.CipdPackages, bb.PayloadPath()) |
| } |
| |
| props := bb.BbagentArgs.GetBuild().GetInput().GetProperties() |
| // everything in here is reflected elsewhere in the Build and will be |
| // re-synthesized by kitchen support or the recipe engine itself, depending |
| // on the final kitchen/bbagent execution mode. |
| delete(props.GetFields(), "$recipe_engine/runtime") |
| |
| // drop legacy recipe fields |
| if recipe := bb.BbagentArgs.Build.Infra.Recipe; recipe != nil { |
| bb.BbagentArgs.Build.Infra.Recipe = nil |
| } |
| } |
| |
| // ensure isolate/rbe-cas source consistency |
| casUserPayload := &swarmingpb.CASReference{ |
| Digest: &swarmingpb.Digest{}, |
| } |
| for i, slice := range r.TaskSlices { |
| if cir := slice.Properties.CasInputRoot; cir != nil { |
| if err := populateCasPayload(casUserPayload, cir); err != nil { |
| return nil, errors.Annotate(err, "task slice %d", i).Err() |
| } |
| } |
| } |
| if casUserPayload.Digest.GetHash() == "" { |
| return ret, err |
| } |
| |
| if ret.GetSwarming() != nil { |
| ret.GetSwarming().CasUserPayload = casUserPayload |
| } |
| if ret.GetBuildbucket() != nil { |
| // `led get-builder` is still using swarmingbucket.get_task_def, so |
| // we need to fill in the data to ret.GetBuildbucket() for its builds. |
| // TODO(crbug.com/1345722): remove this after we migrate away from |
| // swarmingbucket.get_task_def. |
| payloadPath := ret.GetBuildbucket().BbagentArgs.PayloadPath |
| updates := &bbpb.BuildInfra_Buildbucket_Agent{ |
| Input: &bbpb.BuildInfra_Buildbucket_Agent_Input{ |
| Data: map[string]*bbpb.InputDataRef{ |
| payloadPath: { |
| DataType: &bbpb.InputDataRef_Cas{ |
| Cas: &bbpb.InputDataRef_CAS{ |
| CasInstance: casUserPayload.GetCasInstance(), |
| Digest: &bbpb.InputDataRef_CAS_Digest{ |
| Hash: casUserPayload.GetDigest().GetHash(), |
| SizeBytes: casUserPayload.GetDigest().GetSizeBytes(), |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| Purposes: map[string]bbpb.BuildInfra_Buildbucket_Agent_Purpose{ |
| payloadPath: bbpb.BuildInfra_Buildbucket_Agent_PURPOSE_EXE_PAYLOAD, |
| }, |
| } |
| ret.GetBuildbucket().UpdateBuildbucketAgent(updates) |
| } |
| |
| return ret, err |
| } |
| |
| func populateCasPayload(cas *swarmingpb.CASReference, cir *swarmingpb.CASReference) error { |
| if cas.CasInstance == "" { |
| cas.CasInstance = cir.CasInstance |
| } else if cas.CasInstance != cir.CasInstance { |
| return errors.Reason("RBE-CAS instance inconsistency: %q != %q", cas.CasInstance, cir.CasInstance).Err() |
| } |
| |
| if cas.Digest.Hash != "" && (cir.Digest == nil || cir.Digest.Hash != cas.Digest.Hash) { |
| return errors.Reason("RBE-CAS digest hash inconsistency: %+v != %+v", cas.Digest, cir.Digest).Err() |
| } else if cir.Digest != nil { |
| cas.Digest.Hash = cir.Digest.Hash |
| } |
| |
| if cas.Digest.SizeBytes != 0 && (cir.Digest == nil || cir.Digest.SizeBytes != cas.Digest.SizeBytes) { |
| return errors.Reason("RBE-CAS digest size bytes inconsistency: %+v != %+v", cas.Digest, cir.Digest).Err() |
| } else if cir.Digest != nil { |
| cas.Digest.SizeBytes = cir.Digest.SizeBytes |
| } |
| |
| return nil |
| } |
| |
| func getBbagentArgsFromCMD(ctx context.Context, cmd []string, authClient *http.Client) (*bbpb.BBAgentArgs, error) { |
| var hostname string |
| var bID int64 |
| for i, s := range cmd { |
| switch { |
| case s == "-host" && i < len(cmd)-1: |
| hostname = cmd[i+1] |
| case s == "-build-id" && i < len(cmd)-1: |
| var err error |
| if bID, err = strconv.ParseInt(cmd[i+1], 10, 64); err != nil { |
| return nil, errors.Annotate(err, "cmd -build-id").Err() |
| } |
| } |
| } |
| if hostname == "" && bID == 0 { |
| // This could happen if the cmd was for a led build like |
| // `bbagent${EXECUTABLE_SUFFIX} --output ${ISOLATED_OUTDIR}/build.proto.json <encoded bbinput>` |
| return bbinput.Parse(cmd[len(cmd)-1]) |
| } |
| if hostname == "" { |
| return nil, errors.New("host is required in cmd") |
| } |
| if bID == 0 { |
| return nil, errors.New("build-id is required in cmd") |
| } |
| bbclient := bbpb.NewBuildsPRPCClient(&prpc.Client{ |
| C: authClient, |
| Host: hostname, |
| }) |
| bld, err := bbclient.GetBuild(ctx, &bbpb.GetBuildRequest{ |
| Id: bID, |
| Mask: &bbpb.BuildMask{ |
| Fields: &fieldmaskpb.FieldMask{ |
| Paths: []string{ |
| "builder", |
| "infra", |
| "input", |
| "scheduling_timeout", |
| "execution_timeout", |
| "grace_period", |
| "exe", |
| "tags", |
| }, |
| }, |
| }, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| return bbagentArgsFromBuild(bld), nil |
| } |
| |
| // TODO(crbug.com/1098551): Invert this and make led use the build proto directly. |
| func bbagentArgsFromBuild(bld *bbpb.Build) *bbpb.BBAgentArgs { |
| return &bbpb.BBAgentArgs{ |
| PayloadPath: bld.Infra.Bbagent.PayloadPath, |
| CacheDir: bld.Infra.Bbagent.CacheDir, |
| KnownPublicGerritHosts: bld.Infra.Buildbucket.KnownPublicGerritHosts, |
| Build: bld, |
| } |
| } |
| |
| // FromBuild generates a new job.Definition using the provided Build. |
| func FromBuild(build *bbpb.Build, hostname, name string, priorityDiff int, extraTags []string) *job.Definition { |
| ret := &job.Definition{} |
| |
| setPriority(build, priorityDiff) |
| |
| // Attach tags. |
| tags := build.Tags |
| tags = append(tags, &bbpb.StringPair{ |
| Key: "led-job-name", |
| Value: name, |
| }) |
| tags = append(tags, &bbpb.StringPair{ |
| Key: "user_agent", |
| Value: "led", |
| }) |
| for _, tag := range extraTags { |
| k, v := strpair.Parse(tag) |
| tags = append(tags, &bbpb.StringPair{ |
| Key: k, |
| Value: v, |
| }) |
| } |
| sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key }) |
| build.Tags = tags |
| |
| // Set buildbucket hostname. |
| if build.Infra.Buildbucket.Hostname == "" { |
| build.Infra.Buildbucket.Hostname = hostname |
| } |
| |
| // Set build to be experimental. |
| build.Input.Experimental = true // Legacy field, set it for now. |
| enabled := stringset.NewFromSlice(build.Input.Experiments...) |
| enabled.Add(buildbucket.ExperimentNonProduction) |
| build.Input.Experiments = enabled.ToSortedSlice() |
| |
| build.Infra.Buildbucket.ExperimentReasons[buildbucket.ExperimentNonProduction] = bbpb.BuildInfra_Buildbucket_EXPERIMENT_REASON_REQUESTED |
| ret.JobType = &job.Definition_Buildbucket{ |
| Buildbucket: &job.Buildbucket{ |
| Name: name, |
| FinalBuildProtoPath: "build.proto.json", |
| BbagentArgs: &bbpb.BBAgentArgs{ |
| Build: build, |
| }, |
| RealBuild: true, |
| }, |
| } |
| |
| return ret |
| } |