8
8
"fmt"
9
9
"io"
10
10
"io/fs"
11
+ "net/http"
11
12
"os"
12
13
"path/filepath"
13
14
"runtime"
@@ -48,6 +49,43 @@ type sshConfigOptions struct {
48
49
sshOptions []string
49
50
}
50
51
52
+ // addOptions expects options in the form of "option=value" or "option value".
53
+ // It will override any existing option with the same key to prevent duplicates.
54
+ // Invalid options will return an error.
55
+ func (o * sshConfigOptions ) addOptions (options ... string ) error {
56
+ for _ , option := range options {
57
+ err := o .addOption (option )
58
+ if err != nil {
59
+ return err
60
+ }
61
+ }
62
+ return nil
63
+ }
64
+
65
+ func (o * sshConfigOptions ) addOption (option string ) error {
66
+ key , _ , err := codersdk .ParseSSHConfigOption (option )
67
+ if err != nil {
68
+ return err
69
+ }
70
+ for i , existing := range o .sshOptions {
71
+ // Override existing option if they share the same key.
72
+ // This is case-insensitive. Parsing each time might be a little slow,
73
+ // but it is ok.
74
+ existingKey , _ , err := codersdk .ParseSSHConfigOption (existing )
75
+ if err != nil {
76
+ // Don't mess with original values if there is an error.
77
+ // This could have come from the user's manual edits.
78
+ continue
79
+ }
80
+ if strings .EqualFold (existingKey , key ) {
81
+ o .sshOptions [i ] = option
82
+ return nil
83
+ }
84
+ }
85
+ o .sshOptions = append (o .sshOptions , option )
86
+ return nil
87
+ }
88
+
51
89
func (o sshConfigOptions ) equal (other sshConfigOptions ) bool {
52
90
// Compare without side-effects or regard to order.
53
91
opt1 := slices .Clone (o .sshOptions )
@@ -139,6 +177,7 @@ func configSSH() *cobra.Command {
139
177
usePreviousOpts bool
140
178
dryRun bool
141
179
skipProxyCommand bool
180
+ userHostPrefix string
142
181
)
143
182
cmd := & cobra.Command {
144
183
Annotations : workspaceCommand ,
@@ -156,12 +195,13 @@ func configSSH() *cobra.Command {
156
195
),
157
196
Args : cobra .ExactArgs (0 ),
158
197
RunE : func (cmd * cobra.Command , _ []string ) error {
198
+ ctx := cmd .Context ()
159
199
client , err := CreateClient (cmd )
160
200
if err != nil {
161
201
return err
162
202
}
163
203
164
- recvWorkspaceConfigs := sshPrepareWorkspaceConfigs (cmd . Context () , client )
204
+ recvWorkspaceConfigs := sshPrepareWorkspaceConfigs (ctx , client )
165
205
166
206
out := cmd .OutOrStdout ()
167
207
if dryRun {
@@ -220,6 +260,13 @@ func configSSH() *cobra.Command {
220
260
if usePreviousOpts && lastConfig != nil {
221
261
sshConfigOpts = * lastConfig
222
262
} else if lastConfig != nil && ! sshConfigOpts .equal (* lastConfig ) {
263
+ for _ , v := range sshConfigOpts .sshOptions {
264
+ // If the user passes an invalid option, we should catch
265
+ // this early.
266
+ if _ , _ , err := codersdk .ParseSSHConfigOption (v ); err != nil {
267
+ return xerrors .Errorf ("invalid option from flag: %w" , err )
268
+ }
269
+ }
223
270
newOpts := sshConfigOpts .asList ()
224
271
newOptsMsg := "\n \n New options: none"
225
272
if len (newOpts ) > 0 {
@@ -269,42 +316,85 @@ func configSSH() *cobra.Command {
269
316
if err != nil {
270
317
return xerrors .Errorf ("fetch workspace configs failed: %w" , err )
271
318
}
319
+
320
+ coderdConfig , err := client .SSHConfiguration (ctx )
321
+ if err != nil {
322
+ // If the error is 404, this deployment does not support
323
+ // this endpoint yet. Do not error, just assume defaults.
324
+ // TODO: Remove this in 2 months (May 31, 2023). Just return the error
325
+ // and remove this 404 check.
326
+ var sdkErr * codersdk.Error
327
+ if ! (xerrors .As (err , & sdkErr ) && sdkErr .StatusCode () == http .StatusNotFound ) {
328
+ return xerrors .Errorf ("fetch coderd config failed: %w" , err )
329
+ }
330
+ coderdConfig .HostnamePrefix = "coder."
331
+ }
332
+
333
+ if userHostPrefix != "" {
334
+ // Override with user flag.
335
+ coderdConfig .HostnamePrefix = userHostPrefix
336
+ }
337
+
272
338
// Ensure stable sorting of output.
273
339
slices .SortFunc (workspaceConfigs , func (a , b sshWorkspaceConfig ) bool {
274
340
return a .Name < b .Name
275
341
})
276
342
for _ , wc := range workspaceConfigs {
277
343
sort .Strings (wc .Hosts )
278
344
// Write agent configuration.
279
- for _ , hostname := range wc .Hosts {
280
- configOptions := []string {
281
- "Host coder." + hostname ,
282
- }
283
- for _ , option := range sshConfigOpts .sshOptions {
284
- configOptions = append (configOptions , "\t " + option )
285
- }
286
- configOptions = append (configOptions ,
287
- "\t HostName coder." + hostname ,
288
- "\t ConnectTimeout=0" ,
289
- "\t StrictHostKeyChecking=no" ,
345
+ for _ , workspaceHostname := range wc .Hosts {
346
+ sshHostname := fmt .Sprintf ("%s%s" , coderdConfig .HostnamePrefix , workspaceHostname )
347
+ defaultOptions := []string {
348
+ "HostName " + sshHostname ,
349
+ "ConnectTimeout=0" ,
350
+ "StrictHostKeyChecking=no" ,
290
351
// Without this, the "REMOTE HOST IDENTITY CHANGED"
291
352
// message will appear.
292
- "\t UserKnownHostsFile =/dev/null" ,
353
+ "UserKnownHostsFile =/dev/null" ,
293
354
// This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
294
355
// message from appearing on every SSH. This happens because we ignore the known hosts.
295
- "\t LogLevel ERROR" ,
296
- )
356
+ "LogLevel ERROR" ,
357
+ }
358
+
297
359
if ! skipProxyCommand {
298
- configOptions = append (
299
- configOptions ,
300
- fmt .Sprintf (
301
- "\t ProxyCommand %s --global-config %s ssh --stdio %s" ,
302
- escapedCoderBinary , escapedGlobalConfig , hostname ,
303
- ),
304
- )
360
+ defaultOptions = append (defaultOptions , fmt .Sprintf (
361
+ "ProxyCommand %s --global-config %s ssh --stdio %s" ,
362
+ escapedCoderBinary , escapedGlobalConfig , workspaceHostname ,
363
+ ))
364
+ }
365
+
366
+ var configOptions sshConfigOptions
367
+ // Add standard options.
368
+ err := configOptions .addOptions (defaultOptions ... )
369
+ if err != nil {
370
+ return err
371
+ }
372
+
373
+ // Override with deployment options
374
+ for k , v := range coderdConfig .SSHConfigOptions {
375
+ opt := fmt .Sprintf ("%s %s" , k , v )
376
+ err := configOptions .addOptions (opt )
377
+ if err != nil {
378
+ return xerrors .Errorf ("add coderd config option %q: %w" , opt , err )
379
+ }
380
+ }
381
+ // Override with flag options
382
+ for _ , opt := range sshConfigOpts .sshOptions {
383
+ err := configOptions .addOptions (opt )
384
+ if err != nil {
385
+ return xerrors .Errorf ("add flag config option %q: %w" , opt , err )
386
+ }
387
+ }
388
+
389
+ hostBlock := []string {
390
+ "Host " + sshHostname ,
391
+ }
392
+ // Prefix with '\t'
393
+ for _ , v := range configOptions .sshOptions {
394
+ hostBlock = append (hostBlock , "\t " + v )
305
395
}
306
396
307
- _ , _ = buf .WriteString (strings .Join (configOptions , "\n " ))
397
+ _ , _ = buf .WriteString (strings .Join (hostBlock , "\n " ))
308
398
_ = buf .WriteByte ('\n' )
309
399
}
310
400
}
@@ -363,7 +453,7 @@ func configSSH() *cobra.Command {
363
453
364
454
if len (workspaceConfigs ) > 0 {
365
455
_ , _ = fmt .Fprintln (out , "You should now be able to ssh into your workspace." )
366
- _ , _ = fmt .Fprintf (out , "For example, try running:\n \n \t $ ssh coder.%s \n " , workspaceConfigs [0 ].Name )
456
+ _ , _ = fmt .Fprintf (out , "For example, try running:\n \n \t $ ssh %s%s \n " , coderdConfig . HostnamePrefix , workspaceConfigs [0 ].Name )
367
457
} else {
368
458
_ , _ = fmt .Fprint (out , "You don't have any workspaces yet, try creating one with:\n \n \t $ coder create <workspace>\n " )
369
459
}
@@ -376,6 +466,7 @@ func configSSH() *cobra.Command {
376
466
cmd .Flags ().BoolVarP (& skipProxyCommand , "skip-proxy-command" , "" , false , "Specifies whether the ProxyCommand option should be skipped. Useful for testing." )
377
467
_ = cmd .Flags ().MarkHidden ("skip-proxy-command" )
378
468
cliflag .BoolVarP (cmd .Flags (), & usePreviousOpts , "use-previous-options" , "" , "CODER_SSH_USE_PREVIOUS_OPTIONS" , false , "Specifies whether or not to keep options from previous run of config-ssh." )
469
+ cmd .Flags ().StringVarP (& userHostPrefix , "ssh-host-prefix" , "" , "" , "Override the default host prefix." )
379
470
cliui .AllowSkipPrompt (cmd )
380
471
381
472
return cmd
0 commit comments