@@ -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,78 @@ 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 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
+ _ , _ = 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
+
110
186
// slicesSortedEqual compares two slices without side-effects or regard to order.
111
187
func slicesSortedEqual [S ~ []E , E constraints.Ordered ](a , b S ) bool {
112
188
if len (a ) != len (b ) {
@@ -147,13 +223,11 @@ func (o sshConfigOptions) asList() (list []string) {
147
223
148
224
func (r * RootCmd ) configSSH () * serpent.Command {
149
225
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
157
231
)
158
232
client := new (codersdk.Client )
159
233
cmd := & serpent.Command {
@@ -177,7 +251,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
177
251
Handler : func (inv * serpent.Invocation ) error {
178
252
ctx := inv .Context ()
179
253
180
- if sshConfigOpts .waitEnum != "auto" && skipProxyCommand {
254
+ if sshConfigOpts .waitEnum != "auto" && sshConfigOpts . skipProxyCommand {
181
255
// The wait option is applied to the ProxyCommand. If the user
182
256
// specifies skip-proxy-command, then wait cannot be applied.
183
257
return xerrors .Errorf ("cannot specify both --skip-proxy-command and --wait" )
@@ -207,18 +281,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
207
281
return err
208
282
}
209
283
}
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
284
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
285
homedir , err := os .UserHomeDir ()
223
286
if err != nil {
224
287
return xerrors .Errorf ("user home dir failed: %w" , err )
@@ -320,94 +383,15 @@ func (r *RootCmd) configSSH() *serpent.Command {
320
383
coderdConfig .HostnamePrefix = "coder."
321
384
}
322
385
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 {
397
388
return err
398
389
}
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
406
393
}
407
394
408
- _ , _ = buf .WriteString (strings .Join (hostBlock , "\n " ))
409
- _ = buf .WriteByte ('\n' )
410
-
411
395
sshConfigWriteSectionEnd (buf )
412
396
413
397
// Write the remainder of the users config file to buf.
@@ -523,7 +507,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
523
507
Flag : "skip-proxy-command" ,
524
508
Env : "CODER_SSH_SKIP_PROXY_COMMAND" ,
525
509
Description : "Specifies whether the ProxyCommand option should be skipped. Useful for testing." ,
526
- Value : serpent .BoolOf (& skipProxyCommand ),
510
+ Value : serpent .BoolOf (& sshConfigOpts . skipProxyCommand ),
527
511
Hidden : true ,
528
512
},
529
513
{
@@ -564,7 +548,7 @@ func (r *RootCmd) configSSH() *serpent.Command {
564
548
Description : "By default, 'config-ssh' uses the os path separator when writing the ssh config. " +
565
549
"This might be an issue in Windows machine that use a unix-like shell. " +
566
550
"This flag forces the use of unix file paths (the forward slash '/')." ,
567
- Value : serpent .BoolOf (& forceUnixSeparators ),
551
+ Value : serpent .BoolOf (& sshConfigOpts . forceUnixSeparators ),
568
552
// On non-windows showing this command is useless because it is a noop.
569
553
// Hide vs disable it though so if a command is copied from a Windows
570
554
// machine to a unix machine it will still work and not throw an
@@ -577,6 +561,63 @@ func (r *RootCmd) configSSH() *serpent.Command {
577
561
return cmd
578
562
}
579
563
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
+
580
621
//nolint:revive
581
622
func sshConfigWriteSectionHeader (w io.Writer , addNewline bool , o sshConfigOptions ) {
582
623
nl := "\n "
@@ -844,19 +885,3 @@ func diffBytes(name string, b1, b2 []byte, color bool) ([]byte, error) {
844
885
}
845
886
return b , nil
846
887
}
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