Add 'apps/arv-web/' from commit 'f9732ad8460d013c2f28363655d0d1b91894dca5'
[arvados.git] / sdk / go / arvadosclient / arvadosclient.go
1 /* Simple Arvados Go SDK for communicating with API server. */
2
3 package arvadosclient
4
5 import (
6         "bytes"
7         "crypto/tls"
8         "encoding/json"
9         "errors"
10         "fmt"
11         "io"
12         "net/http"
13         "net/url"
14         "os"
15         "strings"
16 )
17
18 // Errors
19 var MissingArvadosApiHost = errors.New("Missing required environment variable ARVADOS_API_HOST")
20 var MissingArvadosApiToken = errors.New("Missing required environment variable ARVADOS_API_TOKEN")
21
22 type ArvadosApiError struct {
23         error
24         HttpStatusCode int
25         HttpStatus string
26 }
27
28 func (e ArvadosApiError) Error() string { return e.error.Error() }
29
30 // Helper type so we don't have to write out 'map[string]interface{}' every time.
31 type Dict map[string]interface{}
32
33 // Information about how to contact the Arvados server
34 type ArvadosClient struct {
35         // Arvados API server, form "host:port"
36         ApiServer string
37
38         // Arvados API token for authentication
39         ApiToken string
40
41         // Whether to require a valid SSL certificate or not
42         ApiInsecure bool
43
44         // Client object shared by client requests.  Supports HTTP KeepAlive.
45         Client *http.Client
46
47         // If true, sets the X-External-Client header to indicate
48         // the client is outside the cluster.
49         External bool
50 }
51
52 // Create a new KeepClient, initialized with standard Arvados environment
53 // variables ARVADOS_API_HOST, ARVADOS_API_TOKEN, and (optionally)
54 // ARVADOS_API_HOST_INSECURE.
55 func MakeArvadosClient() (kc ArvadosClient, err error) {
56         insecure := (os.Getenv("ARVADOS_API_HOST_INSECURE") == "true")
57         external := (os.Getenv("ARVADOS_EXTERNAL_CLIENT") == "true")
58
59         kc = ArvadosClient{
60                 ApiServer:   os.Getenv("ARVADOS_API_HOST"),
61                 ApiToken:    os.Getenv("ARVADOS_API_TOKEN"),
62                 ApiInsecure: insecure,
63                 Client: &http.Client{Transport: &http.Transport{
64                         TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}}},
65                 External: external}
66
67         if os.Getenv("ARVADOS_API_HOST") == "" {
68                 return kc, MissingArvadosApiHost
69         }
70         if os.Getenv("ARVADOS_API_TOKEN") == "" {
71                 return kc, MissingArvadosApiToken
72         }
73
74         return kc, err
75 }
76
77 // Low-level access to a resource.
78 //
79 //   method - HTTP method, one of GET, HEAD, PUT, POST or DELETE
80 //   resource - the arvados resource to act on
81 //   uuid - the uuid of the specific item to access (may be empty)
82 //   action - sub-action to take on the resource or uuid (may be empty)
83 //   parameters - method parameters
84 //
85 // return
86 //   reader - the body reader, or nil if there was an error
87 //   err - error accessing the resource, or nil if no error
88 func (this ArvadosClient) CallRaw(method string, resource string, uuid string, action string, parameters Dict) (reader io.ReadCloser, err error) {
89         var req *http.Request
90
91         u := url.URL{
92                 Scheme: "https",
93                 Host:   this.ApiServer}
94
95         u.Path = "/arvados/v1"
96
97         if resource != "" {
98                 u.Path = u.Path + "/" + resource
99         }
100         if uuid != "" {
101                 u.Path = u.Path + "/" + uuid
102         }
103         if action != "" {
104                 u.Path = u.Path + "/" + action
105         }
106
107         if parameters == nil {
108                 parameters = make(Dict)
109         }
110
111         parameters["format"] = "json"
112
113         vals := make(url.Values)
114         for k, v := range parameters {
115                 m, err := json.Marshal(v)
116                 if err == nil {
117                         vals.Set(k, string(m))
118                 }
119         }
120
121         if method == "GET" || method == "HEAD" {
122                 u.RawQuery = vals.Encode()
123                 if req, err = http.NewRequest(method, u.String(), nil); err != nil {
124                         return nil, err
125                 }
126         } else {
127                 if req, err = http.NewRequest(method, u.String(), bytes.NewBufferString(vals.Encode())); err != nil {
128                         return nil, err
129                 }
130                 req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
131         }
132
133         // Add api token header
134         req.Header.Add("Authorization", fmt.Sprintf("OAuth2 %s", this.ApiToken))
135         if this.External {
136                 req.Header.Add("X-External-Client", "1")
137         }
138
139         // Make the request
140         var resp *http.Response
141         if resp, err = this.Client.Do(req); err != nil {
142                 return nil, err
143         }
144
145         if resp.StatusCode == http.StatusOK {
146                 return resp.Body, nil
147         }
148
149         defer resp.Body.Close()
150         errorText := fmt.Sprintf("API response: %s", resp.Status)
151
152         // If the response body has {"errors":["reason1","reason2"]}
153         // then return those reasons.
154         var errInfo = Dict{}
155         if err := json.NewDecoder(resp.Body).Decode(&errInfo); err == nil {
156                 if errorList, ok := errInfo["errors"]; ok {
157                         var errorStrings []string
158                         if errArray, ok := errorList.([]interface{}); ok {
159                                 for _, errItem := range errArray {
160                                         // We expect an array of strings here.
161                                         // Non-strings will be passed along
162                                         // JSON-encoded.
163                                         if s, ok := errItem.(string); ok {
164                                                 errorStrings = append(errorStrings, s)
165                                         } else if j, err := json.Marshal(errItem); err == nil {
166                                                 errorStrings = append(errorStrings, string(j))
167                                         }
168                                 }
169                                 errorText = strings.Join(errorStrings, "; ")
170                         }
171                 }
172         }
173         return nil, ArvadosApiError{errors.New(errorText), resp.StatusCode, resp.Status}
174 }
175
176 // Access to a resource.
177 //
178 //   method - HTTP method, one of GET, HEAD, PUT, POST or DELETE
179 //   resource - the arvados resource to act on
180 //   uuid - the uuid of the specific item to access (may be empty)
181 //   action - sub-action to take on the resource or uuid (may be empty)
182 //   parameters - method parameters
183 //   output - a map or annotated struct which is a legal target for encoding/json/Decoder
184 // return
185 //   err - error accessing the resource, or nil if no error
186 func (this ArvadosClient) Call(method string, resource string, uuid string, action string, parameters Dict, output interface{}) (err error) {
187         var reader io.ReadCloser
188         reader, err = this.CallRaw(method, resource, uuid, action, parameters)
189         if reader != nil {
190                 defer reader.Close()
191         }
192         if err != nil {
193                 return err
194         }
195
196         if output != nil {
197                 dec := json.NewDecoder(reader)
198                 if err = dec.Decode(output); err != nil {
199                         return err
200                 }
201         }
202         return nil
203 }
204
205 // Create a new instance of a resource.
206 //
207 //   resource - the arvados resource on which to create an item
208 //   parameters - method parameters
209 //   output - a map or annotated struct which is a legal target for encoding/json/Decoder
210 // return
211 //   err - error accessing the resource, or nil if no error
212 func (this ArvadosClient) Create(resource string, parameters Dict, output interface{}) (err error) {
213         return this.Call("POST", resource, "", "", parameters, output)
214 }
215
216 // Delete an instance of a resource.
217 //
218 //   resource - the arvados resource on which to delete an item
219 //   uuid - the item to delete
220 //   parameters - method parameters
221 //   output - a map or annotated struct which is a legal target for encoding/json/Decoder
222 // return
223 //   err - error accessing the resource, or nil if no error
224 func (this ArvadosClient) Delete(resource string, uuid string, parameters Dict, output interface{}) (err error) {
225         return this.Call("DELETE", resource, uuid, "", parameters, output)
226 }
227
228 // Update fields of an instance of a resource.
229 //
230 //   resource - the arvados resource on which to update the item
231 //   uuid - the item to update
232 //   parameters - method parameters
233 //   output - a map or annotated struct which is a legal target for encoding/json/Decoder
234 // return
235 //   err - error accessing the resource, or nil if no error
236 func (this ArvadosClient) Update(resource string, uuid string, parameters Dict, output interface{}) (err error) {
237         return this.Call("PUT", resource, uuid, "", parameters, output)
238 }
239
240 // List the instances of a resource
241 //
242 //   resource - the arvados resource on which to list
243 //   parameters - method parameters
244 //   output - a map or annotated struct which is a legal target for encoding/json/Decoder
245 // return
246 //   err - error accessing the resource, or nil if no error
247 func (this ArvadosClient) List(resource string, parameters Dict, output interface{}) (err error) {
248         return this.Call("GET", resource, "", "", parameters, output)
249 }