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 }