Merge branch 'master' into github-3408-production-datamanager
[arvados.git] / sdk / go / arvadosclient / arvadosclient.go
index 5ea2524aa63c730632c567edb10132224b919b3a..99b08186a533f101a88f0ca3910cc1fcbe06ab3d 100644 (file)
@@ -12,6 +12,7 @@ import (
        "net/http"
        "net/url"
        "os"
+       "regexp"
        "strings"
 )
 
@@ -19,13 +20,33 @@ import (
 var MissingArvadosApiHost = errors.New("Missing required environment variable ARVADOS_API_HOST")
 var MissingArvadosApiToken = errors.New("Missing required environment variable ARVADOS_API_TOKEN")
 
-type ArvadosApiError struct {
-       error
-       HttpStatusCode int
-       HttpStatus string
+// 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 ArvadosApiError) Error() string { return e.error.Error() }
+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{}
@@ -47,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,
@@ -64,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.
@@ -92,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
@@ -147,30 +174,36 @@ func (this ArvadosClient) CallRaw(method string, resource string, uuid string, a
        }
 
        defer resp.Body.Close()
-       errorText := fmt.Sprintf("API response: %s", resp.Status)
+       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 {
-                       var errorStrings []string
                        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 {
-                                               errorStrings = append(errorStrings, s)
+                                               ase.ErrorDetails = append(ase.ErrorDetails, s)
                                        } else if j, err := json.Marshal(errItem); err == nil {
-                                               errorStrings = append(errorStrings, string(j))
+                                               ase.ErrorDetails = append(ase.ErrorDetails, string(j))
                                        }
                                }
-                               errorText = strings.Join(errorStrings, "; ")
                        }
                }
        }
-       return nil, ArvadosApiError{errors.New(errorText), resp.StatusCode, resp.Status}
+       return ase
 }
 
 // Access to a resource.
@@ -247,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")
+       }
+}