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