diff --git a/cli/configssh.go b/cli/configssh.go index 92da6cb5f8c0b..6d20205e865bb 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -45,7 +45,9 @@ const ( // sshConfigOptions represents options that can be stored and read // from the coder config in ~/.ssh/coder. type sshConfigOptions struct { - sshOptions []string + waitEnum string + userHostPrefix string + sshOptions []string } // addOptions expects options in the form of "option=value" or "option value". @@ -100,10 +102,19 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool { sort.Strings(opt1) opt2 := slices.Clone(other.sshOptions) sort.Strings(opt2) - return slices.Equal(opt1, opt2) + if !slices.Equal(opt1, opt2) { + return false + } + return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix } func (o sshConfigOptions) asList() (list []string) { + if o.waitEnum != "auto" { + list = append(list, fmt.Sprintf("wait: %s", o.waitEnum)) + } + if o.userHostPrefix != "" { + list = append(list, fmt.Sprintf("ssh-host-prefix: %s", o.userHostPrefix)) + } for _, opt := range o.sshOptions { list = append(list, fmt.Sprintf("ssh-option: %s", opt)) } @@ -178,6 +189,7 @@ func sshPrepareWorkspaceConfigs(ctx context.Context, client *codersdk.Client) (r } } +//nolint:gocyclo func (r *RootCmd) configSSH() *clibase.Cmd { var ( sshConfigFile string @@ -185,7 +197,6 @@ func (r *RootCmd) configSSH() *clibase.Cmd { usePreviousOpts bool dryRun bool skipProxyCommand bool - userHostPrefix string ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -207,6 +218,10 @@ func (r *RootCmd) configSSH() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { + if sshConfigOpts.waitEnum != "auto" && skipProxyCommand { + return xerrors.Errorf("cannot specify both --skip-proxy-command and --wait") + } + recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(inv.Context(), client) out := inv.Stdout @@ -295,7 +310,7 @@ func (r *RootCmd) configSSH() *clibase.Cmd { // Selecting "no" will use the last config. sshConfigOpts = *lastConfig } else { - changes = append(changes, "Use new SSH options") + changes = append(changes, "Use new options") } // Only print when prompts are shown. if yes, _ := inv.ParsedFlags().GetBool("yes"); !yes { @@ -336,9 +351,9 @@ func (r *RootCmd) configSSH() *clibase.Cmd { coderdConfig.HostnamePrefix = "coder." } - if userHostPrefix != "" { + if sshConfigOpts.userHostPrefix != "" { // Override with user flag. - coderdConfig.HostnamePrefix = userHostPrefix + coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix } // Ensure stable sorting of output. @@ -363,13 +378,20 @@ func (r *RootCmd) configSSH() *clibase.Cmd { } if !skipProxyCommand { + flags := "" + if sshConfigOpts.waitEnum != "auto" { + flags += " --wait=" + sshConfigOpts.waitEnum + } defaultOptions = append(defaultOptions, fmt.Sprintf( - "ProxyCommand %s --global-config %s ssh --stdio %s", - escapedCoderBinary, escapedGlobalConfig, workspaceHostname, + "ProxyCommand %s --global-config %s ssh --stdio%s %s", + escapedCoderBinary, escapedGlobalConfig, flags, workspaceHostname, )) } - var configOptions sshConfigOptions + // Create a copy of the options so we can modify them. + configOptions := sshConfigOpts + configOptions.sshOptions = nil + // Add standard options. err := configOptions.addOptions(defaultOptions...) if err != nil { @@ -505,9 +527,16 @@ func (r *RootCmd) configSSH() *clibase.Cmd { }, { Flag: "ssh-host-prefix", - Env: "", + Env: "CODER_CONFIGSSH_SSH_HOST_PREFIX", Description: "Override the default host prefix.", - Value: clibase.StringOf(&userHostPrefix), + Value: clibase.StringOf(&sshConfigOpts.userHostPrefix), + }, + { + Flag: "wait", + Env: "CODER_CONFIGSSH_WAIT", // Not to be mixed with CODER_SSH_WAIT. + 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.", + Default: "auto", + Value: clibase.EnumOf(&sshConfigOpts.waitEnum, "yes", "no", "auto"), }, cliui.SkipPromptOption(), } @@ -524,12 +553,22 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption _, _ = fmt.Fprint(w, nl+sshStartToken+"\n") _, _ = fmt.Fprint(w, sshConfigSectionHeader) _, _ = fmt.Fprint(w, sshConfigDocsHeader) - if len(o.sshOptions) > 0 { + + var ow strings.Builder + if o.waitEnum != "auto" { + _, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "wait", o.waitEnum) + } + if o.userHostPrefix != "" { + _, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-host-prefix", o.userHostPrefix) + } + for _, opt := range o.sshOptions { + _, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-option", opt) + } + if ow.Len() > 0 { _, _ = fmt.Fprint(w, sshConfigOptionsHeader) - for _, opt := range o.sshOptions { - _, _ = fmt.Fprintf(w, "# :%s=%s\n", "ssh-option", opt) - } + _, _ = fmt.Fprint(w, ow.String()) } + _, _ = fmt.Fprint(w, "#\n") } @@ -538,6 +577,9 @@ func sshConfigWriteSectionEnd(w io.Writer) { } func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) { + // Default values. + o.waitEnum = "auto" + s := bufio.NewScanner(r) for s.Scan() { line := s.Text() @@ -545,6 +587,10 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) { line = strings.TrimPrefix(line, "# :") parts := strings.SplitN(line, "=", 2) switch parts[0] { + case "wait": + o.waitEnum = parts[1] + case "ssh-host-prefix": + o.userHostPrefix = parts[1] case "ssh-option": o.sshOptions = append(o.sshOptions, parts[1]) default: diff --git a/cli/configssh_test.go b/cli/configssh_test.go index 1d1ab44de86cd..f502304373f80 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -481,12 +481,32 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { }, args: []string{"--yes"}, }, + { + name: "Serialize supported flags", + wantConfig: wantConfig{ + ssh: strings.Join([]string{ + headerStart, + "# Last config-ssh options:", + "# :wait=yes", + "# :ssh-host-prefix=coder-test.", + "#", + headerEnd, + "", + }, "\n"), + }, + args: []string{ + "--yes", + "--wait=yes", + "--ssh-host-prefix", "coder-test.", + }, + }, { name: "Do not prompt for new options when prev opts flag is set", writeConfig: writeConfig{ ssh: strings.Join([]string{ headerStart, "# Last config-ssh options:", + "# :wait=no", "# :ssh-option=ForwardAgent=yes", "#", headerEnd, @@ -497,6 +517,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { ssh: strings.Join([]string{ headerStart, "# Last config-ssh options:", + "# :wait=no", "# :ssh-option=ForwardAgent=yes", "#", headerEnd, @@ -589,8 +610,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { clitest.SetupConfig(t, client, root) pty := ptytest.New(t) - inv.Stdin = pty.Input() - inv.Stdout = pty.Output() + pty.Attach(inv) done := tGo(t, func() { err := inv.Run() if !tt.wantErr { diff --git a/cli/testdata/coder_config-ssh_--help.golden b/cli/testdata/coder_config-ssh_--help.golden index 299eeb39ddaa6..712c958ad3b88 100644 --- a/cli/testdata/coder_config-ssh_--help.golden +++ b/cli/testdata/coder_config-ssh_--help.golden @@ -18,7 +18,7 @@ Add an SSH Host entry for your workspaces "ssh coder.workspace" --ssh-config-file string, $CODER_SSH_CONFIG_FILE (default: ~/.ssh/config) Specifies the path to an SSH config. - --ssh-host-prefix string + --ssh-host-prefix string, $CODER_CONFIGSSH_SSH_HOST_PREFIX Override the default host prefix. -o, --ssh-option string-array, $CODER_SSH_CONFIG_OPTS @@ -28,6 +28,11 @@ Add an SSH Host entry for your workspaces "ssh coder.workspace" Specifies whether or not to keep options from previous run of config-ssh. + --wait yes|no|auto, $CODER_CONFIGSSH_WAIT (default: auto) + 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. + -y, --yes bool Bypass prompts. diff --git a/docs/cli/config-ssh.md b/docs/cli/config-ssh.md index 9339e9c0d49ff..6178e207e1d30 100644 --- a/docs/cli/config-ssh.md +++ b/docs/cli/config-ssh.md @@ -46,9 +46,10 @@ Specifies the path to an SSH config. ### --ssh-host-prefix -| | | -| ---- | ------------------- | -| Type | string | +| | | +| ----------- | --------------------------------------------- | +| Type | string | +| Environment | $CODER_CONFIGSSH_SSH_HOST_PREFIX | Override the default host prefix. @@ -70,6 +71,16 @@ Specifies additional SSH options to embed in each host stanza. Specifies whether or not to keep options from previous run of config-ssh. +### --wait + +| | | +| ----------- | ---------------------------------- | --- | ------------ | +| Type | enum[yes | no | auto] | +| Environment | $CODER_CONFIGSSH_WAIT | +| Default | auto | + +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. + ### -y, --yes | | |