X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/55aafbb07904ca24390dd47ea960eae7cb2b909a..b3d57ff3ccf9c612a11fcf53a451a0f61a362da6:/sdk/go/arvados/client.go diff --git a/sdk/go/arvados/client.go b/sdk/go/arvados/client.go index 47a953ac2c..cbc2ca72f0 100644 --- a/sdk/go/arvados/client.go +++ b/sdk/go/arvados/client.go @@ -5,6 +5,8 @@ package arvados import ( + "bytes" + "context" "crypto/tls" "encoding/json" "fmt" @@ -18,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 @@ -49,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 @@ -63,6 +69,21 @@ var InsecureHTTPClient = &http.Client{ var DefaultSecureClient = &http.Client{ Timeout: 5 * time.Minute} +// NewClientFromConfig creates a new Client that uses the endpoints in +// the given cluster. +// +// AuthToken is left empty for the caller to populate. +func NewClientFromConfig(cluster *Cluster) (*Client, error) { + ctrlURL := cluster.Services.Controller.ExternalURL + if ctrlURL.Host == "" { + return nil, fmt.Errorf("no host in config Services.Controller.ExternalURL: %v", ctrlURL) + } + return &Client{ + APIHost: ctrlURL.Host, + Insecure: cluster.TLS.Insecure, + }, nil +} + // NewClientFromEnv creates a new Client that uses the default HTTP // client with the API endpoint and credentials given by the // ARVADOS_API_* environment variables. @@ -91,11 +112,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) } @@ -180,19 +216,28 @@ 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 { return err } - if (method == "GET" || body != nil) && urlValues != nil { - // FIXME: what if params don't fit in URL + if urlValues == nil { + // Nothing to send + } else if method == "GET" || method == "HEAD" || body != nil { + // Must send params in query part of URL (FIXME: what + // if resulting URL is too long?) u, err := url.Parse(urlString) if err != nil { return err } u.RawQuery = urlValues.Encode() urlString = u.String() + } else { + body = strings.NewReader(urlValues.Encode()) } req, err := http.NewRequest(method, urlString, body) if err != nil { @@ -202,6 +247,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: @@ -222,6 +302,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"` }