blob: c8bb76c04bc8c4a94dd5b298f9651a92ced90fa5 [file] [log] [blame]
// Copyright 2022 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 rpc
import (
"context"
"google.golang.org/grpc/codes"
"go.chromium.org/luci/grpc/appstatus"
"go.chromium.org/luci/common/sync/parallel"
apiv0pb "go.chromium.org/luci/cv/api/v0"
"go.chromium.org/luci/cv/internal/acls"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/rpc/pagination"
"go.chromium.org/luci/cv/internal/run"
)
// Default and max numbers of result paging.
// If you update either of these values, please update the service proto.
const defaultPageSize = 32
const maxPageSize = 128
// SearchRuns implements RunsServer; it fetches multiple Runs given search criteria.
func (s *RunsServer) SearchRuns(ctx context.Context, req *apiv0pb.SearchRunsRequest) (resp *apiv0pb.SearchRunsResponse, err error) {
defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
if err = checkCanUseAPI(ctx, "SearchRuns"); err != nil {
return
}
limit, err := pagination.ValidatePageSize(req, defaultPageSize, maxPageSize)
if err != nil {
return nil, err
}
var pt *run.PageToken
if s := req.GetPageToken(); s != "" {
pt = &run.PageToken{}
if err := pagination.DecryptPageToken(ctx, s, pt); err != nil {
return nil, err
}
}
if req.GetPredicate() == nil {
return nil, appstatus.Errorf(codes.InvalidArgument, "Predicate is required")
}
pred := req.GetPredicate()
project := pred.GetProject()
if project == "" {
return nil, appstatus.Errorf(codes.InvalidArgument, "Project is required")
}
switch ok, err := acls.CheckProjectAccess(ctx, project); {
case err != nil:
return nil, err
case !ok:
// Return empty response error in the case of access denied.
//
// Rationale: the caller shouldn't be able to distinguish between
// not having access to the project, and project not existing.
// This is similar to the case of when a CL is not found.
return &apiv0pb.SearchRunsResponse{}, nil
}
var qb interface {
LoadRuns(context.Context, ...run.LoadRunChecker) ([]*run.Run, *run.PageToken, error)
}
if gcs := pred.GetGerritChanges(); len(gcs) == 0 {
qb = run.ProjectQueryBuilder{
Project: project,
Limit: limit,
}.PageToken(pt)
} else {
eids := make([]changelist.ExternalID, len(gcs))
for i, gc := range gcs {
if gc.Patchset != 0 {
return nil, appstatus.Errorf(codes.InvalidArgument, "Patchset is disallowed in GerritChange %v", gc)
}
eids[i], err = changelist.GobID(gc.Host, gc.Change)
if err != nil {
return nil, appstatus.Errorf(codes.InvalidArgument, "invalid GerritChange %v: %s", gc, err)
}
}
// Look up CLIDs from CL ExternalIDs.
clids, err := changelist.Lookup(ctx, eids)
if err != nil {
return nil, err
}
// changelist.Lookup returns 0 for unknown external ID, i.e. CL not found.
// In that case, no Runs match; return empty response.
for _, clid := range clids {
if clid == 0 {
return &apiv0pb.SearchRunsResponse{}, nil
}
}
additionalCLIDs := make(common.CLIDsSet, len(clids)-1)
for _, clid := range clids[1:] {
additionalCLIDs.Add(clid)
}
qb = run.CLQueryBuilder{
CLID: clids[0],
AdditionalCLIDs: additionalCLIDs,
Project: project,
Limit: limit,
}.PageToken(pt)
}
runs, nextPageToken, err := qb.LoadRuns(ctx, acls.NewRunReadChecker())
if err != nil {
return nil, err
}
// Convert run.Runs to apiv0pb.Runs.
respRuns, err := populateRuns(ctx, runs)
if err != nil {
return nil, err
}
encryptedNextPageToken, err := pagination.EncryptPageToken(ctx, nextPageToken)
if err != nil {
return nil, err
}
return &apiv0pb.SearchRunsResponse{
Runs: respRuns,
NextPageToken: encryptedNextPageToken,
}, nil
}
// populateRuns converts run.Runs to apiv0pb.Runs for the response.
//
// This includes fetching and populating extra information, including RunCLs
// and Tryjobs, to fill in details in each apiv0pb.Run.
//
// TODO(qyearsley): Consider optimizing this by using datastore.Get batch
// calls, e.g. for fetching all Tryjob entities with one call.
func populateRuns(ctx context.Context, runs []*run.Run) ([]*apiv0pb.Run, error) {
respRuns := make([]*apiv0pb.Run, len(runs))
errs := parallel.WorkPool(min(len(runs), 16), func(work chan<- func() error) {
for i, r := range runs {
i, r := i, r
work <- func() (err error) {
respRuns[i], err = populateRunResponse(ctx, r)
return err
}
}
})
return respRuns, common.MostSevereError(errs)
}