@@ -48,13 +48,17 @@ const (
48
48
type sshConfigOptions struct {
49
49
waitEnum string
50
50
// 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
58
62
}
59
63
60
64
// addOptions expects options in the form of "option=value" or "option value".
@@ -107,6 +111,80 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
107
111
o .hostnameSuffix == other .hostnameSuffix
108
112
}
109
113
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 , "\n Host *.%s\n " , o .hostnameSuffix )
169
+ for _ , v := range o .sshOptions {
170
+ _ , _ = buf .WriteString ("\t " )
171
+ _ , _ = buf .WriteString (v )
172
+ _ , _ = buf .WriteString ("\n " )
173
+ }
174
+ // the ^^ options should always apply, but we only want to use the proxy command if Coder Connect is not running.
175
+ if ! o .skipProxyCommand {
176
+ _ , _ = fmt .Fprintf (buf , "\n Match host *.%s !exec \" %s connect exists %%h\" \n " ,
177
+ o .hostnameSuffix , escapedCoderBinary )
178
+ _ , _ = buf .WriteString ("\t " )
179
+ _ , _ = fmt .Fprintf (buf ,
180
+ "ProxyCommand %s %s ssh --stdio%s --hostname-suffix %s %%h" ,
181
+ escapedCoderBinary , rootFlags , flags , o .hostnameSuffix ,
182
+ )
183
+ _ , _ = buf .WriteString ("\n " )
184
+ }
185
+ return nil
186
+ }
187
+
110
188
// slicesSortedEqual compares two slices without side-effects or regard to order.
111
189
func slicesSortedEqual [S ~ []E , E constraints.Ordered ](a , b S ) bool {
112
190
if len (a ) != len (b ) {
@@ -147,13 +225,11 @@ func (o sshConfigOptions) asList() (list []string) {
147
225
148
226
func (r * RootCmd ) configSSH () * serpent.Command {
149
227
var (
150
- sshConfigFile string
151
- sshConfigOpts sshConfigOptions
152
- usePreviousOpts bool
153
- dryRun bool
154
- skipProxyCommand bool
155
- forceUnixSeparators bool
156
- coderCliPath string
228
+ sshConfigFile string
229
+ sshConfigOpts sshConfigOptions
230
+ usePreviousOpts bool
231
+ dryRun bool
232
+ coderCliPath string
157
233
)
158
234
client := new (codersdk.Client )
159
235
cmd := & serpent.Command {
@@ -177,7 +253,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
177
253
Handler : func (inv * serpent.Invocation ) error {
178
254
ctx := inv .Context ()
179
255
180
- if sshConfigOpts .waitEnum != "auto" && skipProxyCommand {
256
+ if sshConfigOpts .waitEnum != "auto" && sshConfigOpts . skipProxyCommand {
181
257
// The wait option is applied to the ProxyCommand. If the user
182
258
// specifies skip-proxy-command, then wait cannot be applied.
183
259
return xerrors .Errorf ("cannot specify both --skip-proxy-command and --wait" )
@@ -207,18 +283,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
207
283
return err
208
284
}
209
285
}
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
-
216
286
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
-
222
287
homedir , err := os .UserHomeDir ()
223
288
if err != nil {
224
289
return xerrors .Errorf ("user home dir failed: %w" , err )
@@ -320,94 +385,15 @@ func (r *RootCmd) configSSH() *serpent.Command {
320
385
coderdConfig .HostnamePrefix = "coder."
321
386
}
322
387
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 {
388
+ configOptions , err := mergeSSHOptions (sshConfigOpts , coderdConfig , string (root ), coderBinary )
389
+ if err != nil {
397
390
return err
398
391
}
399
-
400
- hostBlock := []string {
401
- sshConfigHostLinePatterns (coderdConfig ),
402
- }
403
- // Prefix with '\t'
404
- for _ , v := range configOptions .sshOptions {
405
- hostBlock = append (hostBlock , "\t " + v )
392
+ err = configOptions .writeToBuffer (buf )
393
+ if err != nil {
394
+ return err
406
395
}
407
396
408
- _ , _ = buf .WriteString (strings .Join (hostBlock , "\n " ))
409
- _ = buf .WriteByte ('\n' )
410
-
411
397
sshConfigWriteSectionEnd (buf )
412
398
413
399
// Write the remainder of the users config file to buf.
@@ -523,7 +509,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
523
509
Flag : "skip-proxy-command" ,
524
510
Env : "CODER_SSH_SKIP_PROXY_COMMAND" ,
525
511
Description : "Specifies whether the ProxyCommand option should be skipped. Useful for testing." ,
526
- Value : serpent .BoolOf (& skipProxyCommand ),
512
+ Value : serpent .BoolOf (& sshConfigOpts . skipProxyCommand ),
527
513
Hidden : true ,
528
514
},
529
515
{
@@ -564,7 +550,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
564
550
Description : "By default, 'config-ssh' uses the os path separator when writing the ssh config. " +
565
551
"This might be an issue in Windows machine that use a unix-like shell. " +
566
552
"This flag forces the use of unix file paths (the forward slash '/')." ,
567
- Value : serpent .BoolOf (& forceUnixSeparators ),
553
+ Value : serpent .BoolOf (& sshConfigOpts . forceUnixSeparators ),
568
554
// On non-windows showing this command is useless because it is a noop.
569
555
// Hide vs disable it though so if a command is copied from a Windows
570
556
// machine to a unix machine it will still work and not throw an
@@ -577,6 +563,63 @@ func (r *RootCmd) configSSH() *serpent.Command {
577
563
return cmd
578
564
}
579
565
566
+ func mergeSSHOptions (
567
+ user sshConfigOptions , coderd codersdk.SSHConfigResponse , globalConfigPath , coderBinaryPath string ,
568
+ ) (
569
+ sshConfigOptions , error ,
570
+ ) {
571
+ // Write agent configuration.
572
+ defaultOptions := []string {
573
+ "ConnectTimeout=0" ,
574
+ "StrictHostKeyChecking=no" ,
575
+ // Without this, the "REMOTE HOST IDENTITY CHANGED"
576
+ // message will appear.
577
+ "UserKnownHostsFile=/dev/null" ,
578
+ // This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
579
+ // message from appearing on every SSH. This happens because we ignore the known hosts.
580
+ "LogLevel ERROR" ,
581
+ }
582
+
583
+ // Create a copy of the options so we can modify them.
584
+ configOptions := user
585
+ configOptions .sshOptions = nil
586
+
587
+ configOptions .globalConfigPath = globalConfigPath
588
+ configOptions .coderBinaryPath = coderBinaryPath
589
+ // user config takes precedence
590
+ if user .userHostPrefix == "" {
591
+ configOptions .userHostPrefix = coderd .HostnamePrefix
592
+ }
593
+ if user .hostnameSuffix == "" {
594
+ configOptions .hostnameSuffix = coderd .HostnameSuffix
595
+ }
596
+
597
+ // User options first (SSH only uses the first
598
+ // option unless it can be given multiple times)
599
+ for _ , opt := range user .sshOptions {
600
+ err := configOptions .addOptions (opt )
601
+ if err != nil {
602
+ return sshConfigOptions {}, xerrors .Errorf ("add flag config option %q: %w" , opt , err )
603
+ }
604
+ }
605
+
606
+ // Deployment options second, allow them to
607
+ // override standard options.
608
+ for k , v := range coderd .SSHConfigOptions {
609
+ opt := fmt .Sprintf ("%s %s" , k , v )
610
+ err := configOptions .addOptions (opt )
611
+ if err != nil {
612
+ return sshConfigOptions {}, xerrors .Errorf ("add coderd config option %q: %w" , opt , err )
613
+ }
614
+ }
615
+
616
+ // Finally, add the standard options.
617
+ if err := configOptions .addOptions (defaultOptions ... ); err != nil {
618
+ return sshConfigOptions {}, err
619
+ }
620
+ return configOptions , nil
621
+ }
622
+
580
623
//nolint:revive
581
624
func sshConfigWriteSectionHeader (w io.Writer , addNewline bool , o sshConfigOptions ) {
582
625
nl := "\n "
@@ -844,19 +887,3 @@ func diffBytes(name string, b1, b2 []byte, color bool) ([]byte, error) {
844
887
}
845
888
return b , nil
846
889
}
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
- }
0 commit comments