1
1
package cli
2
2
3
3
import (
4
+ "bufio"
4
5
"bytes"
5
6
"errors"
6
7
"fmt"
@@ -33,9 +34,9 @@ const (
33
34
sshCoderConfigDocsHeader = `
34
35
#
35
36
# You should not hand-edit this file, all changes will be lost upon workspace
36
- # creation, deletion or when running "coder config-ssh".
37
- `
38
- sshCoderConfigOptionsHeader = ` #
37
+ # creation, deletion or when running "coder config-ssh".`
38
+ sshCoderConfigOptionsHeader = `
39
+ #
39
40
# Last config-ssh options:
40
41
`
41
42
// Relative paths are assumed to be in ~/.ssh, except when
@@ -53,11 +54,50 @@ var (
53
54
sshCoderIncludedRe = regexp .MustCompile (`^\s*((?i)Include) coder(\s|$)` )
54
55
)
55
56
57
+ // sshCoderConfigOptions represents options that can be stored and read
58
+ // from the coder config in ~/.ssh/coder.
59
+ type sshCoderConfigOptions struct {
60
+ sshConfigFile string
61
+ sshOptions []string
62
+ }
63
+
64
+ func (o sshCoderConfigOptions ) isZero () bool {
65
+ return o .sshConfigFile == sshDefaultConfigFileName && len (o .sshOptions ) == 0
66
+ }
67
+
68
+ func (o sshCoderConfigOptions ) equal (other sshCoderConfigOptions ) bool {
69
+ // Compare without side-effects or regard to order.
70
+ opt1 := slices .Clone (o .sshOptions )
71
+ sort .Strings (opt1 )
72
+ opt2 := slices .Clone (other .sshOptions )
73
+ sort .Strings (opt2 )
74
+ return o .sshConfigFile == other .sshConfigFile && slices .Equal (opt1 , opt2 )
75
+ }
76
+
77
+ func (o sshCoderConfigOptions ) asArgs () (args []string ) {
78
+ if o .sshConfigFile != sshDefaultConfigFileName {
79
+ args = append (args , "--ssh-config-file" , o .sshConfigFile )
80
+ }
81
+ for _ , opt := range o .sshOptions {
82
+ args = append (args , "--ssh-option" , fmt .Sprintf ("%q" , opt ))
83
+ }
84
+ return args
85
+ }
86
+
87
+ func (o sshCoderConfigOptions ) asList () (list []string ) {
88
+ if o .sshConfigFile != sshDefaultConfigFileName {
89
+ list = append (list , fmt .Sprintf ("ssh-config-file: %s" , o .sshConfigFile ))
90
+ }
91
+ for _ , opt := range o .sshOptions {
92
+ list = append (list , fmt .Sprintf ("ssh-option: %s" , opt ))
93
+ }
94
+ return list
95
+ }
96
+
56
97
func configSSH () * cobra.Command {
57
98
var (
58
- sshConfigFile string
99
+ coderConfig sshCoderConfigOptions
59
100
coderConfigFile string
60
- sshOptions []string
61
101
showDiff bool
62
102
skipProxyCommand bool
63
103
@@ -97,30 +137,26 @@ func configSSH() *cobra.Command {
97
137
return err
98
138
}
99
139
140
+ out := cmd .OutOrStdout ()
141
+ if showDiff {
142
+ out = cmd .OutOrStderr ()
143
+ }
144
+ binaryFile , err := currentBinPath (out )
145
+ if err != nil {
146
+ return err
147
+ }
148
+
100
149
dirname , err := os .UserHomeDir ()
101
150
if err != nil {
102
151
return xerrors .Errorf ("user home dir failed: %w" , err )
103
152
}
104
153
105
- sshConfigFileOrig := sshConfigFile // Store the pre ~/ replacement name for serializing options.
154
+ sshConfigFile := coderConfig . sshConfigFile // Store the pre ~/ replacement name for serializing options.
106
155
if strings .HasPrefix (sshConfigFile , "~/" ) {
107
156
sshConfigFile = filepath .Join (dirname , sshConfigFile [2 :])
108
157
}
109
- coderConfigFileOrig := coderConfigFile
110
158
coderConfigFile = filepath .Join (dirname , coderConfigFile [2 :]) // Replace ~/ with home dir.
111
159
112
- // TODO(mafredri): Check coderConfigFile for previous options
113
- // coderConfigFile.
114
-
115
- out := cmd .OutOrStdout ()
116
- if showDiff {
117
- out = cmd .OutOrStderr ()
118
- }
119
- binaryFile , err := currentBinPath (out )
120
- if err != nil {
121
- return err
122
- }
123
-
124
160
// Only allow not-exist errors to avoid trashing
125
161
// the users SSH config.
126
162
configRaw , err := os .ReadFile (sshConfigFile )
@@ -137,6 +173,25 @@ func configSSH() *cobra.Command {
137
173
return xerrors .Errorf ("unexpected content in %s: remove the file and rerun the command to continue" , coderConfigFile )
138
174
}
139
175
}
176
+ lastCoderConfig := sshCoderConfigParseLastOptions (bytes .NewReader (coderConfigRaw ))
177
+
178
+ // Only prompt when no arguments are provided and avoid
179
+ // prompting in diff mode (unexpected behavior).
180
+ if ! showDiff && coderConfig .isZero () && ! lastCoderConfig .isZero () {
181
+ line , err := cliui .Prompt (cmd , cliui.PromptOptions {
182
+ Text : fmt .Sprintf ("Found previous configuration option(s):\n \n - %s\n \n Use previous option(s)?" , strings .Join (lastCoderConfig .asList (), "\n - " )),
183
+ IsConfirm : true ,
184
+ })
185
+ if err != nil {
186
+ // TODO(mafredri): Better way to differ between "no" and Ctrl+C?
187
+ if line == "" && xerrors .Is (err , cliui .Canceled ) {
188
+ return nil
189
+ }
190
+ } else {
191
+ coderConfig = lastCoderConfig
192
+ }
193
+ _ , _ = fmt .Fprint (out , "\n " )
194
+ }
140
195
141
196
// Keep track of changes we are making.
142
197
var changes []string
@@ -145,14 +200,14 @@ func configSSH() *cobra.Command {
145
200
// remove if present.
146
201
configModified , ok := stripOldConfigBlock (configRaw )
147
202
if ok {
148
- changes = append (changes , fmt .Sprintf ("Remove old config block (START-CODER/END-CODER) from %s" , sshConfigFileOrig ))
203
+ changes = append (changes , fmt .Sprintf ("Remove old config block (START-CODER/END-CODER) from %s" , sshConfigFile ))
149
204
}
150
205
151
206
// Check for the presence of the coder Include
152
207
// statement is present and add if missing.
153
208
configModified , ok = sshConfigAddCoderInclude (configModified )
154
209
if ok {
155
- changes = append (changes , fmt .Sprintf ("Add %q to %s" , "Include coder" , sshConfigFileOrig ))
210
+ changes = append (changes , fmt .Sprintf ("Add %q to %s" , "Include coder" , sshConfigFile ))
156
211
}
157
212
158
213
root := createConfig (cmd )
@@ -195,19 +250,13 @@ func configSSH() *cobra.Command {
195
250
}
196
251
197
252
buf := & bytes.Buffer {}
198
- _ , _ = buf .WriteString (sshCoderConfigHeader )
199
- _ , _ = buf .WriteString (sshCoderConfigDocsHeader )
200
-
201
- // Store the provided flags as part of the
202
- // config for future (re)use.
203
- _ , _ = buf .WriteString (sshCoderConfigOptionsHeader )
204
- if sshConfigFileOrig != sshDefaultConfigFileName {
205
- _ , _ = fmt .Fprintf (buf , "# :%s=%s\n " , "ssh-config-file" , sshConfigFileOrig )
206
- }
207
- for _ , opt := range sshOptions {
208
- _ , _ = fmt .Fprintf (buf , "# :%s=%s\n " , "ssh-option" , opt )
253
+
254
+ // Write header and store the provided options as part
255
+ // of the config for future (re)use.
256
+ err = sshCoderConfigWriteHeader (buf , coderConfig )
257
+ if err != nil {
258
+ return xerrors .Errorf ("write coder config header failed: %w" , err )
209
259
}
210
- _ , _ = buf .WriteString ("#\n " )
211
260
212
261
// Ensure stable sorting of output.
213
262
slices .SortFunc (workspaceConfigs , func (a , b workspaceConfig ) bool {
@@ -220,7 +269,7 @@ func configSSH() *cobra.Command {
220
269
configOptions := []string {
221
270
"Host coder." + hostname ,
222
271
}
223
- for _ , option := range sshOptions {
272
+ for _ , option := range coderConfig . sshOptions {
224
273
configOptions = append (configOptions , "\t " + option )
225
274
}
226
275
configOptions = append (configOptions ,
@@ -246,9 +295,9 @@ func configSSH() *cobra.Command {
246
295
modifyCoderConfig := ! bytes .Equal (coderConfigRaw , buf .Bytes ())
247
296
if modifyCoderConfig {
248
297
if len (coderConfigRaw ) == 0 {
249
- changes = append (changes , fmt .Sprintf ("Write auto-generated coder config file to %s" , coderConfigFileOrig ))
298
+ changes = append (changes , fmt .Sprintf ("Write auto-generated coder config file to %s" , coderConfigFile ))
250
299
} else {
251
- changes = append (changes , fmt .Sprintf ("Update auto-generated coder config file in %s" , coderConfigFileOrig ))
300
+ changes = append (changes , fmt .Sprintf ("Update auto-generated coder config file in %s" , coderConfigFile ))
252
301
}
253
302
}
254
303
@@ -257,7 +306,7 @@ func configSSH() *cobra.Command {
257
306
// Write to stderr to avoid dirtying the diff output.
258
307
_ , _ = fmt .Fprint (out , "Changes:\n \n " )
259
308
for _ , change := range changes {
260
- _ , _ = fmt .Fprintf (out , "* %s\n " , change )
309
+ _ , _ = fmt .Fprintf (out , " * %s\n " , change )
261
310
}
262
311
}
263
312
@@ -281,8 +330,11 @@ func configSSH() *cobra.Command {
281
330
}
282
331
283
332
if len (changes ) > 0 {
333
+ // In diff mode we don't prompt re-using the previous
334
+ // configuration, so we output the entire command.
335
+ diffCommand := fmt .Sprintf ("$ %s %s" , cmd .CommandPath (), strings .Join (append (coderConfig .asArgs (), "--diff" ), " " ))
284
336
_ , err = cliui .Prompt (cmd , cliui.PromptOptions {
285
- Text : fmt .Sprintf ("The following changes will be made to your SSH configuration (use --diff to see changes) :\n \n * %s\n \n Continue?" , strings .Join (changes , "\n * " )),
337
+ Text : fmt .Sprintf ("The following changes will be made to your SSH configuration: \n \n * %s \n \n To see changes, run with --diff :\n \n %s\n \n Continue?" , strings .Join (changes , "\n * " ), diffCommand ),
286
338
IsConfirm : true ,
287
339
})
288
340
if err != nil {
@@ -313,10 +365,10 @@ func configSSH() *cobra.Command {
313
365
return nil
314
366
},
315
367
}
316
- cliflag .StringVarP (cmd .Flags (), & sshConfigFile , "ssh-config-file" , "" , "CODER_SSH_CONFIG_FILE" , sshDefaultConfigFileName , "Specifies the path to an SSH config." )
368
+ cliflag .StringVarP (cmd .Flags (), & coderConfig . sshConfigFile , "ssh-config-file" , "" , "CODER_SSH_CONFIG_FILE" , sshDefaultConfigFileName , "Specifies the path to an SSH config." )
317
369
cmd .Flags ().StringVar (& coderConfigFile , "ssh-coder-config-file" , sshDefaultCoderConfigFileName , "Specifies the path to an Coder SSH config file. Useful for testing." )
318
370
_ = cmd .Flags ().MarkHidden ("ssh-coder-config-file" )
319
- cmd .Flags ().StringArrayVarP (& sshOptions , "ssh-option" , "o" , []string {}, "Specifies additional SSH options to embed in each host stanza." )
371
+ cmd .Flags ().StringArrayVarP (& coderConfig . sshOptions , "ssh-option" , "o" , []string {}, "Specifies additional SSH options to embed in each host stanza." )
320
372
cmd .Flags ().BoolVarP (& showDiff , "diff" , "D" , false , "Show diff of changes that will be made." )
321
373
cmd .Flags ().BoolVarP (& skipProxyCommand , "skip-proxy-command" , "" , false , "Specifies whether the ProxyCommand option should be skipped. Useful for testing." )
322
374
_ = cmd .Flags ().MarkHidden ("skip-proxy-command" )
@@ -354,6 +406,46 @@ func sshConfigAddCoderInclude(data []byte) (modifiedData []byte, modified bool)
354
406
return data , true
355
407
}
356
408
409
+ func sshCoderConfigWriteHeader (w io.Writer , o sshCoderConfigOptions ) error {
410
+ _ , _ = fmt .Fprint (w , sshCoderConfigHeader )
411
+ _ , _ = fmt .Fprint (w , sshCoderConfigDocsHeader )
412
+ _ , _ = fmt .Fprint (w , sshCoderConfigOptionsHeader )
413
+ if o .sshConfigFile != sshDefaultConfigFileName {
414
+ _ , _ = fmt .Fprintf (w , "# :%s=%s\n " , "ssh-config-file" , o .sshConfigFile )
415
+ }
416
+ for _ , opt := range o .sshOptions {
417
+ _ , _ = fmt .Fprintf (w , "# :%s=%s\n " , "ssh-option" , opt )
418
+ }
419
+ _ , _ = fmt .Fprint (w , "#\n " )
420
+ return nil
421
+ }
422
+
423
+ func sshCoderConfigParseLastOptions (r io.Reader ) (o sshCoderConfigOptions ) {
424
+ o .sshConfigFile = sshDefaultConfigFileName // Default value is not written.
425
+
426
+ s := bufio .NewScanner (r )
427
+ for s .Scan () {
428
+ line := s .Text ()
429
+ if strings .HasPrefix (line , "# :" ) {
430
+ line = strings .TrimPrefix (line , "# :" )
431
+ parts := strings .SplitN (line , "=" , 2 )
432
+ switch parts [0 ] {
433
+ case "ssh-config-file" :
434
+ o .sshConfigFile = parts [1 ]
435
+ case "ssh-option" :
436
+ o .sshOptions = append (o .sshOptions , parts [1 ])
437
+ default :
438
+ // Unknown option, ignore.
439
+ }
440
+ }
441
+ }
442
+ if err := s .Err (); err != nil {
443
+ panic (err )
444
+ }
445
+
446
+ return o
447
+ }
448
+
357
449
// writeWithTempFileAndMove writes to a temporary file in the same
358
450
// directory as path and renames the temp file to the file provided in
359
451
// path. This ensure we avoid trashing the file we are writing due to
0 commit comments