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 }