Skip to content

Commit b86d5a0

Browse files
sandbox: drive SSH config from server-stamped gateway_host (drop heuristic, --gateway flag, cursor-hiding spinner, add multi-gateway support) (#5543)
## Summary Consolidates the gateway-resolution path simplification into a single PR. Now that the server stamps `gateway_host` on every SshKey and Sandbox response, `register` always populates the cache, every subsequent operation that touches the API gets the workspace's real gateway directly, and a bunch of compensating code falls out as dead weight. ## What changes ### Code that goes away - **`resolveGatewayHost(workspaceHost string) string`** — the hardcoded heuristic that returned `uw2.dbrx.dev` for non-staging and `ue1.s.dbrx.dev` for staging. Wrong for any workspace whose gateway doesn't match those two values. - **`defaultGatewayHost` + `stagingDefaultGatewayHost`** — the two constants behind the heuristic. - **`--gateway` flag on `sandbox ssh`** — a manual override that made sense only when the CLI couldn't learn the gateway itself. With the cache always populated post-register, the only thing the flag does now is let a user typo their way into dialing a random host. - **The "Connecting…" Bubble Tea spinner before `execSSHDirect`** — it hid the terminal cursor at init (`initTerminal()`) and the deferred `Close()` never ran because `execv` replaces the CLI process. Result: cursor invisible for the entire ssh session and after it exited. Co-authored with Anwell — pulled in from #5545. - **Synthetic `Host sandbox-gw` alias** — UI deep links use the literal gateway hostname, so the alias was buying nothing and adding an indirection the UI had to know about. ### New behavior - **Server-stamped gateway flows through to the SSH config.** `registerKey` now returns `*sshKeyEntry` carrying `GatewayHost`; `register` caches it via `setGatewayHost` and passes it to `maybeWriteSSHConfig`. - **One Host stanza per cached gateway.** A user with profiles in multiple regions accumulates one block per unique gateway, instead of the second `register` overwriting the first's block. New `allGatewayHosts` helper in `state.go` returns the deduplicated set. - **`saveState` is atomic** (tmp + Rename) — concurrent CLI invocations can't see a half-written `sandbox.json` and trip a parse error that `loadState` silently swallows. - **Post-register hint branches on `getDefault`** — suggests `databricks sandbox create` when no default exists, `databricks sandbox ssh` when one does. - **Path output uses forward slashes on Windows** — `filepath.ToSlash` on the `Generated SSH key at …` line so acceptance goldens are stable across Linux/macOS/Windows. ### New SSH config block shape Before (9 lines, synthetic alias, several overrides): ``` # Managed by `databricks sandbox register`. Host sandbox-gw HostName uw2.dbrx.dev Port 2222 IdentityFile ~/.ssh/sandbox_ed25519 IdentitiesOnly yes StrictHostKeyChecking no UserKnownHostsFile /dev/null LogLevel ERROR ``` After (literal hostname, one stanza per gateway, only the directives that actually differ from SSH defaults): ``` # Managed by `databricks sandbox register`. Manual edits will be overwritten. Host us-west-2.service-direct.dev.databricks.com Port 2222 IdentityFile ~/.ssh/sandbox_ed25519 IdentitiesOnly yes Host us-east-1.service-direct.prod.databricks.com Port 2222 IdentityFile ~/.ssh/sandbox_ed25519 IdentitiesOnly yes ``` Mirrors the workspace UI's "First time setup?" snippet exactly, so CLI users and paste-the-snippet users converge. ### New resolution chain in `sandbox ssh` Fresh API response → cached value. If both are empty: ``` Error: could not resolve the sandbox SSH gateway — run `databricks sandbox register` first ``` ## Tests - `acceptance/cmd/sandbox/register/success/` — pins the new contract: registerKey returns SshKey with gateway_host, register caches it, prints the consent-skip notice in the non-interactive acceptance path. Parent `test.toml` Ignores `.ssh`. - `TestMaybeWriteSSHConfigSkipsWhenNoGatewaysCached` — pins the no-gateway short-circuit. - `TestBuildSSHConfigBlockMultipleGateways` — pins per-gateway repetition in the new block shape. ## Test plan - [x] `go test ./cmd/sandbox/...` passes - [x] `go test ./acceptance -run TestAccept/cmd/sandbox` passes - [x] `go build ./...` clean - [x] `./task lint` clean - [x] Windows variant passes (after `filepath.ToSlash` fix) This pull request and its description were written by Isaac. --------- Co-authored-by: Pieter Noordhuis <pieter.noordhuis@databricks.com>
1 parent 1618170 commit b86d5a0

10 files changed

Lines changed: 198 additions & 70 deletions

File tree

acceptance/cmd/sandbox/register/success/out.test.toml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
✓ Generated SSH key at [TEST_TMP_DIR]/.ssh/sandbox_ed25519
2+
✓ SSH key registered
3+
skipped SSH config update (non-interactive); re-run `databricks sandbox register` from a terminal to add the sandbox gateway block(s)
4+
5+
Run databricks sandbox create to provision your first sandbox.
6+
7+
{
8+
"[DATABRICKS_URL]": "us-west-2.service-direct.dev.databricks.com"
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
$CLI sandbox register --name test-machine
2+
3+
# Verify the gateway host was cached in the local state file.
4+
# The framework Ignores the .databricks directory by default, so we
5+
# print the cached value to stdout for assertion instead.
6+
cat "$HOME/.databricks/sandbox.json" | jq -r '.gatewayHosts // {}'
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[[Server]]
2+
Pattern = "POST /api/2.0/lakebox/ssh-keys"
3+
Response.Body = '''
4+
{
5+
"keyHash": "a0b1c2d3e4f5a0b1c2d3e4f5a0b1c2d3",
6+
"name": "test-machine",
7+
"createTime": "2026-06-10T17:00:00Z",
8+
"gatewayHost": "us-west-2.service-direct.dev.databricks.com"
9+
}
10+
'''
11+
12+
# Pin the synthetic test hash so the framework's generic 3+-digit-run →
13+
# [NUMID] regex doesn't mangle it.
14+
[[Repls]]
15+
Old = 'a0b1c2d3e4f5a0b1c2d3e4f5a0b1c2d3'
16+
New = '[KEY_HASH]'
17+
Order = 5

acceptance/cmd/sandbox/test.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Acceptance tests for `databricks sandbox` subcommands.
22
#
3-
# Ignore the local state file the CLI writes (sandbox.json) so it
4-
# doesn't bleed into the test directory's diff.
5-
Ignore = [".databricks"]
3+
# Ignore the local state file the CLI writes (sandbox.json) and the
4+
# generated SSH key + managed SSH config — register-path tests would
5+
# otherwise produce these on every run.
6+
Ignore = [".databricks", ".ssh"]
67

78
# Sandbox commands are independent of the bundle engine. Override the
89
# root EnvMatrix so the two engines don't both run in parallel for

cmd/sandbox/register.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,14 @@ Examples:
5959
return fmt.Errorf("failed to ensure sandbox SSH key: %w", err)
6060
}
6161

62+
// Always print paths with forward slashes so acceptance
63+
// goldens are stable across Linux/macOS/Windows.
64+
displayKeyPath := filepath.ToSlash(keyPath)
6265
stderr := cmd.ErrOrStderr()
6366
if generated {
64-
ok(ctx, "Generated SSH key at "+cmdio.Faint(ctx, keyPath))
67+
ok(ctx, "Generated SSH key at "+cmdio.Faint(ctx, displayKeyPath))
6568
} else {
66-
field(ctx, stderr, "key", keyPath)
69+
field(ctx, stderr, "key", displayKeyPath)
6770
}
6871

6972
pubKeyData, err := os.ReadFile(keyPath + ".pub")
@@ -94,18 +97,26 @@ Examples:
9497
_ = setGatewayHost(ctx, profile, registered.GatewayHost)
9598
}
9699

