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  }