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