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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/cli/ssh-connect-stable-ide-authority.md
Original file line number Diff line number Diff line change
@@ -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))
2 changes: 1 addition & 1 deletion experimental/ssh/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
28 changes: 20 additions & 8 deletions experimental/ssh/internal/vscode/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<user>@" 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-<uuid>), 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+<connection_name> /Workspace/Users/<databricks_user_name>/
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+<server_user_name>@<connection_name> /Workspace/Users/<databricks_user_name>/
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
Expand Down
30 changes: 30 additions & 0 deletions experimental/ssh/internal/vscode/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-<uuid> on every serverless instance, so a "<user>@" 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)
Expand Down
Loading