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