X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/2bc1a7a89597ab02aaeef84b82fdc51f8e375b79..f1f74069850d8c5e987ef7d7fc246735ff94d58d:/sdk/go/arvados/client.go diff --git a/sdk/go/arvados/client.go b/sdk/go/arvados/client.go index 562c8c1e7d..24d5ac3e33 100644 --- a/sdk/go/arvados/client.go +++ b/sdk/go/arvados/client.go @@ -9,8 +9,10 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "io" + "io/fs" "io/ioutil" "log" "net/http" @@ -67,16 +69,20 @@ type Client struct { dd *DiscoveryDocument defaultRequestID string + + // APIHost and AuthToken were loaded from ARVADOS_* env vars + // (used to customize "no host/token" error messages) + loadedFromEnv bool } -// The default http.Client used by a Client with Insecure==true and -// Client==nil. +// InsecureHTTPClient is the default http.Client used by a Client with +// Insecure==true and Client==nil. var InsecureHTTPClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true}}} -// The default http.Client used by a Client otherwise. +// DefaultSecureClient is the default http.Client used by a Client otherwise. var DefaultSecureClient = &http.Client{} // NewClientFromConfig creates a new Client that uses the endpoints in @@ -97,11 +103,60 @@ func NewClientFromConfig(cluster *Cluster) (*Client, error) { } // NewClientFromEnv creates a new Client that uses the default HTTP -// client with the API endpoint and credentials given by the -// ARVADOS_API_* environment variables. +// client, and loads API endpoint and credentials from ARVADOS_* +// environment variables (if set) and +// $HOME/.config/arvados/settings.conf (if readable). +// +// If a config exists in both locations, the environment variable is +// used. +// +// If there is an error (other than ENOENT) reading settings.conf, +// NewClientFromEnv logs the error to log.Default(), then proceeds as +// if settings.conf did not exist. +// +// Space characters are trimmed when reading the settings file, so +// these are equivalent: +// +// ARVADOS_API_HOST=localhost\n +// ARVADOS_API_HOST=localhost\r\n +// ARVADOS_API_HOST = localhost \n +// \tARVADOS_API_HOST = localhost\n func NewClientFromEnv() *Client { + vars := map[string]string{} + home := os.Getenv("HOME") + conffile := home + "/.config/arvados/settings.conf" + if home == "" { + // no $HOME => just use env vars + } else if settings, err := os.ReadFile(conffile); errors.Is(err, fs.ErrNotExist) { + // no config file => just use env vars + } else if err != nil { + // config file unreadable => log message, then use env vars + log.Printf("continuing without loading %s: %s", conffile, err) + } else { + for _, line := range bytes.Split(settings, []byte{'\n'}) { + kv := bytes.SplitN(line, []byte{'='}, 2) + k := string(bytes.TrimSpace(kv[0])) + if len(kv) != 2 || !strings.HasPrefix(k, "ARVADOS_") { + // Same behavior as python sdk: + // silently skip leading # (comments), + // blank lines, typos, and non-Arvados + // vars. + continue + } + vars[k] = string(bytes.TrimSpace(kv[1])) + } + } + for _, env := range os.Environ() { + if !strings.HasPrefix(env, "ARVADOS_") { + continue + } + kv := strings.SplitN(env, "=", 2) + if len(kv) == 2 { + vars[kv[0]] = kv[1] + } + } var svcs []string - for _, s := range strings.Split(os.Getenv("ARVADOS_KEEP_SERVICES"), " ") { + for _, s := range strings.Split(vars["ARVADOS_KEEP_SERVICES"], " ") { if s == "" { continue } else if u, err := url.Parse(s); err != nil { @@ -113,16 +168,17 @@ func NewClientFromEnv() *Client { } } var insecure bool - if s := strings.ToLower(os.Getenv("ARVADOS_API_HOST_INSECURE")); s == "1" || s == "yes" || s == "true" { + if s := strings.ToLower(vars["ARVADOS_API_HOST_INSECURE"]); s == "1" || s == "yes" || s == "true" { insecure = true } return &Client{ Scheme: "https", - APIHost: os.Getenv("ARVADOS_API_HOST"), - AuthToken: os.Getenv("ARVADOS_API_TOKEN"), + APIHost: vars["ARVADOS_API_HOST"], + AuthToken: vars["ARVADOS_API_TOKEN"], Insecure: insecure, KeepServiceURIs: svcs, Timeout: 5 * time.Minute, + loadedFromEnv: true, } } @@ -211,6 +267,8 @@ func (c *Client) DoAndDecode(dst interface{}, req *http.Request) error { return err } switch { + case resp.StatusCode == http.StatusNoContent: + return nil case resp.StatusCode == http.StatusOK && dst == nil: return nil case resp.StatusCode == http.StatusOK: @@ -306,11 +364,18 @@ func (c *Client) RequestAndDecode(dst interface{}, method, path string, body io. return c.RequestAndDecodeContext(context.Background(), dst, method, path, body, params) } +// RequestAndDecodeContext does the same as RequestAndDecode, but with a context func (c *Client) RequestAndDecodeContext(ctx context.Context, dst interface{}, method, path string, body io.Reader, params interface{}) error { if body, ok := body.(io.Closer); ok { // Ensure body is closed even if we error out early defer body.Close() } + if c.APIHost == "" { + if c.loadedFromEnv { + return errors.New("ARVADOS_API_HOST and/or ARVADOS_API_TOKEN environment variables are not set") + } + return errors.New("arvados.Client cannot perform request: APIHost is not set") + } urlString := c.apiURL(path) urlValues, err := anythingToValues(params) if err != nil {