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  }