Skip to content

Commit ceab646

Browse files
committed
feat: use wildcard Host entry in config-ssh
Rather than create a separate Host entry for every workspace, configure a wildcard such as `coder.*` which can accomodate all of a user's workspaces.
1 parent 20c36a6 commit ceab646

File tree

2 files changed

+192
-403
lines changed

2 files changed

+192
-403
lines changed

cli/configssh.go

+75-158
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cli
33
import (
44
"bufio"
55
"bytes"
6-
"context"
76
"errors"
87
"fmt"
98
"io"
@@ -12,7 +11,6 @@ import (
1211
"os"
1312
"path/filepath"
1413
"runtime"
15-
"sort"
1614
"strconv"
1715
"strings"
1816

@@ -22,11 +20,9 @@ import (
2220
"github.com/pkg/diff/write"
2321
"golang.org/x/exp/constraints"
2422
"golang.org/x/exp/slices"
25-
"golang.org/x/sync/errgroup"
2623
"golang.org/x/xerrors"
2724

2825
"github.com/coder/coder/v2/cli/cliui"
29-
"github.com/coder/coder/v2/coderd/util/slice"
3026
"github.com/coder/coder/v2/codersdk"
3127
"github.com/coder/serpent"
3228
)
@@ -139,74 +135,6 @@ func (o sshConfigOptions) asList() (list []string) {
139135
return list
140136
}
141137

142-
type sshWorkspaceConfig struct {
143-
Name string
144-
Hosts []string
145-
}
146-
147-
func sshFetchWorkspaceConfigs(ctx context.Context, client *codersdk.Client) ([]sshWorkspaceConfig, error) {
148-
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
149-
Owner: codersdk.Me,
150-
})
151-
if err != nil {
152-
return nil, err
153-
}
154-
155-
var errGroup errgroup.Group
156-
workspaceConfigs := make([]sshWorkspaceConfig, len(res.Workspaces))
157-
for i, workspace := range res.Workspaces {
158-
i := i
159-
workspace := workspace
160-
errGroup.Go(func() error {
161-
resources, err := client.TemplateVersionResources(ctx, workspace.LatestBuild.TemplateVersionID)
162-
if err != nil {
163-
return err
164-
}
165-
166-
wc := sshWorkspaceConfig{Name: workspace.Name}
167-
var agents []codersdk.WorkspaceAgent
168-
for _, resource := range resources {
169-
if resource.Transition != codersdk.WorkspaceTransitionStart {
170-
continue
171-
}
172-
agents = append(agents, resource.Agents...)
173-
}
174-
175-
// handle both WORKSPACE and WORKSPACE.AGENT syntax
176-
if len(agents) == 1 {
177-
wc.Hosts = append(wc.Hosts, workspace.Name)
178-
}
179-
for _, agent := range agents {
180-
hostname := workspace.Name + "." + agent.Name
181-
wc.Hosts = append(wc.Hosts, hostname)
182-
}
183-
184-
workspaceConfigs[i] = wc
185-
186-
return nil
187-
})
188-
}
189-
err = errGroup.Wait()
190-
if err != nil {
191-
return nil, err
192-
}
193-
194-
return workspaceConfigs, nil
195-
}
196-
197-
func sshPrepareWorkspaceConfigs(ctx context.Context, client *codersdk.Client) (receive func() ([]sshWorkspaceConfig, error)) {
198-
wcC := make(chan []sshWorkspaceConfig, 1)
199-
errC := make(chan error, 1)
200-
go func() {
201-
wc, err := sshFetchWorkspaceConfigs(ctx, client)
202-
wcC <- wc
203-
errC <- err
204-
}()
205-
return func() ([]sshWorkspaceConfig, error) {
206-
return <-wcC, <-errC
207-
}
208-
}
209-
210138
func (r *RootCmd) configSSH() *serpent.Command {
211139
var (
212140
sshConfigFile string
@@ -254,8 +182,6 @@ func (r *RootCmd) configSSH() *serpent.Command {
254182
// warning at any time.
255183
_, _ = client.BuildInfo(ctx)
256184

257-
recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(ctx, client)
258-
259185
out := inv.Stdout
260186
if dryRun {
261187
// Print everything except diff to stderr so
@@ -371,11 +297,6 @@ func (r *RootCmd) configSSH() *serpent.Command {
371297
newline := len(before) > 0
372298
sshConfigWriteSectionHeader(buf, newline, sshConfigOpts)
373299

374-
workspaceConfigs, err := recvWorkspaceConfigs()
375-
if err != nil {
376-
return xerrors.Errorf("fetch workspace configs failed: %w", err)
377-
}
378-
379300
coderdConfig, err := client.SSHConfiguration(ctx)
380301
if err != nil {
381302
// If the error is 404, this deployment does not support
@@ -394,91 +315,79 @@ func (r *RootCmd) configSSH() *serpent.Command {
394315
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
395316
}
396317

397-
// Ensure stable sorting of output.
398-
slices.SortFunc(workspaceConfigs, func(a, b sshWorkspaceConfig) int {
399-
return slice.Ascending(a.Name, b.Name)
400-
})
401-
for _, wc := range workspaceConfigs {
402-
sort.Strings(wc.Hosts)
403-
// Write agent configuration.
404-
for _, workspaceHostname := range wc.Hosts {
405-
sshHostname := fmt.Sprintf("%s%s", coderdConfig.HostnamePrefix, workspaceHostname)
406-
defaultOptions := []string{
407-
"HostName " + sshHostname,
408-
"ConnectTimeout=0",
409-
"StrictHostKeyChecking=no",
410-
// Without this, the "REMOTE HOST IDENTITY CHANGED"
411-
// message will appear.
412-
"UserKnownHostsFile=/dev/null",
413-
// This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
414-
// message from appearing on every SSH. This happens because we ignore the known hosts.
415-
"LogLevel ERROR",
416-
}
417-
418-
if !skipProxyCommand {
419-
rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig)
420-
for _, h := range sshConfigOpts.header {
421-
rootFlags += fmt.Sprintf(" --header %q", h)
422-
}
423-
if sshConfigOpts.headerCommand != "" {
424-
rootFlags += fmt.Sprintf(" --header-command %q", sshConfigOpts.headerCommand)
425-
}
426-
427-
flags := ""
428-
if sshConfigOpts.waitEnum != "auto" {
429-
flags += " --wait=" + sshConfigOpts.waitEnum
430-
}
431-
if sshConfigOpts.disableAutostart {
432-
flags += " --disable-autostart=true"
433-
}
434-
defaultOptions = append(defaultOptions, fmt.Sprintf(
435-
"ProxyCommand %s %s ssh --stdio%s %s",
436-
escapedCoderBinary, rootFlags, flags, workspaceHostname,
437-
))
438-
}
318+
// Write agent configuration.
319+
defaultOptions := []string{
320+
"ConnectTimeout=0",
321+
"StrictHostKeyChecking=no",
322+
// Without this, the "REMOTE HOST IDENTITY CHANGED"
323+
// message will appear.
324+
"UserKnownHostsFile=/dev/null",
325+
// This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
326+
// message from appearing on every SSH. This happens because we ignore the known hosts.
327+
"LogLevel ERROR",
328+
}
439329

440-
// Create a copy of the options so we can modify them.
441-
configOptions := sshConfigOpts
442-
configOptions.sshOptions = nil
443-
444-
// User options first (SSH only uses the first
445-
// option unless it can be given multiple times)
446-
for _, opt := range sshConfigOpts.sshOptions {
447-
err := configOptions.addOptions(opt)
448-
if err != nil {
449-
return xerrors.Errorf("add flag config option %q: %w", opt, err)
450-
}
451-
}
330+
if !skipProxyCommand {
331+
rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig)
332+
for _, h := range sshConfigOpts.header {
333+
rootFlags += fmt.Sprintf(" --header %q", h)
334+
}
335+
if sshConfigOpts.headerCommand != "" {
336+
rootFlags += fmt.Sprintf(" --header-command %q", sshConfigOpts.headerCommand)
337+
}
452338

453-
// Deployment options second, allow them to
454-
// override standard options.
455-
for k, v := range coderdConfig.SSHConfigOptions {
456-
opt := fmt.Sprintf("%s %s", k, v)
457-
err := configOptions.addOptions(opt)
458-
if err != nil {
459-
return xerrors.Errorf("add coderd config option %q: %w", opt, err)
460-
}
461-
}
339+
flags := ""
340+
if sshConfigOpts.waitEnum != "auto" {
341+
flags += " --wait=" + sshConfigOpts.waitEnum
342+
}
343+
if sshConfigOpts.disableAutostart {
344+
flags += " --disable-autostart=true"
345+
}
346+
defaultOptions = append(defaultOptions, fmt.Sprintf(
347+
"ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h",
348+
escapedCoderBinary, rootFlags, flags, coderdConfig.HostnamePrefix,
349+
))
350+
}
462351

463-
// Finally, add the standard options.
464-
err := configOptions.addOptions(defaultOptions...)
465-
if err != nil {
466-
return err
467-
}
352+
// Create a copy of the options so we can modify them.
353+
configOptions := sshConfigOpts
354+
configOptions.sshOptions = nil
468355

469-
hostBlock := []string{
470-
"Host " + sshHostname,
471-
}
472-
// Prefix with '\t'
473-
for _, v := range configOptions.sshOptions {
474-
hostBlock = append(hostBlock, "\t"+v)
475-
}
356+
// User options first (SSH only uses the first
357+
// option unless it can be given multiple times)
358+
for _, opt := range sshConfigOpts.sshOptions {
359+
err := configOptions.addOptions(opt)
360+
if err != nil {
361+
return xerrors.Errorf("add flag config option %q: %w", opt, err)
362+
}
363+
}
476364

477-
_, _ = buf.WriteString(strings.Join(hostBlock, "\n"))
478-
_ = buf.WriteByte('\n')
365+
// Deployment options second, allow them to
366+
// override standard options.
367+
for k, v := range coderdConfig.SSHConfigOptions {
368+
opt := fmt.Sprintf("%s %s", k, v)
369+
err := configOptions.addOptions(opt)
370+
if err != nil {
371+
return xerrors.Errorf("add coderd config option %q: %w", opt, err)
479372
}
480373
}
481374

375+
// Finally, add the standard options.
376+
if err := configOptions.addOptions(defaultOptions...); err != nil {
377+
return err
378+
}
379+
380+
hostBlock := []string{
381+
"Host " + coderdConfig.HostnamePrefix + "*",
382+
}
383+
// Prefix with '\t'
384+
for _, v := range configOptions.sshOptions {
385+
hostBlock = append(hostBlock, "\t"+v)
386+
}
387+
388+
_, _ = buf.WriteString(strings.Join(hostBlock, "\n"))
389+
_ = buf.WriteByte('\n')
390+
482391
sshConfigWriteSectionEnd(buf)
483392

484393
// Write the remainder of the users config file to buf.
@@ -532,9 +441,17 @@ func (r *RootCmd) configSSH() *serpent.Command {
532441
_, _ = fmt.Fprintf(out, "Updated %q\n", sshConfigFile)
533442
}
534443

535-
if len(workspaceConfigs) > 0 {
444+
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
445+
Owner: codersdk.Me,
446+
Limit: 1,
447+
})
448+
if err != nil {
449+
return xerrors.Errorf("fetch workspaces failed: %w", err)
450+
}
451+
452+
if len(res.Workspaces) > 0 {
536453
_, _ = fmt.Fprintln(out, "You should now be able to ssh into your workspace.")
537-
_, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh %s%s\n", coderdConfig.HostnamePrefix, workspaceConfigs[0].Name)
454+
_, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh %s%s\n", coderdConfig.HostnamePrefix, res.Workspaces[0].Name)
538455
} else {
539456
_, _ = fmt.Fprint(out, "You don't have any workspaces yet, try creating one with:\n\n\t$ coder create <workspace>\n")
540457
}

0 commit comments

Comments
 (0)