Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Notable Changes

### CLI
* `databricks ssh connect` now opens an interactive `bash` login shell by default instead of the compute image's default `/bin/sh`, falling back gracefully when `bash` is unavailable. Passing an explicit remote command (`-- <cmd>`) is unaffected ([#5687](https://github.com/databricks/cli/pull/5687)).

### Bundles
* `bundle run` now prints the modern job run URL (`/jobs/<id>/runs/<id>`) so that non-admin users permitted to view the run are taken to the run instead of the workspace homepage.
Expand Down
18 changes: 17 additions & 1 deletion experimental/ssh/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,22 @@ func submitSSHTunnelJob(ctx context.Context, client *databricks.WorkspaceClient,
return waiter.RunId, waitForJobToStart(ctx, client, waiter.RunId, opts.TaskStartupTimeout)
}

// buildRemoteShellArgs returns the ssh arguments that follow the hostname.
//
// For the interactive case (no remote command given), it forces PTY allocation
// and launches a login bash, because the default login shell on Databricks
// compute images is /bin/sh. If bash is unavailable it falls back to $SHELL or
// /bin/sh so the connection never breaks.
//
// For the non-interactive case (e.g. `databricks ssh connect ... -- ls -la`),
// the user's command is returned verbatim so behavior is unchanged.
func buildRemoteShellArgs(opts ClientOptions) []string {
if len(opts.AdditionalArgs) > 0 {
return opts.AdditionalArgs
}
return []string{"-t", `command -v bash >/dev/null 2>&1 && exec bash -l || exec "${SHELL:-/bin/sh}" -l`}
}

func spawnSSHClient(ctx context.Context, client *databricks.WorkspaceClient, userName, privateKeyPath string, serverPort int, clusterID string, opts ClientOptions) error {
// Create a copy with metadata for the ProxyCommand
optsWithMetadata := opts
Expand All @@ -636,7 +652,7 @@ func spawnSSHClient(ctx context.Context, client *databricks.WorkspaceClient, use
sshArgs = append(sshArgs, "-o", "UserKnownHostsFile="+opts.UserKnownHostsFile)
}
sshArgs = append(sshArgs, hostName)
sshArgs = append(sshArgs, opts.AdditionalArgs...)
sshArgs = append(sshArgs, buildRemoteShellArgs(opts)...)

log.Debugf(ctx, "Launching SSH client: ssh %s", strings.Join(sshArgs, " "))
sshCmd := exec.CommandContext(ctx, "ssh", sshArgs...)
Expand Down
16 changes: 16 additions & 0 deletions experimental/ssh/internal/client/client_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,22 @@ func TestHostKeyChangedHint(t *testing.T) {
}
}

func TestBuildRemoteShellArgs(t *testing.T) {
t.Run("interactive launches login bash with PTY", func(t *testing.T) {
args := buildRemoteShellArgs(ClientOptions{})
require.Len(t, args, 2)
assert.Equal(t, "-t", args[0])
assert.Equal(t, `command -v bash >/dev/null 2>&1 && exec bash -l || exec "${SHELL:-/bin/sh}" -l`, args[1])
})

t.Run("non-interactive passes additional args verbatim", func(t *testing.T) {
additional := []string{"ls", "-la"}
args := buildRemoteShellArgs(ClientOptions{AdditionalArgs: additional})
assert.Equal(t, additional, args)
assert.NotContains(t, args, "-t")
})
}

func TestTailWriterRetainsTail(t *testing.T) {
t.Run("retains only the tail", func(t *testing.T) {
w := &tailWriter{maxBytes: 4}
Expand Down
Loading