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