97-
// Write the `Host <gateway>` block to ~/.ssh/config so
100+
// Write the `Host <gateway>` block(s) to ~/.ssh/config so
98101
// editor Remote-SSH ("Open in VS Code/Cursor") deep links
99102
// and plain `ssh <id>@<gateway>` both work without the
100-
// user pasting any config block. The gateway host comes
101-
// straight from the registerKey response — no probe call
102-
// needed, no heuristic.
103-
if err := maybeWriteSSHConfig(ctx, keyPath, registered.GatewayHost); err != nil {
103+
// user pasting any config block. Reads the full gateway
104+
// set from local state so a user with profiles in
105+
// multiple regions accumulates one block per gateway
106+
// across registers, rather than overwriting.
107+
if err := maybeWriteSSHConfig(ctx, keyPath); err != nil {
104108
warn(ctx, fmt.Sprintf("Registered key, but failed to update ~/.ssh/config: %v", err))
105109
}
106110

111+
// Tell the user what to do next based on whether they
112+
// already have a sandbox: a brand-new register with no
113+
// default yet should land on `create`, not `ssh`.
107114
blank(stderr)
108-
fmt.Fprintf(stderr, " Run %s to connect.\n\n", cmdio.Bold(ctx, "databricks sandbox ssh"))
115+
if getDefault(ctx, profile) == "" {
116+
fmt.Fprintf(stderr, " Run %s to provision your first sandbox.\n\n", cmdio.Bold(ctx, "databricks sandbox create"))
117+
} else {
118+
fmt.Fprintf(stderr, " Run %s to connect.\n\n", cmdio.Bold(ctx, "databricks sandbox ssh"))
119+
}
109120
return nil
110121
},
111122
}

