@@ -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
3031const (
@@ -47,13 +48,17 @@ const (
4748type 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 ("\n Match 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.
110187func 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
147224func (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
580622func 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