feat: add --no-wildcard flag to coder config-ssh (#26753)

Add `--no-wildcard` (`CODER_CONFIGSSH_NO_WILDCARD`) to `coder
config-ssh` that generates an individual `Host` entry per workspace
instead of a single wildcard block (`Host *.coder`).

The wildcard approach cannot be enumerated by third-party SSH clients,
the VS Code Remote-SSH sidebar, or scripts that parse `~/.ssh/config` to
discover hosts. With `--no-wildcard`, each workspace gets its own entry
so those tools work without Coder-specific extensions.

The flag is persisted in the config section header so re-running without
it prompts the user about the option change. Workspaces are fetched with
pagination before writing so the diff shows actual hostnames.

## Manual testing

**Unit tests (no server needed):**

```sh
go test ./cli/ -run TestSSHConfigOptions_writeToBuffer -v
go test ./cli/ -run TestConfigSSH_NoWildcard -v
```

**End-to-end with a dev server:**

1. Build: `go build -o ./coder .`
2. Start dev server in a separate terminal: `./scripts/develop.sh`
3. Log in: `./coder login http://localhost:3000`
4. Create two workspaces
5. Run both variants into temp files:
```sh
./coder config-ssh --no-wildcard --hostname-suffix coder --ssh-config-file /tmp/test-ssh-config --yes
./coder config-ssh --hostname-suffix coder --ssh-config-file /tmp/test-ssh-config-wildcard --yes
diff /tmp/test-ssh-config-wildcard /tmp/test-ssh-config
```

<details>
<summary>Output: <code>--no-wildcard</code></summary>

```
# ------------START-CODER-----------
# This section is managed by coder. DO NOT EDIT.
#
# You should not hand-edit this section unless you are removing it, all
# changes will be lost when running "coder config-ssh".
#
# Last config-ssh options:
# :hostname-suffix=coder
# :no-wildcard=true
#
Host coder.myworkspace
    ConnectTimeout=0
    StrictHostKeyChecking=no
    UserKnownHostsFile=/dev/null
    LogLevel ERROR
    ProxyCommand <coder> --global-config <config> ssh --stdio --ssh-host-prefix coder. %h

Host coder.myworkspace2
    ConnectTimeout=0
    StrictHostKeyChecking=no
    UserKnownHostsFile=/dev/null
    LogLevel ERROR
    ProxyCommand <coder> --global-config <config> ssh --stdio --ssh-host-prefix coder. %h

Host myworkspace.coder
    ConnectTimeout=0
    StrictHostKeyChecking=no
    UserKnownHostsFile=/dev/null
    LogLevel ERROR

Match host myworkspace.coder !exec "<coder> connect exists %h"
    ProxyCommand <coder> --global-config <config> ssh --stdio --hostname-suffix coder %h

Host myworkspace2.coder
    ConnectTimeout=0
    StrictHostKeyChecking=no
    UserKnownHostsFile=/dev/null
    LogLevel ERROR

Match host myworkspace2.coder !exec "<coder> connect exists %h"
    ProxyCommand <coder> --global-config <config> ssh --stdio --hostname-suffix coder %h
# ------------END-CODER------------
```

</details>

<details>
<summary>Output: wildcard (default)</summary>

```
# ------------START-CODER-----------
# This section is managed by coder. DO NOT EDIT.
#
# You should not hand-edit this section unless you are removing it, all
# changes will be lost when running "coder config-ssh".
#
# Last config-ssh options:
# :hostname-suffix=coder
#
Host coder.*
    ConnectTimeout=0
    StrictHostKeyChecking=no
    UserKnownHostsFile=/dev/null
    LogLevel ERROR
    ProxyCommand <coder> --global-config <config> ssh --stdio --ssh-host-prefix coder. %h

Host *.coder
    ConnectTimeout=0
    StrictHostKeyChecking=no
    UserKnownHostsFile=/dev/null
    LogLevel ERROR

Match host *.coder !exec "<coder> connect exists %h"
    ProxyCommand <coder> --global-config <config> ssh --stdio --hostname-suffix coder %h
# ------------END-CODER------------
```

</details>

<details>
<summary>diff wildcard → --no-wildcard</summary>

```diff
8a9
> # :no-wildcard=true
10c11
< Host coder.*
---
> Host coder.myworkspace
17c18
< Host *.coder
---
> Host coder.myworkspace2
21a23
>     ProxyCommand <coder> ssh --stdio --ssh-host-prefix coder. %h
23c25,31
< Match host *.coder !exec "<coder> connect exists %h"
---
> Host myworkspace.coder
>     ConnectTimeout=0
>     StrictHostKeyChecking=no
>     UserKnownHostsFile=/dev/null
>     LogLevel ERROR
>
> Match host myworkspace.coder !exec "<coder> connect exists %h"
```

</details>

Closes https://github.com/coder/coder/issues/17153 (Phase 1: CLI flag)
This commit is contained in:
Bobby Ho
2026-06-30 13:02:15 -07:00
committed by GitHub
parent 7baf0d4a9a
commit dcb120d6ab
7 changed files with 468 additions and 34 deletions
+131 -34
View File
@@ -51,6 +51,7 @@ type sshConfigOptions struct {
hostnameSuffix string
sshOptions []string
disableAutostart bool
noWildcard bool
header []string
headerCommand string
removedKeys map[string]bool
@@ -58,6 +59,10 @@ type sshConfigOptions struct {
coderBinaryPath string
skipProxyCommand bool
forceUnixSeparators bool
// workspaceNames is populated when noWildcard is true. It holds the
// workspace names used to generate individual host entries. It is not
// persisted to the SSH config header.
workspaceNames []string
}
// addOptions expects options in the form of "option=value" or "option value".
@@ -107,7 +112,8 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
o.userHostPrefix == other.userHostPrefix &&
o.disableAutostart == other.disableAutostart &&
o.headerCommand == other.headerCommand &&
o.hostnameSuffix == other.hostnameSuffix
o.hostnameSuffix == other.hostnameSuffix &&
o.noWildcard == other.noWildcard
}
func (o sshConfigOptions) writeToBuffer(buf *bytes.Buffer) error {
@@ -142,26 +148,51 @@ func (o sshConfigOptions) writeToBuffer(buf *bytes.Buffer) error {
flags += " --disable-autostart=true"
}
// TODO: this function has grown complex enough that it would benefit from
// being rewritten using text/template rather than manual buf.WriteString
// and fmt.Fprintf calls.
// Prefix block:
if o.userHostPrefix != "" {
_, _ = buf.WriteString("Host")
if o.noWildcard {
for i, wsName := range o.workspaceNames {
if i > 0 {
_, _ = buf.WriteString("\n")
}
_, _ = fmt.Fprintf(buf, "Host %s%s\n", o.userHostPrefix, wsName)
for _, v := range o.sshOptions {
_, _ = buf.WriteString("\t")
_, _ = buf.WriteString(v)
_, _ = buf.WriteString("\n")
}
if !o.skipProxyCommand {
_, _ = buf.WriteString("\t")
_, _ = fmt.Fprintf(buf,
"ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h",
escapedCoderBinaryProxy, rootFlags, flags, o.userHostPrefix,
)
_, _ = buf.WriteString("\n")
}
}
} else {
_, _ = buf.WriteString("Host")
_, _ = buf.WriteString(" ")
_, _ = buf.WriteString(o.userHostPrefix)
_, _ = buf.WriteString("*\n")
_, _ = buf.WriteString(" ")
_, _ = buf.WriteString(o.userHostPrefix)
_, _ = buf.WriteString("*\n")
for _, v := range o.sshOptions {
_, _ = buf.WriteString("\t")
_, _ = buf.WriteString(v)
_, _ = buf.WriteString("\n")
}
if !o.skipProxyCommand && o.userHostPrefix != "" {
_, _ = buf.WriteString("\t")
_, _ = fmt.Fprintf(buf,
"ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h",
escapedCoderBinaryProxy, rootFlags, flags, o.userHostPrefix,
)
_, _ = buf.WriteString("\n")
for _, v := range o.sshOptions {
_, _ = buf.WriteString("\t")
_, _ = buf.WriteString(v)
_, _ = buf.WriteString("\n")
}
if !o.skipProxyCommand {
_, _ = buf.WriteString("\t")
_, _ = fmt.Fprintf(buf,
"ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h",
escapedCoderBinaryProxy, rootFlags, flags, o.userHostPrefix,
)
_, _ = buf.WriteString("\n")
}
}
}
@@ -169,22 +200,46 @@ func (o sshConfigOptions) writeToBuffer(buf *bytes.Buffer) error {
if o.hostnameSuffix == "" {
return nil
}
_, _ = fmt.Fprintf(buf, "\nHost *.%s\n", o.hostnameSuffix)
for _, v := range o.sshOptions {
_, _ = buf.WriteString("\t")
_, _ = buf.WriteString(v)
_, _ = buf.WriteString("\n")
}
// the ^^ options should always apply, but we only want to use the proxy command if Coder Connect is not running.
if !o.skipProxyCommand {
_, _ = fmt.Fprintf(buf, "\nMatch host *.%s !exec \"%s connect exists %%h\"\n",
o.hostnameSuffix, escapedCoderBinaryMatchExec)
_, _ = buf.WriteString("\t")
_, _ = fmt.Fprintf(buf,
"ProxyCommand %s %s ssh --stdio%s --hostname-suffix %s %%h",
escapedCoderBinaryProxy, rootFlags, flags, o.hostnameSuffix,
)
_, _ = buf.WriteString("\n")
if o.noWildcard {
for _, wsName := range o.workspaceNames {
hostname := wsName + "." + o.hostnameSuffix
_, _ = fmt.Fprintf(buf, "\nHost %s\n", hostname)
for _, v := range o.sshOptions {
_, _ = buf.WriteString("\t")
_, _ = buf.WriteString(v)
_, _ = buf.WriteString("\n")
}
// Options always apply; only use the proxy command when Coder Connect is not running.
if !o.skipProxyCommand {
_, _ = fmt.Fprintf(buf, "\nMatch host %s !exec \"%s connect exists %%h\"\n",
hostname, escapedCoderBinaryMatchExec)
_, _ = buf.WriteString("\t")
_, _ = fmt.Fprintf(buf,
"ProxyCommand %s %s ssh --stdio%s --hostname-suffix %s %%h",
escapedCoderBinaryProxy, rootFlags, flags, o.hostnameSuffix,
)
_, _ = buf.WriteString("\n")
}
}
} else {
_, _ = fmt.Fprintf(buf, "\nHost *.%s\n", o.hostnameSuffix)
for _, v := range o.sshOptions {
_, _ = buf.WriteString("\t")
_, _ = buf.WriteString(v)
_, _ = buf.WriteString("\n")
}
// Options above always apply; only use the proxy command when Coder Connect is not running.
if !o.skipProxyCommand {
_, _ = fmt.Fprintf(buf, "\nMatch host *.%s !exec \"%s connect exists %%h\"\n",
o.hostnameSuffix, escapedCoderBinaryMatchExec)
_, _ = buf.WriteString("\t")
_, _ = fmt.Fprintf(buf,
"ProxyCommand %s %s ssh --stdio%s --hostname-suffix %s %%h",
escapedCoderBinaryProxy, rootFlags, flags, o.hostnameSuffix,
)
_, _ = buf.WriteString("\n")
}
}
return nil
}
@@ -214,6 +269,9 @@ func (o sshConfigOptions) asList() (list []string) {
if o.disableAutostart {
list = append(list, fmt.Sprintf("disable-autostart: %v", o.disableAutostart))
}
if o.noWildcard {
list = append(list, "no-wildcard: true")
}
for _, opt := range o.sshOptions {
list = append(list, fmt.Sprintf("ssh-option: %s", opt))
}
@@ -395,6 +453,32 @@ func (r *RootCmd) configSSH() *serpent.Command {
if err != nil {
return err
}
if configOptions.noWildcard {
// Fetch all workspaces to generate individual host entries.
var wsNames []string
offset := 0
const pageSize = 100
for {
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Owner: codersdk.Me,
Offset: offset,
Limit: pageSize,
})
if err != nil {
return xerrors.Errorf("fetch workspaces: %w", err)
}
for _, ws := range res.Workspaces {
wsNames = append(wsNames, ws.Name)
}
if len(res.Workspaces) < pageSize {
break
}
offset += pageSize
}
configOptions.workspaceNames = wsNames
}
err = configOptions.writeToBuffer(buf)
if err != nil {
return err
@@ -559,6 +643,14 @@ func (r *RootCmd) configSSH() *serpent.Command {
Value: serpent.BoolOf(&sshConfigOpts.disableAutostart),
Default: "false",
},
{
Flag: "no-wildcard",
Env: "CODER_CONFIGSSH_NO_WILDCARD",
Description: "Generate an individual host entry for each workspace instead of a wildcard host block. " +
"This allows third-party tools and SSH clients to discover workspaces by reading the config file.",
Value: serpent.BoolOf(&sshConfigOpts.noWildcard),
Default: "false",
},
{
Flag: "force-unix-filepaths",
Env: "CODER_CONFIGSSH_UNIX_FILEPATHS",
@@ -657,6 +749,9 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption
if o.disableAutostart {
_, _ = fmt.Fprintf(&ow, "# :%s=%v\n", "disable-autostart", o.disableAutostart)
}
if o.noWildcard {
_, _ = fmt.Fprintf(&ow, "# :%s=%v\n", "no-wildcard", o.noWildcard)
}
for _, opt := range o.sshOptions {
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-option", opt)
}
@@ -699,6 +794,8 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
o.sshOptions = append(o.sshOptions, parts[1])
case "disable-autostart":
o.disableAutostart, _ = strconv.ParseBool(parts[1])
case "no-wildcard":
o.noWildcard, _ = strconv.ParseBool(parts[1])
case "header":
o.header = append(o.header, parts[1])
case "header-command":
+180
View File
@@ -1,6 +1,7 @@
package cli
import (
"bytes"
"os"
"os/exec"
"path/filepath"
@@ -9,6 +10,7 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/codersdk"
@@ -513,3 +515,181 @@ func Test_sshConfigOptions_addOption(t *testing.T) {
})
}
}
func TestSSHConfigOptions_writeToBuffer(t *testing.T) {
t.Parallel()
tests := []struct {
name string
opts sshConfigOptions
want []string // substrings that must appear
notWant []string // substrings that must not appear
}{
{
name: "wildcard suffix",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
hostnameSuffix: "coder",
waitEnum: "auto",
},
want: []string{"Host *.coder\n", "ProxyCommand", "--hostname-suffix coder %h"},
notWant: []string{"Host workspace"},
},
{
name: "wildcard prefix",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
userHostPrefix: "coder.",
waitEnum: "auto",
},
want: []string{"Host coder.*\n", "ProxyCommand", "--ssh-host-prefix coder. %h"},
notWant: []string{"Host coder.workspace"},
},
{
name: "no-wildcard suffix with workspaces",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
hostnameSuffix: "coder",
noWildcard: true,
workspaceNames: []string{"workspace1", "workspace2"},
waitEnum: "auto",
},
want: []string{
"Host workspace1.coder\n",
"Host workspace2.coder\n",
"Match host workspace1.coder !exec",
"Match host workspace2.coder !exec",
"--hostname-suffix coder %h",
},
notWant: []string{"Host *.coder", "Match host *.coder"},
},
{
name: "no-wildcard suffix with zero workspaces produces no host entries",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
hostnameSuffix: "coder",
noWildcard: true,
workspaceNames: nil,
waitEnum: "auto",
},
notWant: []string{"Host", "ProxyCommand", "Match"},
},
{
name: "no-wildcard prefix with workspaces",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
userHostPrefix: "coder.",
noWildcard: true,
workspaceNames: []string{"workspace1", "workspace2"},
waitEnum: "auto",
},
want: []string{
"Host coder.workspace1\n",
"Host coder.workspace2\n",
"--ssh-host-prefix coder. %h",
},
notWant: []string{"Host coder.*"},
},
{
name: "no-wildcard suffix skips proxy command when skipProxyCommand is set",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
hostnameSuffix: "coder",
noWildcard: true,
workspaceNames: []string{"workspace1"},
skipProxyCommand: true,
waitEnum: "auto",
},
want: []string{"Host workspace1.coder\n"},
notWant: []string{"ProxyCommand", "Match host", "Host *.coder"},
},
{
name: "no-wildcard prefix skips proxy command when skipProxyCommand is set",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
userHostPrefix: "coder.",
noWildcard: true,
workspaceNames: []string{"workspace1"},
skipProxyCommand: true,
waitEnum: "auto",
},
want: []string{"Host coder.workspace1\n"},
notWant: []string{"ProxyCommand", "Host coder.*"},
},
{
name: "no-wildcard suffix SSH options appear in every workspace entry",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
hostnameSuffix: "coder",
noWildcard: true,
workspaceNames: []string{"workspace1", "workspace2"},
sshOptions: []string{"ForwardAgent=yes", "LogLevel=DEBUG"},
waitEnum: "auto",
},
want: []string{
"Host workspace1.coder\n",
"\tForwardAgent=yes\n",
"\tLogLevel=DEBUG\n",
"Host workspace2.coder\n",
},
},
{
name: "wildcard suffix SSH options appear in host block",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
hostnameSuffix: "coder",
sshOptions: []string{"ForwardAgent=yes"},
waitEnum: "auto",
},
want: []string{
"Host *.coder\n",
"\tForwardAgent=yes\n",
},
},
{
name: "no-wildcard with both prefix and suffix generates entries for both",
opts: sshConfigOptions{
coderBinaryPath: "/usr/bin/coder",
globalConfigPath: "/tmp/coder",
userHostPrefix: "coder.",
hostnameSuffix: "testy",
noWildcard: true,
workspaceNames: []string{"workspace1"},
waitEnum: "auto",
},
want: []string{
"Host coder.workspace1\n",
"Host workspace1.testy\n",
"Match host workspace1.testy !exec",
},
notWant: []string{"Host coder.*", "Host *.testy"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
err := tt.opts.writeToBuffer(&buf)
require.NoError(t, err)
got := buf.String()
for _, w := range tt.want {
assert.Contains(t, got, w, "expected substring not found")
}
for _, nw := range tt.notWant {
assert.NotContains(t, got, nw, "unexpected substring found")
}
})
}
}
+129
View File
@@ -14,6 +14,7 @@ import (
"sync"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -24,6 +25,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
"github.com/coder/coder/v2/testutil/expecter"
)
@@ -554,6 +556,45 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
"--header-command", "echo h1=v1 h2=\"v2\" h3='v3'",
},
},
{
name: "Serialize no-wildcard flag",
wantConfig: wantConfig{
ssh: []string{
strings.Join([]string{
headerStart,
"# Last config-ssh options:",
"# :hostname-suffix=coder-suffix",
"# :no-wildcard=true",
"#",
}, "\n"),
strings.Join([]string{
headerEnd,
"",
}, "\n"),
},
},
args: []string{
"--yes",
"--hostname-suffix", "coder-suffix",
"--no-wildcard",
},
},
{
name: "No wildcard generates per-workspace entries",
args: []string{
"--yes",
"--hostname-suffix", "coder",
"--no-wildcard",
},
hasAgent: true,
wantConfig: wantConfig{
ssh: []string{
"# :hostname-suffix=coder",
"# :no-wildcard=true",
},
regexMatch: `Host [a-z0-9_-]+\.coder`,
},
},
{
name: "Do not prompt for new options when prev opts flag is set",
writeConfig: writeConfig{
@@ -811,3 +852,91 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
})
}
}
func TestConfigSSH_NoWildcard(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("See coder/internal#117")
}
ctx := testutil.Context(t, testutil.WaitMedium)
client, db := coderdtest.NewWithDatabase(t, nil)
user := coderdtest.CreateFirstUser(t, client)
// Create two workspaces with names in reverse lexical order so that we can
// verify the SSH config entries are sorted by name, not by creation order.
// ws1 sorts after ws2 alphabetically.
ws1 := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
Name: "ws-beta",
}).WithAgent(func(a []*sdkproto.Agent) []*sdkproto.Agent {
a[0].Name = "agent-beta"
return a
}).Do()
ws2 := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OrganizationID: user.OrganizationID,
OwnerID: user.UserID,
Name: "ws-alpha",
}).WithAgent(func(a []*sdkproto.Agent) []*sdkproto.Agent {
a[0].Name = "agent-alpha"
return a
}).Do()
sshConfigPath := sshConfigFileName(t)
runConfigSSH := func() {
inv, root := clitest.New(t,
"config-ssh",
"--ssh-config-file", sshConfigPath,
"--hostname-suffix", "coder",
"--no-wildcard",
"--yes",
)
//nolint:gocritic // This has always ran with the admin user.
clitest.SetupConfig(t, client, root)
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
}
// hostLines extracts lines beginning with "Host " from the SSH config.
// ProxyCommand lines embed a per-invocation temp path and are excluded so
// that two runs with different global-config dirs can still be compared.
hostLines := func(s string) []string {
var out []string
for line := range strings.SplitSeq(s, "\n") {
if strings.HasPrefix(line, "Host ") {
out = append(out, line)
}
}
return out
}
runConfigSSH()
config := sshConfigFileRead(t, sshConfigPath)
// The server always injects a "coder." hostname prefix in addition to the
// user-supplied "--hostname-suffix coder" entries. With stable workspace
// names we can assert the complete, ordered host-entry list exactly.
// ws-alpha sorts before ws-beta even though ws-alpha was created second.
wantHosts := []string{
"Host coder." + ws2.Workspace.Name, // coder.ws-alpha
"Host coder." + ws1.Workspace.Name, // coder.ws-beta
"Host " + ws2.Workspace.Name + ".coder", // ws-alpha.coder
"Host " + ws1.Workspace.Name + ".coder", // ws-beta.coder
}
require.Empty(t, cmp.Diff(wantHosts, hostLines(config)))
// No wildcard entries must appear in the Coder section.
require.NotContains(t, config, "Host *.coder")
require.NotContains(t, config, "Host *.")
// The no-wildcard option must be persisted in the header.
require.Contains(t, config, "# :no-wildcard=true")
// Running the command again must yield identical host entries, confirming
// that the ordering is stable across runs.
runConfigSSH()
require.Empty(t, cmp.Diff(wantHosts, hostLines(sshConfigFileRead(t, sshConfigPath))))
}
+5
View File
@@ -36,6 +36,11 @@ OPTIONS:
--hostname-suffix string, $CODER_CONFIGSSH_HOSTNAME_SUFFIX
Override the default hostname suffix.
--no-wildcard bool, $CODER_CONFIGSSH_NO_WILDCARD (default: false)
Generate an individual host entry for each workspace instead of a
wildcard host block. This allows third-party tools and SSH clients to
discover workspaces by reading the config file.
--ssh-config-file string, $CODER_SSH_CONFIG_FILE (default: ~/.ssh/config)
Specifies the path to an SSH config.