go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/gerrit/rest.go (about) 1 // Copyright 2018 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 gerrit 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "fmt" 22 "io" 23 "net/http" 24 "net/url" 25 "strconv" 26 "strings" 27 28 "golang.org/x/net/context/ctxhttp" 29 "google.golang.org/grpc" 30 "google.golang.org/grpc/codes" 31 "google.golang.org/grpc/status" 32 "google.golang.org/protobuf/types/known/emptypb" 33 34 "go.chromium.org/luci/common/clock" 35 "go.chromium.org/luci/common/errors" 36 "go.chromium.org/luci/common/logging" 37 gerritpb "go.chromium.org/luci/common/proto/gerrit" 38 ) 39 40 const ( 41 // OAuthScope is the OAuth 2.0 scope that must be included when acquiring an 42 // access token for Gerrit RPCs. 43 OAuthScope = "https://www.googleapis.com/auth/gerritcodereview" 44 45 // contentTypeJSON is the http header content-type value for json encoded 46 // objects in the body. 47 contentTypeJSON = "application/json; charset=UTF-8" 48 49 // contentTypeText is the http header content-type value for plain text body. 50 contentTypeText = "application/x-www-form-urlencoded; charset=UTF-8" 51 52 // defaultQueryLimit is the default limit for ListChanges, and 53 // maxQueryLimit is the maximum allowed limit. If either of these are 54 // changed, the proto comments should also be updated. 55 defaultQueryLimit = 25 56 maxQueryLimit = 1000 57 ) 58 59 // This file implements Gerrit proto service client on top of Gerrit REST API. 60 // WARNING: The returned client is incomplete, so if you want access to a 61 // particular field from this API, you may need to update the relevant struct 62 // and add an unmarshalling of that field. 63 64 // NewRESTClient creates a new Gerrit client based on Gerrit's REST API. 65 // 66 // The host must be a full Gerrit host, e.g. "chromium-review.googlesource.com". 67 // 68 // If `auth` is true, this indicates that the given HTTP client sends 69 // authenticated requests. If so, the requests to Gerrit will include "/a/" URL 70 // path prefix. 71 // 72 // RPC methods of the returned client return an error if a grpc.CallOption is 73 // passed. 74 func NewRESTClient(httpClient *http.Client, host string, auth bool) (gerritpb.GerritClient, error) { 75 if strings.Contains(host, "/") { 76 return nil, errors.Reason("invalid host %q", host).Err() 77 } 78 return &client{ 79 hClient: httpClient, 80 auth: auth, 81 host: host, 82 }, nil 83 } 84 85 // Implementation. 86 87 // jsonPrefix is expected in all JSON responses from the Gerrit REST API. 88 var jsonPrefix = []byte(")]}'") 89 90 // client implements gerritpb.GerritClient. 91 type client struct { 92 hClient *http.Client 93 94 auth bool 95 host string 96 // testBaseURL overrides auth & host args in tests. 97 testBaseURL string 98 } 99 100 func (c *client) ListChanges(ctx context.Context, req *gerritpb.ListChangesRequest, opts ...grpc.CallOption) (*gerritpb.ListChangesResponse, error) { 101 limit := req.Limit 102 if req.Limit < 0 { 103 return nil, errors.Reason("field Limit %d must be nonnegative", req.Limit).Err() 104 } else if req.Limit == 0 { 105 limit = defaultQueryLimit 106 } else if req.Limit > maxQueryLimit { 107 return nil, errors.Reason("field Limit %d should be at most %d", req.Limit, maxQueryLimit).Err() 108 } 109 if req.Offset < 0 { 110 return nil, errors.Reason("field Offset %d must be nonnegative", req.Offset).Err() 111 } 112 113 params := url.Values{} 114 params.Add("q", req.Query) 115 for _, o := range req.Options { 116 params.Add("o", o.String()) 117 } 118 params.Add("n", strconv.FormatInt(limit, 10)) 119 params.Add("S", strconv.FormatInt(req.Offset, 10)) 120 121 var changes []*changeInfo 122 if _, err := c.call(ctx, "GET", "/changes/", params, nil, &changes, opts); err != nil { 123 return nil, err 124 } 125 126 resp := &gerritpb.ListChangesResponse{} 127 for _, c := range changes { 128 p, err := c.ToProto() 129 if err != nil { 130 return nil, err 131 } 132 resp.Changes = append(resp.Changes, p) 133 } 134 if len(changes) > 0 { 135 resp.MoreChanges = changes[len(changes)-1].MoreChanges 136 } 137 138 return resp, nil 139 } 140 141 func (c *client) GetChange(ctx context.Context, req *gerritpb.GetChangeRequest, opts ...grpc.CallOption) ( 142 *gerritpb.ChangeInfo, error) { 143 if err := checkArgs(opts, req); err != nil { 144 return nil, err 145 } 146 147 path := fmt.Sprintf("/changes/%s", gerritChangeIDForRouting(req.Number, req.Project)) 148 149 params := url.Values{} 150 for _, o := range req.Options { 151 params.Add("o", o.String()) 152 } 153 if meta := req.GetMeta(); meta != "" { 154 params.Add("meta", meta) 155 } 156 157 var resp changeInfo 158 if _, err := c.call(ctx, "GET", path, params, nil, &resp, opts); err != nil { 159 return nil, err 160 } 161 return resp.ToProto() 162 } 163 164 type changeInput struct { 165 Project string `json:"project"` 166 Branch string `json:"branch"` 167 Subject string `json:"subject"` 168 BaseCommit string `json:"base_commit"` 169 } 170 171 func (c *client) CreateChange(ctx context.Context, req *gerritpb.CreateChangeRequest, opts ...grpc.CallOption) (*gerritpb.ChangeInfo, error) { 172 var resp changeInfo 173 data := &changeInput{ 174 Project: req.Project, 175 Branch: req.Ref, 176 Subject: req.Subject, 177 BaseCommit: req.BaseCommit, 178 } 179 180 if _, err := c.call(ctx, "POST", "/changes/", url.Values{}, data, &resp, opts, http.StatusCreated); err != nil { 181 return nil, errors.Annotate(err, "create empty change").Err() 182 } 183 184 switch ci, err := resp.ToProto(); { 185 case err != nil: 186 return nil, err 187 case ci.Status != gerritpb.ChangeStatus_NEW: 188 return nil, errors.Reason("unknown status %s for newly created change", ci.Status).Err() 189 default: 190 return ci, nil 191 } 192 } 193 194 func (c *client) ChangeEditFileContent(ctx context.Context, req *gerritpb.ChangeEditFileContentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 195 // Use QueryEscape instead of PathEscape since "+" can be ambigious 196 // (" " or "+") and it's not escaped in PathEscape. 197 path := fmt.Sprintf("/changes/%s/edit/%s", gerritChangeIDForRouting(req.Number, req.Project), url.QueryEscape(req.FilePath)) 198 if _, _, err := c.callRaw(ctx, "PUT", path, url.Values{}, textInputHeaders(), req.Content, opts, http.StatusNoContent); err != nil { 199 return nil, errors.Annotate(err, "change edit file content").Err() 200 } 201 return &emptypb.Empty{}, nil 202 } 203 204 func (c *client) DeleteEditFileContent(ctx context.Context, req *gerritpb.DeleteEditFileContentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 205 // Use QueryEscape instead of PathEscape since "+" can be ambigious 206 // (" " or "+") and it's not escaped in PathEscape. 207 path := fmt.Sprintf("/changes/%s/edit/%s", gerritChangeIDForRouting(req.Number, req.Project), url.QueryEscape(req.FilePath)) 208 var data struct{} 209 // The response cannot be JSON-deserialized. 210 if _, err := c.call(ctx, "DELETE", path, url.Values{}, &data, nil, opts, http.StatusNoContent); err != nil { 211 return nil, errors.Annotate(err, "delete edit file content").Err() 212 } 213 return &emptypb.Empty{}, nil 214 } 215 216 func (c *client) ChangeEditPublish(ctx context.Context, req *gerritpb.ChangeEditPublishRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 217 path := fmt.Sprintf("/changes/%s/edit:publish", gerritChangeIDForRouting(req.Number, req.Project)) 218 if _, _, err := c.callRaw(ctx, "POST", path, url.Values{}, textInputHeaders(), []byte{}, opts, http.StatusNoContent); err != nil { 219 return nil, errors.Annotate(err, "change edit publish").Err() 220 } 221 return &emptypb.Empty{}, nil 222 } 223 224 func (c *client) AddReviewer(ctx context.Context, req *gerritpb.AddReviewerRequest, opts ...grpc.CallOption) (*gerritpb.AddReviewerResult, error) { 225 var resp addReviewerResult 226 data := &addReviewerRequest{ 227 Reviewer: req.Reviewer, 228 State: enumToString(int32(req.State.Number()), gerritpb.AddReviewerRequest_State_name), 229 Confirmed: req.Confirmed, 230 Notify: enumToString(int32(req.Notify.Number()), gerritpb.Notify_name), 231 } 232 path := fmt.Sprintf("/changes/%s/reviewers", gerritChangeIDForRouting(req.Number, req.Project)) 233 if _, err := c.call(ctx, "POST", path, url.Values{}, data, &resp, opts); err != nil { 234 return nil, errors.Annotate(err, "add reviewers").Err() 235 } 236 rr, err := resp.ToProto() 237 if err != nil { 238 return nil, errors.Annotate(err, "decoding response").Err() 239 } 240 return rr, nil 241 } 242 243 func (c *client) DeleteReviewer(ctx context.Context, req *gerritpb.DeleteReviewerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 244 path := fmt.Sprintf("/changes/%s/reviewers/%s/delete", gerritChangeIDForRouting(req.Number, req.Project), url.PathEscape(req.AccountId)) 245 if _, err := c.call(ctx, "POST", path, url.Values{}, nil, nil, opts, http.StatusNoContent); err != nil { 246 return nil, errors.Annotate(err, "delete reviewer").Err() 247 } 248 return &emptypb.Empty{}, nil 249 } 250 251 func (c *client) SetReview(ctx context.Context, in *gerritpb.SetReviewRequest, opts ...grpc.CallOption) (*gerritpb.ReviewResult, error) { 252 if err := checkArgs(opts, in); err != nil { 253 return nil, err 254 } 255 path := fmt.Sprintf("/changes/%s/revisions/%s/review", gerritChangeIDForRouting(in.Number, in.Project), in.RevisionId) 256 data := reviewInput{ 257 Message: in.Message, 258 Tag: in.Tag, 259 Notify: enumToString(int32(in.Notify.Number()), gerritpb.Notify_name), 260 NotifyDetails: toNotifyDetails(in.GetNotifyDetails()), 261 OnBehalfOf: in.OnBehalfOf, 262 Ready: in.Ready, 263 WorkInProgress: in.WorkInProgress, 264 AddToAttentionSet: toAttentionSetInputs(in.GetAddToAttentionSet()), 265 RemoveFromAttentionSet: toAttentionSetInputs(in.GetRemoveFromAttentionSet()), 266 IgnoreAutomaticAttentionSetRules: in.IgnoreAutomaticAttentionSetRules, 267 Reviewers: toReviewerInputs(in.GetReviewers()), 268 } 269 if in.Labels != nil { 270 data.Labels = make(map[string]int32) 271 for k, v := range in.Labels { 272 data.Labels[k] = v 273 } 274 } 275 var resp reviewResult 276 if _, err := c.call(ctx, "POST", path, url.Values{}, &data, &resp, opts); err != nil { 277 return nil, errors.Annotate(err, "set review").Err() 278 } 279 rr, err := resp.ToProto() 280 if err != nil { 281 return nil, errors.Annotate(err, "decoding response").Err() 282 } 283 return rr, nil 284 } 285 286 func (c *client) AddToAttentionSet(ctx context.Context, req *gerritpb.AttentionSetRequest, opts ...grpc.CallOption) (*gerritpb.AccountInfo, error) { 287 if err := checkArgs(opts, req); err != nil { 288 return nil, err 289 } 290 path := fmt.Sprintf("/changes/%s/attention", gerritChangeIDForRouting(req.Number, req.Project)) 291 data := toAttentionSetInput(req.GetInput()) 292 var resp accountInfo 293 if _, err := c.call(ctx, "POST", path, url.Values{}, data, &resp, opts); err != nil { 294 return nil, errors.Annotate(err, "add to attention set").Err() 295 } 296 return resp.ToProto(), nil 297 } 298 299 func (c *client) SubmitChange(ctx context.Context, req *gerritpb.SubmitChangeRequest, opts ...grpc.CallOption) (*gerritpb.ChangeInfo, error) { 300 var resp changeInfo 301 path := fmt.Sprintf("/changes/%s/submit", gerritChangeIDForRouting(req.Number, req.Project)) 302 var data struct{} 303 if _, err := c.call(ctx, "POST", path, url.Values{}, &data, &resp, opts); err != nil { 304 return nil, errors.Annotate(err, "submit change").Err() 305 } 306 return resp.ToProto() 307 } 308 309 func (c *client) RevertChange(ctx context.Context, req *gerritpb.RevertChangeRequest, opts ...grpc.CallOption) (*gerritpb.ChangeInfo, error) { 310 if err := checkArgs(opts, req); err != nil { 311 return nil, err 312 } 313 314 var resp changeInfo 315 path := fmt.Sprintf("/changes/%s/revert", gerritChangeIDForRouting(req.Number, req.Project)) 316 data := map[string]string{ 317 "message": req.Message, 318 } 319 if _, err := c.call(ctx, "POST", path, url.Values{}, &data, &resp, opts); err != nil { 320 return nil, errors.Annotate(err, "revert change").Err() 321 } 322 return resp.ToProto() 323 } 324 325 func (c *client) AbandonChange(ctx context.Context, req *gerritpb.AbandonChangeRequest, opts ...grpc.CallOption) (*gerritpb.ChangeInfo, error) { 326 var resp changeInfo 327 path := fmt.Sprintf("/changes/%s/abandon", gerritChangeIDForRouting(req.Number, req.Project)) 328 data := map[string]string{ 329 "message": req.Message, 330 } 331 if _, err := c.call(ctx, "POST", path, url.Values{}, &data, &resp, opts); err != nil { 332 return nil, errors.Annotate(err, "abandon change").Err() 333 } 334 return resp.ToProto() 335 } 336 337 func (c *client) SubmitRevision(ctx context.Context, req *gerritpb.SubmitRevisionRequest, opts ...grpc.CallOption) (*gerritpb.SubmitInfo, error) { 338 var resp submitInfo 339 path := fmt.Sprintf("/changes/%s/revisions/%s/submit", gerritChangeIDForRouting(req.Number, req.Project), req.RevisionId) 340 var data struct{} 341 if _, err := c.call(ctx, "POST", path, url.Values{}, &data, &resp, opts); err != nil { 342 return nil, errors.Annotate(err, "submit revision").Err() 343 } 344 return resp.ToProto(), nil 345 } 346 347 func (c *client) GetMergeable(ctx context.Context, in *gerritpb.GetMergeableRequest, opts ...grpc.CallOption) (*gerritpb.MergeableInfo, error) { 348 var resp mergeableInfo 349 path := fmt.Sprintf("/changes/%s/revisions/%s/mergeable", gerritChangeIDForRouting(in.Number, in.Project), in.RevisionId) 350 if _, err := c.call(ctx, "GET", path, url.Values{}, nil, &resp, opts); err != nil { 351 return nil, errors.Annotate(err, "get mergeable").Err() 352 } 353 return resp.ToProto() 354 } 355 356 func (c *client) ListFiles(ctx context.Context, req *gerritpb.ListFilesRequest, opts ...grpc.CallOption) (*gerritpb.ListFilesResponse, error) { 357 var resp map[string]fileInfo 358 params := url.Values{} 359 if req.Parent > 0 { 360 params.Add("parent", strconv.FormatInt(req.Parent, 10)) 361 } 362 if req.SubstringQuery != "" { 363 params.Add("q", req.SubstringQuery) 364 } 365 if req.Base != "" { 366 params.Add("base", req.Base) 367 } 368 path := fmt.Sprintf("/changes/%s/revisions/%s/files/", gerritChangeIDForRouting(req.Number, req.Project), req.RevisionId) 369 if _, err := c.call(ctx, "GET", path, params, nil, &resp, opts); err != nil { 370 return nil, errors.Annotate(err, "list files").Err() 371 } 372 lfr := &gerritpb.ListFilesResponse{ 373 Files: make(map[string]*gerritpb.FileInfo, len(resp)), 374 } 375 for k, v := range resp { 376 lfr.Files[k] = v.ToProto() 377 } 378 return lfr, nil 379 } 380 381 func (c *client) GetRelatedChanges(ctx context.Context, req *gerritpb.GetRelatedChangesRequest, opts ...grpc.CallOption) (*gerritpb.GetRelatedChangesResponse, error) { 382 // Example: 383 // https://chromium-review.googlesource.com/changes/1563638/revisions/2/related 384 path := fmt.Sprintf("/changes/%s/revisions/%s/related", 385 gerritChangeIDForRouting(req.Number, req.Project), req.RevisionId) 386 out := struct { 387 Changes []relatedChangeAndCommitInfo `json:"changes"` 388 }{} 389 if _, err := c.call(ctx, "GET", path, nil, nil, &out, opts); err != nil { 390 return nil, errors.Annotate(err, "related changes").Err() 391 } 392 changes := make([]*gerritpb.GetRelatedChangesResponse_ChangeAndCommit, len(out.Changes)) 393 for i, c := range out.Changes { 394 changes[i] = c.ToProto() 395 } 396 return &gerritpb.GetRelatedChangesResponse{Changes: changes}, nil 397 } 398 399 func (c *client) GetPureRevert(ctx context.Context, req *gerritpb.GetPureRevertRequest, opts ...grpc.CallOption) (*gerritpb.PureRevertInfo, error) { 400 var resp gerritpb.PureRevertInfo 401 path := fmt.Sprintf("/changes/%s/pure_revert", gerritChangeIDForRouting(req.Number, req.Project)) 402 if _, err := c.call(ctx, "GET", path, url.Values{}, nil, &resp, opts); err != nil { 403 return nil, errors.Annotate(err, "pure revert").Err() 404 } 405 return &resp, nil 406 } 407 408 func (c *client) ListFileOwners(ctx context.Context, req *gerritpb.ListFileOwnersRequest, opts ...grpc.CallOption) (*gerritpb.ListOwnersResponse, error) { 409 resp := struct { 410 CodeOwners []ownerInfo `json:"code_owners"` 411 }{} 412 params := url.Values{} 413 if req.Options.Details { 414 params.Add("o", "DETAILS") 415 } 416 if req.Options.AllEmails { 417 params.Add("o", "ALL_EMAILS") 418 } 419 420 path := fmt.Sprintf("/projects/%s/branches/%s/code_owners/%s", url.PathEscape(req.Project), url.PathEscape(req.Ref), url.PathEscape(req.Path)) 421 if _, err := c.call(ctx, "GET", path, params, nil, &resp, opts); err != nil { 422 return nil, errors.Annotate(err, "list file owners").Err() 423 } 424 owners := make([]*gerritpb.OwnerInfo, len(resp.CodeOwners)) 425 for i, owner := range resp.CodeOwners { 426 owners[i] = &gerritpb.OwnerInfo{Account: owner.Account.ToProto()} 427 } 428 return &gerritpb.ListOwnersResponse{ 429 Owners: owners, 430 }, nil 431 } 432 433 func (c *client) ListProjects(ctx context.Context, req *gerritpb.ListProjectsRequest, opts ...grpc.CallOption) (*gerritpb.ListProjectsResponse, error) { 434 resp := map[string]*projectInfo{} 435 params := url.Values{} 436 for _, ref := range req.Refs { 437 params.Add("b", ref) 438 } 439 if _, err := c.call(ctx, "GET", "/projects/", params, nil, &resp, opts); err != nil { 440 return nil, errors.Annotate(err, "list projects").Err() 441 } 442 projectProtos := make(map[string]*gerritpb.ProjectInfo, len(resp)) 443 for id, p := range resp { 444 projectInfo, err := p.ToProto() 445 if err != nil { 446 return nil, errors.Annotate(err, "decoding response").Err() 447 } 448 projectProtos[id] = projectInfo 449 } 450 return &gerritpb.ListProjectsResponse{ 451 Projects: projectProtos, 452 }, nil 453 } 454 455 func (c *client) GetRefInfo(ctx context.Context, req *gerritpb.RefInfoRequest, opts ...grpc.CallOption) (*gerritpb.RefInfo, error) { 456 var resp gerritpb.RefInfo 457 path := fmt.Sprintf("/projects/%s/branches/%s", url.PathEscape(req.Project), url.PathEscape(req.Ref)) 458 if _, err := c.call(ctx, "GET", path, url.Values{}, nil, &resp, opts); err != nil { 459 return nil, errors.Annotate(err, "get branch info").Err() 460 } 461 resp.Ref = branchToRef(resp.Ref) 462 return &resp, nil 463 } 464 465 func (c *client) GetMetaDiff(ctx context.Context, req *gerritpb.GetMetaDiffRequest, opts ...grpc.CallOption) (*gerritpb.MetaDiff, error) { 466 if err := checkArgs(opts, req); err != nil { 467 return nil, err 468 } 469 changeID := gerritChangeIDForRouting(req.GetNumber(), req.GetProject()) 470 path := fmt.Sprintf("/changes/%s/meta_diff", changeID) 471 params := url.Values{} 472 if req.GetOld() != "" { 473 params.Add("old", req.GetOld()) 474 } 475 if req.GetMeta() != "" { 476 params.Add("meta", req.GetMeta()) 477 } 478 for _, o := range req.Options { 479 params.Add("o", o.String()) 480 } 481 482 var resp metaDiff 483 if _, err := c.call(ctx, "GET", path, params, nil, &resp, opts); err != nil { 484 return nil, err 485 } 486 return resp.ToProto() 487 } 488 489 // call executes a request to Gerrit REST API with JSON input/output. 490 // If data is nil, request will be made without a body. 491 // 492 // call returns HTTP status code and gRPC error. 493 // If error happens before HTTP status code was determined, HTTP status code 494 // will be -1. 495 func (c *client) call( 496 ctx context.Context, 497 method, urlPath string, 498 params url.Values, 499 data, dest any, 500 opts []grpc.CallOption, 501 expectedHTTPCodes ...int, 502 ) (int, error) { 503 headers := make(map[string]string) 504 505 var rawData []byte 506 if data != nil { 507 if method == "GET" { 508 // This error prevents a more cryptic HTTP error that would otherwise be 509 // returned by Gerrit if you try to call it with a GET request and a request 510 // body. 511 panic("data cannot be provided for a GET request") 512 } 513 var err error 514 rawData, err = json.Marshal(data) 515 if err != nil { 516 return -1, status.Errorf(codes.Internal, "failed to serialize request message: %s", err) 517 } 518 headers["Content-Type"] = contentTypeJSON 519 } 520 521 ret, body, err := c.callRaw(ctx, method, urlPath, params, headers, rawData, opts, expectedHTTPCodes...) 522 body = bytes.TrimPrefix(body, jsonPrefix) 523 if err == nil && dest != nil { 524 if err = json.Unmarshal(body, dest); err != nil { 525 // Special case for hosts which respond with a redirect when ACLs are 526 // misconfigured. 527 if bytes.Contains(body, []byte("<html")) && bytes.Contains(body, []byte("Single Sign On")) { 528 return ret, status.Errorf(codes.PermissionDenied, "redirected to Single Sign On") 529 } 530 logging.Errorf(ctx, "failed to deserialize response %s; body:\n\n%s", err, string(body)) 531 return ret, status.Errorf(codes.Internal, "failed to deserialize response: %s", err) 532 } 533 } 534 return ret, err 535 } 536 537 // callRaw executes a request to Gerrit REST API with raw bytes input/output. 538 // 539 // callRaw returns HTTP status code and gRPC error. 540 // If error happens before HTTP status code was determined, HTTP status code 541 // will be -1. 542 func (c *client) callRaw( 543 ctx context.Context, 544 method, urlPath string, 545 params url.Values, 546 headers map[string]string, 547 data []byte, 548 opts []grpc.CallOption, 549 expectedHTTPCodes ...int, 550 ) (int, []byte, error) { 551 url := c.buildURL(urlPath, params, opts) 552 var requestBody io.Reader 553 if data != nil { 554 requestBody = bytes.NewBuffer(data) 555 } 556 req, err := http.NewRequest(method, url, requestBody) 557 if err != nil { 558 return 0, []byte{}, status.Errorf(codes.Internal, "failed to create an HTTP request: %s", err) 559 } 560 561 for k, v := range headers { 562 req.Header.Set(k, v) 563 } 564 565 res, err := ctxhttp.Do(ctx, c.hClient, req) 566 switch { 567 case err == context.DeadlineExceeded: 568 return -1, []byte{}, status.Errorf(codes.DeadlineExceeded, "deadline exceeded") 569 case err == context.Canceled: 570 // TODO(crbug/1289476): Remove this HACK that tries to identify Gerrit 571 // timeout after fixing the bug in the clock package. 572 if deadline, ok := ctx.Deadline(); ok && clock.Now(ctx).After(deadline) { 573 return -1, []byte{}, status.Errorf(codes.DeadlineExceeded, "deadline exceeded") 574 } 575 return -1, []byte{}, status.Errorf(codes.Canceled, "context is cancelled") 576 case err != nil: 577 return -1, []byte{}, status.Errorf(codes.Internal, "failed to execute %s HTTP request: %s", method, err) 578 } 579 defer res.Body.Close() 580 581 body, err := io.ReadAll(res.Body) 582 if err != nil { 583 return res.StatusCode, []byte{}, status.Errorf(codes.Internal, "failed to read response: %s", err) 584 } 585 logging.Debugf(ctx, "HTTP %d %d <= %s %s", res.StatusCode, len(body), method, url) 586 587 expectedHTTPCodes = append(expectedHTTPCodes, http.StatusOK) 588 for _, s := range expectedHTTPCodes { 589 if res.StatusCode == s { 590 return res.StatusCode, body, nil 591 } 592 } 593 594 switch res.StatusCode { 595 case http.StatusTooManyRequests: 596 logging.Debugf(ctx, "Gerrit quota error.\nResponse headers: %v\nResponse body: %s", res.Header, body) 597 return res.StatusCode, body, status.Errorf(codes.ResourceExhausted, "insufficient Gerrit quota") 598 599 case http.StatusForbidden: 600 logging.Debugf(ctx, "Gerrit permission denied:\nResponse headers: %v\nResponse body: %s", res.Header, body) 601 return res.StatusCode, body, status.Errorf(codes.PermissionDenied, "permission denied") 602 603 case http.StatusNotFound: 604 return res.StatusCode, body, status.Errorf(codes.NotFound, "not found") 605 606 // Both codes are mapped to codes.FailedPrecondition so that apps using 607 // this Gerrit client wouldn't be able to distinguish them by the grpc code. 608 // However, 609 // - http.StatusConflict(409) is returned by mutation Gerrit APIs only, but 610 // - http.StatusPreconditionFailed(412) is returned by the fetch APIs only. 611 // 612 // Hence, apps shouldn't have to distinguish them from 613 // codes.FailedPrecondition. If Gerrit changes the rest APIs so that a 614 // single API can return both of them, then this can be revisited. 615 case http.StatusConflict, http.StatusPreconditionFailed: 616 // Gerrit returns error message in the response body. 617 return res.StatusCode, body, status.Errorf(codes.FailedPrecondition, "%s", string(body)) 618 619 case http.StatusBadRequest: 620 // Gerrit returns error message in the response body. 621 return res.StatusCode, body, status.Errorf(codes.InvalidArgument, "%s", string(body)) 622 623 case http.StatusBadGateway: 624 return res.StatusCode, body, status.Errorf(codes.Unavailable, "bad gateway") 625 626 case http.StatusServiceUnavailable: 627 return res.StatusCode, body, status.Errorf(codes.Unavailable, "%s", string(body)) 628 629 default: 630 logging.Errorf(ctx, "gerrit: unexpected HTTP %d response.\nResponse headers: %v\nResponse body: %s", 631 res.StatusCode, 632 res.Header, body) 633 return res.StatusCode, body, status.Errorf(codes.Internal, "unexpected HTTP %d from Gerrit", res.StatusCode) 634 } 635 } 636 637 func (c *client) buildURL(path string, params url.Values, opts []grpc.CallOption) string { 638 var url strings.Builder 639 640 host := c.host 641 for _, opt := range opts { 642 if g, ok := opt.(*gerritMirrorOption); ok { 643 host = g.altHost(host) 644 } 645 } 646 647 if c.testBaseURL != "" { 648 url.WriteString(c.testBaseURL) 649 } else { 650 url.WriteString("https://") 651 url.WriteString(host) 652 if c.auth { 653 url.WriteString("/a") 654 } 655 } 656 657 url.WriteString(path) 658 if len(params) > 0 { 659 url.WriteRune('?') 660 url.WriteString(params.Encode()) 661 } 662 return url.String() 663 } 664 665 type validatable interface { 666 Validate() error 667 } 668 669 type gerritMirrorOption struct { 670 grpc.EmptyCallOption // to implement a grpc.CallOption 671 altHost func(host string) string 672 } 673 674 // UseGerritMirror can be passed as grpc.CallOption to Gerrit client to select 675 // an alternative Gerrit host to the one configured in the client "on the fly". 676 func UseGerritMirror(altHost func(host string) string) grpc.CallOption { 677 if altHost == nil { 678 panic(fmt.Errorf("altHost must be non-nil")) 679 } 680 return &gerritMirrorOption{altHost: altHost} 681 } 682 683 func checkArgs(opts []grpc.CallOption, req validatable) error { 684 for _, opt := range opts { 685 if _, ok := opt.(*gerritMirrorOption); !ok { 686 return errors.New("gerrit.client supports only UseGerritMirror option") 687 } 688 } 689 if err := req.Validate(); err != nil { 690 return errors.Annotate(err, "request is invalid").Err() 691 } 692 return nil 693 } 694 695 func branchToRef(ref string) string { 696 if strings.HasPrefix(ref, "refs/") { 697 // Assume it's actually an explicit full ref. 698 // Also assume that ref "refs/heads/refs/XXX will be passed as is by Gerrit 699 // instead of "refs/XXX" 700 return ref 701 } 702 return "refs/heads/" + ref 703 } 704 705 func gerritChangeIDForRouting(number int64, project string) string { 706 if project != "" { 707 return fmt.Sprintf("%s~%d", url.PathEscape(project), number) 708 } 709 return strconv.Itoa(int(number)) 710 } 711 712 func textInputHeaders() map[string]string { 713 return map[string]string{ 714 "Content-Type": contentTypeText, 715 "Accept": contentTypeJSON, 716 } 717 }