Skip to content

Commit a1c3295

Browse files
authored
feat(cli/configssh): add support for wait yes/no/auto (#7893)
Refs #7768
1 parent 94aa9be commit a1c3295

File tree

4 files changed

+103
-21
lines changed

4 files changed

+103
-21
lines changed

cli/configssh.go

+61-15
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ const (
4545
// sshConfigOptions represents options that can be stored and read
4646
// from the coder config in ~/.ssh/coder.
4747
type sshConfigOptions struct {
48-
sshOptions []string
48+
waitEnum string
49+
userHostPrefix string
50+
sshOptions []string
4951
}
5052

5153
// addOptions expects options in the form of "option=value" or "option value".
@@ -100,10 +102,19 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
100102
sort.Strings(opt1)
101103
opt2 := slices.Clone(other.sshOptions)
102104
sort.Strings(opt2)
103-
return slices.Equal(opt1, opt2)
105+
if !slices.Equal(opt1, opt2) {
106+
return false
107+
}
108+
return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix
104109
}
105110

106111
func (o sshConfigOptions) asList() (list []string) {
112+
if o.waitEnum != "auto" {
113+
list = append(list, fmt.Sprintf("wait: %s", o.waitEnum))
114+
}
115+
if o.userHostPrefix != "" {
116+
list = append(list, fmt.Sprintf("ssh-host-prefix: %s", o.userHostPrefix))
117+
}
107118
for _, opt := range o.sshOptions {
108119
list = append(list, fmt.Sprintf("ssh-option: %s", opt))
109120
}
@@ -178,14 +189,14 @@ func sshPrepareWorkspaceConfigs(ctx context.Context, client *codersdk.Client) (r
178189
}
179190
}
180191

192+
//nolint:gocyclo
181193
func (r *RootCmd) configSSH() *clibase.Cmd {
182194
var (
183195
sshConfigFile string
184196
sshConfigOpts sshConfigOptions
185197
usePreviousOpts bool
186198
dryRun bool
187199
skipProxyCommand bool
188-
userHostPrefix string
189200
)
190201
client := new(codersdk.Client)
191202
cmd := &clibase.Cmd{
@@ -207,6 +218,10 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
207218
r.InitClient(client),
208219
),
209220
Handler: func(inv *clibase.Invocation) error {
221+
if sshConfigOpts.waitEnum != "auto" && skipProxyCommand {
222+
return xerrors.Errorf("cannot specify both --skip-proxy-command and --wait")
223+
}
224+
210225
recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(inv.Context(), client)
211226

212227
out := inv.Stdout
@@ -295,7 +310,7 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
295310
// Selecting "no" will use the last config.
296311
sshConfigOpts = *lastConfig
297312
} else {
298-
changes = append(changes, "Use new SSH options")
313+
changes = append(changes, "Use new options")
299314
}
300315
// Only print when prompts are shown.
301316
if yes, _ := inv.ParsedFlags().GetBool("yes"); !yes {
@@ -336,9 +351,9 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
336351
coderdConfig.HostnamePrefix = "coder."
337352
}
338353

339-
if userHostPrefix != "" {
354+
if sshConfigOpts.userHostPrefix != "" {
340355
// Override with user flag.
341-
coderdConfig.HostnamePrefix = userHostPrefix
356+
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
342357
}
343358

344359
// Ensure stable sorting of output.
@@ -363,13 +378,20 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
363378
}
364379

365380
if !skipProxyCommand {
381+
flags := ""
382+
if sshConfigOpts.waitEnum != "auto" {
383+
flags += " --wait=" + sshConfigOpts.waitEnum
384+
}
366385
defaultOptions = append(defaultOptions, fmt.Sprintf(
367-
"ProxyCommand %s --global-config %s ssh --stdio %s",
368-
escapedCoderBinary, escapedGlobalConfig, workspaceHostname,
386+
"ProxyCommand %s --global-config %s ssh --stdio%s %s",
387+
escapedCoderBinary, escapedGlobalConfig, flags, workspaceHostname,
369388
))
370389
}
371390

372-
var configOptions sshConfigOptions
391+
// Create a copy of the options so we can modify them.
392+
configOptions := sshConfigOpts
393+
configOptions.sshOptions = nil
394+
373395
// Add standard options.
374396
err := configOptions.addOptions(defaultOptions...)
375397
if err != nil {
@@ -505,9 +527,16 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
505527
},
506528
{
507529
Flag: "ssh-host-prefix",
508-
Env: "",
530+
Env: "CODER_CONFIGSSH_SSH_HOST_PREFIX",
509531
Description: "Override the default host prefix.",
510-
Value: clibase.StringOf(&userHostPrefix),
532+
Value: clibase.StringOf(&sshConfigOpts.userHostPrefix),
533+
},
534+
{
535+
Flag: "wait",
536+
Env: "CODER_CONFIGSSH_WAIT", // Not to be mixed with CODER_SSH_WAIT.
537+
Description: "Specifies whether or not to wait for the startup script to finish executing. Auto means that the agent startup script behavior configured in the workspace template is used.",
538+
Default: "auto",
539+
Value: clibase.EnumOf(&sshConfigOpts.waitEnum, "yes", "no", "auto"),
511540
},
512541
cliui.SkipPromptOption(),
513542
}
@@ -524,12 +553,22 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption
524553
_, _ = fmt.Fprint(w, nl+sshStartToken+"\n")
525554
_, _ = fmt.Fprint(w, sshConfigSectionHeader)
526555
_, _ = fmt.Fprint(w, sshConfigDocsHeader)
527-
if len(o.sshOptions) > 0 {
556+
557+
var ow strings.Builder
558+
if o.waitEnum != "auto" {
559+
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "wait", o.waitEnum)
560+
}
561+
if o.userHostPrefix != "" {
562+
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-host-prefix", o.userHostPrefix)
563+
}
564+
for _, opt := range o.sshOptions {
565+
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-option", opt)
566+
}
567+
if ow.Len() > 0 {
528568
_, _ = fmt.Fprint(w, sshConfigOptionsHeader)
529-
for _, opt := range o.sshOptions {
530-
_, _ = fmt.Fprintf(w, "# :%s=%s\n", "ssh-option", opt)
531-
}
569+
_, _ = fmt.Fprint(w, ow.String())
532570
}
571+
533572
_, _ = fmt.Fprint(w, "#\n")
534573
}
535574

