diff --git a/.nextchanges/cli/ssh-connect-stable-ide-authority.md b/.nextchanges/cli/ssh-connect-stable-ide-authority.md new file mode 100644 index 00000000000..318341fd083 --- /dev/null +++ b/.nextchanges/cli/ssh-connect-stable-ide-authority.md @@ -0,0 +1 @@ +* Stop `databricks ssh connect --ide` from adding a duplicate entry to the IDE's Remote Explorer on every connect: the remote authority is now the SSH host alias alone, instead of embedding the per-instance remote OS user. ([#6550](https://github.com/databricks/cli/pull/6550)) diff --git a/experimental/ssh/internal/client/client.go b/experimental/ssh/internal/client/client.go index 031bbd4d7af..a46cfd4b7e7 100644 --- a/experimental/ssh/internal/client/client.go +++ b/experimental/ssh/internal/client/client.go @@ -476,7 +476,7 @@ func runIDE(ctx context.Context, client *databricks.WorkspaceClient, userName, k return fmt.Errorf("failed to ensure SSH config entry: %w", err) } - return vscode.LaunchIDE(ctx, opts.IDE, connectionName, userName, currentUser.UserName) + return vscode.LaunchIDE(ctx, opts.IDE, connectionName, currentUser.UserName) } func ensureSSHConfigEntry(ctx context.Context, configPath, hostName, userName, keyPath string, serverPort int, clusterID string, opts ClientOptions) error { diff --git a/experimental/ssh/internal/vscode/run.go b/experimental/ssh/internal/vscode/run.go index db52cff9f5d..e28302c349d 100644 --- a/experimental/ssh/internal/vscode/run.go +++ b/experimental/ssh/internal/vscode/run.go @@ -170,18 +170,30 @@ func CheckIDESSHExtension(ctx context.Context, option string, autoApprove bool) return nil } +// remoteLaunchArgs builds the arguments that open a remote window on the tunnel +// host at the user's workspace home folder. +// +// The remote authority is the SSH host alias alone, without a "@" prefix. +// The host config the CLI writes for the connection already carries a User +// directive (see sshconfig.GenerateHostConfig), and on serverless the remote OS +// user is a fresh per-instance name (spark-), so including it would give +// every connect a different authority. VS Code keys the "previously opened +// folders" it lists under a host by URI, so a per-connect authority added a +// duplicate row to the Remote Explorer on every `ssh connect --ide`. +func remoteLaunchArgs(ide ideDescriptor, connectionName, databricksUserName string) []string { + // Format: ssh-remote+ /Workspace/Users// + remoteURI := "ssh-remote+" + connectionName + remotePath := fmt.Sprintf("/Workspace/Users/%s/", databricksUserName) + return append(append([]string{}, ide.LaunchArgs...), "--remote", remoteURI, remotePath) +} + // LaunchIDE launches the IDE with a remote SSH connection using special "ssh-remote" URI format. -func LaunchIDE(ctx context.Context, ideOption, connectionName, userName, databricksUserName string) error { +func LaunchIDE(ctx context.Context, ideOption, connectionName, databricksUserName string) error { ide := getIDE(ideOption) + args := remoteLaunchArgs(ide, connectionName, databricksUserName) - // Construct the remote SSH URI - // Format: ssh-remote+@ /Workspace/Users// - remoteURI := fmt.Sprintf("ssh-remote+%s@%s", userName, connectionName) - remotePath := fmt.Sprintf("/Workspace/Users/%s/", databricksUserName) - - log.Infof(ctx, "Launching %s with remote URI: %s and path: %s", ideOption, remoteURI, remotePath) + log.Infof(ctx, "Launching %s with args: %v", ideOption, args) - args := append(append([]string{}, ide.LaunchArgs...), "--remote", remoteURI, remotePath) ideCmd := exec.CommandContext(ctx, ide.Command, args...) ideCmd.Stdout = os.Stdout ideCmd.Stderr = os.Stderr diff --git a/experimental/ssh/internal/vscode/run_test.go b/experimental/ssh/internal/vscode/run_test.go index 2a33b7b3828..e4c33c290dc 100644 --- a/experimental/ssh/internal/vscode/run_test.go +++ b/experimental/ssh/internal/vscode/run_test.go @@ -308,6 +308,36 @@ func TestCheckIDESSHExtension_AutoApproveMissing_Installs(t *testing.T) { assert.NoError(t, err) } +// The authority must be the host alias alone. The remote OS user is a fresh +// spark- on every serverless instance, so a "@" prefix would give each +// connect a different authority and VS Code would remember a separate +// previously-opened folder for each one. +func TestRemoteLaunchArgs(t *testing.T) { + tests := []struct { + name string + ide string + want []string + }{ + { + name: "vscode", + ide: VSCodeOption, + want: []string{"--remote", "ssh-remote+databricks-cpu-7f189c39", "/Workspace/Users/me@example.com/"}, + }, + { + name: "cursor keeps its launch args first", + ide: CursorOption, + want: []string{"--classic", "--remote", "ssh-remote+databricks-cpu-7f189c39", "/Workspace/Users/me@example.com/"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := remoteLaunchArgs(getIDE(tt.ide), "databricks-cpu-7f189c39", "me@example.com") + assert.Equal(t, tt.want, args) + }) + } +} + func TestCheckIDESSHExtension_NoPrompt_WithoutAutoApprove_Errors(t *testing.T) { tmpDir := t.TempDir() t.Setenv("PATH", tmpDir)