go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/rpc/v0/tryjobs.go (about) 1 // Copyright 2023 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 20 "google.golang.org/grpc/codes" 21 22 bbutil "go.chromium.org/luci/buildbucket/protoutil" 23 "go.chromium.org/luci/common/logging" 24 "go.chromium.org/luci/grpc/appstatus" 25 26 cfgpb "go.chromium.org/luci/cv/api/config/v2" 27 apiv0pb "go.chromium.org/luci/cv/api/v0" 28 "go.chromium.org/luci/cv/internal/configs/prjcfg" 29 "go.chromium.org/luci/cv/internal/rpc/versioning" 30 "go.chromium.org/luci/cv/internal/run" 31 "go.chromium.org/luci/cv/internal/tryjob" 32 ) 33 34 func makeTryjobInvocations(ctx context.Context, r *run.Run) ([]*apiv0pb.TryjobInvocation, error) { 35 if r.Tryjobs.GetState() == nil { 36 return nil, nil // early return if there is no tryjob 37 } 38 // TODO(yiwzhang): store the config group ID in the 39 // r.Tryjobs.State.Requirement. Using r.ConfigGroupID has a slight chance 40 // causing unexpected behavior. For example: 41 // T1: config is at revision X and builder A is triggered 42 // T2: config gets a new revision X+1 and builder A is removed and the 43 // run is cancelled due to config change. 44 // T3: user loads the cancelled run. r.Tryjobs may contain information about 45 // triggering builder A. However, the config associated with the run 46 // no longer has builder A definition. Causing error in a later stage. 47 // Saving the config group ID (i.e. pinned at the revision) that is used to 48 // compute the requirement inside the requirement would solve the problem. 49 cg, err := prjcfg.GetConfigGroup(ctx, r.ID.LUCIProject(), r.ConfigGroupID) 50 if err != nil { 51 return nil, appstatus.Attachf(err, codes.Internal, "failed to load config group for the run") 52 } 53 builderByName := computeBuilderByName(cg) 54 ret := make([]*apiv0pb.TryjobInvocation, 0, len(r.Tryjobs.GetState().GetExecutions())) 55 for i, execution := range r.Tryjobs.GetState().GetExecutions() { 56 ti := &apiv0pb.TryjobInvocation{} 57 definition := r.Tryjobs.GetState().GetRequirement().GetDefinitions()[i] 58 switch definition.GetBackend().(type) { 59 case *tryjob.Definition_Buildbucket_: 60 builderName := bbutil.FormatBuilderID(definition.GetBuildbucket().GetBuilder()) 61 builderConfig, ok := builderByName[builderName] 62 if !ok { 63 // Error out once the TODO above is implemented 64 logging.Warningf(ctx, "builder %q is triggered by LUCI CV but can NOT be found in the config. Skip including the builder in the response", builderName) 65 continue 66 } 67 ti.BuilderConfig = builderConfig 68 default: 69 return nil, appstatus.Errorf(codes.Unimplemented, "unknown tryjob backend %T", definition.GetBackend()) 70 } 71 ti.Critical = definition.GetCritical() 72 if len(execution.GetAttempts()) == 0 { 73 logging.Errorf(ctx, "NEEDS INVESTIGATION: tryjob execution has empty attempts. tryjob definition: %s", definition) 74 continue 75 } 76 ti.Attempts = make([]*apiv0pb.TryjobInvocation_Attempt, len(execution.GetAttempts())) 77 for i, attempt := range execution.GetAttempts() { 78 // attempts in execution are sorted with earliest first and the response 79 // need to be sorted with latest first. 80 var err error 81 ti.Attempts[len(execution.GetAttempts())-1-i], err = makeTryjobAttempt(attempt, definition) 82 if err != nil { 83 return nil, err 84 } 85 } 86 ti.Status = ti.Attempts[0].GetStatus() 87 ret = append(ret, ti) 88 } 89 return ret, nil 90 } 91 92 func computeBuilderByName(cg *prjcfg.ConfigGroup) map[string]*cfgpb.Verifiers_Tryjob_Builder { 93 ret := make(map[string]*cfgpb.Verifiers_Tryjob_Builder, len(cg.Content.GetVerifiers().GetTryjob().GetBuilders())) 94 for _, b := range cg.Content.GetVerifiers().GetTryjob().GetBuilders() { 95 ret[b.GetName()] = b 96 if equiName := b.GetEquivalentTo().GetName(); equiName != "" { 97 ret[equiName] = b // map equivalent builder name to this builder too. 98 } 99 } 100 return ret 101 } 102 103 func makeTryjobAttempt(attempt *tryjob.ExecutionState_Execution_Attempt, def *tryjob.Definition) (*apiv0pb.TryjobInvocation_Attempt, error) { 104 ret := &apiv0pb.TryjobInvocation_Attempt{} 105 if attempt.GetResult() != nil { 106 switch attempt.GetResult().GetBackend().(type) { 107 case *tryjob.Result_Buildbucket_: 108 bbResult := attempt.GetResult().GetBuildbucket() 109 ret.Result = &apiv0pb.TryjobResult{ 110 Backend: &apiv0pb.TryjobResult_Buildbucket_{ 111 Buildbucket: &apiv0pb.TryjobResult_Buildbucket{ 112 Host: def.GetBuildbucket().GetHost(), 113 Id: bbResult.GetId(), 114 Builder: bbResult.GetBuilder(), 115 }, 116 }, 117 } 118 default: 119 return nil, appstatus.Errorf(codes.Unimplemented, "unknown tryjob backend %T", attempt.GetResult().GetBackend()) 120 } 121 } 122 ret.Reuse = attempt.GetReused() 123 ret.Status = versioning.TryjobStatusV0(attempt.GetStatus(), attempt.Result.GetStatus()) 124 return ret, nil 125 }