Merge branch '9180-avoid-overreplication-keepclient'
[arvados.git] / sdk / go / arvados / client.go
1 package arvados
2
3 import (
4         "crypto/tls"
5         "encoding/json"
6         "fmt"
7         "io"
8         "io/ioutil"
9         "math"
10         "net/http"
11         "net/url"
12         "os"
13 )
14
15 // A Client is an HTTP client with an API endpoint and a set of
16 // Arvados credentials.
17 //
18 // It offers methods for accessing individual Arvados APIs, and
19 // methods that implement common patterns like fetching multiple pages
20 // of results using List APIs.
21 type Client struct {
22         // HTTP client used to make requests. If nil,
23         // http.DefaultClient or InsecureHTTPClient will be used.
24         Client *http.Client
25
26         // Hostname (or host:port) of Arvados API server.
27         APIHost string
28
29         // User authentication token.
30         AuthToken string
31
32         // Accept unverified certificates. This works only if the
33         // Client field is nil: otherwise, it has no effect.
34         Insecure bool
35 }
36
37 // The default http.Client used by a Client with Insecure==true and
38 // Client==nil.
39 var InsecureHTTPClient = &http.Client{
40         Transport: &http.Transport{
41                 TLSClientConfig: &tls.Config{
42                         InsecureSkipVerify: true}}}
43
44 // NewClientFromEnv creates a new Client that uses the default HTTP
45 // client with the API endpoint and credentials given by the
46 // ARVADOS_API_* environment variables.
47 func NewClientFromEnv() *Client {
48         return &Client{
49                 APIHost:   os.Getenv("ARVADOS_API_HOST"),
50                 AuthToken: os.Getenv("ARVADOS_API_TOKEN"),
51                 Insecure:  os.Getenv("ARVADOS_API_HOST_INSECURE") != "",
52         }
53 }
54
55 // Do adds authentication headers and then calls (*http.Client)Do().
56 func (c *Client) Do(req *http.Request) (*http.Response, error) {
57         if c.AuthToken != "" {
58                 req.Header.Add("Authorization", "OAuth2 "+c.AuthToken)
59         }
60         return c.httpClient().Do(req)
61 }
62
63 // DoAndDecode performs req and unmarshals the response (which must be
64 // JSON) into dst. Use this instead of RequestAndDecode if you need
65 // more control of the http.Request object.
66 func (c *Client) DoAndDecode(dst interface{}, req *http.Request) error {
67         resp, err := c.Do(req)
68         if err != nil {
69                 return err
70         }
71         defer resp.Body.Close()
72         buf, err := ioutil.ReadAll(resp.Body)
73         if err != nil {
74                 return err
75         }
76         if resp.StatusCode != 200 {
77                 return newTransactionError(req, resp, buf)
78         }
79         if dst == nil {
80                 return nil
81         }
82         return json.Unmarshal(buf, dst)
83 }
84
85 // Convert an arbitrary struct to url.Values. For example,
86 //
87 //     Foo{Bar: []int{1,2,3}, Baz: "waz"}
88 //
89 // becomes
90 //
91 //     url.Values{`bar`:`{"a":[1,2,3]}`,`Baz`:`waz`}
92 //
93 // params itself is returned if it is already an url.Values.
94 func anythingToValues(params interface{}) (url.Values, error) {
95         if v, ok := params.(url.Values); ok {
96                 return v, nil
97         }
98         // TODO: Do this more efficiently, possibly using
99         // json.Decode/Encode, so the whole thing doesn't have to get
100         // encoded, decoded, and re-encoded.
101         j, err := json.Marshal(params)
102         if err != nil {
103                 return nil, err
104         }
105         var generic map[string]interface{}
106         err = json.Unmarshal(j, &generic)
107         if err != nil {
108                 return nil, err
109         }
110         urlValues := url.Values{}
111         for k, v := range generic {
112                 if v, ok := v.(string); ok {
113                         urlValues.Set(k, v)
114                         continue
115                 }
116                 if v, ok := v.(float64); ok {
117                         // Unmarshal decodes all numbers as float64,
118                         // which can be written as 1.2345e4 in JSON,
119                         // but this form is not accepted for ints in
120                         // url params. If a number fits in an int64,
121                         // encode it as int64 rather than float64.
122                         if v, frac := math.Modf(v); frac == 0 && v <= math.MaxInt64 && v >= math.MinInt64 {
123                                 urlValues.Set(k, fmt.Sprintf("%d", int64(v)))
124                                 continue
125                         }
126                 }
127                 j, err := json.Marshal(v)
128                 if err != nil {
129                         return nil, err
130                 }
131                 urlValues.Set(k, string(j))
132         }
133         return urlValues, nil
134 }
135
136 // RequestAndDecode performs an API request and unmarshals the
137 // response (which must be JSON) into dst. Method and body arguments
138 // are the same as for http.NewRequest(). The given path is added to
139 // the server's scheme/host/port to form the request URL. The given
140 // params are passed via POST form or query string.
141 //
142 // path must not contain a query string.
143 func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io.Reader, params interface{}) error {
144         urlString := c.apiURL(path)
145         urlValues, err := anythingToValues(params)
146         if err != nil {
147                 return err
148         }
149         if (method == "GET" || body != nil) && urlValues != nil {
150                 // FIXME: what if params don't fit in URL
151                 u, err := url.Parse(urlString)
152                 if err != nil {
153                         return err
154                 }
155                 u.RawQuery = urlValues.Encode()
156                 urlString = u.String()
157         }
158         req, err := http.NewRequest(method, urlString, body)
159         if err != nil {
160                 return err
161         }
162         return c.DoAndDecode(dst, req)
163 }
164
165 func (c *Client) httpClient() *http.Client {
166         switch {
167         case c.Client != nil:
168                 return c.Client
169         case c.Insecure:
170                 return InsecureHTTPClient
171         default:
172                 return http.DefaultClient
173         }
174 }
175
176 func (c *Client) apiURL(path string) string {
177         return "https://" + c.APIHost + "/" + path
178 }
179
180 // DiscoveryDocument is the Arvados server's description of itself.
181 type DiscoveryDocument struct {
182         DefaultCollectionReplication int   `json:"defaultCollectionReplication"`
183         BlobSignatureTTL             int64 `json:"blobSignatureTtl"`
184 }
185
186 // DiscoveryDocument returns a *DiscoveryDocument. The returned object
187 // should not be modified: the same object may be returned by
188 // subsequent calls.
189 func (c *Client) DiscoveryDocument() (*DiscoveryDocument, error) {
190         var dd DiscoveryDocument
191         return &dd, c.RequestAndDecode(&dd, "GET", "discovery/v1/apis/arvados/v1/rest", nil, nil)
192 }