go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/gerrit/trigger/reset.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 trigger 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strconv" 22 "strings" 23 "time" 24 25 "google.golang.org/grpc" 26 "google.golang.org/grpc/codes" 27 28 "go.chromium.org/luci/common/data/stringset" 29 "go.chromium.org/luci/common/errors" 30 "go.chromium.org/luci/common/logging" 31 gerritpb "go.chromium.org/luci/common/proto/gerrit" 32 "go.chromium.org/luci/common/retry/transient" 33 "go.chromium.org/luci/gae/service/datastore" 34 "go.chromium.org/luci/grpc/grpcutil" 35 36 "go.chromium.org/luci/cv/internal/changelist" 37 "go.chromium.org/luci/cv/internal/common" 38 "go.chromium.org/luci/cv/internal/common/lease" 39 "go.chromium.org/luci/cv/internal/configs/prjcfg" 40 "go.chromium.org/luci/cv/internal/gerrit" 41 "go.chromium.org/luci/cv/internal/gerrit/botdata" 42 "go.chromium.org/luci/cv/internal/run" 43 "go.chromium.org/luci/cv/internal/usertext" 44 ) 45 46 // ErrResetPreconditionFailedTag is an error tag indicating that the 47 // precondition of resetting a trigger has not been met, 48 var ErrResetPreconditionFailedTag = errors.BoolTag{ 49 Key: errors.NewTagKey("reset precondition not met"), 50 } 51 52 // ErrResetPermanentTag is an error tag indicating that error occurs during the 53 // reset is permanent (e.g. lack of vote permission). 54 var ErrResetPermanentTag = errors.BoolTag{ 55 Key: errors.NewTagKey("permanent error while resetting triggers"), 56 } 57 58 var errGerritTagKey = errors.NewTagKey("this is a Gerrit error") 59 60 func applyGerritErrTag(err error, grpcCode codes.Code) error { 61 return errors.TagValue{Key: errGerritTagKey, Value: grpcCode}.Apply(err) 62 } 63 64 // IsResetErrFromGerrit returns gerrit grpc error code if the `trigger.Reset` 65 // fails because of Gerrit. 66 func IsResetErrFromGerrit(err error) (codes.Code, bool) { 67 switch v, ok := errors.TagValueIn(errGerritTagKey, err); { 68 case err == nil: 69 return codes.OK, false 70 case !ok: 71 return codes.Unknown, false 72 default: 73 return v.(codes.Code), true 74 } 75 } 76 77 // ResetInput contains info to reset triggers of Run on a CL. 78 type ResetInput struct { 79 // CL is a Gerrit CL entity. 80 // 81 // Must have CL.Snapshot set. 82 CL *changelist.CL 83 // Trigger identifies the triggering vote. Required. 84 // 85 // Removed only after all other votes on CQ label are removed. 86 Triggers *run.Triggers 87 // LUCIProject is the project that initiates this reset. 88 // 89 // The project scoped account of this LUCI project SHOULD have the permission 90 // to set the CQ label on behalf of other users in Gerrit. 91 LUCIProject string 92 // Message to be posted along with the triggering vote removal 93 Message string 94 // Requester describes the caller (e.g. Project Manager, Run Manager). 95 Requester string 96 // Notify describes whom to notify regarding the reset. 97 // 98 // If empty, notifies no one. 99 Notify gerrit.Whoms 100 // AddToAttentionSet describes whom to add in the attention set. 101 // 102 // If empty, no change will be made to attention set. 103 AddToAttentionSet gerrit.Whoms 104 // AttentionReason describes the reason of the attention change. 105 // 106 // It is attached to the attention set change, and rendered in UI to explain 107 // the reason of the attention to users. 108 // 109 // This is noop, if AddAttentionSet is empty. 110 AttentionReason string 111 // LeaseDuration is how long a lease will be held for this reset. 112 // 113 // If the passed context has a closer deadline, uses that deadline as lease 114 // `ExpireTime`. 115 LeaseDuration time.Duration 116 // ConfigGroups are the ConfigGroups that are watching this CL. 117 // 118 // They are used to remove votes for additional modes. Normally, there is 119 // just 1 ConfigGroup. 120 ConfigGroups []*prjcfg.ConfigGroup 121 // GFactory is used to create the gerrit client needed to perform the reset. 122 GFactory gerrit.Factory 123 // CLMutator performs mutations to the CL entity and notifies relevant parts 124 // of CV when appropriate. 125 CLMutator *changelist.Mutator 126 } 127 128 func (in *ResetInput) panicIfInvalid() { 129 // These are the conditions that shouldn't be met, and likely require code 130 // changes for fixes. 131 var err error 132 switch { 133 case in.CL.Snapshot == nil: 134 err = fmt.Errorf("cl.Snapshot must be non-nil") 135 case in.Triggers == nil: 136 err = fmt.Errorf("trigger must be non-nil") 137 case in.Triggers.CqVoteTrigger == nil && in.Triggers.NewPatchsetRunTrigger == nil: 138 err = fmt.Errorf("at least one of {CqVoteTrigger, NewPatchsetRunTrigger} must be non-nil") 139 case in.LUCIProject != in.CL.Snapshot.GetLuciProject(): 140 err = fmt.Errorf("mismatched LUCI Project: got %q in input and %q in CL snapshot", in.LUCIProject, in.CL.Snapshot.GetLuciProject()) 141 case len(in.ConfigGroups) == 0: 142 err = fmt.Errorf("config_groups must be given") 143 case in.GFactory == nil: 144 err = fmt.Errorf("gerrit factory must be non-nil") 145 case in.CLMutator == nil: 146 err = fmt.Errorf("mutator must be non-nil") 147 case len(in.Notify) > 0: 148 for _, enum := range in.Notify { 149 if _, ok := gerrit.Whom_name[int32(enum)]; !ok { 150 err = fmt.Errorf("notify: unknown Whom value %d", enum) 151 break 152 } 153 } 154 case len(in.AddToAttentionSet) > 0: 155 for _, enum := range in.AddToAttentionSet { 156 if _, ok := gerrit.Whom_name[int32(enum)]; !ok { 157 err = fmt.Errorf("add_to_attention: unknown Whom value %d", enum) 158 break 159 } 160 } 161 } 162 if err != nil { 163 panic(err) 164 } 165 } 166 167 // Reset removes or "deactivates" the trigger that made CV start processing the 168 // current run, whether by removing votes on a CL and posting the given message, 169 // or by updating the datastore entity associated with the CL; this, depending 170 // on the RunMode of the Run. 171 // 172 // For vote-removal-based reset: 173 // 174 // Returns error tagged with `ErrPreconditionFailedTag` if one of the 175 // following conditions is matched. 176 // - The patchset of the provided CL is not the latest in Gerrit. 177 // - The provided CL gets `changelist.AccessDenied` or 178 // `changelist.AccessDeniedProbably` from Gerrit. 179 // 180 // Normally, the triggering vote(s) is removed last and all other votes 181 // are removed in chronological order (latest to earliest). 182 // After all votes are removed, the message is posted to Gerrit. 183 // 184 // Abnormally, e.g. lack of permission to remove votes, falls back to post a 185 // special message which "deactivates" the triggering votes. This special 186 // message is a combination of: 187 // - the original message in the input 188 // - reason for abnormality, 189 // - special `botdata.BotData` which ensures CV won't consider previously 190 // triggering votes as triggering in the future. 191 // 192 // Alternatively, in the case of a new patchset run: 193 // 194 // Updates the CLEntity to record that CV is not to create new patchset runs 195 // with the current patchset or lower. This prevents trigger.Find() from 196 // continuing to return a trigger for this patchset, analog to the effect of 197 // removing a cq vote on gerrit. 198 func Reset(ctx context.Context, in ResetInput) error { 199 in.panicIfInvalid() 200 if in.CL.AccessKindFromCodeReviewSite(ctx, in.LUCIProject) != changelist.AccessGranted { 201 return errors.New("failed to reset trigger because CV lost access to this CL", ErrResetPreconditionFailedTag) 202 } 203 if len(in.AddToAttentionSet) > 0 && in.AttentionReason == "" { 204 logging.Warningf(ctx, "FIXME reset was given empty in AttentionReason.") 205 in.AttentionReason = usertext.StoppedRun 206 } 207 208 client, err := in.GFactory.MakeClient(ctx, in.CL.Snapshot.GetGerrit().GetHost(), in.LUCIProject) 209 if err != nil { 210 return err 211 } 212 cl := in.CL 213 if in.Triggers.GetNewPatchsetRunTrigger() != nil { 214 cl, err = resetByUpdatingCLEntity(ctx, &in) 215 if err != nil { 216 return err 217 } 218 } 219 if in.Message == "" && in.Triggers.GetCqVoteTrigger() == nil { 220 return nil 221 } 222 223 leaseCtx, close, lErr := lease.ApplyOnCL(ctx, cl.ID, in.LeaseDuration, in.Requester) 224 if lErr != nil { 225 return lErr 226 } 227 defer close() 228 229 switch { 230 case in.Triggers.GetCqVoteTrigger() != nil: 231 if err := ensurePSLatestInCV(ctx, cl); err != nil { 232 return err 233 } 234 return resetLeased(leaseCtx, client, &in, cl) 235 case in.Message != "": 236 // If there is a CQ Vote trigger to purge, resetLeased() will have 237 // taken care of posting any appropriate message. 238 // If the Reset() call _only_ applies to an NPR trigger, _and_ 239 // a message has been specified, then this function needs to post a 240 // message. 241 return makeChange(client, &in, cl).postGerritMsg( 242 leaseCtx, cl.Snapshot.GetGerrit().GetInfo(), in.Message, in.Triggers.NewPatchsetRunTrigger, in.Notify, in.AddToAttentionSet, in.AttentionReason) 243 default: 244 panic("unreachable") 245 } 246 } 247 248 func makeChange(client gerrit.Client, in *ResetInput, cl *changelist.CL) *change { 249 return &change{ 250 Host: cl.Snapshot.GetGerrit().GetHost(), 251 LUCIProject: in.LUCIProject, 252 Project: cl.Snapshot.GetGerrit().GetInfo().GetProject(), 253 Number: cl.Snapshot.GetGerrit().GetInfo().GetNumber(), 254 Revision: cl.Snapshot.GetGerrit().GetInfo().GetCurrentRevision(), 255 gf: in.GFactory, 256 gc: client, 257 } 258 } 259 260 func resetByUpdatingCLEntity(ctx context.Context, in *ResetInput) (*changelist.CL, error) { 261 return in.CLMutator.Update(ctx, in.LUCIProject, in.CL.ID, func(cl *changelist.CL) error { 262 switch { 263 case cl.TriggerNewPatchsetRunAfterPS < in.CL.Snapshot.GetPatchset(): 264 cl.TriggerNewPatchsetRunAfterPS = in.CL.Snapshot.GetPatchset() 265 return nil 266 default: 267 logging.Warningf(ctx, "cl.TriggerNewPatchsetRunAfterPS has already been updated, race?") 268 return changelist.ErrStopMutation 269 } 270 }) 271 } 272 273 // TODO(tandrii): merge with prjmanager/purger's error messages. 274 var failMessage = "CV failed to unset the " + CQLabelName + 275 " label on your behalf. Please unvote and revote on the " + 276 CQLabelName + " label to retry." 277 278 func resetLeased(ctx context.Context, client gerrit.Client, in *ResetInput, cl *changelist.CL) error { 279 c := makeChange(client, in, cl) 280 logging.Infof(ctx, "Resetting triggers on %s/%d", c.Host, c.Number) 281 ci, err := c.getLatest(ctx, cl.Snapshot.GetGerrit().GetInfo().GetUpdated().AsTime()) 282 switch { 283 case err != nil: 284 return err 285 case ci.GetCurrentRevision() != c.Revision: 286 return errors.Reason("failed to reset because ps %d is not current for %s/%d", cl.Snapshot.GetPatchset(), c.Host, c.Number).Tag(ErrResetPreconditionFailedTag).Err() 287 } 288 289 labelsToRemove := stringset.NewFromSlice(CQLabelName) 290 if modeDef := in.Triggers.GetCqVoteTrigger().GetModeDefinition(); modeDef != nil { 291 labelsToRemove.Add(modeDef.GetTriggeringLabel()) 292 } 293 for _, cg := range in.ConfigGroups { 294 for _, am := range cg.Content.GetAdditionalModes() { 295 if l := am.GetTriggeringLabel(); l != "" { 296 labelsToRemove.Add(l) 297 } 298 } 299 } 300 labelsToRemove.Iter(func(label string) bool { 301 c.recordVotesToRemove(label, ci) 302 return true 303 }) 304 305 removeErr := c.removeVotesAndPostMsg(ctx, ci, in.Triggers.GetCqVoteTrigger(), in.Message, in.Notify, in.AddToAttentionSet, in.AttentionReason) 306 switch { 307 case removeErr == nil: 308 _, outErr := in.CLMutator.Update(ctx, in.LUCIProject, in.CL.ID, func(cl *changelist.CL) error { 309 if cl.Snapshot == nil || cl.Snapshot.GetOutdated() != nil { 310 return changelist.ErrStopMutation // noop 311 } 312 cl.Snapshot.Outdated = &changelist.Snapshot_Outdated{} 313 return nil 314 }) 315 if outErr != nil { 316 // Let's log and ignore the error. 317 logging.Errorf(ctx, "CLMutator.Update: ignoring %s", outErr) 318 } 319 return nil 320 case !ErrResetPermanentTag.In(removeErr): 321 return removeErr 322 } 323 324 // Received permanent error, try posting message. 325 logging.Warningf(ctx, "Falling back to resetting via botdata message %s/%d", c.Host, c.Number) 326 var msgBuilder strings.Builder 327 if in.Message != "" { 328 msgBuilder.WriteString(in.Message) 329 msgBuilder.WriteString("\n\n") 330 } 331 msgBuilder.WriteString(failMessage) 332 if err := c.postResetMessage(ctx, ci, msgBuilder.String(), in.Triggers.GetCqVoteTrigger(), in.Notify, in.AddToAttentionSet, in.AttentionReason); err != nil { 333 // Return the original error, but add details from just posting a message. 334 return errors.Annotate(removeErr, "even just posting message also failed: %s", err).Err() 335 } 336 return nil 337 } 338 339 func ensurePSLatestInCV(ctx context.Context, cl *changelist.CL) error { 340 curCLInCV := &changelist.CL{ID: cl.ID} 341 switch err := datastore.Get(ctx, curCLInCV); { 342 case err == datastore.ErrNoSuchEntity: 343 return errors.Reason("cl(id=%d) doesn't exist in datastore", cl.ID).Err() 344 case err != nil: 345 return errors.Annotate(err, "failed to load cl: %d", cl.ID).Tag(transient.Tag).Err() 346 case curCLInCV.Snapshot.GetPatchset() > cl.Snapshot.GetPatchset(): 347 return errors.Reason("failed to reset because ps %d is not current for cl(%d)", cl.Snapshot.GetPatchset(), cl.ID).Tag(ErrResetPreconditionFailedTag).Err() 348 } 349 return nil 350 } 351 352 type change struct { 353 Host string 354 LUCIProject string 355 Project string 356 Number int64 357 Revision string 358 359 gf gerrit.Factory 360 gc gerrit.Client 361 // votesToRemove maps accountID to a set of labels. 362 // 363 // For ease of passing to SetReview API, each label maps to 0. 364 votesToRemove map[int64]map[string]int32 365 } 366 367 func (c *change) getLatest(ctx context.Context, knownUpdatedTime time.Time) (*gerritpb.ChangeInfo, error) { 368 var ci *gerritpb.ChangeInfo 369 var gerritErr error 370 outerErr := c.gf.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error { 371 ci, gerritErr = c.gc.GetChange(ctx, &gerritpb.GetChangeRequest{ 372 Number: c.Number, 373 Project: c.Project, 374 Options: []gerritpb.QueryOption{ 375 gerritpb.QueryOption_CURRENT_REVISION, 376 gerritpb.QueryOption_DETAILED_LABELS, 377 gerritpb.QueryOption_DETAILED_ACCOUNTS, 378 gerritpb.QueryOption_MESSAGES, 379 }, 380 }, opt) 381 switch { 382 case grpcutil.Code(gerritErr) == codes.NotFound: 383 // If a Run fails right after this CL is uploaded, it is possible that 384 // CV receives NotFound when fetching this CL due to eventual consistency 385 // of Gerrit. Therefore, consider this error as stale data. It is also 386 // possible that user actually deleted this CL. In that case, it is also 387 // okay to retry here because theoretically, Gerrit should consistently 388 // return 404 and fail the task. When the task retries, CV should figure 389 // that it has lost its access to this CL at the beginning and give up 390 // resetting the trigger. But, even if Gerrit accidentally return 391 // the deleted CL, the subsequent SetReview call will also fail the task. 392 return gerrit.ErrStaleData 393 case gerritErr != nil: 394 return gerritErr 395 case ci.GetUpdated().AsTime().Before(knownUpdatedTime): 396 return gerrit.ErrStaleData 397 } 398 return nil 399 }) 400 401 switch { 402 case gerritErr != nil: 403 return nil, c.annotateGerritErr(ctx, gerritErr, "get") 404 case outerErr != nil: 405 // Should never happen unless MirrorIterator itself errors out for some 406 // reason. 407 return nil, outerErr 408 default: 409 return ci, nil 410 } 411 } 412 413 func (c *change) recordVotesToRemove(label string, ci *gerritpb.ChangeInfo) { 414 for _, vote := range ci.GetLabels()[label].GetAll() { 415 if vote.GetValue() == 0 { 416 continue 417 } 418 if c.votesToRemove == nil { 419 c.votesToRemove = make(map[int64]map[string]int32, 1) 420 } 421 accountID := vote.GetUser().GetAccountId() 422 if labels, exists := c.votesToRemove[accountID]; exists { 423 labels[label] = 0 424 } else { 425 c.votesToRemove[accountID] = map[string]int32{label: 0} 426 } 427 } 428 } 429 430 func (c *change) sortedVoterAccountIDs() []int64 { 431 ids := make([]int64, 0, len(c.votesToRemove)) 432 for id := range c.votesToRemove { 433 ids = append(ids, id) 434 } 435 sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) 436 return ids 437 } 438 439 func (c *change) removeVotesAndPostMsg(ctx context.Context, ci *gerritpb.ChangeInfo, t *run.Trigger, msg string, notify, addAttn gerrit.Whoms, reason string) error { 440 var nonTriggeringVotesRemovalErrs errors.MultiError 441 needRemoveTriggerVote := false 442 for _, voter := range c.sortedVoterAccountIDs() { 443 if voter == t.GetGerritAccountId() { 444 needRemoveTriggerVote = true 445 continue 446 } 447 if err := c.removeVote(ctx, voter, run.Mode(t.GetMode())); err != nil { 448 nonTriggeringVotesRemovalErrs = append(nonTriggeringVotesRemovalErrs, err) 449 } 450 } 451 if err := common.MostSevereError(nonTriggeringVotesRemovalErrs); err != nil { 452 // Return if failure occurs during removal of non-triggering votes so that 453 // triggering votes will be kept. Otherwise, CV may create a new Run using 454 // the non-triggering votes that CV fails to remove. 455 return err 456 } 457 458 if needRemoveTriggerVote { 459 // Remove the triggering vote last. 460 if err := c.removeVote(ctx, t.GetGerritAccountId(), run.Mode(t.GetMode())); err != nil { 461 return err 462 } 463 } 464 return c.postGerritMsg(ctx, ci, msg, t, notify, addAttn, reason) 465 } 466 467 func (c *change) removeVote(ctx context.Context, accountID int64, mode run.Mode) error { 468 var gerritErr error 469 470 outerErr := c.gf.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error { 471 _, gerritErr = c.gc.SetReview(ctx, &gerritpb.SetReviewRequest{ 472 Number: c.Number, 473 Project: c.Project, 474 RevisionId: c.Revision, 475 Labels: c.votesToRemove[accountID], 476 Tag: mode.GerritMessageTag(), 477 Notify: gerritpb.Notify_NOTIFY_NONE, 478 OnBehalfOf: accountID, 479 }, opt) 480 switch grpcutil.Code(gerritErr) { 481 case codes.NotFound: 482 // This is known to happen on new CLs or on recently created revisions. 483 return gerrit.ErrStaleData 484 default: 485 return gerritErr 486 } 487 }) 488 489 switch { 490 case gerritErr != nil: 491 return c.annotateGerritErr(ctx, gerritErr, "remove vote") 492 case outerErr != nil: 493 // Should never happen unless MirrorIterator itself errors out for some 494 // reason. 495 return outerErr 496 default: 497 return nil 498 } 499 } 500 501 func (c *change) postResetMessage(ctx context.Context, ci *gerritpb.ChangeInfo, msg string, t *run.Trigger, notify, addAttn gerrit.Whoms, reason string) (err error) { 502 bd := botdata.BotData{ 503 Action: botdata.Cancel, 504 TriggeredAt: t.GetTime().AsTime(), 505 Revision: c.Revision, 506 } 507 // TODO(crbug.com/1414898) - deprecate botdata 508 if msg, err = botdata.Append(msg, bd); err != nil { 509 return err 510 } 511 return c.postGerritMsg(ctx, ci, msg, t, notify, addAttn, reason) 512 } 513 514 // postGerritMsg posts the given message to Gerrit. 515 // 516 // Skips if duplicate message is found after triggering time. 517 func (c *change) postGerritMsg(ctx context.Context, ci *gerritpb.ChangeInfo, msg string, t *run.Trigger, notify, addAttn gerrit.Whoms, reason string) (err error) { 518 for _, m := range ci.GetMessages() { 519 switch { 520 case m.GetDate().AsTime().Before(t.GetTime().AsTime()): 521 case strings.Contains(m.Message, strings.TrimSpace(msg)): 522 return nil 523 } 524 } 525 nd := makeGerritNotifyDetails(notify, ci) 526 reason = fmt.Sprintf("ps#%d: %s", ci.GetRevisions()[ci.GetCurrentRevision()].GetNumber(), reason) 527 attention := makeGerritAttentionSetInputs(addAttn, ci, reason) 528 msg = gerrit.TruncateMessage(msg) 529 // Post message with unique tag per Run so that Gerrit will always display 530 // these messages. The uniqueness is achieved by appending the Run triggering 531 // time. Otherwise, users may falsely believe LUCI CV is not doing anything 532 // to handle their CLs because Gerrit will hide old messages with the same 533 // tag (See: crbug.com/1359521). The message in trigger reset normally 534 // contains the result for the Run (e.g. passing or why the Run fails) so it 535 // is a good indication of LUCI CV is working fine without introducing too 536 // much noise. 537 tag := fmt.Sprintf("%s:%d", run.Mode(t.Mode).GerritMessageTag(), t.GetTime().AsTime().Unix()) 538 var gerritErr error 539 outerErr := c.gf.MakeMirrorIterator(ctx).RetryIfStale(func(opt grpc.CallOption) error { 540 _, gerritErr = c.gc.SetReview(ctx, &gerritpb.SetReviewRequest{ 541 Number: c.Number, 542 Project: c.Project, 543 RevisionId: ci.GetCurrentRevision(), 544 Message: msg, 545 Tag: tag, 546 // Set `Notify` to NONE because LUCI CV has the knowledge on all the 547 // accounts to notify. All of them are included through `NotifyDetails`. 548 // Therefore, there is no point using the special enum provided via 549 // `Notify`. 550 Notify: gerritpb.Notify_NOTIFY_NONE, 551 NotifyDetails: nd, 552 AddToAttentionSet: attention, 553 }, opt) 554 switch grpcutil.Code(gerritErr) { 555 case codes.NotFound: 556 // This is known to happen on new CLs or on recently created revisions. 557 return gerrit.ErrStaleData 558 default: 559 return gerritErr 560 } 561 }) 562 563 switch { 564 case gerritErr != nil: 565 return c.annotateGerritErr(ctx, gerritErr, "post message") 566 case outerErr != nil: 567 // Should never happen unless MirrorIterator itself errors out for some 568 // reason. 569 return outerErr 570 default: 571 return nil 572 } 573 } 574 575 func makeGerritNotifyDetails(notify gerrit.Whoms, ci *gerritpb.ChangeInfo) *gerritpb.NotifyDetails { 576 accounts := notify.ToAccountIDsSorted(ci) 577 if len(accounts) == 0 { 578 return nil 579 } 580 581 return &gerritpb.NotifyDetails{ 582 Recipients: []*gerritpb.NotifyDetails_Recipient{ 583 { 584 RecipientType: gerritpb.NotifyDetails_RECIPIENT_TYPE_TO, 585 Info: &gerritpb.NotifyDetails_Info{ 586 Accounts: accounts, 587 }, 588 }, 589 }, 590 } 591 } 592 593 func makeGerritAttentionSetInputs(addAttn gerrit.Whoms, ci *gerritpb.ChangeInfo, reason string) []*gerritpb.AttentionSetInput { 594 accounts := addAttn.ToAccountIDsSorted(ci) 595 if len(accounts) == 0 { 596 return nil 597 } 598 ret := make([]*gerritpb.AttentionSetInput, len(accounts)) 599 for i, acct := range accounts { 600 ret[i] = &gerritpb.AttentionSetInput{ 601 // The accountID supports various formats, including a bare account ID, 602 // email, full-name, and others. 603 User: strconv.Itoa(int(acct)), 604 Reason: reason, 605 } 606 } 607 return ret 608 } 609 610 func (c *change) annotateGerritErr(ctx context.Context, err error, action string) error { 611 if err == nil { 612 return nil 613 } 614 code := grpcutil.Code(err) 615 var retErr error 616 switch code { 617 case codes.OK: 618 return nil 619 case codes.PermissionDenied: 620 retErr = errors.Reason("no permission to %s %s/%d", action, c.Host, c.Number).Tag(ErrResetPermanentTag).Err() 621 case codes.NotFound: 622 retErr = errors.Reason("change %s/%d not found", c.Host, c.Number).Tag(ErrResetPermanentTag).Err() 623 case codes.FailedPrecondition: 624 retErr = errors.Reason("change %s/%d in an unexpected state for action %s: %s", c.Host, c.Number, action, err).Tag(ErrResetPermanentTag).Err() 625 case codes.DeadlineExceeded: 626 retErr = errors.Reason("timeout when calling Gerrit to %s %s/%d", action, c.Host, c.Number).Tag(transient.Tag).Err() 627 default: 628 retErr = gerrit.UnhandledError(ctx, err, "failed to %s %s/%d", action, c.Host, c.Number) 629 } 630 retErr = applyGerritErrTag(retErr, code) 631 return retErr 632 }