8
8
"io/fs"
9
9
"os"
10
10
"path/filepath"
11
+ "regexp"
11
12
"runtime"
12
13
"sort"
13
14
"strings"
@@ -26,8 +27,7 @@ import (
26
27
)
27
28
28
29
const (
29
- sshDefaultConfigFileName = "~/.ssh/config"
30
- // TODO(mafredri): Consider moving to coder config dir, i.e. ~/.config/coderv2/ssh_config?
30
+ sshDefaultConfigFileName = "~/.ssh/config"
31
31
sshDefaultCoderConfigFileName = "~/.ssh/coder"
32
32
sshCoderConfigHeader = `# This file is managed by coder. DO NOT EDIT.
33
33
#
@@ -42,6 +42,16 @@ const (
42
42
sshConfigIncludeStatement = "Include coder"
43
43
)
44
44
45
+ // Regular expressions are used because SSH configs do not have
46
+ // meaningful indentation and keywords are case-insensitive.
47
+ var (
48
+ // Find the first Host and Match statement as these restrict the
49
+ // following declarations to be used conditionally.
50
+ sshHostRe = regexp .MustCompile (`^\s*((?i)Host|Match)` )
51
+ // Find the semantically correct include statement.
52
+ sshCoderIncludedRe = regexp .MustCompile (`^\s*((?i)Include) coder(\s|$)` )
53
+ )
54
+
45
55
func configSSH () * cobra.Command {
46
56
var (
47
57
sshConfigFile string
@@ -132,15 +142,11 @@ func configSSH() *cobra.Command {
132
142
changes = append (changes , fmt .Sprintf ("Remove old config block (START-CODER/END-CODER) from %s" , sshConfigFileOrig ))
133
143
}
134
144
135
- if found := bytes .Index (configModified , []byte (sshConfigIncludeStatement )); found == - 1 || (found > 0 && configModified [found - 1 ] != '\n' ) {
136
- changes = append (changes , fmt .Sprintf ("Add 'Include coder' to %s" , sshConfigFileOrig ))
137
- // Separate Include statement from user content with an empty newline.
138
- configModified = bytes .TrimRight (configModified , "\n " )
139
- sep := "\n \n "
140
- if len (configModified ) == 0 {
141
- sep = ""
142
- }
143
- configModified = append (configModified , []byte (sep + sshConfigIncludeStatement + "\n " )... )
145
+ // Check for the presence of the coder Include
146
+ // statement is present and add if missing.
147
+ configModified , ok = sshConfigAddCoderInclude (configModified )
148
+ if ok {
149
+ changes = append (changes , fmt .Sprintf ("Add %q to %s" , "Include coder" , sshConfigFileOrig ))
144
150
}
145
151
146
152
root := createConfig (cmd )
@@ -278,7 +284,7 @@ func configSSH() *cobra.Command {
278
284
_ , _ = fmt .Fprint (out , "\n " )
279
285
280
286
if ! bytes .Equal (configRaw , configModified ) {
281
- err = writeWithTempFileAndMove (sshConfigFile , bytes .NewReader (configRaw ))
287
+ err = writeWithTempFileAndMove (sshConfigFile , bytes .NewReader (configModified ))
282
288
if err != nil {
283
289
return xerrors .Errorf ("write ssh config failed: %w" , err )
284
290
}
@@ -311,6 +317,36 @@ func configSSH() *cobra.Command {
311
317
return cmd
312
318
}
313
319
320
+ // sshConfigAddCoderInclude checks for the coder Include statement and
321
+ // returns modified = true if it was added.
322
+ func sshConfigAddCoderInclude (data []byte ) (modifiedData []byte , modified bool ) {
323
+ found := false
324
+ firstHost := sshHostRe .FindIndex (data )
325
+ coderInclude := sshCoderIncludedRe .FindIndex (data )
326
+ if firstHost != nil && coderInclude != nil {
327
+ // If the Coder Include statement exists
328
+ // before a Host entry, we're good.
329
+ found = coderInclude [1 ] < firstHost [0 ]
330
+ } else if coderInclude != nil {
331
+ found = true
332
+ }
333
+ if found {
334
+ return data , false
335
+ }
336
+
337
+ // Add Include statement to the top of SSH config.
338
+ // The user is allowed to move it as long as it
339
+ // stays above the first Host (or Match) statement.
340
+ sep := "\n \n "
341
+ if len (data ) == 0 {
342
+ // If SSH config is empty, a single newline will suffice.
343
+ sep = "\n "
344
+ }
345
+ data = append ([]byte (sshConfigIncludeStatement + sep ), data ... )
346
+
347
+ return data , true
348
+ }
349
+
314
350
// writeWithTempFileAndMove writes to a temporary file in the same
315
351
// directory as path and renames the temp file to the file provided in
316
352
// path. This ensure we avoid trashing the file we are writing due to
0 commit comments