Merge branch 'master' into github-3408-production-datamanager
[arvados.git] / sdk / go / arvadosclient / arvadosclient.go
index 3d5aff7b2325b4d93e3a0afacd71c44329c42f6f..99b08186a533f101a88f0ca3910cc1fcbe06ab3d 100644 (file)
@@ -12,15 +12,41 @@ import (
        "net/http"
        "net/url"
        "os"
+       "regexp"
+       "strings"
 )
 
 // Errors
 var MissingArvadosApiHost = errors.New("Missing required environment variable ARVADOS_API_HOST")
 var MissingArvadosApiToken = errors.New("Missing required environment variable ARVADOS_API_TOKEN")
-var ArvadosErrorForbidden = errors.New("Forbidden")
-var ArvadosErrorNotFound = errors.New("Not found")
-var ArvadosErrorBadRequest = errors.New("Bad request")
-var ArvadosErrorServerError = errors.New("Server error")
+
+// Indicates an error that was returned by the API server.
+type APIServerError struct {
+       // Address of server returning error, of the form "host:port".
+       ServerAddress string
+
+       // Components of server response.
+       HttpStatusCode    int
+       HttpStatusMessage string
+
+       // Additional error details from response body.
+       ErrorDetails []string
+}
+
+func (e APIServerError) Error() string {
+       if len(e.ErrorDetails) > 0 {
+               return fmt.Sprintf("arvados API server error: %s (%d: %s) returned by %s",
+                       strings.Join(e.ErrorDetails, "; "),
+                       e.HttpStatusCode,
+                       e.HttpStatusMessage,
+                       e.ServerAddress)
+       } else {
+               return fmt.Sprintf("arvados API server error: %d: %s returned by %s",
+                       e.HttpStatusCode,
+                       e.HttpStatusMessage,
+                       e.ServerAddress)
+       }
+}
 
 // Helper type so we don't have to write out 'map[string]interface{}' every time.
 type Dict map[string]interface{}
@@ -42,16 +68,20 @@ type ArvadosClient struct {
        // If true, sets the X-External-Client header to indicate
        // the client is outside the cluster.
        External bool
+
+       // Discovery document
+       DiscoveryDoc Dict
 }
 
-// Create a new KeepClient, initialized with standard Arvados environment
+// Create a new ArvadosClient, initialized with standard Arvados environment
 // variables ARVADOS_API_HOST, ARVADOS_API_TOKEN, and (optionally)
 // ARVADOS_API_HOST_INSECURE.
-func MakeArvadosClient() (kc ArvadosClient, err error) {
-       insecure := (os.Getenv("ARVADOS_API_HOST_INSECURE") == "true")
-       external := (os.Getenv("ARVADOS_EXTERNAL_CLIENT") == "true")
+func MakeArvadosClient() (ac ArvadosClient, err error) {
+       var matchTrue = regexp.MustCompile("^(?i:1|yes|true)$")
+       insecure := matchTrue.MatchString(os.Getenv("ARVADOS_API_HOST_INSECURE"))
+       external := matchTrue.MatchString(os.Getenv("ARVADOS_EXTERNAL_CLIENT"))
 
-       kc = ArvadosClient{
+       ac = ArvadosClient{
                ApiServer:   os.Getenv("ARVADOS_API_HOST"),
                ApiToken:    os.Getenv("ARVADOS_API_TOKEN"),
                ApiInsecure: insecure,
@@ -59,14 +89,14 @@ func MakeArvadosClient() (kc ArvadosClient, err error) {
                        TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}}},
                External: external}
 
-       if os.Getenv("ARVADOS_API_HOST") == "" {
-               return kc, MissingArvadosApiHost
+       if ac.ApiServer == "" {
+               return ac, MissingArvadosApiHost
        }
-       if os.Getenv("ARVADOS_API_TOKEN") == "" {
-               return kc, MissingArvadosApiToken
+       if ac.ApiToken == "" {
+               return ac, MissingArvadosApiToken
        }
 
-       return kc, err
+       return ac, err
 }
 
 // Low-level access to a resource.
@@ -87,7 +117,9 @@ func (this ArvadosClient) CallRaw(method string, resource string, uuid string, a
                Scheme: "https",
                Host:   this.ApiServer}
 
-       u.Path = "/arvados/v1"
+       if resource != API_DISCOVERY_RESOURCE {
+               u.Path = "/arvados/v1"
+       }
 
        if resource != "" {
                u.Path = u.Path + "/" + resource
@@ -137,23 +169,41 @@ func (this ArvadosClient) CallRaw(method string, resource string, uuid string, a
                return nil, err
        }
 
-       switch resp.StatusCode {
-       case http.StatusOK:
+       if resp.StatusCode == http.StatusOK {
                return resp.Body, nil
-       case http.StatusForbidden:
-               resp.Body.Close()
-               return nil, ArvadosErrorForbidden
-       case http.StatusNotFound:
-               resp.Body.Close()
-               return nil, ArvadosErrorNotFound
-       default:
-               resp.Body.Close()
-               if resp.StatusCode >= 400 && resp.StatusCode <= 499 {
-                       return nil, ArvadosErrorBadRequest
-               } else {
-                       return nil, ArvadosErrorServerError
+       }
+
+       defer resp.Body.Close()
+       return nil, newAPIServerError(this.ApiServer, resp)
+}
+
+func newAPIServerError(ServerAddress string, resp *http.Response) APIServerError {
+
+       ase := APIServerError{
+               ServerAddress:     ServerAddress,
+               HttpStatusCode:    resp.StatusCode,
+               HttpStatusMessage: resp.Status}
+
+       // If the response body has {"errors":["reason1","reason2"]}
+       // then return those reasons.
+       var errInfo = Dict{}
+       if err := json.NewDecoder(resp.Body).Decode(&errInfo); err == nil {
+               if errorList, ok := errInfo["errors"]; ok {
+                       if errArray, ok := errorList.([]interface{}); ok {
+                               for _, errItem := range errArray {
+                                       // We expect an array of strings here.
+                                       // Non-strings will be passed along
+                                       // JSON-encoded.
+                                       if s, ok := errItem.(string); ok {
+                                               ase.ErrorDetails = append(ase.ErrorDetails, s)
+                                       } else if j, err := json.Marshal(errItem); err == nil {
+                                               ase.ErrorDetails = append(ase.ErrorDetails, string(j))
+                                       }
+                               }
+                       }
                }
        }
+       return ase
 }
 
 // Access to a resource.
@@ -230,3 +280,29 @@ func (this ArvadosClient) Update(resource string, uuid string, parameters Dict,
 func (this ArvadosClient) List(resource string, parameters Dict, output interface{}) (err error) {
        return this.Call("GET", resource, "", "", parameters, output)
 }
+
+// API Discovery
+//
+//   parameter - name of parameter to be discovered
+// return
+//   value - value of the discovered parameter
+//   err - error accessing the resource, or nil if no error
+var API_DISCOVERY_RESOURCE string = "discovery/v1/apis/arvados/v1/rest"
+
+func (this *ArvadosClient) Discovery(parameter string) (value interface{}, err error) {
+       if len(this.DiscoveryDoc) == 0 {
+               this.DiscoveryDoc = make(Dict)
+               err = this.Call("GET", API_DISCOVERY_RESOURCE, "", "", nil, &this.DiscoveryDoc)
+               if err != nil {
+                       return nil, err
+               }
+       }
+
+       var found bool
+       value, found = this.DiscoveryDoc[parameter]
+       if found {
+               return value, nil
+       } else {
+               return value, errors.New("Not found")
+       }
+}