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

Lines changed: 153 additions & 128 deletions
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

Lines changed: 14 additions & 4 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy