Skip to content

Commit d3ccb07

Browse files
JoshVeemafredri
andauthored
feat(cli): support header and header-command in config-ssh (#10413)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
1 parent d6cdaae commit d3ccb07

File tree

2 files changed

+104
-9
lines changed

2 files changed

+104
-9
lines changed

cli/configssh.go

+49-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/cli/safeexec"
2020
"github.com/pkg/diff"
2121
"github.com/pkg/diff/write"
22+
"golang.org/x/exp/constraints"
2223
"golang.org/x/exp/slices"
2324
"golang.org/x/sync/errgroup"
2425
"golang.org/x/xerrors"
@@ -51,6 +52,8 @@ type sshConfigOptions struct {
5152
userHostPrefix string
5253
sshOptions []string
5354
disableAutostart bool
55+
header []string
56+
headerCommand string
5457
}
5558

5659
// addOptions expects options in the form of "option=value" or "option value".
@@ -100,15 +103,25 @@ func (o *sshConfigOptions) addOption(option string) error {
100103
}
101104

102105
func (o sshConfigOptions) equal(other sshConfigOptions) bool {
103-
// Compare without side-effects or regard to order.
104-
opt1 := slices.Clone(o.sshOptions)
105-
sort.Strings(opt1)
106-
opt2 := slices.Clone(other.sshOptions)
107-
sort.Strings(opt2)
108-
if !slices.Equal(opt1, opt2) {
106+
if !slicesSortedEqual(o.sshOptions, other.sshOptions) {
109107
return false
110108
}
111-
return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix && o.disableAutostart == other.disableAutostart
109+
if !slicesSortedEqual(o.header, other.header) {
110+
return false
111+
}
112+
return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix && o.disableAutostart == other.disableAutostart && o.headerCommand == other.headerCommand
113+
}
114+
115+
// slicesSortedEqual compares two slices without side-effects or regard to order.
116+
func slicesSortedEqual[S ~[]E, E constraints.Ordered](a, b S) bool {
117+
if len(a) != len(b) {
118+
return false
119+
}
120+
a = slices.Clone(a)
121+
slices.Sort(a)
122+
b = slices.Clone(b)
123+
slices.Sort(b)
124+
return slices.Equal(a, b)
112125
}
113126

114127
func (o sshConfigOptions) asList() (list []string) {
@@ -124,6 +137,13 @@ func (o sshConfigOptions) asList() (list []string) {
124137
for _, opt := range o.sshOptions {
125138
list = append(list, fmt.Sprintf("ssh-option: %s", opt))
126139
}
140+
for _, h := range o.header {
141+
list = append(list, fmt.Sprintf("header: %s", h))
142+
}
143+
if o.headerCommand != "" {
144+
list = append(list, fmt.Sprintf("header-command: %s", o.headerCommand))
145+
}
146+
127147
return list
128148
}
129149

@@ -230,6 +250,8 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
230250
// specifies skip-proxy-command, then wait cannot be applied.
231251
return xerrors.Errorf("cannot specify both --skip-proxy-command and --wait")
232252
}
253+
sshConfigOpts.header = r.header
254+
sshConfigOpts.headerCommand = r.headerCommand
233255

234256
recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(inv.Context(), client)
235257

@@ -393,6 +415,14 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
393415
}
394416

395417
if !skipProxyCommand {
418+
rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig)
419+
for _, h := range sshConfigOpts.header {
420+
rootFlags += fmt.Sprintf(" --header %q", h)
421+
}
422+
if sshConfigOpts.headerCommand != "" {
423+
rootFlags += fmt.Sprintf(" --header-command %q", sshConfigOpts.headerCommand)
424+
}
425+
396426
flags := ""
397427
if sshConfigOpts.waitEnum != "auto" {
398428
flags += " --wait=" + sshConfigOpts.waitEnum
@@ -401,8 +431,8 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
401431
flags += " --disable-autostart=true"
402432
}
403433
defaultOptions = append(defaultOptions, fmt.Sprintf(
404-
"ProxyCommand %s --global-config %s ssh --stdio%s %s",
405-
escapedCoderBinary, escapedGlobalConfig, flags, workspaceHostname,
434+
"ProxyCommand %s %s ssh --stdio%s %s",
435+
escapedCoderBinary, rootFlags, flags, workspaceHostname,
406436
))
407437
}
408438

