Skip to content

Commit f8bc3b3

Browse files
authored
Add "ssh connect" command (#3471)
## Changes See the base PR for the context: - #3470 This PR adds `databricks ssh connect` subcommand and all related utilities. The main logic here is in `libs/ssh/client.go` and `libs/ssh/proxy.go` files. See `proxy_test.go` for an overview of how client and server interact with each other through the proxy. Overview of the `ssh client` logic: - Generate local ssh keys if necessary (`~/.databricks/ssh-tunnel-keys/<cluster-id>`) - Save the public key in the secret scope (`<username>-<cluster-id>-ssh-tunnel-keys`) - Upload databricks releases (linux arm and amd) with the marching version to the /Workspace - Get /Workspace/metadata.json file with the server port info - If the metadata is not there, execute `ssh-server-bootsrap.py` file as a job (it runs `databricks server` command that's implemented in the follow up PR) - Get server metadata by sending a driver-proxy request to the known port, it will return a user name - Spawn `ssh client` with the right User and a ProxyCommand that executed `ssh connect --proxy --metadata` - New instance of the `connect` command can now start a proxy over websocket connection backed by Driver Proxy API Overview of the Proxy logic: - The main interesting part is that the proxy does automatic re-connects every 30 minutes (configurable) to avoid auth token expiration problems. In all the tricky places (mostly related to concurrency and locks) there are comments explaining the logic. The final follow up PR: - #3475 ## Tests Manual and unit
1 parent d3ac142 commit f8bc3b3

File tree

14 files changed

+1288
-3
lines changed

14 files changed

+1288
-3
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ tools/yamlfmt.exe
3636

3737
# Cache for tools/gh_report.py
3838
.gh-logs
39+
40+
# Release artifacts
41+
dist/

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ build-vm: tidy
9191
snapshot:
9292
go build -o .databricks/databricks
9393

94+
# Produce release binaries and archives in the dist folder without uploading them anywhere.
95+
# Useful for "databricks ssh" development, as it needs to upload linux releases to the /Workspace.
96+
snapshot-release:
97+
goreleaser release --clean --skip docker --snapshot
98+
9499
schema:
95100
go run ./bundle/internal/schema ./bundle/internal/schema ./bundle/schema/jsonschema.json
96101

cmd/ssh/connect.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package ssh
2+
3+
import (
4+
"time"
5+
6+
"github.com/databricks/cli/cmd/root"
7+
"github.com/databricks/cli/libs/cmdctx"
8+
"github.com/databricks/cli/libs/ssh"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
const (
13+
defaultClientPublicKeyName = "client-public-key"
14+
defaultShutdownDelay = 10 * time.Minute
15+
defaultHandoverTimeout = 30 * time.Minute
16+
defaultMaxClients = 10
17+
)
18+
19+
func newConnectCommand() *cobra.Command {
20+
cmd := &cobra.Command{
21+
Use: "connect",
22+
Short: "Connect to Databricks compute via SSH",
23+
Long: `Connect to Databricks compute via SSH.
24+
25+
This command establishes an SSH connection to Databricks compute, setting up
26+
the SSH server and handling the connection proxy.
27+
28+
` + disclaimer,
29+
}
30+
31+
var clusterID string
32+
var proxyMode bool
33+
var serverMetadata string
34+
var shutdownDelay time.Duration
35+
var maxClients int
36+
var handoverTimeout time.Duration
37+
var releasesDir string
38+
39+
cmd.Flags().StringVar(&clusterID, "cluster", "", "Databricks cluster ID (required)")
40+
cmd.MarkFlagRequired("cluster")
41+
cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", defaultShutdownDelay, "Delay before shutting down the server after the last client disconnects")
42+
cmd.Flags().IntVar(&maxClients, "max-clients", defaultMaxClients, "Maximum number of SSH clients")
43+
44+
cmd.Flags().BoolVar(&proxyMode, "proxy", false, "ProxyCommand mode")
45+
cmd.Flags().MarkHidden("proxy")
46+
cmd.Flags().StringVar(&serverMetadata, "metadata", "", "Metadata of the running SSH server (format: <user_name>,<port>)")
47+
cmd.Flags().MarkHidden("metadata")
48+
cmd.Flags().DurationVar(&handoverTimeout, "handover-timeout", defaultHandoverTimeout, "How often the CLI should reconnect to the server with new auth")
49+
cmd.Flags().MarkHidden("handover-timeout")
50+
51+
cmd.Flags().StringVar(&releasesDir, "releases-dir", "", "Directory for local SSH tunnel development releases")
52+
cmd.Flags().MarkHidden("releases-dir")
53+
54+
cmd.PreRunE = root.MustWorkspaceClient
55+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
56+
ctx := cmd.Context()
57+
client := cmdctx.WorkspaceClient(ctx)
58+
opts := ssh.ClientOptions{
59+
ClusterID: clusterID,
60+
ProxyMode: proxyMode,
61+
ServerMetadata: serverMetadata,
62+
ShutdownDelay: shutdownDelay,
63+
MaxClients: maxClients,
64+
HandoverTimeout: handoverTimeout,
65+
ReleasesDir: releasesDir,
66+
AdditionalArgs: args,
67+
ClientPublicKeyName: defaultClientPublicKeyName,
68+
}
69+
return ssh.RunClient(ctx, client, opts)
70+
}
71+
72+
return cmd
73+
}

cmd/ssh/ssh.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Common workflows:
2828
}
2929

3030
cmd.AddCommand(newSetupCommand())
31+
cmd.AddCommand(newConnectCommand())
3132

3233
return cmd
3334
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
github.com/spf13/cobra v1.10.0 // Apache 2.0
2929
github.com/spf13/pflag v1.0.9 // BSD-3-Clause
3030
github.com/stretchr/testify v1.11.1 // MIT
31+
golang.org/x/crypto v0.41.0 // BSD-3-Clause
3132
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
3233
golang.org/x/mod v0.27.0
3334
golang.org/x/oauth2 v0.30.0
@@ -75,7 +76,6 @@ require (
7576
go.opentelemetry.io/otel v1.36.0 // indirect
7677
go.opentelemetry.io/otel/metric v1.36.0 // indirect
7778
go.opentelemetry.io/otel/trace v1.36.0 // indirect
78-
golang.org/x/crypto v0.40.0 // indirect
7979
golang.org/x/net v0.42.0 // indirect
8080
golang.org/x/time v0.12.0 // indirect
8181
golang.org/x/tools v0.35.0 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw
173173
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
174174
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
175175
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
176-
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
177-
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
176+
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
177+
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
178178
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
179179
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
180180
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=

0 commit comments

Comments
 (0)