X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/0b898bf7c9337ac0f2385159329d2f3e6a1694ec..080c940d7a8134a6e277a53b7e45eb27e2b2c87f:/sdk/go/arvados/client.go diff --git a/sdk/go/arvados/client.go b/sdk/go/arvados/client.go index fc937494e5..cca9f9bf1b 100644 --- a/sdk/go/arvados/client.go +++ b/sdk/go/arvados/client.go @@ -1,17 +1,27 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 + package arvados import ( + "bytes" + "context" "crypto/tls" "encoding/json" "fmt" "io" "io/ioutil" + "log" "math" "net/http" "net/url" "os" + "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 @@ -43,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 @@ -62,22 +74,49 @@ var DefaultSecureClient = &http.Client{ // ARVADOS_API_* environment variables. func NewClientFromEnv() *Client { var svcs []string - if s := os.Getenv("ARVADOS_KEEP_SERVICES"); s != "" { - svcs = strings.Split(s, " ") + for _, s := range strings.Split(os.Getenv("ARVADOS_KEEP_SERVICES"), " ") { + if s == "" { + continue + } else if u, err := url.Parse(s); err != nil { + log.Printf("ARVADOS_KEEP_SERVICES: %q: %s", s, err) + } else if !u.IsAbs() { + log.Printf("ARVADOS_KEEP_SERVICES: %q: not an absolute URI", s) + } else { + svcs = append(svcs, s) + } + } + var insecure bool + if s := strings.ToLower(os.Getenv("ARVADOS_API_HOST_INSECURE")); s == "1" || s == "yes" || s == "true" { + insecure = true } return &Client{ APIHost: os.Getenv("ARVADOS_API_HOST"), AuthToken: os.Getenv("ARVADOS_API_TOKEN"), - Insecure: os.Getenv("ARVADOS_API_HOST_INSECURE") != "", + Insecure: insecure, KeepServiceURIs: svcs, } } -// 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) } @@ -162,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 { @@ -180,9 +223,45 @@ func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io. if err != nil { return err } + req.Header.Set("Content-type", "application/x-www-form-urlencoded") 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: @@ -203,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"` } @@ -241,7 +321,12 @@ func (c *Client) DiscoveryDocument() (*DiscoveryDocument, error) { return c.dd, nil } +var pdhRegexp = regexp.MustCompile(`^[0-9a-f]{32}\+\d+$`) + func (c *Client) modelForUUID(dd *DiscoveryDocument, uuid string) (string, error) { + if pdhRegexp.MatchString(uuid) { + return "Collection", nil + } if len(uuid) != 27 { return "", fmt.Errorf("invalid UUID: %q", uuid) }