Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit afbcc03

Browse files
committed
feat: modifies config-ssh to check for Coder Connect
1 parent dc5fab3 commit afbcc03

File tree

2 files changed

+167
-132
lines changed

2 files changed

+167
-132
lines changed

cli/configssh.go

+153-128
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,17 @@ const (
4848
type sshConfigOptions struct {
4949
waitEnum string
5050
// Deprecated: moving away from prefix to hostnameSuffix
51-
userHostPrefix string
52-
hostnameSuffix string
53-
sshOptions []string
54-
disableAutostart bool
55-
header []string
56-
headerCommand string
57-
removedKeys map[string]bool
51+
userHostPrefix string
52+
hostnameSuffix string
53+
sshOptions []string
54+
disableAutostart bool
55+
header []string
56+
headerCommand string
57+
removedKeys map[string]bool
58+
globalConfigPath string
59+
coderBinaryPath string
60+
skipProxyCommand bool
61+
forceUnixSeparators bool
5862
}
5963

6064
// addOptions expects options in the form of "option=value" or "option value".
@@ -107,6 +111,78 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
107111
o.hostnameSuffix == other.hostnameSuffix
108112
}
109113

114+
func (o sshConfigOptions) writeToBuffer(buf *bytes.Buffer) error {
115+
escapedCoderBinary, err := sshConfigExecEscape(o.coderBinaryPath, o.forceUnixSeparators)
116+
if err != nil {
117+
return xerrors.Errorf("escape coder binary for ssh failed: %w", err)
118+
}
119+
120+
escapedGlobalConfig, err := sshConfigExecEscape(o.globalConfigPath, o.forceUnixSeparators)
121+
if err != nil {
122+
return xerrors.Errorf("escape global config for ssh failed: %w", err)
123+
}
124+
125+
rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig)
126+
for _, h := range o.header {
127+
rootFlags += fmt.Sprintf(" --header %q", h)
128+
}
129+
if o.headerCommand != "" {
130+
rootFlags += fmt.Sprintf(" --header-command %q", o.headerCommand)
131+
}
132+
133+
flags := ""
134+
if o.waitEnum != "auto" {
135+
flags += " --wait=" + o.waitEnum
136+
}
137+
if o.disableAutostart {
138+
flags += " --disable-autostart=true"
139+
}
140+
141+
// Prefix block:
142+
if o.userHostPrefix != "" {
143+
_, _ = buf.WriteString("Host")
144+
145+
_, _ = buf.WriteString(" ")
146+
_, _ = buf.WriteString(o.userHostPrefix)
147+
_, _ = buf.WriteString("*\n")
148+
149+
for _, v := range o.sshOptions {
150+
_, _ = buf.WriteString("\t")
151+
_, _ = buf.WriteString(v)
152+
_, _ = buf.WriteString("\n")
153+
}
154+
if !o.skipProxyCommand && o.userHostPrefix != "" {
155+
_, _ = buf.WriteString("\t")
156+
_, _ = fmt.Fprintf(buf,
157+
"ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h",
158+
escapedCoderBinary, rootFlags, flags, o.userHostPrefix,
159+
)
160+
_, _ = buf.WriteString("\n")
161+
}
162+
}
163+
164+
// Suffix block
165+
if o.hostnameSuffix == "" {
166+
return nil
167+
}
168+
_, _ = fmt.Fprintf(buf, "\nMatch host *.%s !exec \"%s connect exists %%h\"\n",
169+
o.hostnameSuffix, escapedCoderBinary)
170+
for _, v := range o.sshOptions {
171+
_, _ = buf.WriteString("\t")
172+
_, _ = buf.WriteString(v)
173+
_, _ = buf.WriteString("\n")
174+
}
175+
if !o.skipProxyCommand {
176+
_, _ = buf.WriteString("\t")
177+
_, _ = fmt.Fprintf(buf,
178+
"ProxyCommand %s %s ssh --stdio%s --hostname-suffix %s %%h",
179+
escapedCoderBinary, rootFlags, flags, o.hostnameSuffix,
180+
)
181+
_, _ = buf.WriteString("\n")
182+
}
183+
return nil
184+
}
185+
110186
// slicesSortedEqual compares two slices without side-effects or regard to order.
111187
func slicesSortedEqual[S ~[]E, E constraints.Ordered](a, b S) bool {
112188
if len(a) != len(b) {
@@ -147,13 +223,11 @@ func (o sshConfigOptions) asList() (list []string) {
147223

148224
func (r *RootCmd) configSSH() *serpent.Command {
149225
var (
150-
sshConfigFile string
151-
sshConfigOpts sshConfigOptions
152-
usePreviousOpts bool
153-
dryRun bool
154-
skipProxyCommand bool
155-
forceUnixSeparators bool
156-
coderCliPath string
226+
sshConfigFile string
227+
sshConfigOpts sshConfigOptions
228+
usePreviousOpts bool
229+
dryRun bool
230+
coderCliPath string
157231
)
158232
client := new(codersdk.Client)
159233
cmd := &serpent.Command{
@@ -177,7 +251,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
177251
Handler: func(inv *serpent.Invocation) error {
178252
ctx := inv.Context()
179253

180-
if sshConfigOpts.waitEnum != "auto" && skipProxyCommand {
254+
if sshConfigOpts.waitEnum != "auto" && sshConfigOpts.skipProxyCommand {
181255
// The wait option is applied to the ProxyCommand. If the user
182256
// specifies skip-proxy-command, then wait cannot be applied.
183257
return xerrors.Errorf("cannot specify both --skip-proxy-command and --wait")
@@ -207,18 +281,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
207281
return err
208282
}
209283
}
210-
211-
escapedCoderBinary, err := sshConfigExecEscape(coderBinary, forceUnixSeparators)
212-
if err != nil {
213-
return xerrors.Errorf("escape coder binary for ssh failed: %w", err)
214-
}
215-
216284
root := r.createConfig()
217-
escapedGlobalConfig, err := sshConfigExecEscape(string(root), forceUnixSeparators)
218-
if err != nil {
219-
return xerrors.Errorf("escape global config for ssh failed: %w", err)
220-
}
221-
222285
homedir, err := os.UserHomeDir()
223286
if err != nil {
224287
return xerrors.Errorf("user home dir failed: %w", err)
@@ -320,94 +383,15 @@ func (r *RootCmd) configSSH() *serpent.Command {
320383
coderdConfig.HostnamePrefix = "coder."
321384
}
322385

323-
if sshConfigOpts.userHostPrefix != "" {
324-
// Override with user flag.
325-
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
326-
}
327-
if sshConfigOpts.hostnameSuffix != "" {
328-
// Override with user flag.
329-
coderdConfig.HostnameSuffix = sshConfigOpts.hostnameSuffix
330-
}
331-
332-
// Write agent configuration.
333-
defaultOptions := []string{
334-
"ConnectTimeout=0",
335-
"StrictHostKeyChecking=no",
336-
// Without this, the "REMOTE HOST IDENTITY CHANGED"
337-
// message will appear.
338-
"UserKnownHostsFile=/dev/null",
339-
// This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
340-
// message from appearing on every SSH. This happens because we ignore the known hosts.
341-
"LogLevel ERROR",
342-
}
343-
344-
if !skipProxyCommand {
345-
rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig)
346-
for _, h := range sshConfigOpts.header {
347-
rootFlags += fmt.Sprintf(" --header %q", h)
348-
}
349-
if sshConfigOpts.headerCommand != "" {
350-
rootFlags += fmt.Sprintf(" --header-command %q", sshConfigOpts.headerCommand)
351-
}
352-
353-
flags := ""
354-
if sshConfigOpts.waitEnum != "auto" {
355-
flags += " --wait=" + sshConfigOpts.waitEnum
356-
}
357-
if sshConfigOpts.disableAutostart {
358-
flags += " --disable-autostart=true"
359-
}
360-
if coderdConfig.HostnamePrefix != "" {
361-
flags += " --ssh-host-prefix " + coderdConfig.HostnamePrefix
362-
}
363-
if coderdConfig.HostnameSuffix != "" {
364-
flags += " --hostname-suffix " + coderdConfig.HostnameSuffix
365-
}
366-
defaultOptions = append(defaultOptions, fmt.Sprintf(
367-
"ProxyCommand %s %s ssh --stdio%s %%h",
368-
escapedCoderBinary, rootFlags, flags,
369-
))
370-
}
371-
372-
// Create a copy of the options so we can modify them.
373-
configOptions := sshConfigOpts
374-
configOptions.sshOptions = nil
375-
376-
// User options first (SSH only uses the first
377-
// option unless it can be given multiple times)
378-
for _, opt := range sshConfigOpts.sshOptions {
379-
err := configOptions.addOptions(opt)
380-
if err != nil {
381-
return xerrors.Errorf("add flag config option %q: %w", opt, err)
382-
}
383-
}
384-
385-
// Deployment options second, allow them to
386-
// override standard options.
387-
for k, v := range coderdConfig.SSHConfigOptions {
388-
opt := fmt.Sprintf("%s %s", k, v)
389-
err := configOptions.addOptions(opt)
390-
if err != nil {
391-
return xerrors.Errorf("add coderd config option %q: %w", opt, err)
392-
}
393-
}
394-
395-
// Finally, add the standard options.
396-
if err := configOptions.addOptions(defaultOptions...); err != nil {
386+
configOptions, err := mergeSSHOptions(sshConfigOpts, coderdConfig, string(root), coderBinary)
387+
if err != nil {
397388
return err
398389
}
399-
400-
hostBlock := []string{
401-
sshConfigHostLinePatterns(coderdConfig),
402-
}
403-
// Prefix with '\t'
404-
for _, v := range configOptions.sshOptions {
405-
hostBlock = append(hostBlock, "\t"+v)
390+
err = configOptions.writeToBuffer(buf)
391+
if err != nil {
392+
return err
406393
}
407394

408-
_, _ = buf.WriteString(strings.Join(hostBlock, "\n"))
409-
_ = buf.WriteByte('\n')
410-
411395
sshConfigWriteSectionEnd(buf)
412396

413397
// Write the remainder of the users config file to buf.
@@ -523,7 +507,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
523507
Flag: "skip-proxy-command",
524508
Env: "CODER_SSH_SKIP_PROXY_COMMAND",
525509
Description: "Specifies whether the ProxyCommand option should be skipped. Useful for testing.",
526-
Value: serpent.BoolOf(&skipProxyCommand),
510+
Value: serpent.BoolOf(&sshConfigOpts.skipProxyCommand),
527511
Hidden: true,
528512
},
529513
{
@@ -564,7 +548,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
564548
Description: "By default, 'config-ssh' uses the os path separator when writing the ssh config. " +
565549
"This might be an issue in Windows machine that use a unix-like shell. " +
566550
"This flag forces the use of unix file paths (the forward slash '/').",
567-
Value: serpent.BoolOf(&forceUnixSeparators),
551+
Value: serpent.BoolOf(&sshConfigOpts.forceUnixSeparators),
568552
// On non-windows showing this command is useless because it is a noop.
569553
// Hide vs disable it though so if a command is copied from a Windows
570554
// machine to a unix machine it will still work and not throw an
@@ -577,6 +561,63 @@ func (r *RootCmd) configSSH() *serpent.Command {
577561
return cmd
578562
}
579563

564+
func mergeSSHOptions(
565+
user sshConfigOptions, coderd codersdk.SSHConfigResponse, globalConfigPath, coderBinaryPath string,
566+
) (
567+
sshConfigOptions, error,
568+
) {
569+
// Write agent configuration.
570+
defaultOptions := []string{
571+
"ConnectTimeout=0",
572+
"StrictHostKeyChecking=no",
573+
// Without this, the "REMOTE HOST IDENTITY CHANGED"
574+
// message will appear.
575+
"UserKnownHostsFile=/dev/null",
576+
// This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
577+
// message from appearing on every SSH. This happens because we ignore the known hosts.
578+
"LogLevel ERROR",
579+
}
580+
581+
// Create a copy of the options so we can modify them.
582+
configOptions := user
583+
configOptions.sshOptions = nil
584+
585+
configOptions.globalConfigPath = globalConfigPath
586+
configOptions.coderBinaryPath = coderBinaryPath
587+
// user config takes precedence
588+
if user.userHostPrefix == "" {
589+
configOptions.userHostPrefix = coderd.HostnamePrefix
590+
}
591+
if user.hostnameSuffix == "" {
592+
configOptions.hostnameSuffix = coderd.HostnameSuffix
593+
}
594+
595+
// User options first (SSH only uses the first
596+
// option unless it can be given multiple times)
597+
for _, opt := range user.sshOptions {
598+
err := configOptions.addOptions(opt)
599+
if err != nil {
600+
return sshConfigOptions{}, xerrors.Errorf("add flag config option %q: %w", opt, err)
601+
}
602+
}
603+
604+
// Deployment options second, allow them to
605+
// override standard options.
606+
for k, v := range coderd.SSHConfigOptions {
607+
opt := fmt.Sprintf("%s %s", k, v)
608+
err := configOptions.addOptions(opt)
609+
if err != nil {
610+
return sshConfigOptions{}, xerrors.Errorf("add coderd config option %q: %w", opt, err)
611+
}
612+
}
613+
614+
// Finally, add the standard options.
615+
if err := configOptions.addOptions(defaultOptions...); err != nil {
616+
return sshConfigOptions{}, err
617+
}
618+
return configOptions, nil
619+
}
620+
580621
//nolint:revive
581622
func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOptions) {
582623
nl := "\n"
@@ -844,19 +885,3 @@ func diffBytes(name string, b1, b2 []byte, color bool) ([]byte, error) {
844885
}
845886
return b, nil
846887
}
847-
848-
func sshConfigHostLinePatterns(config codersdk.SSHConfigResponse) string {
849-
builder := strings.Builder{}
850-
// by inspection, WriteString always returns nil error
851-
_, _ = builder.WriteString("Host")
852-
if config.HostnamePrefix != "" {
853-
_, _ = builder.WriteString(" ")
854-
_, _ = builder.WriteString(config.HostnamePrefix)
855-
_, _ = builder.WriteString("*")
856-
}
857-
if config.HostnameSuffix != "" {
858-
_, _ = builder.WriteString(" *.")
859-
_, _ = builder.WriteString(config.HostnameSuffix)
860-
}
861-
return builder.String()
862-
}

cli/configssh_test.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -612,18 +612,29 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
612612
},
613613
},
614614
{
615-
name: "Hostname Suffix",
615+
name: "Hostname Suffix ProxyCommand",
616616
args: []string{
617617
"--yes",
618618
"--hostname-suffix", "testy",
619619
},
620620
wantErr: false,
621621
hasAgent: true,
622622
wantConfig: wantConfig{
623-
ssh: []string{"Host coder.* *.testy"},
624623
regexMatch: `ProxyCommand .* ssh .* --hostname-suffix testy %h`,
625624
},
626625
},
626+
{
627+
name: "Hostname Suffix Match",
628+
args: []string{
629+
"--yes",
630+
"--hostname-suffix", "testy",
631+
},
632+
wantErr: false,
633+
hasAgent: true,
634+
wantConfig: wantConfig{
635+
regexMatch: `Match host \*\.testy !exec ".* connect exists %h"`,
636+
},
637+
},
627638
{
628639
name: "Hostname Prefix and Suffix",
629640
args: []string{
@@ -634,8 +645,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
634645
wantErr: false,
635646
hasAgent: true,
636647
wantConfig: wantConfig{
637-
ssh: []string{"Host presto.* *.testy"},
638-
regexMatch: `ProxyCommand .* ssh .* --ssh-host-prefix presto\. --hostname-suffix testy %h`,
648+
ssh: []string{"Host presto.*", "Match host *.testy !exec"},
639649
},
640650
},
641651
}

0 commit comments

Comments
 (0)