go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/rpc/v0/search_runs.go (about) 1 // Copyright 2022 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 "go.chromium.org/luci/grpc/appstatus" 23 24 "go.chromium.org/luci/common/logging" 25 "go.chromium.org/luci/common/sync/parallel" 26 apiv0pb "go.chromium.org/luci/cv/api/v0" 27 "go.chromium.org/luci/cv/internal/acls" 28 "go.chromium.org/luci/cv/internal/changelist" 29 "go.chromium.org/luci/cv/internal/common" 30 "go.chromium.org/luci/cv/internal/rpc/pagination" 31 "go.chromium.org/luci/cv/internal/run" 32 "go.chromium.org/luci/cv/internal/run/runquery" 33 ) 34 35 // Default and max numbers of result paging. 36 // If you update either of these values, please update the service proto. 37 38 const defaultPageSize = 32 39 const maxPageSize = 128 40 41 // SearchRuns implements RunsServer; it fetches multiple Runs given search criteria. 42 func (s *RunsServer) SearchRuns(ctx context.Context, req *apiv0pb.SearchRunsRequest) (resp *apiv0pb.SearchRunsResponse, err error) { 43 defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }() 44 if err = checkCanUseAPI(ctx, "SearchRuns"); err != nil { 45 return 46 } 47 limit, err := pagination.ValidatePageSize(req, defaultPageSize, maxPageSize) 48 if err != nil { 49 return nil, err 50 } 51 var pt *runquery.PageToken 52 if s := req.GetPageToken(); s != "" { 53 pt = &runquery.PageToken{} 54 if err := pagination.DecryptPageToken(ctx, s, pt); err != nil { 55 return nil, err 56 } 57 } 58 59 if req.GetPredicate() == nil { 60 return nil, appstatus.Errorf(codes.InvalidArgument, "Predicate is required") 61 } 62 pred := req.GetPredicate() 63 project := pred.GetProject() 64 if project == "" { 65 return nil, appstatus.Errorf(codes.InvalidArgument, "Project is required") 66 } 67 switch ok, err := acls.CheckProjectAccess(ctx, project); { 68 case err != nil: 69 return nil, err 70 case !ok: 71 // Return empty response error in the case of access denied. 72 // 73 // Rationale: the caller shouldn't be able to distinguish between 74 // not having access to the project, and project not existing. 75 // This is similar to the case of when a CL is not found. 76 return &apiv0pb.SearchRunsResponse{}, nil 77 } 78 79 var qb interface { 80 LoadRuns(context.Context, ...run.LoadRunChecker) ([]*run.Run, *runquery.PageToken, error) 81 } 82 if gcs := pred.GetGerritChanges(); len(gcs) == 0 { 83 qb = runquery.ProjectQueryBuilder{ 84 Project: project, 85 Limit: limit, 86 }.PageToken(pt) 87 } else { 88 eids := make([]changelist.ExternalID, len(gcs)) 89 for i, gc := range gcs { 90 if gc.Patchset != 0 { 91 return nil, appstatus.Errorf(codes.InvalidArgument, "Patchset is disallowed in GerritChange %v", gc) 92 } 93 eids[i], err = changelist.GobID(gc.Host, gc.Change) 94 if err != nil { 95 return nil, appstatus.Errorf(codes.InvalidArgument, "invalid GerritChange %v: %s", gc, err) 96 } 97 } 98 // Look up CLIDs from CL ExternalIDs. 99 clids, err := changelist.Lookup(ctx, eids) 100 if err != nil { 101 return nil, err 102 } 103 // changelist.Lookup returns 0 for unknown external ID, i.e. CL not found. 104 // In that case, no Runs match; return empty response. 105 for _, clid := range clids { 106 if clid == 0 { 107 return &apiv0pb.SearchRunsResponse{}, nil 108 } 109 } 110 additionalCLIDs := make(common.CLIDsSet, len(clids)-1) 111 for _, clid := range clids[1:] { 112 additionalCLIDs.Add(clid) 113 } 114 qb = runquery.CLQueryBuilder{ 115 CLID: clids[0], 116 AdditionalCLIDs: additionalCLIDs, 117 Project: project, 118 Limit: limit, 119 }.PageToken(pt) 120 } 121 runs, nextPageToken, err := qb.LoadRuns(ctx, acls.NewRunReadChecker()) 122 if err != nil { 123 return nil, err 124 } 125 126 // Convert run.Runs to apiv0pb.Runs. 127 respRuns, err := populateRuns(ctx, runs) 128 if err != nil { 129 return nil, err 130 } 131 132 encryptedNextPageToken, err := pagination.EncryptPageToken(ctx, nextPageToken) 133 if err != nil { 134 return nil, err 135 } 136 return &apiv0pb.SearchRunsResponse{ 137 Runs: respRuns, 138 NextPageToken: encryptedNextPageToken, 139 }, nil 140 } 141 142 // populateRuns converts run.Runs to apiv0pb.Runs for the response. 143 // 144 // This includes fetching and populating extra information, including RunCLs 145 // and Tryjobs, to fill in details in each apiv0pb.Run. 146 // 147 // TODO(qyearsley): Consider optimizing this by using datastore.Get batch 148 // calls, e.g. for fetching all Tryjob entities with one call. 149 func populateRuns(ctx context.Context, runs []*run.Run) ([]*apiv0pb.Run, error) { 150 respRuns := make([]*apiv0pb.Run, len(runs)) 151 errs := parallel.WorkPool(min(len(runs), 16), func(work chan<- func() error) { 152 for i, r := range runs { 153 i, r := i, r 154 work <- func() (err error) { 155 ctx := logging.SetField(ctx, "run", r.ID) 156 respRuns[i], err = populateRunResponse(ctx, r) 157 return err 158 } 159 } 160 }) 161 return respRuns, common.MostSevereError(errs) 162 }