@@ -623,6 +653,12 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption
623653
for _, opt := range o.sshOptions {
624654
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-option", opt)
625655
}
656+
for _, h := range o.header {
657+
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "header", h)
658+
}
659+
if o.headerCommand != "" {
660+
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "header-command", o.headerCommand)
661+
}
626662
if ow.Len() > 0 {
627663
_, _ = fmt.Fprint(w, sshConfigOptionsHeader)
628664
_, _ = fmt.Fprint(w, ow.String())
@@ -654,6 +690,10 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
654690
o.sshOptions = append(o.sshOptions, parts[1])
655691
case "disable-autostart":
656692
o.disableAutostart, _ = strconv.ParseBool(parts[1])
693+
case "header":
694+
o.header = append(o.header, parts[1])
695+
case "header-command":
696+
o.headerCommand = parts[1]
657697
default:
658698
// Unknown option, ignore.
659699
}

cli/configssh_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,9 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
462462
"# Last config-ssh options:",
463463
"# :wait=yes",
464464
"# :ssh-host-prefix=coder-test.",
465+
"# :header=X-Test-Header=foo",
466+
"# :header=X-Test-Header2=bar",
467+
"# :header-command=printf h1=v1 h2=\"v2\" h3='v3'",
465468
"#",
466469
headerEnd,
467470
"",
@@ -471,6 +474,9 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
471474
"--yes",
472475
"--wait=yes",
473476
"--ssh-host-prefix", "coder-test.",
477+
"--header", "X-Test-Header=foo",
478+
"--header", "X-Test-Header2=bar",
479+
"--header-command", "printf h1=v1 h2=\"v2\" h3='v3'",
474480
},
475481
},
476482
{
@@ -563,6 +569,55 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
563569
regexMatch: "ProxyCommand /foo/bar/coder",
564570
},
565571
},
572+
{
573+
name: "Header",
574+
args: []string{
575+
"--yes",
576+
"--header", "X-Test-Header=foo",
577+
"--header", "X-Test-Header2=bar",
578+
},
579+
wantErr: false,
580+
hasAgent: true,
581+
wantConfig: wantConfig{
582+
regexMatch: `ProxyCommand .* --header "X-Test-Header=foo" --header "X-Test-Header2=bar" ssh`,
583+
},
584+
},
585+
{
586+
name: "Header command",
587+
args: []string{
588+
"--yes",
589+
"--header-command", "printf h1=v1",
590+
},
591+
wantErr: false,
592+
hasAgent: true,
593+
wantConfig: wantConfig{
594+
regexMatch: `ProxyCommand .* --header-command "printf h1=v1" ssh`,
595+
},
596+
},
597+
{
598+
name: "Header command with double quotes",
599+
args: []string{
600+
"--yes",
601+
"--header-command", "printf h1=v1 h2=\"v2\"",
602+
},
603+
wantErr: false,
604+
hasAgent: true,
605+
wantConfig: wantConfig{
606+
regexMatch: `ProxyCommand .* --header-command "printf h1=v1 h2=\\\"v2\\\"" ssh`,
607+
},
608+
},
609+
{
610+
name: "Header command with single quotes",
611+
args: []string{
612+
"--yes",
613+
"--header-command", "printf h1=v1 h2='v2'",
614+
},
615+
wantErr: false,
616+
hasAgent: true,
617+
wantConfig: wantConfig{
618+
regexMatch: `ProxyCommand .* --header-command "printf h1=v1 h2='v2'" ssh`,
619+
},
620+
},
566621
}
567622
for _, tt := range tests {
568623
tt := tt

0 commit comments

Comments
 (0)