Merge branch 'master' into 13804-no-shutdown-wanted-nodes
[arvados.git] / sdk / go / arvados / client.go
index d7eb811b8a7a26a08931b07b9c66fcfec83d78a3..cca9f9bf1be8e946b7b9594f1ed839e92aa73485 100644 (file)
@@ -1,6 +1,12 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
 package arvados
 
 import (
+       "bytes"
+       "context"
        "crypto/tls"
        "encoding/json"
        "fmt"
@@ -14,6 +20,8 @@ import (
        "regexp"
        "strings"
        "time"
+
+       "git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
 // A Client is an HTTP client with an API endpoint and a set of
@@ -45,6 +53,8 @@ type Client struct {
        KeepServiceURIs []string `json:",omitempty"`
 
        dd *DiscoveryDocument
+
+       ctx context.Context
 }
 
 // The default http.Client used by a Client with Insecure==true and
@@ -87,11 +97,26 @@ func NewClientFromEnv() *Client {
        }
 }
 
-// Do adds authentication headers and then calls (*http.Client)Do().
+var reqIDGen = httpserver.IDGenerator{Prefix: "req-"}
+
+// Do adds Authorization and X-Request-Id headers and then calls
+// (*http.Client)Do().
 func (c *Client) Do(req *http.Request) (*http.Response, error) {
        if c.AuthToken != "" {
                req.Header.Add("Authorization", "OAuth2 "+c.AuthToken)
        }
+
+       if req.Header.Get("X-Request-Id") == "" {
+               reqid, _ := c.context().Value(contextKeyRequestID).(string)
+               if reqid == "" {
+                       reqid = reqIDGen.Next()
+               }
+               if req.Header == nil {
+                       req.Header = http.Header{"X-Request-Id": {reqid}}
+               } else {
+                       req.Header.Set("X-Request-Id", reqid)
+               }
+       }
        return c.httpClient().Do(req)
 }
 
@@ -176,6 +201,10 @@ func anythingToValues(params interface{}) (url.Values, error) {
 //
 // path must not contain a query string.
 func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io.Reader, params interface{}) error {
+       if body, ok := body.(io.Closer); ok {
+               // Ensure body is closed even if we error out early
+               defer body.Close()
+       }
        urlString := c.apiURL(path)
        urlValues, err := anythingToValues(params)
        if err != nil {
@@ -198,6 +227,41 @@ func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io.
        return c.DoAndDecode(dst, req)
 }
 
+type resource interface {
+       resourceName() string
+}
+
+// UpdateBody returns an io.Reader suitable for use as an http.Request
+// Body for a create or update API call.
+func (c *Client) UpdateBody(rsc resource) io.Reader {
+       j, err := json.Marshal(rsc)
+       if err != nil {
+               // Return a reader that returns errors.
+               r, w := io.Pipe()
+               w.CloseWithError(err)
+               return r
+       }
+       v := url.Values{rsc.resourceName(): {string(j)}}
+       return bytes.NewBufferString(v.Encode())
+}
+
+type contextKey string
+
+var contextKeyRequestID contextKey = "X-Request-Id"
+
+func (c *Client) WithRequestID(reqid string) *Client {
+       cc := *c
+       cc.ctx = context.WithValue(cc.context(), contextKeyRequestID, reqid)
+       return &cc
+}
+
+func (c *Client) context() context.Context {
+       if c.ctx == nil {
+               return context.Background()
+       }
+       return c.ctx
+}
+
 func (c *Client) httpClient() *http.Client {
        switch {
        case c.Client != nil:
@@ -218,6 +282,7 @@ type DiscoveryDocument struct {
        BasePath                     string              `json:"basePath"`
        DefaultCollectionReplication int                 `json:"defaultCollectionReplication"`
        BlobSignatureTTL             int64               `json:"blobSignatureTtl"`
+       GitURL                       string              `json:"gitUrl"`
        Schemas                      map[string]Schema   `json:"schemas"`
        Resources                    map[string]Resource `json:"resources"`
 }