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