go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/rpc/v0/get_cl_run_info.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 "fmt" 20 "sort" 21 "strings" 22 "sync" 23 24 "golang.org/x/sync/errgroup" 25 "google.golang.org/grpc/codes" 26 27 "go.chromium.org/luci/auth/identity" 28 "go.chromium.org/luci/common/logging" 29 gerritpb "go.chromium.org/luci/common/proto/gerrit" 30 "go.chromium.org/luci/gae/service/datastore" 31 "go.chromium.org/luci/grpc/appstatus" 32 "go.chromium.org/luci/server/gerritauth" 33 34 apiv0pb "go.chromium.org/luci/cv/api/v0" 35 "go.chromium.org/luci/cv/internal/changelist" 36 "go.chromium.org/luci/cv/internal/common" 37 "go.chromium.org/luci/cv/internal/run" 38 "go.chromium.org/luci/cv/internal/run/runquery" 39 ) 40 41 // GetCLRunInfo implements GerritIntegrationServer; it returns ongoing Run information related to the given CL. 42 func (g *GerritIntegrationServer) GetCLRunInfo(ctx context.Context, req *apiv0pb.GetCLRunInfoRequest) (resp *apiv0pb.GetCLRunInfoResponse, err error) { 43 defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }() 44 gc := req.GetGerritChange() 45 eid, err := changelist.GobID(gc.GetHost(), gc.GetChange()) 46 if err != nil { 47 return nil, appstatus.Errorf(codes.InvalidArgument, "invalid GerritChange %v: %s", gc, err) 48 } 49 50 if gerritInfo := gerritauth.GetAssertedInfo(ctx); gerritInfo == nil { 51 if err := checkCanUseAPI(ctx, "GetCLRunInfo"); err != nil { 52 return nil, err 53 } 54 } else { 55 // If Gerrit JWT was provided, check that it matches the request change. 56 // The JWT-provided host does not include the "-review" prefix. 57 jwtChange := gerritInfo.Change 58 if jwtChange.Host+"-review.googlesource.com" != gc.GetHost() || jwtChange.ChangeNumber != gc.GetChange() { 59 return nil, appstatus.Errorf(codes.InvalidArgument, "JWT change does not match GerritChange %v: got %s/%d", gc, jwtChange.Host, jwtChange.ChangeNumber) 60 } 61 62 preferredEmail := gerritInfo.User.PreferredEmail 63 if preferredEmail == "" { 64 logging.Warningf(ctx, "jwt provided but user preferred email is missing") 65 return &apiv0pb.GetCLRunInfoResponse{}, nil 66 } 67 userID, err := identity.MakeIdentity(fmt.Sprintf("%s:%s", identity.User, preferredEmail)) 68 if err != nil { 69 return nil, appstatus.Attachf(err, codes.InvalidArgument, "failed to construct user identity") 70 } 71 if !common.IsInstantTriggerDogfooder(ctx, userID) { 72 return &apiv0pb.GetCLRunInfoResponse{}, nil 73 } 74 } 75 76 cl, err := eid.Load(ctx) 77 switch { 78 case err != nil: 79 return nil, err 80 case cl == nil: 81 return nil, appstatus.Errorf(codes.NotFound, "change %s not found", eid) 82 } 83 84 qb := runquery.CLQueryBuilder{CLID: cl.ID} 85 runs, _, err := qb.LoadRuns(ctx) 86 if err != nil { 87 return nil, err 88 } 89 90 respRunInfo, err := populateRunInfo(ctx, filterOngoingRuns(runs)) 91 if err != nil { 92 return nil, err 93 } 94 95 depChangeInfos, err := queryDepChangeInfos(ctx, cl) 96 if err != nil { 97 return nil, err 98 } 99 100 return &apiv0pb.GetCLRunInfoResponse{ 101 // TODO(crbug.com/1486976): Split RunInfo into RunsAsOrigin and RunsAsDep. 102 RunsAsOrigin: respRunInfo, 103 RunsAsDep: respRunInfo, 104 DepChangeInfos: depChangeInfos, 105 }, nil 106 } 107 108 // queryDepChangeInfos queries for dependent CLs. 109 func queryDepChangeInfos(ctx context.Context, cl *changelist.CL) ([]*apiv0pb.GetCLRunInfoResponse_DepChangeInfo, error) { 110 if len(cl.Snapshot.Deps) == 0 { 111 return nil, nil 112 } 113 114 eg, ectx := errgroup.WithContext(ctx) 115 eg.SetLimit(8) 116 infos := make([]*apiv0pb.GetCLRunInfoResponse_DepChangeInfo, 0, len(cl.Snapshot.Deps)) 117 var infosMu sync.Mutex 118 for _, dep := range cl.Snapshot.Deps { 119 dep := dep // See https://go.dev/blog/loopvar-preview for why this is needed before Go 1.22. 120 121 eg.Go(func() error { 122 depClid := common.CLID(dep.Clid) 123 124 depCl := &changelist.CL{ID: depClid} 125 if err := datastore.Get(ectx, depCl); err != nil { 126 return err 127 } 128 gerrit := depCl.Snapshot.GetGerrit() 129 switch { 130 case gerrit == nil: 131 return fmt.Errorf("dep CL %d has non-Gerrit snapshot", depClid) 132 case gerrit.GetInfo().GetStatus() != gerritpb.ChangeStatus_NEW: 133 return nil // only returns active CLs 134 } 135 136 // Query for runs. 137 qb := runquery.CLQueryBuilder{CLID: depClid} 138 runs, _, err := qb.LoadRuns(ectx) 139 if err != nil { 140 return nil 141 } 142 runInfo, err := populateRunInfo(ectx, filterOngoingRuns(runs)) 143 if err != nil { 144 return err 145 } 146 infosMu.Lock() 147 infos = append(infos, &apiv0pb.GetCLRunInfoResponse_DepChangeInfo{ 148 GerritChange: &apiv0pb.GerritChange{ 149 Host: gerrit.Host, 150 Change: gerrit.Info.Number, 151 Patchset: depCl.Snapshot.Patchset, 152 }, 153 Runs: runInfo, 154 ChangeOwner: gerrit.Info.Owner.Email, 155 }) 156 infosMu.Unlock() 157 return nil 158 }) 159 } 160 if err := eg.Wait(); err != nil { 161 return nil, appstatus.Errorf(codes.Internal, "%s", err) 162 } 163 164 sort.Slice(infos, func(i, j int) bool { 165 if infos[i].GetGerritChange().GetHost() == infos[j].GetGerritChange().GetHost() { 166 return infos[i].GetGerritChange().GetChange() < infos[j].GetGerritChange().GetChange() 167 } 168 return strings.Compare(infos[i].GetGerritChange().GetHost(), infos[j].GetGerritChange().GetHost()) < 0 169 }) 170 return infos, nil 171 } 172 173 // filterOngoingRuns filters out ended runs. 174 func filterOngoingRuns(runs []*run.Run) []*run.Run { 175 ongoingRuns := []*run.Run{} 176 for _, r := range runs { 177 if !run.IsEnded(r.Status) { 178 ongoingRuns = append(ongoingRuns, r) 179 } 180 } 181 return ongoingRuns 182 }