Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func (c *Config) NewApiClient() (*httpclient.ApiClient, error) {
DebugHeaders: c.DebugHeaders,
DebugTruncateBytes: c.DebugTruncateBytes,
InsecureSkipVerify: c.InsecureSkipVerify,
CABundle: c.CABundle,
Transport: c.HTTPTransport,
Visitors: []httpclient.RequestVisitor{
c.Authenticate,
Expand Down Expand Up @@ -76,7 +77,7 @@ func (c *Config) NewApiClient() (*httpclient.ApiClient, error) {
}
return false
},
}), nil
})
}

func orDefault(configured, _default int) int {
Expand Down
9 changes: 8 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ type Config struct {
// Number of seconds to keep retrying HTTP requests. Default is 300 (5 minutes)
RetryTimeoutSeconds int `name:"retry_timeout_seconds" auth:"-"`

// The path to a CA certificate bundle that is used to verify SSL certificates.
CABundle string `name:"ca_bundle" env:"DATABRICKS_CA_BUNDLE" auth:"-"`

// HTTPTransport can be overriden for unit testing and together with tooling like https://github.com/google/go-replayers
HTTPTransport http.RoundTripper

Expand Down Expand Up @@ -271,10 +274,11 @@ func (c *Config) EnsureResolved() error {
return c.wrapDebug(fmt.Errorf("validate: %w", err))
}
c.refreshCtx = ctx
c.refreshClient = httpclient.NewApiClient(httpclient.ClientConfig{
c.refreshClient, err = httpclient.NewApiClient(httpclient.ClientConfig{
DebugHeaders: c.DebugHeaders,
DebugTruncateBytes: c.DebugTruncateBytes,
InsecureSkipVerify: c.InsecureSkipVerify,
CABundle: c.CABundle,
RetryTimeout: time.Duration(c.RetryTimeoutSeconds) * time.Second,
HTTPTimeout: time.Duration(c.HTTPTimeoutSeconds) * time.Second,
Transport: c.HTTPTransport,
Expand All @@ -287,6 +291,9 @@ func (c *Config) EnsureResolved() error {
"rate limit",
},
})
if err != nil {
return c.wrapDebug(fmt.Errorf("create refresh client: %w", err))
}
c.resolved = true
return nil
}
Expand Down
59 changes: 59 additions & 0 deletions examples/http-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# HTTP Proxy Example

This example demonstrates how to use the Databricks SDK for Go with an HTTPS proxy. The `proxy` directory contains a simple HTTP proxy server that forwards requests to the Databricks REST API. The `client` directory contains a simple client that uses the Databricks SDK for Go to make requests to the proxy server.

The proxy server generates a self-signed certificate and listens on port 8443 with this certificate. The generated certificate can be found at `proxy/proxy.crt`, and it is cleaned up when the proxy is terminated. The client must trust this certificate to make requests to Databricks through the proxy server.

## Run the Example: Auto-registration of the Proxy Server Certificate

This example demonstrates how to automatically register the proxy server's certificate with the system's certificate store. This allows the client to trust the proxy server's certificate without needing to manually configure the client to trust the certificate. This is the preferred way to configure your system, as it doesn't require application-specific configuration to trust the proxy server's certificate.

Note: this example requires root access to register the proxy server's certificate with the system's certificate store. On Windows, this requires running the proxy server as an administrator.

1. Run the proxy server:

```bash
go run ./proxy --register-certificate
```

This will attempt to register the proxy server's certificate with the system's certificate store. If you're using Windows, you will need to run Powershell as an administrator to register the certificate.

2. In another terminal, run the client:

```bash
HTTPS_PROXY=https://localhost:8443 go run ./client
```

or on Windows:

```powershell
$env:HTTPS_PROXY="https://localhost:8443" go run ./client
```

Note that on Windows, this does not require administrator privileges.

3. When done, terminate the proxy server by typing `CMD+C` or `Ctrl+C` in the terminal where it is running.

## Run the Example: Configure the Proxy Server Certificate at Runtime

This example demonstrates how to configure the client to trust the proxy server's certificate at runtime. This is useful when you don't have root access to register the proxy server's certificate with the system's certificate store, or when you want to avoid modifying the system's certificate store.

1. Run the proxy server:

```bash
go run ./proxy
```

2. In another terminal, run the client, this time specifying the path to the CA file:

```bash
DATABRICKS_CA_BUNDLE=proxy/proxy.crt HTTPS_PROXY=https://localhost:8443 go run ./client
```

or on Windows:

```powershell
$env:DATABRICKS_CA_BUNDLE="proxy/proxy.crt" $env:HTTPS_PROXY="https://localhost:8443" go run ./client
```

3. When done, terminate the proxy server by typing `CMD+C` or `Ctrl+C` in the terminal where it is running.
34 changes: 2 additions & 32 deletions examples/http-proxy/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package main

import (
"context"
"crypto/tls"
"log"
"net/http"
"os"

"github.com/databricks/databricks-sdk-go"
Expand All @@ -14,42 +12,14 @@ import (

func main() {
if os.Getenv("HTTPS_PROXY") != "https://localhost:8443" {
log.Printf(`To run this example, first start the proxy server in the examples/http-proxy/proxy directory:

$ go run ../proxy

On Windows, you must do this from a command prompt with administrator privileges.

Then, run this example setting the HTTP_PROXY or HTTPS_PROXY environment variable to the proxy server:

$ HTTPS_PROXY=https://localhost:8443 go run .

on macOS or Linux, or

$ $env:HTTPS_PROXY="https://localhost:8443"; go run .

on Windows to see the list of clusters in your Databricks workspace using this proxy.
`)
log.Printf(`To run this example, see the instructions in examples/http-proxy/README.md.`)
os.Exit(1)
}
log.Printf("Constructing client...")
logger.DefaultLogger = &logger.SimpleLogger{
Level: logger.LevelDebug,
}
var tlsNextProto map[string]func(authority string, c *tls.Conn) http.RoundTripper
if os.Getenv("HTTPS_PROXY") != "" {
// Go's HTTP client only supports HTTP/1.1 proxies when using TLS. See
// https://github.com/golang/go/issues/26479 for more information. Configuring
// this property to be a non-nil empty map will disable HTTP/2 on the HTTP
// transport.
tlsNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
}
w := databricks.Must(databricks.NewWorkspaceClient(&databricks.Config{
HTTPTransport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSNextProto: tlsNextProto,
},
}))
w := databricks.Must(databricks.NewWorkspaceClient())
log.Printf("Listing clusters...")
all, err := w.Clusters.ListAll(context.Background(), compute.ListClustersRequest{})
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions examples/http-proxy/proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
proxy.crt
proxy.key
31 changes: 27 additions & 4 deletions examples/http-proxy/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
Expand All @@ -26,9 +27,11 @@ func main() {
createX509Certificate()
defer cleanup()

util := newCertUtil()
runCommands(util.GetRegisterCertificateCommands("proxy.crt"))
defer runCommands(util.GetDeregisterCertificateCommands("proxy.crt"))
if len(os.Args) == 2 && os.Args[1] == "--register-certificate" {
util := newCertUtil()
runCommands(util.GetRegisterCertificateCommands("proxy.crt"))
defer runCommands(util.GetDeregisterCertificateCommands("proxy.crt"))
}

server := startServer()
defer server.Close()
Expand Down Expand Up @@ -176,9 +179,29 @@ func startServer() *http.Server {
panic(err)
}
}()

// Load CA cert
caCert, err := os.ReadFile("proxy.crt")
if err != nil {
log.Fatalf("Reading CA cert failed: %v", err)
}

// Create a CA certificate pool and add cert to it
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

// Create a HTTPS client and supply the created CA pool
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
},
},
}

log.Printf("Waiting for server to start...")
for {
_, err := http.Get("https://localhost:8443/ping")
_, err = httpClient.Get("https://localhost:8443/ping")
if err == nil {
break
}
Expand Down
76 changes: 62 additions & 14 deletions httpclient/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package httpclient
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"time"
Expand All @@ -32,6 +34,7 @@ type ClientConfig struct {
DebugHeaders bool
DebugTruncateBytes int
RateLimitPerSecond int
CABundle string

ErrorMapper func(ctx context.Context, resp common.ResponseWrapper) error
ErrorRetriable func(ctx context.Context, err error) bool
Expand All @@ -40,11 +43,36 @@ type ClientConfig struct {
Transport http.RoundTripper
}

func (cfg ClientConfig) httpTransport() http.RoundTripper {
func loadRootCAs(caBundle string) (*x509.CertPool, error) {
if caBundle == "" {
return nil, nil
}

// Load CA cert
caCert, err := os.ReadFile(caBundle)
if err != nil {
return nil, fmt.Errorf("reading CA cert failed: %v", err)
}

// Create a CA certificate pool and add cert to it
caCertPool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
caCertPool.AppendCertsFromPEM(caCert)

return caCertPool, nil
}

func (cfg ClientConfig) httpTransport() (http.RoundTripper, error) {
if cfg.Transport != nil {
return cfg.Transport
return cfg.Transport, nil
}
return &http.Transport{
certPool, err := loadRootCAs(cfg.CABundle)
if err != nil {
return nil, err
}
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
Expand All @@ -58,11 +86,20 @@ func (cfg ClientConfig) httpTransport() http.RoundTripper {
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: cfg.InsecureSkipVerify,
RootCAs: certPool,
},
}

// Disable HTTP/2 when using an HTTPS proxy
// Needed until
req, _ := http.NewRequest("GET", "https://databricks.com", nil)
if url, _ := transport.Proxy(req); url != nil {
transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
}
return transport, nil
}

func NewApiClient(cfg ClientConfig) *ApiClient {
func NewHttpClient(cfg ClientConfig) (*http.Client, error) {
cfg.HTTPTimeout = time.Duration(orDefault(int(cfg.HTTPTimeout), int(30*time.Second)))
cfg.DebugTruncateBytes = orDefault(cfg.DebugTruncateBytes, 96)
cfg.RetryTimeout = time.Duration(orDefault(int(cfg.RetryTimeout), int(5*time.Minute)))
Expand All @@ -75,10 +112,27 @@ func NewApiClient(cfg ClientConfig) *ApiClient {
// by default, we just retry on HTTP 429/504
cfg.ErrorRetriable = DefaultErrorRetriable
}
transport := cfg.httpTransport()
transport, err := cfg.httpTransport()
if err != nil {
return nil, fmt.Errorf("failed to construct http transport: %w", err)
}
return &http.Client{
// We deal with request timeouts ourselves such that we do not
// time out during request or response body reads that make
// progress (e.g. on a slower network connection).
Timeout: 0,
Transport: transport,
}, nil
}

func NewApiClient(cfg ClientConfig) (*ApiClient, error) {
client, err := NewHttpClient(cfg)
if err != nil {
return nil, err
}
rateLimit := rate.Limit(orDefault(cfg.RateLimitPerSecond, 15))
// depend on the HTTP fixture interface to prevent any coupling
if skippable, ok := transport.(interface {
if skippable, ok := client.Transport.(interface {
SkipRetryOnIO() bool
}); ok && skippable.SkipRetryOnIO() {
rateLimit = rate.Inf
Expand All @@ -87,14 +141,8 @@ func NewApiClient(cfg ClientConfig) *ApiClient {
return &ApiClient{
config: cfg,
rateLimiter: rate.NewLimiter(rateLimit, 1),
httpClient: &http.Client{
// We deal with request timeouts ourselves such that we do not
// time out during request or response body reads that make
// progress (e.g. on a slower network connection).
Timeout: 0,
Transport: transport,
},
}
httpClient: client,
}, nil
}

type ApiClient struct {
Expand Down
Loading