cmd/sandbox/ssh.go

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,9 @@ import (
1717
"github.com/spf13/cobra"
1818
)
1919

20-
const (
21-
defaultGatewayHost = "uw2.dbrx.dev"
22-
stagingDefaultGatewayHost = "ue1.s.dbrx.dev"
23-
defaultGatewayPort = "2222"
24-
)
25-
26-
// resolveGatewayHost picks the SSH gateway hostname based on the workspace host.
27-
// Staging workspaces (*.staging.cloud.databricks.com etc.) route through
28-
// ue1.s.dbrx.dev; everything else uses uw2.dbrx.dev.
29-
func resolveGatewayHost(workspaceHost string) string {
30-
if strings.Contains(workspaceHost, ".staging.") {
31-
return stagingDefaultGatewayHost
32-
}
33-
return defaultGatewayHost
34-
}
20+
const defaultGatewayPort = "2222"
3521

3622
func newSSHCommand() *cobra.Command {
37-
var gatewayHost string
3823
var gatewayPort string
3924

4025
cmd := &cobra.Command{
@@ -167,17 +152,17 @@ Examples:
167152
}
168153
}
169154

170-
// Resolution precedence: --gateway flag → fresh API response →
171-
// cached value for this profile → workspace-host heuristic.
172-
host := gatewayHost
173-
if host == "" {
174-
host = sandboxGatewayHost
175-
}
155+
// Resolution precedence: fresh API response → cached value
156+
// for this profile. The server stamps the workspace's
157+
// gateway on every Sandbox / SshKey response, so the cache
158+
// is populated after the first call against the workspace
159+
// (sandbox register / list / create / etc.).
160+
host := sandboxGatewayHost
176161
if host == "" {
177162
host = getGatewayHost(ctx, profile)
178163
}
179164
if host == "" {
180-
host = resolveGatewayHost(w.Config.Host)
165+
return errors.New("could not resolve the sandbox SSH gateway — run `databricks sandbox register` first")
181166
}
182167

183168
// Persist whatever the server just told us, so the next invocation
@@ -186,16 +171,17 @@ Examples:
186171
_ = setGatewayHost(ctx, profile, sandboxGatewayHost)
187172
}
188173

189-
// Don't print "Connected" here — ssh hasn't completed the
190-
// handshake yet, so any success message would race ssh's
191-
// own error output on the failure path.
192-
s := spin(ctx, "Connecting to "+cmdio.Bold(ctx, sandboxID)+"…")
193-
defer s.Close()
174+
// No "Connecting…" spinner here. execSSHDirect replaces
175+
// the CLI process via execv, so a deferred Close() would
176+
// never run — and Bubble Tea hides the terminal cursor at
177+
// init and only restores it on clean shutdown, leaving the
178+
// cursor invisible for the entire ssh session and after it
179+
// exits. The spinner couldn't animate during the handshake
180+
// anyway (the CLI process is gone by then).
194181
return execSSHDirect(sandboxID, host, gatewayPort, keyPath, extraArgs)
195182
},
196183
}
197184

