mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat(cli/configssh): add support for wait yes/no/auto (#7893)
Refs #7768
This commit is contained in:
committed by
GitHub
parent
94aa9be33a
commit
a1c32954d9
@ -45,7 +45,9 @@ const (
|
|||||||
// sshConfigOptions represents options that can be stored and read
|
// sshConfigOptions represents options that can be stored and read
|
||||||
// from the coder config in ~/.ssh/coder.
|
// from the coder config in ~/.ssh/coder.
|
||||||
type sshConfigOptions struct {
|
type sshConfigOptions struct {
|
||||||
sshOptions []string
|
waitEnum string
|
||||||
|
userHostPrefix string
|
||||||
|
sshOptions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// addOptions expects options in the form of "option=value" or "option value".
|
// 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)
|
sort.Strings(opt1)
|
||||||
opt2 := slices.Clone(other.sshOptions)
|
opt2 := slices.Clone(other.sshOptions)
|
||||||
sort.Strings(opt2)
|
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) {
|
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 {
|
for _, opt := range o.sshOptions {
|
||||||
list = append(list, fmt.Sprintf("ssh-option: %s", opt))
|
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 {
|
func (r *RootCmd) configSSH() *clibase.Cmd {
|
||||||
var (
|
var (
|
||||||
sshConfigFile string
|
sshConfigFile string
|
||||||
@ -185,7 +197,6 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
|
|||||||
usePreviousOpts bool
|
usePreviousOpts bool
|
||||||
dryRun bool
|
dryRun bool
|
||||||
skipProxyCommand bool
|
skipProxyCommand bool
|
||||||
userHostPrefix string
|
|
||||||
)
|
)
|
||||||
client := new(codersdk.Client)
|
client := new(codersdk.Client)
|
||||||
cmd := &clibase.Cmd{
|
cmd := &clibase.Cmd{
|
||||||
@ -207,6 +218,10 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
|
|||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
),
|
),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
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)
|
recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(inv.Context(), client)
|
||||||
|
|
||||||
out := inv.Stdout
|
out := inv.Stdout
|
||||||
@ -295,7 +310,7 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
|
|||||||
// Selecting "no" will use the last config.
|
// Selecting "no" will use the last config.
|
||||||
sshConfigOpts = *lastConfig
|
sshConfigOpts = *lastConfig
|
||||||
} else {
|
} else {
|
||||||
changes = append(changes, "Use new SSH options")
|
changes = append(changes, "Use new options")
|
||||||
}
|
}
|
||||||
// Only print when prompts are shown.
|
// Only print when prompts are shown.
|
||||||
if yes, _ := inv.ParsedFlags().GetBool("yes"); !yes {
|
if yes, _ := inv.ParsedFlags().GetBool("yes"); !yes {
|
||||||
@ -336,9 +351,9 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
|
|||||||
coderdConfig.HostnamePrefix = "coder."
|
coderdConfig.HostnamePrefix = "coder."
|
||||||
}
|
}
|
||||||
|
|
||||||
if userHostPrefix != "" {
|
if sshConfigOpts.userHostPrefix != "" {
|
||||||
// Override with user flag.
|
// Override with user flag.
|
||||||
coderdConfig.HostnamePrefix = userHostPrefix
|
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure stable sorting of output.
|
// Ensure stable sorting of output.
|
||||||
@ -363,13 +378,20 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !skipProxyCommand {
|
if !skipProxyCommand {
|
||||||
|
flags := ""
|
||||||
|
if sshConfigOpts.waitEnum != "auto" {
|
||||||
|
flags += " --wait=" + sshConfigOpts.waitEnum
|
||||||
|
}
|
||||||
defaultOptions = append(defaultOptions, fmt.Sprintf(
|
defaultOptions = append(defaultOptions, fmt.Sprintf(
|
||||||
"ProxyCommand %s --global-config %s ssh --stdio %s",
|
"ProxyCommand %s --global-config %s ssh --stdio%s %s",
|
||||||
escapedCoderBinary, escapedGlobalConfig, workspaceHostname,
|
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.
|
// Add standard options.
|
||||||
err := configOptions.addOptions(defaultOptions...)
|
err := configOptions.addOptions(defaultOptions...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -505,9 +527,16 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Flag: "ssh-host-prefix",
|
Flag: "ssh-host-prefix",
|
||||||
Env: "",
|
Env: "CODER_CONFIGSSH_SSH_HOST_PREFIX",
|
||||||
Description: "Override the default 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(),
|
cliui.SkipPromptOption(),
|
||||||
}
|
}
|
||||||
@ -524,12 +553,22 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption
|
|||||||
_, _ = fmt.Fprint(w, nl+sshStartToken+"\n")
|
_, _ = fmt.Fprint(w, nl+sshStartToken+"\n")
|
||||||
_, _ = fmt.Fprint(w, sshConfigSectionHeader)
|
_, _ = fmt.Fprint(w, sshConfigSectionHeader)
|
||||||
_, _ = fmt.Fprint(w, sshConfigDocsHeader)
|
_, _ = fmt.Fprint(w, sshConfigDocsHeader)
|
||||||
if len(o.sshOptions) > 0 {
|
|
||||||
_, _ = fmt.Fprint(w, sshConfigOptionsHeader)
|
var ow strings.Builder
|
||||||
for _, opt := range o.sshOptions {
|
if o.waitEnum != "auto" {
|
||||||
_, _ = fmt.Fprintf(w, "# :%s=%s\n", "ssh-option", opt)
|
_, _ = 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)
|
||||||
|
_, _ = fmt.Fprint(w, ow.String())
|
||||||
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprint(w, "#\n")
|
_, _ = fmt.Fprint(w, "#\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,6 +577,9 @@ func sshConfigWriteSectionEnd(w io.Writer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
|
func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
|
||||||
|
// Default values.
|
||||||
|
o.waitEnum = "auto"
|
||||||
|
|
||||||
s := bufio.NewScanner(r)
|
s := bufio.NewScanner(r)
|
||||||
for s.Scan() {
|
for s.Scan() {
|
||||||
line := s.Text()
|
line := s.Text()
|
||||||
@ -545,6 +587,10 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
|
|||||||
line = strings.TrimPrefix(line, "# :")
|
line = strings.TrimPrefix(line, "# :")
|
||||||
parts := strings.SplitN(line, "=", 2)
|
parts := strings.SplitN(line, "=", 2)
|
||||||
switch parts[0] {
|
switch parts[0] {
|
||||||
|
case "wait":
|
||||||
|
o.waitEnum = parts[1]
|
||||||
|
case "ssh-host-prefix":
|
||||||
|
o.userHostPrefix = parts[1]
|
||||||
case "ssh-option":
|
case "ssh-option":
|
||||||
o.sshOptions = append(o.sshOptions, parts[1])
|
o.sshOptions = append(o.sshOptions, parts[1])
|
||||||
default:
|
default:
|
||||||
|
@ -481,12 +481,32 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
|
|||||||
},
|
},
|
||||||
args: []string{"--yes"},
|
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",
|
name: "Do not prompt for new options when prev opts flag is set",
|
||||||
writeConfig: writeConfig{
|
writeConfig: writeConfig{
|
||||||
ssh: strings.Join([]string{
|
ssh: strings.Join([]string{
|
||||||
headerStart,
|
headerStart,
|
||||||
"# Last config-ssh options:",
|
"# Last config-ssh options:",
|
||||||
|
"# :wait=no",
|
||||||
"# :ssh-option=ForwardAgent=yes",
|
"# :ssh-option=ForwardAgent=yes",
|
||||||
"#",
|
"#",
|
||||||
headerEnd,
|
headerEnd,
|
||||||
@ -497,6 +517,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
|
|||||||
ssh: strings.Join([]string{
|
ssh: strings.Join([]string{
|
||||||
headerStart,
|
headerStart,
|
||||||
"# Last config-ssh options:",
|
"# Last config-ssh options:",
|
||||||
|
"# :wait=no",
|
||||||
"# :ssh-option=ForwardAgent=yes",
|
"# :ssh-option=ForwardAgent=yes",
|
||||||
"#",
|
"#",
|
||||||
headerEnd,
|
headerEnd,
|
||||||
@ -589,8 +610,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
|
|||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
pty := ptytest.New(t)
|
pty := ptytest.New(t)
|
||||||
inv.Stdin = pty.Input()
|
pty.Attach(inv)
|
||||||
inv.Stdout = pty.Output()
|
|
||||||
done := tGo(t, func() {
|
done := tGo(t, func() {
|
||||||
err := inv.Run()
|
err := inv.Run()
|
||||||
if !tt.wantErr {
|
if !tt.wantErr {
|
||||||
|
7
cli/testdata/coder_config-ssh_--help.golden
vendored
7
cli/testdata/coder_config-ssh_--help.golden
vendored
@ -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)
|
--ssh-config-file string, $CODER_SSH_CONFIG_FILE (default: ~/.ssh/config)
|
||||||
Specifies the path to an 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.
|
Override the default host prefix.
|
||||||
|
|
||||||
-o, --ssh-option string-array, $CODER_SSH_CONFIG_OPTS
|
-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
|
Specifies whether or not to keep options from previous run of
|
||||||
config-ssh.
|
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
|
-y, --yes bool
|
||||||
Bypass prompts.
|
Bypass prompts.
|
||||||
|
|
||||||
|
@ -46,9 +46,10 @@ Specifies the path to an SSH config.
|
|||||||
|
|
||||||
### --ssh-host-prefix
|
### --ssh-host-prefix
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
| ---- | ------------------- |
|
| ----------- | --------------------------------------------- |
|
||||||
| Type | <code>string</code> |
|
| Type | <code>string</code> |
|
||||||
|
| Environment | <code>$CODER_CONFIGSSH_SSH_HOST_PREFIX</code> |
|
||||||
|
|
||||||
Override the default 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.
|
Specifies whether or not to keep options from previous run of config-ssh.
|
||||||
|
|
||||||
|
### --wait
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| ----------- | ---------------------------------- | --- | ------------ |
|
||||||
|
| Type | <code>enum[yes | no | auto]</code> |
|
||||||
|
| Environment | <code>$CODER_CONFIGSSH_WAIT</code> |
|
||||||
|
| Default | <code>auto</code> |
|
||||||
|
|
||||||
|
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
|
### -y, --yes
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
Reference in New Issue
Block a user