1 /* Simple Arvados Go SDK for communicating with API server. */
19 type StringMatcher func(string) bool
21 var UUIDMatch StringMatcher = regexp.MustCompile(`^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}$`).MatchString
22 var PDHMatch StringMatcher = regexp.MustCompile(`^[0-9a-f]{32}\+\d+$`).MatchString
24 var MissingArvadosApiHost = errors.New("Missing required environment variable ARVADOS_API_HOST")
25 var MissingArvadosApiToken = errors.New("Missing required environment variable ARVADOS_API_TOKEN")
26 var ErrInvalidArgument = errors.New("Invalid argument")
28 // Indicates an error that was returned by the API server.
29 type APIServerError struct {
30 // Address of server returning error, of the form "host:port".
33 // Components of server response.
35 HttpStatusMessage string
37 // Additional error details from response body.
41 func (e APIServerError) Error() string {
42 if len(e.ErrorDetails) > 0 {
43 return fmt.Sprintf("arvados API server error: %s (%d: %s) returned by %s",
44 strings.Join(e.ErrorDetails, "; "),
49 return fmt.Sprintf("arvados API server error: %d: %s returned by %s",
56 // Helper type so we don't have to write out 'map[string]interface{}' every time.
57 type Dict map[string]interface{}
59 // Information about how to contact the Arvados server
60 type ArvadosClient struct {
61 // Arvados API server, form "host:port"
64 // Arvados API token for authentication
67 // Whether to require a valid SSL certificate or not
70 // Client object shared by client requests. Supports HTTP KeepAlive.
73 // If true, sets the X-External-Client header to indicate
74 // the client is outside the cluster.
81 // APIConfig struct consists of:
84 // APIHostInsecure bool
85 // ExternalClient bool
86 type APIConfig struct {
93 // Create a new ArvadosClient, initialized with standard Arvados environment variables
94 // ARVADOS_API_HOST, ARVADOS_API_TOKEN, ARVADOS_API_HOST_INSECURE, ARVADOS_EXTERNAL_CLIENT.
95 func MakeArvadosClient() (ac ArvadosClient, err error) {
97 config.APIToken = os.Getenv("ARVADOS_API_TOKEN")
98 config.APIHost = os.Getenv("ARVADOS_API_HOST")
100 var matchTrue = regexp.MustCompile("^(?i:1|yes|true)$")
102 config.APIHostInsecure = matchTrue.MatchString(os.Getenv("ARVADOS_API_HOST_INSECURE"))
103 config.ExternalClient = matchTrue.MatchString(os.Getenv("ARVADOS_EXTERNAL_CLIENT"))
105 return MakeArvadosClientWithConfig(config)
108 // Create a new ArvadosClient, using the given input parameters.
109 func MakeArvadosClientWithConfig(config APIConfig) (ac ArvadosClient, err error) {
111 ApiServer: config.APIHost,
112 ApiToken: config.APIToken,
113 ApiInsecure: config.APIHostInsecure,
114 Client: &http.Client{Transport: &http.Transport{
115 TLSClientConfig: &tls.Config{InsecureSkipVerify: config.APIHostInsecure}}},
116 External: config.ExternalClient}
118 if ac.ApiServer == "" {
119 return ac, MissingArvadosApiHost
121 if ac.ApiToken == "" {
122 return ac, MissingArvadosApiToken
128 // CallRaw is the same as Call() but returns a Reader that reads the
129 // response body, instead of taking an output object.
130 func (c ArvadosClient) CallRaw(method string, resourceType string, uuid string, action string, parameters Dict) (reader io.ReadCloser, err error) {
131 var req *http.Request
137 if resourceType != API_DISCOVERY_RESOURCE {
138 u.Path = "/arvados/v1"
141 if resourceType != "" {
142 u.Path = u.Path + "/" + resourceType
145 u.Path = u.Path + "/" + uuid
148 u.Path = u.Path + "/" + action
151 if parameters == nil {
152 parameters = make(Dict)
155 vals := make(url.Values)
156 for k, v := range parameters {
157 if s, ok := v.(string); ok {
159 } else if m, err := json.Marshal(v); err == nil {
160 vals.Set(k, string(m))
164 if method == "GET" || method == "HEAD" {
165 u.RawQuery = vals.Encode()
166 if req, err = http.NewRequest(method, u.String(), nil); err != nil {
170 if req, err = http.NewRequest(method, u.String(), bytes.NewBufferString(vals.Encode())); err != nil {
173 req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
176 // Add api token header
177 req.Header.Add("Authorization", fmt.Sprintf("OAuth2 %s", c.ApiToken))
179 req.Header.Add("X-External-Client", "1")
183 var resp *http.Response
184 if resp, err = c.Client.Do(req); err != nil {
188 if resp.StatusCode == http.StatusOK {
189 return resp.Body, nil
192 defer resp.Body.Close()
193 return nil, newAPIServerError(c.ApiServer, resp)
196 func newAPIServerError(ServerAddress string, resp *http.Response) APIServerError {
198 ase := APIServerError{
199 ServerAddress: ServerAddress,
200 HttpStatusCode: resp.StatusCode,
201 HttpStatusMessage: resp.Status}
203 // If the response body has {"errors":["reason1","reason2"]}
204 // then return those reasons.
206 if err := json.NewDecoder(resp.Body).Decode(&errInfo); err == nil {
207 if errorList, ok := errInfo["errors"]; ok {
208 if errArray, ok := errorList.([]interface{}); ok {
209 for _, errItem := range errArray {
210 // We expect an array of strings here.
211 // Non-strings will be passed along
213 if s, ok := errItem.(string); ok {
214 ase.ErrorDetails = append(ase.ErrorDetails, s)
215 } else if j, err := json.Marshal(errItem); err == nil {
216 ase.ErrorDetails = append(ase.ErrorDetails, string(j))
225 // Call an API endpoint and parse the JSON response into an object.
227 // method - HTTP method: GET, HEAD, PUT, POST, PATCH or DELETE.
228 // resourceType - the type of arvados resource to act on (e.g., "collections", "pipeline_instances").
229 // uuid - the uuid of the specific item to access. May be empty.
230 // action - API method name (e.g., "lock"). This is often empty if implied by method and uuid.
231 // parameters - method parameters.
232 // output - a map or annotated struct which is a legal target for encoding/json/Decoder.
234 // Returns a non-nil error if an error occurs making the API call, the
235 // API responds with a non-successful HTTP status, or an error occurs
236 // parsing the response body.
237 func (c ArvadosClient) Call(method string, resourceType string, uuid string, action string, parameters Dict, output interface{}) error {
238 reader, err := c.CallRaw(method, resourceType, uuid, action, parameters)
247 dec := json.NewDecoder(reader)
248 if err = dec.Decode(output); err != nil {
255 // Create a new resource. See Call for argument descriptions.
256 func (c ArvadosClient) Create(resourceType string, parameters Dict, output interface{}) error {
257 return c.Call("POST", resourceType, "", "", parameters, output)
260 // Delete a resource. See Call for argument descriptions.
261 func (c ArvadosClient) Delete(resource string, uuid string, parameters Dict, output interface{}) (err error) {
262 return c.Call("DELETE", resource, uuid, "", parameters, output)
265 // Modify attributes of a resource. See Call for argument descriptions.
266 func (c ArvadosClient) Update(resourceType string, uuid string, parameters Dict, output interface{}) (err error) {
267 return c.Call("PUT", resourceType, uuid, "", parameters, output)
270 // Get a resource. See Call for argument descriptions.
271 func (c ArvadosClient) Get(resourceType string, uuid string, parameters Dict, output interface{}) (err error) {
272 if !UUIDMatch(uuid) && !(resourceType == "collections" && PDHMatch(uuid)) {
273 // No object has uuid == "": there is no need to make
274 // an API call. Furthermore, the HTTP request for such
275 // an API call would be "GET /arvados/v1/type/", which
276 // is liable to be misinterpreted as the List API.
277 return ErrInvalidArgument
279 return c.Call("GET", resourceType, uuid, "", parameters, output)
282 // List resources of a given type. See Call for argument descriptions.
283 func (c ArvadosClient) List(resource string, parameters Dict, output interface{}) (err error) {
284 return c.Call("GET", resource, "", "", parameters, output)
287 const API_DISCOVERY_RESOURCE = "discovery/v1/apis/arvados/v1/rest"
289 // Discovery returns the value of the given parameter in the discovery
290 // document. Returns a non-nil error if the discovery document cannot
291 // be retrieved/decoded. Returns ErrInvalidArgument if the requested
292 // parameter is not found in the discovery document.
293 func (c *ArvadosClient) Discovery(parameter string) (value interface{}, err error) {
294 if len(c.DiscoveryDoc) == 0 {
295 c.DiscoveryDoc = make(Dict)
296 err = c.Call("GET", API_DISCOVERY_RESOURCE, "", "", nil, &c.DiscoveryDoc)
303 value, found = c.DiscoveryDoc[parameter]
307 return value, ErrInvalidArgument