Skip to content

Commit 08cf9ee

Browse files
authored
Skip prompts in ssh connect proxy mode and ssh server command (#3634)
## Changes - Skip prompts in ssh connect proxy mode and ssh server command - Add profile option to the ProxyCommand when spawning ssh client from `databricks ssh connect` (and the setup logic already adds it to the config). - Skip loading bundles in ssh server command, so the bundle doesn't override the auth that's setup by the job logic that executes this command (or worse, the server can fail to start if the bundle happens to be incorrect) ## Tests Only manual
1 parent cbb3b7b commit 08cf9ee

File tree

6 files changed

+81
-17
lines changed

6 files changed

+81
-17
lines changed

experimental/ssh/cmd/connect.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,17 @@ the SSH server and handling the connection proxy.
4646
cmd.Flags().StringVar(&releasesDir, "releases-dir", "", "Directory for local SSH tunnel development releases")
4747
cmd.Flags().MarkHidden("releases-dir")
4848

49-
cmd.PreRunE = root.MustWorkspaceClient
49+
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
50+
// CLI in the proxy mode is executed by the ssh client and can't prompt for input
51+
if proxyMode {
52+
cmd.SetContext(root.SkipPrompt(cmd.Context()))
53+
}
54+
// We want to avoid the situation where the connect command works because it pulls the auth config from a bundle,
55+
// but fails if it's executed outside of it (which will happen when using remote development IDE features).
56+
cmd.SetContext(root.SkipLoadBundle(cmd.Context()))
57+
return root.MustWorkspaceClient(cmd, args)
58+
}
59+
5060
cmd.RunE = func(cmd *cobra.Command, args []string) error {
5161
ctx := cmd.Context()
5262
wsClient := cmdctx.WorkspaceClient(ctx)
@@ -62,6 +72,7 @@ the SSH server and handling the connection proxy.
6272
ClientPublicKeyName: defaultClientPublicKeyName,
6373
ServerTimeout: serverTimeout,
6474
AutoStartCluster: autoStartCluster,
75+
Profile: wsClient.Config.Profile,
6576
}
6677
return client.Run(ctx, wsClient, opts)
6778
}

experimental/ssh/cmd/server.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,16 @@ and proxies them to local SSH daemon processes.
4141
cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", defaultShutdownDelay, "Delay before shutting down after no pings from clients")
4242
cmd.Flags().StringVar(&version, "version", "", "Client version of the Databricks CLI")
4343

