| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package monorail |
| 6 |
| 7 import ( |
| 8 "bytes" |
| 9 "encoding/json" |
| 10 "fmt" |
| 11 "io" |
| 12 "io/ioutil" |
| 13 "net/http" |
| 14 "strings" |
| 15 |
| 16 "golang.org/x/net/context" |
| 17 "golang.org/x/net/context/ctxhttp" |
| 18 "google.golang.org/grpc" |
| 19 |
| 20 "github.com/luci/luci-go/common/clock" |
| 21 "github.com/luci/luci-go/common/errors" |
| 22 "github.com/luci/luci-go/common/logging" |
| 23 ) |
| 24 |
| 25 // epClient implements MonorailClient by sending requests to Monorail's |
| 26 // Cloud Endpoints API. |
| 27 // See https://monorail-staging.appspot.com/_ah/api/explorer#p/monorail/v1/ |
| 28 type epClient struct { |
| 29 HTTP *http.Client |
| 30 // url is root of API without trailing slash, |
| 31 // e.g. "https://monorail-staging.appspot.com/_ah/api/monorail/v1" |
| 32 url string |
| 33 } |
| 34 |
| 35 // NewEndpointsClient creates a MonorailClient that send requests to |
| 36 // Monorail Cloud Endpoints API at url. |
| 37 // |
| 38 // Example of url: "https://monorail-staging.appspot.com/_ah/api/monorail/v1" |
| 39 // |
| 40 // Methods do not implement retries. |
| 41 // Use "github.com/luci/luci-go/common/errors".IsTransient to check |
| 42 // if an error is transient. |
| 43 // |
| 44 // Client methods return an error on any grpc.CallOption. |
| 45 func NewEndpointsClient(client *http.Client, url string) MonorailClient { |
| 46 return &epClient{HTTP: client, url: strings.TrimSuffix(url, "/")} |
| 47 } |
| 48 |
| 49 func (c *epClient) call(ctx context.Context, urlSuffix string, request, response
interface{}) error { |
| 50 client := c.HTTP |
| 51 if client == nil { |
| 52 client = http.DefaultClient |
| 53 } |
| 54 |
| 55 // Limit ctx deadline to timeout set in client. |
| 56 if client.Timeout > 0 { |
| 57 clientDeadline := clock.Now(ctx).Add(client.Timeout) |
| 58 if deadline, ok := ctx.Deadline(); !ok || deadline.After(clientD
eadline) { |
| 59 ctx, _ = context.WithDeadline(ctx, clientDeadline) |
| 60 } |
| 61 } |
| 62 |
| 63 // Convert request object to JSON. |
| 64 reqBuf := &bytes.Buffer{} |
| 65 if request != nil { |
| 66 if err := json.NewEncoder(reqBuf).Encode(request); err != nil { |
| 67 return err |
| 68 } |
| 69 } |
| 70 |
| 71 // Make an HTTP request. |
| 72 req, err := http.NewRequest("POST", c.url + urlSuffix, reqBuf) |
| 73 if err != nil { |
| 74 return fmt.Errorf("could not make a request to %s: %s", req.URL,
err) |
| 75 } |
| 76 req.Header.Set("Content-Type", "application/json") |
| 77 req.Header.Set("Accept", "application/json") |
| 78 |
| 79 // Send the request. |
| 80 logging.Debugf(ctx, "POST %s %s", req.URL, reqBuf.Bytes()) |
| 81 res, err := ctxhttp.Do(ctx, client, req) |
| 82 if err != nil { |
| 83 return errors.WrapTransient(err) |
| 84 } |
| 85 defer res.Body.Close() |
| 86 |
| 87 // Check HTTP status code. |
| 88 if res.StatusCode != http.StatusOK { |
| 89 text, _ := ioutil.ReadAll(io.LimitReader(res.Body, 1024)) |
| 90 err := fmt.Errorf("unexpected status %q. Response: %s", res.Stat
us, text) |
| 91 if res.StatusCode == http.StatusNotFound || res.StatusCode > 500
{ |
| 92 // Cloud Endpoints often flake with HTTP 404. |
| 93 // Treat such responses transient errors. |
| 94 err = errors.WrapTransient(err) |
| 95 } |
| 96 return err |
| 97 } |
| 98 |
| 99 if response == nil { |
| 100 return nil |
| 101 } |
| 102 return json.NewDecoder(res.Body).Decode(response) |
| 103 } |
| 104 |
| 105 func (c *epClient) InsertIssue(ctx context.Context, req *InsertIssueRequest, opt
ions ...grpc.CallOption) (*InsertIssueResponse, error) { |
| 106 if err := checkOptions(options); err != nil { |
| 107 return nil, err |
| 108 } |
| 109 if err := req.Validate(); err != nil { |
| 110 return nil, err |
| 111 } |
| 112 url := fmt.Sprintf("/projects/%s/issues?sendEmail=%v", req.ProjectId, re
q.SendEmail) |
| 113 res := &InsertIssueResponse{&Issue{}} |
| 114 return res, c.call(ctx, url, &req.Issue, res.Issue) |
| 115 } |
| 116 |
| 117 func (c *epClient) InsertComment(ctx context.Context, req *InsertCommentRequest,
options ...grpc.CallOption) (*InsertCommentResponse, error) { |
| 118 if err := checkOptions(options); err != nil { |
| 119 return nil, err |
| 120 } |
| 121 url := fmt.Sprintf("/projects/%s/issues/%d/comments", req.Issue.ProjectI
d, req.Issue.IssueId) |
| 122 return &InsertCommentResponse{}, c.call(ctx, url, req.Comment, nil) |
| 123 } |
| 124 |
| 125 func checkOptions(options []grpc.CallOption) error { |
| 126 if len(options) > 0 { |
| 127 return errGrpcOptions |
| 128 } |
| 129 return nil |
| 130 } |
| OLD | NEW |