Skip to content

Commit efdb86b

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

File tree

2 files changed

+169
-133
lines changed

2 files changed

+169
-133
lines changed

cli/configssh.go

+155-129
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import (
2222
"golang.org/x/exp/constraints"
2323
"golang.org/x/xerrors"
2424

25+
"github.com/coder/serpent"
26+
2527
"github.com/coder/coder/v2/cli/cliui"
2628
"github.com/coder/coder/v2/codersdk"
27-
"github.com/coder/serpent"
2829
)
2930

3031
const (
@@ -47,13 +48,17 @@ const (
4748
type sshConfigOptions struct {
4849
waitEnum string
4950
// Deprecated: moving away from prefix to hostnameSuffix
50-
userHostPrefix string
51-
hostnameSuffix string
52-
sshOptions []string
53-
disableAutostart bool
54-
header []string
55-
headerCommand string
56-
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
5762
}
5863

5964
// addOptions expects options in the form of "option=value" or "option value".
@@ -106,6 +111,78 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
106111
o.hostnameSuffix == other.hostnameSuffix
107112
}
108113

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+
_, _ = buf.WriteString(fmt.Sprintf(
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+
_, _ = buf.WriteString(fmt.Sprintf("\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+
_, _ = buf.WriteString(fmt.Sprintf(
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+
109186
// slicesSortedEqual compares two slices without side-effects or regard to order.
110187
func slicesSortedEqual[S ~[]E, E constraints.Ordered](a, b S) bool {
111188
if len(a) != len(b) {
@@ -146,13 +223,11 @@ func (o sshConfigOptions) asList() (list []string) {
146223

147224
func (r *RootCmd) configSSH() *serpent.Command {
148225
var (
149-
sshConfigFile string
150-
sshConfigOpts sshConfigOptions
151-
usePreviousOpts bool
152-
dryRun bool
153-
skipProxyCommand bool
154-
forceUnixSeparators bool
155-
coderCliPath string
226+
sshConfigFile string
227+
sshConfigOpts sshConfigOptions
228+
usePreviousOpts bool
229+
dryRun bool
230+
coderCliPath string
156231
)
157232
client := new(codersdk.Client)
158233
cmd := &serpent.Command{
@@ -176,7 +251,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
176251
Handler: func(inv *serpent.Invocation) error {
177252
ctx := inv.Context()
178253

179-
if sshConfigOpts.waitEnum != "auto" && skipProxyCommand {
254+
if sshConfigOpts.waitEnum != "auto" && sshConfigOpts.skipProxyCommand {
180255
// The wait option is applied to the ProxyCommand. If the user
181256
// specifies skip-proxy-command, then wait cannot be applied.
182257
return xerrors.Errorf("cannot specify both --skip-proxy-command and --wait")
@@ -206,18 +281,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
206281
return err
207282
}
208283
}
209-
210-
escapedCoderBinary, err := sshConfigExecEscape(coderBinary, forceUnixSeparators)
211-
if err != nil {
212-
return xerrors.Errorf("escape coder binary for ssh failed: %w", err)
213-
}
214-
215284
root := r.createConfig()
216-
escapedGlobalConfig, err := sshConfigExecEscape(string(root), forceUnixSeparators)
217-
if err != nil {
218-
return xerrors.Errorf("escape global config for ssh failed: %w", err)
219-
}
220-
221285
homedir, err := os.UserHomeDir()
222286
if err != nil {
223287
return xerrors.Errorf("user home dir failed: %w", err)
@@ -319,94 +383,15 @@ func (r *RootCmd) configSSH() *serpent.Command {
319383
coderdConfig.HostnamePrefix = "coder."
320384
}
321385

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

407-
_, _ = buf.WriteString(strings.Join(hostBlock, "\n"))
408-
_ = buf.WriteByte('\n')
409-
410395
sshConfigWriteSectionEnd(buf)
411396

412397
// Write the remainder of the users config file to buf.
@@ -522,7 +507,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
522507
Flag: "skip-proxy-command",
523508
Env: "CODER_SSH_SKIP_PROXY_COMMAND",
524509
Description: "Specifies whether the ProxyCommand option should be skipped. Useful for testing.",
525-
Value: serpent.BoolOf(&skipProxyCommand),
510+
Value: serpent.BoolOf(&sshConfigOpts.skipProxyCommand),
526511
Hidden: true,
527512
},
528513
{
@@ -563,7 +548,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
563548
Description: "By default, 'config-ssh' uses the os path separator when writing the ssh config. " +
564549
"This might be an issue in Windows machine that use a unix-like shell. " +
565550
"This flag forces the use of unix file paths (the forward slash '/').",
566-
Value: serpent.BoolOf(&forceUnixSeparators),
551+
Value: serpent.BoolOf(&sshConfigOpts.forceUnixSeparators),
567552
// On non-windows showing this command is useless because it is a noop.
568553
// Hide vs disable it though so if a command is copied from a Windows
569554
// machine to a unix machine it will still work and not throw an
@@ -576,6 +561,63 @@ func (r *RootCmd) configSSH() *serpent.Command {
576561
return cmd
577562
}
578563

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+
579621
//nolint:revive
580622
func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOptions) {
581623
nl := "\n"
@@ -843,19 +885,3 @@ func diffBytes(name string, b1, b2 []byte, color bool) ([]byte, error) {
843885
}
844886
return b, nil
845887
}
846-
847-
func sshConfigHostLinePatterns(config codersdk.SSHConfigResponse) string {
848-
builder := strings.Builder{}
849-
// by inspection, WriteString always returns nil error
850-
_, _ = builder.WriteString("Host")
851-
if config.HostnamePrefix != "" {
852-
_, _ = builder.WriteString(" ")
853-
_, _ = builder.WriteString(config.HostnamePrefix)
854-
_, _ = builder.WriteString("*")
855-
}
856-
if config.HostnameSuffix != "" {
857-
_, _ = builder.WriteString(" *.")
858-
_, _ = builder.WriteString(config.HostnameSuffix)
859-
}
860-
return builder.String()
861-
}

0 commit comments

Comments
 (0)