Merge branch 'master' into 6064-collection-output-of-perf-issue
[arvados.git] / sdk / go / arvadosclient / arvadosclient.go
index 3d5aff7b2325b4d93e3a0afacd71c44329c42f6f..ae40a89a2b6bc9e033828e06fdf1364b0719d490 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{}
@@ -44,14 +70,15 @@ type ArvadosClient struct {
        External bool
 }
 
-// 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 +86,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.
@@ -137,23 +164,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.