@@ -538,13 +577,20 @@ func sshConfigWriteSectionEnd(w io.Writer) {
538577
}
539578

540579
func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
580+
// Default values.
581+
o.waitEnum = "auto"
582+
541583
s := bufio.NewScanner(r)
542584
for s.Scan() {
543585
line := s.Text()
544586
if strings.HasPrefix(line, "# :") {
545587
line = strings.TrimPrefix(line, "# :")
546588
parts := strings.SplitN(line, "=", 2)
547589
switch parts[0] {
590+
case "wait":
591+
o.waitEnum = parts[1]
592+
case "ssh-host-prefix":
593+
o.userHostPrefix = parts[1]
548594
case "ssh-option":
549595
o.sshOptions = append(o.sshOptions, parts[1])
550596
default:

cli/configssh_test.go

+22-2
Original file line numberDiff line numberDiff line change
@@ -481,12 +481,32 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
481481
},
482482
args: []string{"--yes"},
483483
},
484+
{
485+
name: "Serialize supported flags",
486+
wantConfig: wantConfig{
487+
ssh: strings.Join([]string{
488+
headerStart,
489+
"# Last config-ssh options:",
490+
"# :wait=yes",
491+
"# :ssh-host-prefix=coder-test.",
492+
"#",
493+
headerEnd,
494+
"",
495+
}, "\n"),
496+
},
497+
args: []string{
498+
"--yes",
499+
"--wait=yes",
500+
"--ssh-host-prefix", "coder-test.",
501+
},
502+
},
484503
{
485504
name: "Do not prompt for new options when prev opts flag is set",
486505
writeConfig: writeConfig{
487506
ssh: strings.Join([]string{
488507
headerStart,
489508
"# Last config-ssh options:",
509+
"# :wait=no",
490510
"# :ssh-option=ForwardAgent=yes",
491511
"#",
492512
headerEnd,
@@ -497,6 +517,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
497517
ssh: strings.Join([]string{
498518
headerStart,
499519
"# Last config-ssh options:",
520+
"# :wait=no",
500521
"# :ssh-option=ForwardAgent=yes",
501522
"#",
502523
headerEnd,
@@ -589,8 +610,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
589610
clitest.SetupConfig(t, client, root)
590611

591612
pty := ptytest.New(t)
592-
inv.Stdin = pty.Input()
593-
inv.Stdout = pty.Output()
613+
pty.Attach(inv)
594614
done := tGo(t, func() {
595615
err := inv.Run()
596616
if !tt.wantErr {

cli/testdata/coder_config-ssh_--help.golden

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Add an SSH Host entry for your workspaces "ssh coder.workspace"
1818
--ssh-config-file string, $CODER_SSH_CONFIG_FILE (default: ~/.ssh/config)
1919
Specifies the path to an SSH config.
2020

21-
--ssh-host-prefix string
21+
--ssh-host-prefix string, $CODER_CONFIGSSH_SSH_HOST_PREFIX
2222
Override the default host prefix.
2323

2424
-o, --ssh-option string-array, $CODER_SSH_CONFIG_OPTS
@@ -28,6 +28,11 @@ Add an SSH Host entry for your workspaces "ssh coder.workspace"
2828
Specifies whether or not to keep options from previous run of
2929
config-ssh.
3030

31+
--wait yes|no|auto, $CODER_CONFIGSSH_WAIT (default: auto)
32+
Specifies whether or not to wait for the startup script to finish
33+
executing. Auto means that the agent startup script behavior
34+
configured in the workspace template is used.
35+
3136
-y, --yes bool
3237
Bypass prompts.
3338

docs/cli/config-ssh.md

+14-3
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ Specifies the path to an SSH config.
4646

4747
### --ssh-host-prefix
4848

49-
| | |
50-
| ---- | ------------------- |
51-
| Type | <code>string</code> |
49+
| | |
50+
| ----------- | --------------------------------------------- |
51+
| Type | <code>string</code> |
52+
| Environment | <code>$CODER_CONFIGSSH_SSH_HOST_PREFIX</code> |
5253

5354
Override the default host prefix.
5455

@@ -70,6 +71,16 @@ Specifies additional SSH options to embed in each host stanza.
7071

7172
Specifies whether or not to keep options from previous run of config-ssh.
7273

74+
### --wait
75+
76+
| | |
77+
| ----------- | ---------------------------------- | --- | ------------ |
78+
| Type | <code>enum[yes | no | auto]</code> |
79+
| Environment | <code>$CODER_CONFIGSSH_WAIT</code> |
80+
| Default | <code>auto</code> |
81+
82+
Specifies whether or not to wait for the startup script to finish executing. Auto means that the agent startup script behavior configured in the workspace template is used.
83+
7384
### -y, --yes
7485

7586
| | |

0 commit comments

Comments
 (0)