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

Lines changed: 155 additions & 129 deletions
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)
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