44-
cmd.PreRunE = root.MustWorkspaceClient
44+
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
45+
// The server can be executed under a directory with an invalid bundle configuration.
46+
// We do not want to error out in this case.
47+
// The auth is setup by the job logic that executes this command.
48+
cmd.SetContext(root.SkipLoadBundle(cmd.Context()))
49+
// The command should be executed in a non-interactive environment, but let's be explicit about no prompts.
50+
cmd.SetContext(root.SkipPrompt(cmd.Context()))
51+
return root.MustWorkspaceClient(cmd, args)
52+
}
53+
4554
cmd.RunE = func(cmd *cobra.Command, args []string) error {
4655
ctx := cmd.Context()
4756
wsc := cmdctx.WorkspaceClient(ctx)

experimental/ssh/cmd/setup.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ an SSH host configuration to your SSH config file.
3434
cmd.Flags().StringVar(&sshConfigPath, "ssh-config", "", "Path to SSH config file (default ~/.ssh/config)")
3535
cmd.Flags().DurationVar(&shutdownDelay, "shutdown-delay", defaultShutdownDelay, "SSH server will terminate after this delay if there are no active connections")
3636

37-
cmd.PreRunE = root.MustWorkspaceClient
37+
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
38+
// We want to avoid the situation where the setup command works because it pulls the auth config from a bundle,
39+
// but later on the `ssh host-name` command fails when executed outside of the bundle directory.
40+
cmd.SetContext(root.SkipLoadBundle(cmd.Context()))
41+
return root.MustWorkspaceClient(cmd, args)
42+
}
43+
3844
cmd.RunE = func(cmd *cobra.Command, args []string) error {
3945
ctx := cmd.Context()
4046
client := cmdctx.WorkspaceClient(ctx)

experimental/ssh/internal/client/client.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
"github.com/databricks/cli/experimental/ssh/internal/keys"
2121
"github.com/databricks/cli/experimental/ssh/internal/proxy"
22+
"github.com/databricks/cli/experimental/ssh/internal/setup"
2223
sshWorkspace "github.com/databricks/cli/experimental/ssh/internal/workspace"
2324
"github.com/databricks/cli/internal/build"
2425
"github.com/databricks/cli/libs/cmdio"
@@ -61,6 +62,8 @@ type ClientOptions struct {
6162
ClientPublicKeyName string
6263
// If true, the CLI will attempt to start the cluster if it is not running.
6364
AutoStartCluster bool
65+
// Optional auth profile name. If present, will be added as --profile flag to the ProxyCommand while spawning ssh client.
66+
Profile string
6467
// Additional arguments to pass to the SSH client in the non proxy mode.
6568
AdditionalArgs []string
6669
}
@@ -231,14 +234,11 @@ func submitSSHTunnelJob(ctx context.Context, client *databricks.WorkspaceClient,
231234
}
232235

233236
func spawnSSHClient(ctx context.Context, userName, privateKeyPath string, serverPort int, opts ClientOptions) error {
234-
executablePath, err := os.Executable()
237+
proxyCommand, err := setup.GenerateProxyCommand(opts.ClusterID, opts.AutoStartCluster, opts.ShutdownDelay, opts.Profile, userName, serverPort, opts.HandoverTimeout)
235238
if err != nil {
236-
return fmt.Errorf("failed to get current executable path: %w", err)
239+
return fmt.Errorf("failed to generate ProxyCommand: %w", err)
237240
}
238241

239-
proxyCommand := fmt.Sprintf("%s ssh connect --proxy --cluster=%s --handover-timeout=%s --metadata=%s,%d --auto-start-cluster=%t",
240-
executablePath, opts.ClusterID, opts.HandoverTimeout.String(), userName, serverPort, opts.AutoStartCluster)
241-
242242
sshArgs := []string{
243243
"-l", userName,
244244
"-i", privateKeyPath,

experimental/ssh/internal/setup/setup.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"path/filepath"
99
"regexp"
10+
"strconv"
1011
"strings"
1112
"time"
1213

@@ -55,20 +56,39 @@ func resolveConfigPath(configPath string) (string, error) {
5556
return filepath.Join(homeDir, ".ssh", "config"), nil
5657
}
5758

59+
func GenerateProxyCommand(clusterId string, autoStartCluster bool, shutdownDelay time.Duration, profile, userName string, serverPort int, handoverTimeout time.Duration) (string, error) {
60+
executablePath, err := os.Executable()
61+
if err != nil {
62+
return "", fmt.Errorf("failed to get current executable path: %w", err)
63+
}
64+
65+
proxyCommand := fmt.Sprintf("%q ssh connect --proxy --cluster=%s --auto-start-cluster=%t --shutdown-delay=%s",
66+
executablePath, clusterId, autoStartCluster, shutdownDelay.String())
67+
68+
if userName != "" && serverPort != 0 {
69+
proxyCommand += " --metadata=" + userName + "," + strconv.Itoa(serverPort)
70+
}
71+
72+
if handoverTimeout > 0 {
73+
proxyCommand += " --handover-timeout=" + handoverTimeout.String()
74+
}
75+
76+
if profile != "" {
77+
proxyCommand += " --profile=" + profile
78+
}
79+
80+
return proxyCommand, nil
81+
}
82+
5883
func generateHostConfig(opts SetupOptions) (string, error) {
5984
identityFilePath, err := keys.GetLocalSSHKeyPath(opts.ClusterID, opts.SSHKeysDir)
6085
if err != nil {
6186
return "", fmt.Errorf("failed to get local keys folder: %w", err)
6287
}
6388

64-
execPath, err := os.Executable()
89+
proxyCommand, err := GenerateProxyCommand(opts.ClusterID, opts.AutoStartCluster, opts.ShutdownDelay, opts.Profile, "", 0, 0)
6590
if err != nil {
66-
return "", fmt.Errorf("failed to get executable path: %w", err)
67-
}
68-
69-
profileOption := ""
70-
if opts.Profile != "" {
71-
profileOption = "--profile=" + opts.Profile
91+
return "", fmt.Errorf("failed to generate ProxyCommand: %w", err)
7292
}
7393

7494
hostConfig := fmt.Sprintf(`
@@ -77,8 +97,8 @@ Host %s
7797
ConnectTimeout 360
7898
StrictHostKeyChecking accept-new
7999
IdentityFile %q
80-
ProxyCommand %q ssh connect --proxy --cluster=%s --auto-start-cluster=%t --shutdown-delay=%s %s
81-
`, opts.HostName, identityFilePath, execPath, opts.ClusterID, opts.AutoStartCluster, opts.ShutdownDelay, profileOption)
100+
ProxyCommand %s
101+
`, opts.HostName, identityFilePath, proxyCommand)
82102

83103
return hostConfig, nil
84104
}

experimental/ssh/internal/setup/setup_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,24 @@ func TestValidateClusterAccess_ClusterNotFound(t *testing.T) {
5454
assert.Contains(t, err.Error(), "failed to get cluster information for cluster ID 'nonexistent'")
5555
}
5656

57+
func TestGenerateProxyCommand(t *testing.T) {
58+
cmd, err := GenerateProxyCommand("cluster-123", true, 45*time.Second, "", "", 0, 0)
59+
assert.NoError(t, err)
60+
assert.Contains(t, cmd, "ssh connect --proxy --cluster=cluster-123 --auto-start-cluster=true --shutdown-delay=45s")
61+
assert.NotContains(t, cmd, "--metadata")
62+
assert.NotContains(t, cmd, "--profile")
63+
assert.NotContains(t, cmd, "--handover-timeout")
64+
}
65+
66+
func TestGenerateProxyCommand_WithExtraArgs(t *testing.T) {
67+
cmd, err := GenerateProxyCommand("cluster-123", true, 45*time.Second, "test-profile", "user", 2222, 2*time.Minute)
68+
assert.NoError(t, err)
69+
assert.Contains(t, cmd, "ssh connect --proxy --cluster=cluster-123 --auto-start-cluster=true --shutdown-delay=45s")
70+
assert.Contains(t, cmd, " --metadata=user,2222")
71+
assert.Contains(t, cmd, " --handover-timeout=2m0s")
72+
assert.Contains(t, cmd, " --profile=test-profile")
73+
}
74+
5775
func TestGenerateHostConfig_Valid(t *testing.T) {
5876
// Create a temporary directory for testing
5977
tmpDir := t.TempDir()

0 commit comments

Comments
 (0)