198-
cmd.Flags().StringVar(&gatewayHost, "gateway", "", "Sandbox gateway hostname (auto-detected from profile if empty)")
199185
cmd.Flags().StringVar(&gatewayPort, "port", defaultGatewayPort, "Sandbox gateway SSH port")
200186

201187
return cmd

cmd/sandbox/sshconfig.go

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func sshConfigAlreadyManaged(ctx context.Context) (bool, error) {
5959
// writeSSHConfig writes the sandbox-managed SSH config block to a
6060
// managed file and, if not already present, adds an Include directive
6161
// to the user's ~/.ssh/config pointing at that file.
62-
func writeSSHConfig(ctx context.Context, keyPath, gatewayHost, gatewayPort string) (string, string, error) {
62+
func writeSSHConfig(ctx context.Context, keyPath string, gatewayHosts []string, gatewayPort string) (string, string, error) {
6363
home, err := env.UserHomeDir(ctx)
6464
if err != nil {
6565
return "", "", err
@@ -72,7 +72,7 @@ func writeSSHConfig(ctx context.Context, keyPath, gatewayHost, gatewayPort strin
7272
managedPath := filepath.Join(sshDir, sshIncludeFileName)
7373
mainPath := filepath.Join(sshDir, "config")
7474

75-
block := buildSSHConfigBlock(keyPath, gatewayHost, gatewayPort)
75+
block := buildSSHConfigBlock(keyPath, gatewayHosts, gatewayPort)
7676
if err := writeManagedConfig(managedPath, block); err != nil {
7777
return managedPath, mainPath, err
7878
}
@@ -82,12 +82,12 @@ func writeSSHConfig(ctx context.Context, keyPath, gatewayHost, gatewayPort strin
8282
return managedPath, mainPath, nil
8383
}
8484

85-
// buildSSHConfigBlock renders the Host stanza we write to the
86-
// sandbox-managed include file. Mirrors the snippet the workspace UI's
87-
// "First time setup?" disclosure recommends — the Host key is the
88-
// literal gateway hostname (so editor Remote-SSH deep links like
89-
// `ssh-remote+<id>@<gateway>` resolve directly), and only the two
90-
// directives that meaningfully differ from SSH defaults are set:
85+
// buildSSHConfigBlock renders one Host stanza per gateway. Mirrors the
86+
// snippet the workspace UI's "First time setup?" disclosure recommends
87+
// — the Host key is the literal gateway hostname (so editor Remote-SSH
88+
// deep links like `ssh-remote+<id>@<gateway>` resolve directly), and
89+
// only the two directives that meaningfully differ from SSH defaults
90+
// are set:
9191
//
9292
// - Port (the gateway listens on 2222, not 22)
9393
// - IdentityFile + IdentitiesOnly (pin our key so ssh doesn't offer
@@ -99,13 +99,18 @@ func writeSSHConfig(ctx context.Context, keyPath, gatewayHost, gatewayPort strin
9999
// CLI's own `sandbox ssh` does via argv. No User directive either —
100100
// the per-sandbox identifier travels in the destination
101101
// (`ssh <sandbox-id>@<gateway>`).
102-
func buildSSHConfigBlock(keyPath, gatewayHost, gatewayPort string) string {
103-
return fmt.Sprintf(`# Managed by `+"`databricks sandbox register`"+`. Manual edits will be overwritten.
104-
Host %s
105-
Port %s
106-
IdentityFile %s
107-
IdentitiesOnly yes
108-
`, gatewayHost, gatewayPort, keyPath)
102+
//
103+
// One stanza per gateway means a user with profiles in multiple
104+
// regions doesn't lose IDE Remote-SSH for the earlier workspaces when
105+
// they `register` against a new one. Callers pass `gatewayHosts` from
106+
// state.allGatewayHosts.
107+
func buildSSHConfigBlock(keyPath string, gatewayHosts []string, gatewayPort string) string {
108+
var b strings.Builder
109+
b.WriteString("# Managed by `databricks sandbox register`. Manual edits will be overwritten.\n")
110+
for _, gw := range gatewayHosts {
111+
fmt.Fprintf(&b, "Host %s\n Port %s\n IdentityFile %s\n IdentitiesOnly yes\n", gw, gatewayPort, keyPath)
112+
}
113+
return b.String()
109114
}
110115

111116
// writeManagedConfig writes content to path atomically with 0600
@@ -183,14 +188,20 @@ func hasOurMarkedBlock(text string) bool {
183188
// maybeWriteSSHConfig writes the sandbox-managed SSH config, prompting
184189
// for consent the first time on this machine. Re-runs silently refresh
185190
// the managed file. Non-interactive contexts skip the write entirely.
186-
// The gateway host is the workspace-scoped one returned by the server
187-
// on the registerKey response — no heuristic, no probe call.
188-
func maybeWriteSSHConfig(ctx context.Context, keyPath, gatewayHost string) error {
189-
if gatewayHost == "" {
190-
// Server didn't stamp a gateway on the response. Old server
191-
// without the gateway_host field, or genuinely no gateway
192-
// available. Skip rather than guess.
193-
cmdio.LogString(ctx, " "+cmdio.Faint(ctx, "skipped SSH config update — server did not return a gateway host"))
191+
//
192+
// Reads the gateway-host set from state (populated by every API call
193+
// that touches a Sandbox or SshKey response, including the register
194+
// call the caller just made), so users with multiple workspaces in
195+
// different regions accumulate one Host stanza per unique gateway
196+
// instead of losing the earlier ones.
197+
func maybeWriteSSHConfig(ctx context.Context, keyPath string) error {
198+
gateways := allGatewayHosts(ctx)
199+
if len(gateways) == 0 {
200+
// No gateway has been cached yet — register's API call must
201+
// not have stamped one, or state is empty. Skip rather than
202+
// guess; the next API call that returns a Sandbox/SshKey
203+
// will populate the cache and a future register will write.
204+
cmdio.LogString(ctx, " "+cmdio.Faint(ctx, "skipped SSH config update — no sandbox gateway is known yet"))
194205
return nil
195206
}
196207

@@ -204,12 +215,12 @@ func maybeWriteSSHConfig(ctx context.Context, keyPath, gatewayHost string) error
204215
return err
205216
}
206217
if !cmdio.IsPromptSupported(ctx) {
207-
cmdio.LogString(ctx, " "+cmdio.Faint(ctx, "skipped SSH config update (non-interactive); re-run `databricks sandbox register` from a terminal to add the `Host "+gatewayHost+"` block"))
218+
cmdio.LogString(ctx, " "+cmdio.Faint(ctx, "skipped SSH config update (non-interactive); re-run `databricks sandbox register` from a terminal to add the sandbox gateway block(s)"))
208219
return nil
209220
}
210221
question := fmt.Sprintf(
211-
"Add a `Host %s` block to %s? This lets editor Remote-SSH (VS Code / Cursor) and `ssh <id>@%s` connect without further setup.",
212-
gatewayHost, mainPath, gatewayHost,
222+
"Add a sandbox `Host` block to %s? This lets editor Remote-SSH (VS Code / Cursor) and `ssh <id>@<gateway>` connect without further setup.",
223+
mainPath,
213224
)
214225
confirmed, err := cmdio.AskYesOrNo(ctx, question)
215226
if err != nil {
@@ -221,7 +232,7 @@ func maybeWriteSSHConfig(ctx context.Context, keyPath, gatewayHost string) error
221232
}
222233
}
223234

224-
managedPath, _, err := writeSSHConfig(ctx, keyPath, gatewayHost, defaultGatewayPort)
235+
managedPath, _, err := writeSSHConfig(ctx, keyPath, gateways, defaultGatewayPort)
225236
if err != nil {
226237
return err
227238
}

cmd/sandbox/sshconfig_test.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
package sandbox
22

33
import (
4+
"io"
5+
"io/fs"
46
"os"
57
"path/filepath"
68
"runtime"
79
"strings"
810
"testing"
911

12+
"github.com/databricks/cli/libs/cmdio"
13+
"github.com/databricks/cli/libs/env"
14+
"github.com/databricks/cli/libs/flags"
1015
"github.com/stretchr/testify/assert"
1116
"github.com/stretchr/testify/require"
1217
)
1318

1419
func TestBuildSSHConfigBlockShape(t *testing.T) {
15-
block := buildSSHConfigBlock("/home/u/.ssh/sandbox_ed25519", "uw2.dbrx.dev", "2222")
20+
block := buildSSHConfigBlock("/home/u/.ssh/sandbox_ed25519", []string{"uw2.dbrx.dev"}, "2222")
1621

1722
// The Host key is the literal gateway hostname (so UI editor deep
1823
// links like `ssh-remote+<id>@<gateway>` resolve directly), and we
@@ -47,6 +52,23 @@ func TestBuildSSHConfigBlockShape(t *testing.T) {
4752
}
4853
}
4954

55+
// Two registered workspaces in different regions must each get their
56+
// own Host stanza so editor Remote-SSH for one doesn't silently break
57+
// when the user registers against the other.
58+
func TestBuildSSHConfigBlockMultipleGateways(t *testing.T) {
59+
block := buildSSHConfigBlock(
60+
"/home/u/.ssh/sandbox_ed25519",
61+
[]string{"uw2.dbrx.dev", "ue1.dbrx.dev"},
62+
"2222",
63+
)
64+
assert.Contains(t, block, "Host uw2.dbrx.dev\n")
65+
assert.Contains(t, block, "Host ue1.dbrx.dev\n")
66+
// Each Host should be followed by Port + IdentityFile, so a naive
67+
// "Port 2222" count is a quick sanity check on per-gateway repetition.
68+
assert.Equal(t, 2, strings.Count(block, "Port 2222"))
69+
assert.Equal(t, 2, strings.Count(block, "IdentityFile /home/u/.ssh/sandbox_ed25519"))
70+
}
71+
5072
func TestWriteManagedConfigCreatesWithRightPerms(t *testing.T) {
5173
dir := t.TempDir()
5274
path := filepath.Join(dir, sshIncludeFileName)
@@ -143,6 +165,28 @@ func TestEnsureMainIncludesManagedPreservesUserConfigBelow(t *testing.T) {
143165
assert.Less(t, beginIdx, userIdx, "managed block must precede the user's existing config")
144166
}
145167

168+
// When state has no cached gateway hosts (no register has populated
169+
// the cache, or sandbox.json is missing), maybeWriteSSHConfig must
170+
// short-circuit before any disk I/O so it can't accidentally write a
171+
// malformed `Host \n` stanza. Also pins the no-disk-touch guarantee
172+
// against a future refactor that might `os.MkdirAll` before the check.
173+
func TestMaybeWriteSSHConfigSkipsWhenNoGatewaysCached(t *testing.T) {
174+
home := t.TempDir()
175+
ctx := env.WithUserHomeDir(t.Context(), home)
176+
ctx = cmdio.InContext(ctx, cmdio.NewIO(ctx, flags.OutputText,
177+
io.NopCloser(strings.NewReader("")), io.Discard, io.Discard, "", ""))
178+
179+
require.NoError(t, maybeWriteSSHConfig(ctx, filepath.Join(home, ".ssh", "sandbox_ed25519")))
180+
181+
// No managed file should have been created.
182+
_, err := os.Stat(filepath.Join(home, ".ssh", sshIncludeFileName))
183+
assert.ErrorIs(t, err, fs.ErrNotExist, "managed include file must not be created when no gateways are cached")
184+
185+
// And ~/.ssh/config must not have been touched either.
186+
_, err = os.Stat(filepath.Join(home, ".ssh", "config"))
187+
assert.ErrorIs(t, err, fs.ErrNotExist, "main ssh config must not be created when no gateways are cached")
188+
}
189+
146190
func TestHasOurMarkedBlock(t *testing.T) {
147191
cases := []struct {
148192
name string

0 commit comments

Comments
 (0)