Skip to content

Commit 8a7b5dc

Browse files
committed
feat: parse structured hostname arguments to "coder ssh"
If the argument to `coder ssh` fits the pattern `<prefix>--<owner>--<workspace>--<agent?>`, then parse this instead of treating the argument as a literal workspace name or `<owner>/<workspace>`. This will enable the use of `coder ssh` for the VS Code and JetBrains plugins in combination with wildcard `Host` entries in the SSH config. Part of #14986.
1 parent 20c36a6 commit 8a7b5dc

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

cli/ssh.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,17 @@ func (r *RootCmd) ssh() *serpent.Command {
180180
parsedEnv = append(parsedEnv, [2]string{k, v})
181181
}
182182

183-
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
183+
namedWorkspace := inv.Args[0]
184+
if parts := strings.Split(namedWorkspace, "--"); len(parts) >= 3 {
185+
owner := parts[1]
186+
name := parts[2]
187+
namedWorkspace = owner + "/" + name
188+
if len(parts) > 3 {
189+
namedWorkspace += "." + parts[3]
190+
}
191+
}
192+
193+
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, namedWorkspace)
184194
if err != nil {
185195
return err
186196
}

cli/ssh_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,69 @@ func TestSSH(t *testing.T) {
14821482
})
14831483
}
14841484
})
1485+
1486+
t.Run("ParseHostname", func(t *testing.T) {
1487+
t.Parallel()
1488+
client, workspace, agentToken := setupWorkspaceForAgent(t)
1489+
_, _ = tGoContext(t, func(ctx context.Context) {
1490+
// Run this async so the SSH command has to wait for
1491+
// the build and agent to connect!
1492+
_ = agenttest.New(t, client.URL, agentToken)
1493+
<-ctx.Done()
1494+
})
1495+
1496+
clientOutput, clientInput := io.Pipe()
1497+
serverOutput, serverInput := io.Pipe()
1498+
defer func() {
1499+
for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} {
1500+
_ = c.Close()
1501+
}
1502+
}()
1503+
1504+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1505+
defer cancel()
1506+
1507+
user, err := client.User(ctx, codersdk.Me)
1508+
require.NoError(t, err)
1509+
1510+
inv, root := clitest.New(t, "ssh", "--stdio", fmt.Sprintf("coder-vscode.coder.prod.netflix.net--%s--%s", user.Username, workspace.Name))
1511+
clitest.SetupConfig(t, client, root)
1512+
inv.Stdin = clientOutput
1513+
inv.Stdout = serverInput
1514+
inv.Stderr = io.Discard
1515+
1516+
cmdDone := tGo(t, func() {
1517+
err := inv.WithContext(ctx).Run()
1518+
assert.NoError(t, err)
1519+
})
1520+
1521+
conn, channels, requests, err := ssh.NewClientConn(&stdioConn{
1522+
Reader: serverOutput,
1523+
Writer: clientInput,
1524+
}, "", &ssh.ClientConfig{
1525+
// #nosec
1526+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
1527+
})
1528+
require.NoError(t, err)
1529+
defer conn.Close()
1530+
1531+
sshClient := ssh.NewClient(conn, channels, requests)
1532+
session, err := sshClient.NewSession()
1533+
require.NoError(t, err)
1534+
defer session.Close()
1535+
1536+
command := "sh -c exit"
1537+
if runtime.GOOS == "windows" {
1538+
command = "cmd.exe /c exit"
1539+
}
1540+
err = session.Run(command)
1541+
require.NoError(t, err)
1542+
err = sshClient.Close()
1543+
require.NoError(t, err)
1544+
_ = clientOutput.Close()
1545+
1546+
<-cmdDone
1547+
})
14851548
}
14861549

14871550
//nolint:paralleltest // This test uses t.Setenv, parent test MUST NOT be parallel.

0 commit comments

Comments
 (0)