go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/rpc/admin/server.go (about)

     1  // Copyright 2021 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 admin
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"reflect"
    21  
    22  	"golang.org/x/sync/errgroup"
    23  
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/protobuf/proto"
    26  	"google.golang.org/protobuf/types/known/emptypb"
    27  	"google.golang.org/protobuf/types/known/timestamppb"
    28  
    29  	"go.chromium.org/luci/common/api/gerrit"
    30  	"go.chromium.org/luci/common/errors"
    31  	"go.chromium.org/luci/common/logging"
    32  	"go.chromium.org/luci/common/retry/transient"
    33  	"go.chromium.org/luci/common/sync/parallel"
    34  	"go.chromium.org/luci/gae/service/datastore"
    35  	"go.chromium.org/luci/grpc/appstatus"
    36  	"go.chromium.org/luci/server/auth"
    37  	"go.chromium.org/luci/server/dsmapper"
    38  	"go.chromium.org/luci/server/quota"
    39  	"go.chromium.org/luci/server/quota/quotapb"
    40  	"go.chromium.org/luci/server/tq"
    41  
    42  	"go.chromium.org/luci/cv/internal/changelist"
    43  	"go.chromium.org/luci/cv/internal/common"
    44  	"go.chromium.org/luci/cv/internal/common/eventbox"
    45  	"go.chromium.org/luci/cv/internal/gerrit/poller"
    46  	"go.chromium.org/luci/cv/internal/prjmanager"
    47  	"go.chromium.org/luci/cv/internal/prjmanager/prjpb"
    48  	adminpb "go.chromium.org/luci/cv/internal/rpc/admin/api"
    49  	"go.chromium.org/luci/cv/internal/rpc/pagination"
    50  	"go.chromium.org/luci/cv/internal/run"
    51  	"go.chromium.org/luci/cv/internal/run/eventpb"
    52  	"go.chromium.org/luci/cv/internal/run/runquery"
    53  )
    54  
    55  // allowGroup is a Chrome Infra Auth group, members of which are allowed to call
    56  // admin API. See https://crbug.com/1183616.
    57  const allowGroup = "service-luci-change-verifier-admins"
    58  
    59  type AdminServer struct {
    60  	tqDispatcher *tq.Dispatcher
    61  	clUpdater    *changelist.Updater
    62  	pmNotifier   *prjmanager.Notifier
    63  	runNotifier  *run.Notifier
    64  
    65  	dsmapper *dsMapper
    66  
    67  	adminpb.UnimplementedAdminServer
    68  }
    69  
    70  func New(t *tq.Dispatcher, ctrl *dsmapper.Controller, u *changelist.Updater, p *prjmanager.Notifier, r *run.Notifier) *AdminServer {
    71  	return &AdminServer{
    72  		tqDispatcher: t,
    73  		clUpdater:    u,
    74  		pmNotifier:   p,
    75  		runNotifier:  r,
    76  		dsmapper:     newDSMapper(ctrl),
    77  	}
    78  }
    79  
    80  func (a *AdminServer) GetProject(ctx context.Context, req *adminpb.GetProjectRequest) (resp *adminpb.GetProjectResponse, err error) {
    81  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
    82  	if err = checkAllowed(ctx, "GetProject"); err != nil {
    83  		return
    84  	}
    85  	if req.GetProject() == "" {
    86  		return nil, appstatus.Error(codes.InvalidArgument, "project is required")
    87  	}
    88  
    89  	eg, ctx := errgroup.WithContext(ctx)
    90  
    91  	var p *prjmanager.Project
    92  	eg.Go(func() (err error) {
    93  		p, err = prjmanager.Load(ctx, req.GetProject())
    94  		return
    95  	})
    96  
    97  	resp = &adminpb.GetProjectResponse{}
    98  	eg.Go(func() error {
    99  		list, err := eventbox.List(ctx, prjmanager.EventboxRecipient(ctx, req.GetProject()))
   100  		if err != nil {
   101  			return errors.Annotate(err, "failed to fetch Project Events").Err()
   102  		}
   103  		events := make([]*prjpb.Event, len(list))
   104  		for i, item := range list {
   105  			events[i] = &prjpb.Event{}
   106  			if err = proto.Unmarshal(item.Value, events[i]); err != nil {
   107  				return errors.Annotate(err, "failed to unmarshal Event %q", item.ID).Err()
   108  			}
   109  		}
   110  		resp.Events = events
   111  		return nil
   112  	})
   113  
   114  	switch err = eg.Wait(); {
   115  	case err != nil:
   116  		return nil, err
   117  	case p == nil:
   118  		return nil, appstatus.Error(codes.NotFound, "project not found")
   119  	default:
   120  		resp.State = p.State
   121  		resp.State.LuciProject = req.GetProject()
   122  		return resp, nil
   123  	}
   124  }
   125  
   126  func (a *AdminServer) GetProjectLogs(ctx context.Context, req *adminpb.GetProjectLogsRequest) (resp *adminpb.GetProjectLogsResponse, err error) {
   127  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   128  	if err = checkAllowed(ctx, "GetProjectLogs"); err != nil {
   129  		return
   130  	}
   131  	switch {
   132  	case req.GetPageToken() != "":
   133  		return nil, appstatus.Error(codes.Unimplemented, "not implemented yet")
   134  	case req.GetPageSize() < 0:
   135  		return nil, appstatus.Error(codes.InvalidArgument, "negative page size not allowed")
   136  	case req.GetProject() == "":
   137  		return nil, appstatus.Error(codes.InvalidArgument, "project is required")
   138  	case req.GetEversionMin() < 0:
   139  		return nil, appstatus.Error(codes.InvalidArgument, "eversion_min must be non-negative")
   140  	case req.GetEversionMax() < 0:
   141  		return nil, appstatus.Error(codes.InvalidArgument, "eversion_max must be non-negative")
   142  	}
   143  
   144  	q := datastore.NewQuery(prjmanager.ProjectLogKind)
   145  	q = q.Ancestor(datastore.MakeKey(ctx, prjmanager.ProjectKind, req.GetProject()))
   146  
   147  	if m := req.GetEversionMin(); m > 0 {
   148  		q.Gte("EVersion", m)
   149  	}
   150  	if m := req.GetEversionMax(); m > 0 {
   151  		q.Lte("EVersion", m)
   152  	}
   153  	switch s := req.GetPageSize(); {
   154  	case s > 1024:
   155  		q.Limit(1024)
   156  	case s > 0:
   157  		q.Limit(s)
   158  	default:
   159  		q.Limit(128)
   160  	}
   161  
   162  	var out []*prjmanager.ProjectLog
   163  	if err = datastore.GetAll(ctx, q, &out); err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	resp = &adminpb.GetProjectLogsResponse{Logs: make([]*adminpb.ProjectLog, len(out))}
   168  	for i, l := range out {
   169  		resp.Logs[i] = &adminpb.ProjectLog{
   170  			Eversion:   l.EVersion,
   171  			State:      l.State,
   172  			UpdateTime: common.Time2PBNillable(l.UpdateTime),
   173  			Reasons:    &prjpb.LogReasons{Reasons: l.Reasons},
   174  		}
   175  	}
   176  	return resp, nil
   177  }
   178  
   179  func (a *AdminServer) GetRun(ctx context.Context, req *adminpb.GetRunRequest) (resp *adminpb.GetRunResponse, err error) {
   180  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   181  	if err = checkAllowed(ctx, "GetRun"); err != nil {
   182  		return
   183  	}
   184  	if req.GetRun() == "" {
   185  		return nil, appstatus.Error(codes.InvalidArgument, "run ID is required")
   186  	}
   187  	return loadRunAndEvents(ctx, common.RunID(req.GetRun()), nil)
   188  }
   189  
   190  func (a *AdminServer) GetCL(ctx context.Context, req *adminpb.GetCLRequest) (resp *adminpb.GetCLResponse, err error) {
   191  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   192  	if err = checkAllowed(ctx, "GetCL"); err != nil {
   193  		return
   194  	}
   195  
   196  	cl, err := loadCL(ctx, req)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	runs := make([]string, len(cl.IncompleteRuns))
   201  	for i, id := range cl.IncompleteRuns {
   202  		runs[i] = string(id)
   203  	}
   204  	resp = &adminpb.GetCLResponse{
   205  		Id:               int64(cl.ID),
   206  		Eversion:         cl.EVersion,
   207  		ExternalId:       string(cl.ExternalID),
   208  		UpdateTime:       timestamppb.New(cl.UpdateTime),
   209  		Snapshot:         cl.Snapshot,
   210  		ApplicableConfig: cl.ApplicableConfig,
   211  		Access:           cl.Access,
   212  		IncompleteRuns:   runs,
   213  	}
   214  	return resp, nil
   215  }
   216  
   217  func (a *AdminServer) GetPoller(ctx context.Context, req *adminpb.GetPollerRequest) (resp *adminpb.GetPollerResponse, err error) {
   218  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   219  	if err = checkAllowed(ctx, "GetPoller"); err != nil {
   220  		return
   221  	}
   222  	if req.GetProject() == "" {
   223  		return nil, appstatus.Error(codes.InvalidArgument, "project is required")
   224  	}
   225  
   226  	s := poller.State{LuciProject: req.GetProject()}
   227  	switch err := datastore.Get(ctx, &s); {
   228  	case err == datastore.ErrNoSuchEntity:
   229  		return nil, appstatus.Error(codes.NotFound, "poller not found")
   230  	case err != nil:
   231  		return nil, errors.Annotate(err, "failed to fetch Poller state").Tag(transient.Tag).Err()
   232  	}
   233  	resp = &adminpb.GetPollerResponse{
   234  		Project:     s.LuciProject,
   235  		Eversion:    s.EVersion,
   236  		ConfigHash:  s.ConfigHash,
   237  		UpdateTime:  timestamppb.New(s.UpdateTime),
   238  		QueryStates: s.QueryStates,
   239  	}
   240  	return resp, nil
   241  }
   242  
   243  func (a *AdminServer) SearchRuns(ctx context.Context, req *adminpb.SearchRunsRequest) (resp *adminpb.RunsResponse, err error) {
   244  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   245  	if err = checkAllowed(ctx, "SearchRuns"); err != nil {
   246  		return
   247  	}
   248  	limit, err := pagination.ValidatePageSize(req, 16, 128)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  	var pt *runquery.PageToken
   253  	if s := req.GetPageToken(); s != "" {
   254  		pt = &runquery.PageToken{}
   255  		if err := pagination.DecryptPageToken(ctx, req.GetPageToken(), pt); err != nil {
   256  			return nil, err
   257  		}
   258  	}
   259  
   260  	// Compute potentially interesting run keys using the most efficient query.
   261  	var runKeys []*datastore.Key
   262  	var cl *changelist.CL
   263  	switch {
   264  	case req.GetCl() != nil:
   265  		cl, runKeys, err = searchRunsByCL(ctx, req, pt, limit)
   266  	case req.GetProject() != "":
   267  		runKeys, err = searchRunsByProject(ctx, req, pt, limit)
   268  	default:
   269  		runKeys, err = searchRecentRunsSlow(ctx, req, pt, limit)
   270  	}
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	// Fetch individual runs in parallel and apply final filtering.
   276  	shouldSkip := func(r *run.Run) bool {
   277  		if req.GetProject() != "" && req.GetProject() != r.ID.LUCIProject() {
   278  			return true
   279  		}
   280  		if req.GetCl() != nil && !r.CLs.Contains(cl.ID) {
   281  			return true
   282  		}
   283  		switch s := req.GetStatus(); s {
   284  		case run.Status_STATUS_UNSPECIFIED:
   285  		case run.Status_ENDED_MASK:
   286  			if !run.IsEnded(r.Status) {
   287  				return true
   288  			}
   289  		default:
   290  			if s != r.Status {
   291  				return true
   292  			}
   293  		}
   294  		if m := req.GetMode(); m != "" && run.Mode(m) != r.Mode {
   295  			return true
   296  		}
   297  		return false
   298  	}
   299  	runs := make([]*adminpb.GetRunResponse, len(runKeys))
   300  	errs := parallel.WorkPool(min(len(runKeys), 16), func(work chan<- func() error) {
   301  		for i, key := range runKeys {
   302  			i, key := i, key
   303  			work <- func() (err error) {
   304  				runs[i], err = loadRunAndEvents(ctx, common.RunID(key.StringID()), shouldSkip)
   305  				return
   306  			}
   307  		}
   308  	})
   309  	if errs != nil {
   310  		return nil, common.MostSevereError(errs)
   311  	}
   312  
   313  	resp = &adminpb.RunsResponse{}
   314  
   315  	// Remove nil runs, which were skipped above.
   316  	resp.Runs = runs[:0]
   317  	for _, r := range runs {
   318  		if r != nil {
   319  			resp.Runs = append(resp.Runs, r)
   320  		}
   321  	}
   322  
   323  	if l := len(runKeys); int32(l) == limit {
   324  		resp.NextPageToken, err = pagination.EncryptPageToken(ctx, &runquery.PageToken{Run: runKeys[l-1].StringID()})
   325  		if err != nil {
   326  			return nil, err
   327  		}
   328  	}
   329  	return resp, nil
   330  }
   331  
   332  // searchRunsByCL returns CL & Run IDs as Datastore keys, using CL to limit
   333  // results.
   334  func searchRunsByCL(ctx context.Context, req *adminpb.SearchRunsRequest, pt *runquery.PageToken, limit int32) (*changelist.CL, []*datastore.Key, error) {
   335  	cl, err := loadCL(ctx, req.GetCl())
   336  	if err != nil {
   337  		return nil, nil, err
   338  	}
   339  
   340  	qb := runquery.CLQueryBuilder{
   341  		CLID:    cl.ID,
   342  		Limit:   limit,
   343  		Project: req.GetProject(), // optional
   344  	}.PageToken(pt)
   345  	runKeys, err := qb.GetAllRunKeys(ctx)
   346  	return cl, runKeys, err
   347  }
   348  
   349  // searchRunsByProject returns Run IDs as Datastore keys, using LUCI Project to
   350  // limit results.
   351  func searchRunsByProject(ctx context.Context, req *adminpb.SearchRunsRequest, pt *runquery.PageToken, limit int32) ([]*datastore.Key, error) {
   352  	qb := runquery.ProjectQueryBuilder{
   353  		Project: req.GetProject(),
   354  		Limit:   limit,
   355  		Status:  req.GetStatus(), // optional
   356  	}.PageToken(pt)
   357  	return qb.GetAllRunKeys(ctx)
   358  }
   359  
   360  // searchRecentRunsSlow returns Run IDs as Datastore keys for the most recent
   361  // Runs.
   362  func searchRecentRunsSlow(ctx context.Context, req *adminpb.SearchRunsRequest, pt *runquery.PageToken, limit int32) ([]*datastore.Key, error) {
   363  	return runquery.RecentQueryBuilder{
   364  		Status: req.GetStatus(), // optional
   365  		Limit:  limit,
   366  	}.PageToken(pt).GetAllRunKeys(ctx)
   367  }
   368  
   369  // Copy from dsset.
   370  type itemEntity struct {
   371  	_kind string `gae:"$kind,dsset.Item"`
   372  
   373  	ID     string         `gae:"$id"`
   374  	Parent *datastore.Key `gae:"$parent"`
   375  	Value  []byte         `gae:",noindex"`
   376  }
   377  
   378  func (a *AdminServer) DeleteProjectEvents(ctx context.Context, req *adminpb.DeleteProjectEventsRequest) (resp *adminpb.DeleteProjectEventsResponse, err error) {
   379  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   380  	if err = checkAllowed(ctx, "DeleteProjectEvents"); err != nil {
   381  		return
   382  	}
   383  
   384  	switch {
   385  	case req.GetProject() == "":
   386  		return nil, appstatus.Error(codes.InvalidArgument, "project is required")
   387  	case req.GetLimit() <= 0:
   388  		return nil, appstatus.Error(codes.InvalidArgument, "limit must be >0")
   389  	}
   390  
   391  	parent := datastore.MakeKey(ctx, prjmanager.ProjectKind, req.GetProject())
   392  	q := datastore.NewQuery("dsset.Item").Ancestor(parent).Limit(req.GetLimit())
   393  	var entities []*itemEntity
   394  	if err := datastore.GetAll(ctx, q, &entities); err != nil {
   395  		return nil, errors.Annotate(err, "failed to fetch up to %d events", req.GetLimit()).Tag(transient.Tag).Err()
   396  	}
   397  
   398  	stats := make(map[string]int64, 10)
   399  	for _, e := range entities {
   400  		pb := &prjpb.Event{}
   401  		if err := proto.Unmarshal(e.Value, pb); err != nil {
   402  			stats["<unknown>"]++
   403  		} else {
   404  			stats[fmt.Sprintf("%T", pb.GetEvent())]++
   405  		}
   406  	}
   407  	if err := datastore.Delete(ctx, entities); err != nil {
   408  		return nil, errors.Annotate(err, "failed to delete %d events", len(entities)).Tag(transient.Tag).Err()
   409  	}
   410  	return &adminpb.DeleteProjectEventsResponse{Events: stats}, nil
   411  }
   412  
   413  func (a *AdminServer) RefreshProjectCLs(ctx context.Context, req *adminpb.RefreshProjectCLsRequest) (resp *adminpb.RefreshProjectCLsResponse, err error) {
   414  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   415  	if err = checkAllowed(ctx, "RefreshProjectCLs"); err != nil {
   416  		return
   417  	}
   418  	if req.GetProject() == "" {
   419  		return nil, appstatus.Error(codes.InvalidArgument, "project is required")
   420  	}
   421  
   422  	p, err := prjmanager.Load(ctx, req.GetProject())
   423  	if err != nil {
   424  		return nil, errors.Annotate(err, "failed to fetch Project %q", req.GetProject()).Tag(transient.Tag).Err()
   425  	}
   426  
   427  	cls := make([]*changelist.CL, len(p.State.GetPcls()))
   428  	errs := parallel.WorkPool(20, func(work chan<- func() error) {
   429  		for i, pcl := range p.State.GetPcls() {
   430  			i := i
   431  			id := pcl.GetClid()
   432  			work <- func() error {
   433  				// Load individual CL to avoid OOMs.
   434  				cl := changelist.CL{ID: common.CLID(id)}
   435  				if err := datastore.Get(ctx, &cl); err != nil {
   436  					return errors.Annotate(err, "failed to fetch CL %d", id).Tag(transient.Tag).Err()
   437  				}
   438  				cls[i] = &changelist.CL{ID: cl.ID, EVersion: cl.EVersion}
   439  				payload := &changelist.UpdateCLTask{
   440  					LuciProject: req.GetProject(),
   441  					ExternalId:  string(cl.ExternalID),
   442  					Id:          int64(cl.ID),
   443  					Requester:   changelist.UpdateCLTask_RPC_ADMIN,
   444  				}
   445  				return a.clUpdater.Schedule(ctx, payload)
   446  			}
   447  		}
   448  	})
   449  	if err := common.MostSevereError(errs); err != nil {
   450  		return nil, err
   451  	}
   452  
   453  	if err := a.pmNotifier.NotifyCLsUpdated(ctx, req.GetProject(), changelist.ToUpdatedEvents(cls...)); err != nil {
   454  		return nil, err
   455  	}
   456  
   457  	clvs := make(map[int64]int64, len(p.State.GetPcls()))
   458  	for _, cl := range cls {
   459  		clvs[int64(cl.ID)] = cl.EVersion
   460  	}
   461  	return &adminpb.RefreshProjectCLsResponse{ClVersions: clvs}, nil
   462  }
   463  
   464  func (a *AdminServer) SendProjectEvent(ctx context.Context, req *adminpb.SendProjectEventRequest) (_ *emptypb.Empty, err error) {
   465  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   466  	if err = checkAllowed(ctx, "SendProjectEvent"); err != nil {
   467  		return
   468  	}
   469  	switch {
   470  	case req.GetProject() == "":
   471  		return nil, appstatus.Error(codes.InvalidArgument, "project is required")
   472  	case req.GetEvent().GetEvent() == nil:
   473  		return nil, appstatus.Error(codes.InvalidArgument, "event with a specific inner event is required")
   474  	}
   475  
   476  	switch p, err := prjmanager.Load(ctx, req.GetProject()); {
   477  	case err != nil:
   478  		return nil, errors.Annotate(err, "failed to fetch Project").Err()
   479  	case p == nil:
   480  		return nil, appstatus.Error(codes.NotFound, "project not found")
   481  	}
   482  
   483  	if err := a.pmNotifier.SendNow(ctx, req.GetProject(), req.GetEvent()); err != nil {
   484  		return nil, errors.Annotate(err, "failed to send event").Err()
   485  	}
   486  	return &emptypb.Empty{}, nil
   487  }
   488  
   489  func (a *AdminServer) SendRunEvent(ctx context.Context, req *adminpb.SendRunEventRequest) (_ *emptypb.Empty, err error) {
   490  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   491  	if err = checkAllowed(ctx, "SendRunEvent"); err != nil {
   492  		return
   493  	}
   494  	switch {
   495  	case req.GetRun() == "":
   496  		return nil, appstatus.Error(codes.InvalidArgument, "Run is required")
   497  	case req.GetEvent().GetEvent() == nil:
   498  		return nil, appstatus.Error(codes.InvalidArgument, "event with a specific inner event is required")
   499  	}
   500  
   501  	switch r, err := run.LoadRun(ctx, common.RunID(req.GetRun())); {
   502  	case err != nil:
   503  		return nil, errors.Annotate(err, "failed to fetch Run").Tag(transient.Tag).Err()
   504  	case r == nil:
   505  		return nil, appstatus.Error(codes.NotFound, "Run not found")
   506  	}
   507  
   508  	if err := a.runNotifier.SendNow(ctx, common.RunID(req.GetRun()), req.GetEvent()); err != nil {
   509  		return nil, errors.Annotate(err, "failed to send event").Err()
   510  	}
   511  	return &emptypb.Empty{}, nil
   512  }
   513  
   514  func (a *AdminServer) ScheduleTask(ctx context.Context, req *adminpb.ScheduleTaskRequest) (_ *emptypb.Empty, err error) {
   515  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   516  	if err = checkAllowed(ctx, "ScheduleTask"); err != nil {
   517  		return
   518  	}
   519  
   520  	const trans = true
   521  	var possiblePayloads = []struct {
   522  		inTransaction bool
   523  		payload       proto.Message
   524  	}{
   525  		{trans, req.GetBatchUpdateCl()},
   526  		{trans, req.GetBatchOnClUpdated()},
   527  		{false, req.GetExportRunToBq()},
   528  		{trans, req.GetKickManageProject()},
   529  		{trans, req.GetKickManageRun()},
   530  		{false, req.GetManageProject()},
   531  		{false, req.GetManageRun()},
   532  		{false, req.GetPollGerrit()},
   533  		{trans, req.GetPurgeCl()},
   534  		{false, req.GetUpdateCl()},
   535  		{false, req.GetRefreshProjectConfig()},
   536  		{trans, req.GetManageRunLongOp()},
   537  	}
   538  
   539  	chosen := possiblePayloads[0]
   540  	for _, another := range possiblePayloads[1:] {
   541  		switch {
   542  		case reflect.ValueOf(another.payload).IsNil():
   543  		case !reflect.ValueOf(chosen.payload).IsNil():
   544  			return nil, appstatus.Error(codes.InvalidArgument, "exactly one task payload required, but 2+ given")
   545  		default:
   546  			chosen = another
   547  		}
   548  	}
   549  
   550  	if reflect.ValueOf(chosen.payload).IsNil() {
   551  		return nil, appstatus.Error(codes.InvalidArgument, "exactly one task payload required, but none given")
   552  	}
   553  	kind := chosen.payload.ProtoReflect().Type().Descriptor().Name()
   554  	if chosen.inTransaction && req.GetDeduplicationKey() != "" {
   555  		return nil, appstatus.Errorf(codes.InvalidArgument, "task %q is transactional, so the deduplication_key is not allowed", kind)
   556  	}
   557  
   558  	t := &tq.Task{
   559  		Payload:          chosen.payload,
   560  		DeduplicationKey: req.GetDeduplicationKey(),
   561  		Title:            fmt.Sprintf("admin/%s/%s/%s", auth.CurrentIdentity(ctx), kind, req.GetDeduplicationKey()),
   562  	}
   563  
   564  	if chosen.inTransaction {
   565  		err = datastore.RunInTransaction(ctx, func(ctx context.Context) error {
   566  			return a.tqDispatcher.AddTask(ctx, t)
   567  		}, nil)
   568  	} else {
   569  		err = a.tqDispatcher.AddTask(ctx, t)
   570  	}
   571  
   572  	if err != nil {
   573  		return nil, errors.Annotate(err, "failed to schedule task").Err()
   574  	}
   575  	return &emptypb.Empty{}, nil
   576  }
   577  
   578  func (a *AdminServer) GetQuotaAccounts(ctx context.Context, req *quotapb.GetAccountsRequest) (resp *quotapb.GetAccountsResponse, err error) {
   579  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   580  	if err := checkAllowed(ctx, "GetQuotaAccounts"); err != nil {
   581  		return nil, err
   582  	}
   583  
   584  	if err := req.Validate(); err != nil {
   585  		return nil, appstatus.Errorf(codes.InvalidArgument, "request validation error: %s", err)
   586  	}
   587  
   588  	return quota.GetAccounts(ctx, req.Account)
   589  }
   590  
   591  func (a *AdminServer) ApplyQuotaOps(ctx context.Context, req *quotapb.ApplyOpsRequest) (resp *quotapb.ApplyOpsResponse, err error) {
   592  	defer func() { err = appstatus.GRPCifyAndLog(ctx, err) }()
   593  	if err := checkAllowed(ctx, "ApplyQuotaOps"); err != nil {
   594  		return nil, err
   595  	}
   596  
   597  	if err := req.Validate(); err != nil {
   598  		return nil, appstatus.Errorf(codes.InvalidArgument, "request validation error: %s", err)
   599  	}
   600  
   601  	return quota.ApplyOps(ctx, req.RequestId, req.RequestIdTtl, req.Ops)
   602  }
   603  
   604  func checkAllowed(ctx context.Context, name string) error {
   605  	switch yes, err := auth.IsMember(ctx, allowGroup); {
   606  	case err != nil:
   607  		return errors.Annotate(err, "failed to check ACL").Err()
   608  	case !yes:
   609  		return appstatus.Errorf(codes.PermissionDenied, "not a member of %s", allowGroup)
   610  	default:
   611  		logging.Warningf(ctx, "%s is calling admin.%s", auth.CurrentIdentity(ctx), name)
   612  		return nil
   613  	}
   614  }
   615  
   616  func loadRunAndEvents(ctx context.Context, rid common.RunID, shouldSkip func(r *run.Run) bool) (*adminpb.GetRunResponse, error) {
   617  	r, err := run.LoadRun(ctx, rid)
   618  	switch {
   619  	case err != nil:
   620  		return nil, err
   621  	case r == nil:
   622  		return nil, appstatus.Error(codes.NotFound, "Run not found")
   623  	case shouldSkip != nil && shouldSkip(r):
   624  		return nil, nil
   625  	}
   626  
   627  	eg, ctx := errgroup.WithContext(ctx)
   628  	var cls []*adminpb.GetRunResponse_CL
   629  	eg.Go(func() error {
   630  		switch rcls, err := run.LoadRunCLs(ctx, r.ID, r.CLs); {
   631  		case err != nil:
   632  			return errors.Annotate(err, "failed to fetch RunCLs").Err()
   633  		default:
   634  			cls = make([]*adminpb.GetRunResponse_CL, len(rcls))
   635  			for i, rcl := range rcls {
   636  				cls[i] = &adminpb.GetRunResponse_CL{
   637  					Id:         int64(rcl.ID),
   638  					ExternalId: string(rcl.ExternalID),
   639  					Detail:     rcl.Detail,
   640  					Trigger:    rcl.Trigger,
   641  				}
   642  			}
   643  			return nil
   644  		}
   645  	})
   646  
   647  	var logEntries []*run.LogEntry
   648  	eg.Go(func() error {
   649  		var err error
   650  		logEntries, err = run.LoadRunLogEntries(ctx, r.ID)
   651  		if err != nil {
   652  			return errors.Annotate(err, "failed to fetch RunCLs").Err()
   653  		}
   654  		return nil
   655  	})
   656  
   657  	var events []*eventpb.Event
   658  	eg.Go(func() error {
   659  		list, err := eventbox.List(ctx, run.EventboxRecipient(ctx, rid))
   660  		if err != nil {
   661  			return errors.Annotate(err, "failed to fetch Run Events").Err()
   662  		}
   663  		events = make([]*eventpb.Event, len(list))
   664  		for i, item := range list {
   665  			events[i] = &eventpb.Event{}
   666  			if err = proto.Unmarshal(item.Value, events[i]); err != nil {
   667  				return errors.Annotate(err, "failed to unmarshal Event %q", item.ID).Err()
   668  			}
   669  		}
   670  		return nil
   671  	})
   672  
   673  	if err := eg.Wait(); err != nil {
   674  		return nil, err
   675  	}
   676  
   677  	return &adminpb.GetRunResponse{
   678  		Id:                  string(rid),
   679  		Eversion:            r.EVersion,
   680  		Mode:                string(r.Mode),
   681  		Status:              r.Status,
   682  		CreateTime:          common.Time2PBNillable(r.CreateTime),
   683  		StartTime:           common.Time2PBNillable(r.StartTime),
   684  		UpdateTime:          common.Time2PBNillable(r.UpdateTime),
   685  		EndTime:             common.Time2PBNillable(r.EndTime),
   686  		Owner:               string(r.Owner),
   687  		CreatedBy:           string(r.CreatedBy),
   688  		BilledTo:            string(r.BilledTo),
   689  		ConfigGroupId:       string(r.ConfigGroupID),
   690  		Cls:                 cls,
   691  		Options:             r.Options,
   692  		CancellationReasons: r.CancellationReasons,
   693  		Tryjobs:             r.Tryjobs,
   694  		OngoingLongOps:      r.OngoingLongOps,
   695  		Submission:          r.Submission,
   696  		LatestClsRefresh:    common.Time2PBNillable(r.LatestCLsRefresh),
   697  
   698  		LogEntries: logEntries,
   699  		Events:     events,
   700  	}, nil
   701  }
   702  
   703  func loadCL(ctx context.Context, req *adminpb.GetCLRequest) (*changelist.CL, error) {
   704  	var err error
   705  	var cl *changelist.CL
   706  	var eid changelist.ExternalID
   707  	switch {
   708  	case req.GetId() != 0:
   709  		cl = &changelist.CL{ID: common.CLID(req.GetId())}
   710  		err = datastore.Get(ctx, cl)
   711  		switch {
   712  		case err == datastore.ErrNoSuchEntity:
   713  			cl, err = nil, nil
   714  		case err != nil:
   715  			err = errors.Annotate(err, "failed to fetch CL by InternalID %d", req.GetId()).Tag(transient.Tag).Err()
   716  		}
   717  	case req.GetExternalId() != "":
   718  		eid = changelist.ExternalID(req.GetExternalId())
   719  		cl, err = eid.Load(ctx)
   720  	case req.GetGerritUrl() != "":
   721  		var host string
   722  		var change int64
   723  		host, change, err = gerrit.FuzzyParseURL(req.GetGerritUrl())
   724  		if err != nil {
   725  			return nil, appstatus.Errorf(codes.InvalidArgument, "invalid Gerrit URL %q: %s", req.GetGerritUrl(), err)
   726  		}
   727  		eid, err = changelist.GobID(host, change)
   728  		if err != nil {
   729  			return nil, appstatus.Errorf(codes.InvalidArgument, "invalid Gerrit URL %q: %s", req.GetGerritUrl(), err)
   730  		}
   731  		cl, err = eid.Load(ctx)
   732  	default:
   733  		return nil, appstatus.Error(codes.InvalidArgument, "id or external_id or gerrit_url is required")
   734  	}
   735  
   736  	switch {
   737  	case err != nil:
   738  		return nil, err
   739  	case cl == nil:
   740  		if req.GetId() == 0 {
   741  			return nil, appstatus.Errorf(codes.NotFound, "CL %d not found", req.GetId())
   742  		}
   743  		return nil, appstatus.Errorf(codes.NotFound, "CL %s not found", eid)
   744  	}
   745  	return cl, nil
   746  }