Skip to content

Commit 978364b

Browse files
authored
fix(cli): do not overwrite repeated SSH options in config-ssh (#13819)
Fixes #11593
1 parent 5cdfc08 commit 978364b

File tree

3 files changed

+44
-37
lines changed

3 files changed

+44
-37
lines changed

cli/configssh.go

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type sshConfigOptions struct {
5454
disableAutostart bool
5555
header []string
5656
headerCommand string
57+
removedKeys map[string]bool
5758
}
5859

5960
// addOptions expects options in the form of "option=value" or "option value".
@@ -74,30 +75,20 @@ func (o *sshConfigOptions) addOption(option string) error {
7475
if err != nil {
7576
return err
7677
}
77-
for i, existing := range o.sshOptions {
78-
// Override existing option if they share the same key.
79-
// This is case-insensitive. Parsing each time might be a little slow,
80-
// but it is ok.
81-
existingKey, _, err := codersdk.ParseSSHConfigOption(existing)
82-
if err != nil {
83-
// Don't mess with original values if there is an error.
84-
// This could have come from the user's manual edits.
85-
continue
86-
}
87-
if strings.EqualFold(existingKey, key) {
88-
if value == "" {
89-
// Delete existing option.
90-
o.sshOptions = append(o.sshOptions[:i], o.sshOptions[i+1:]...)
91-
} else {
92-
// Override existing option.
93-
o.sshOptions[i] = option
94-
}
95-
return nil
96-
}
78+
lowerKey := strings.ToLower(key)
79+
if o.removedKeys != nil && o.removedKeys[lowerKey] {
80+
// Key marked as removed, skip.
81+
return nil
9782
}
98-
// Only append the option if it is not empty.
83+
// Only append the option if it is not empty
84+
// (we interpret empty as removal).
9985
if value != "" {
10086
o.sshOptions = append(o.sshOptions, option)
87+
} else {
88+
if o.removedKeys == nil {
89+
o.removedKeys = make(map[string]bool)
90+
}
91+
o.removedKeys[lowerKey] = true
10192
}
10293
return nil
10394
}
@@ -440,26 +431,29 @@ func (r *RootCmd) configSSH() *serpent.Command {
440431
configOptions := sshConfigOpts
441432
configOptions.sshOptions = nil
442433

443-
// Add standard options.
444-
err := configOptions.addOptions(defaultOptions...)
445-
if err != nil {
446-
return err
434+
// User options first (SSH only uses the first
435+
// option unless it can be given multiple times)
436+
for _, opt := range sshConfigOpts.sshOptions {
437+
err := configOptions.addOptions(opt)
438+
if err != nil {
439+
return xerrors.Errorf("add flag config option %q: %w", opt, err)
440+
}
447441
}
448442

449-
// Override with deployment options
443+
// Deployment options second, allow them to
444+
// override standard options.
450445
for k, v := range coderdConfig.SSHConfigOptions {
451446
opt := fmt.Sprintf("%s %s", k, v)
452447
err := configOptions.addOptions(opt)
453448
if err != nil {
454449
return xerrors.Errorf("add coderd config option %q: %w", opt, err)
455450
}
456451
}
457-
// Override with flag options
458-
for _, opt := range sshConfigOpts.sshOptions {
459-
err := configOptions.addOptions(opt)
460-
if err != nil {
461-
return xerrors.Errorf("add flag config option %q: %w", opt, err)
462-
}
452+
453+
// Finally, add the standard options.
454+
err := configOptions.addOptions(defaultOptions...)
455+
if err != nil {
456+
return err
463457
}
464458

465459
hostBlock := []string{

cli/configssh_internal_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,32 +272,32 @@ func Test_sshConfigOptions_addOption(t *testing.T) {
272272
},
273273
},
274274
{
275-
Name: "Replace",
275+
Name: "AddTwo",
276276
Start: []string{
277277
"foo bar",
278278
},
279279
Add: []string{"Foo baz"},
280280
Expect: []string{
281+
"foo bar",
281282
"Foo baz",
282283
},
283284
},
284285
{
285-
Name: "AddAndReplace",
286+
Name: "AddAndRemove",
286287
Start: []string{
287-
"a b",
288288
"foo bar",
289289
"buzz bazz",
290290
},
291291
Add: []string{
292292
"b c",
293+
"a ", // Empty value, means remove all following entries that start with "a", i.e. next line.
293294
"A hello",
294295
"hello world",
295296
},
296297
Expect: []string{
297298
"foo bar",
298299
"buzz bazz",
299300
"b c",
300-
"A hello",
301301
"hello world",
302302
},
303303
},

cli/configssh_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestConfigSSH(t *testing.T) {
6565

6666
const hostname = "test-coder."
6767
const expectedKey = "ConnectionAttempts"
68-
const removeKey = "ConnectionTimeout"
68+
const removeKey = "ConnectTimeout"
6969
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
7070
ConfigSSH: codersdk.SSHConfigResponse{
7171
HostnamePrefix: hostname,
@@ -620,6 +620,19 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
620620
regexMatch: `ProxyCommand .* --header-command "printf h1=v1 h2='v2'" ssh`,
621621
},
622622
},
623+
{
624+
name: "Multiple remote forwards",
625+
args: []string{
626+
"--yes",
627+
"--ssh-option", "RemoteForward 2222 192.168.11.1:2222",
628+
"--ssh-option", "RemoteForward 2223 192.168.11.1:2223",
629+
},
630+
wantErr: false,
631+
hasAgent: true,
632+
wantConfig: wantConfig{
633+
regexMatch: "RemoteForward 2222 192.168.11.1:2222.*\n.*RemoteForward 2223 192.168.11.1:2223",
634+
},
635+
},
623636
}
624637
for _, tt := range tests {
625638
tt := tt

0 commit comments

Comments
 (0)