X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/3aaefcb3c76ff470b475d950398d01255e87712a..09070ac166e1855a33657c94c4cea0b5d098e013:/sdk/go/arvados/client.go diff --git a/sdk/go/arvados/client.go b/sdk/go/arvados/client.go index 52c75d5113..5a498b01f0 100644 --- a/sdk/go/arvados/client.go +++ b/sdk/go/arvados/client.go @@ -7,12 +7,17 @@ package arvados import ( "bytes" "context" + "crypto/rand" "crypto/tls" "encoding/json" + "errors" "fmt" "io" + "io/fs" "io/ioutil" "log" + "math/big" + "net" "net/http" "net/url" "os" @@ -67,6 +72,10 @@ 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 } // InsecureHTTPClient is the default http.Client used by a Client with @@ -88,7 +97,40 @@ func NewClientFromConfig(cluster *Cluster) (*Client, error) { if ctrlURL.Host == "" { return nil, fmt.Errorf("no host in config Services.Controller.ExternalURL: %v", ctrlURL) } + var hc *http.Client + if srvaddr := os.Getenv("ARVADOS_SERVER_ADDRESS"); srvaddr != "" { + // When this client is used to make a request to + // https://{ctrlhost}:port/ (any port), it dials the + // indicated port on ARVADOS_SERVER_ADDRESS instead. + // + // This is invoked by arvados-server boot to ensure + // that server->server traffic (e.g., + // keepproxy->controller) only hits local interfaces, + // even if the Controller.ExternalURL host is a load + // balancer / gateway and not a local interface + // address (e.g., when running on a cloud VM). + // + // This avoids unnecessary delay/cost of routing + // external traffic, and also allows controller to + // recognize other services as internal clients based + // on the connection source address. + divertedHost := (*url.URL)(&cluster.Services.Controller.ExternalURL).Hostname() + var dialer net.Dialer + hc = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: cluster.TLS.Insecure}, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err == nil && network == "tcp" && host == divertedHost { + addr = net.JoinHostPort(srvaddr, port) + } + return dialer.DialContext(ctx, network, addr) + }, + }, + } + } return &Client{ + Client: hc, Scheme: ctrlURL.Scheme, APIHost: ctrlURL.Host, Insecure: cluster.TLS.Insecure, @@ -97,11 +139,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 +204,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 +303,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: @@ -236,11 +330,11 @@ func (c *Client) DoAndDecode(dst interface{}, req *http.Request) error { // Convert an arbitrary struct to url.Values. For example, // -// Foo{Bar: []int{1,2,3}, Baz: "waz"} +// Foo{Bar: []int{1,2,3}, Baz: "waz"} // // becomes // -// url.Values{`bar`:`{"a":[1,2,3]}`,`Baz`:`waz`} +// url.Values{`bar`:`{"a":[1,2,3]}`,`Baz`:`waz`} // // params itself is returned if it is already an url.Values. func anythingToValues(params interface{}) (url.Values, error) { @@ -312,6 +406,12 @@ func (c *Client) RequestAndDecodeContext(ctx context.Context, dst interface{}, m // 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 { @@ -501,3 +601,17 @@ func (c *Client) PathForUUID(method, uuid string) (string, error) { } return path, nil } + +var maxUUIDInt = (&big.Int{}).Exp(big.NewInt(36), big.NewInt(15), nil) + +func RandomUUID(clusterID, infix string) string { + n, err := rand.Int(rand.Reader, maxUUIDInt) + if err != nil { + panic(err) + } + nstr := n.Text(36) + for len(nstr) < 15 { + nstr = "0" + nstr + } + return clusterID + "-" + infix + "-" + nstr +}