mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat: support --hostname-suffix flag on coder ssh (#17279)
Adds `hostname-suffix` flag to `coder ssh` command for use in SSH Config ProxyCommands. Also enforces that Coder server doesn't start the suffix with a dot. part of: #16828
This commit is contained in:
@ -620,6 +620,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
return xerrors.Errorf("parse ssh config options %q: %w", vals.SSHConfig.SSHConfigOptions.String(), err)
|
||||
}
|
||||
|
||||
// The workspace hostname suffix is always interpreted as implicitly beginning with a single dot, so it is
|
||||
// a config error to explicitly include the dot. This ensures that we always interpret the suffix as a
|
||||
// separate DNS label, and not just an ordinary string suffix. E.g. a suffix of 'coder' will match
|
||||
// 'en.coder' but not 'encoder'.
|
||||
if strings.HasPrefix(vals.WorkspaceHostnameSuffix.String(), ".") {
|
||||
return xerrors.Errorf("you must omit any leading . in workspace hostname suffix: %s",
|
||||
vals.WorkspaceHostnameSuffix.String())
|
||||
}
|
||||
|
||||
options := &coderd.Options{
|
||||
AccessURL: vals.AccessURL.Value(),
|
||||
AppHostname: appHostname,
|
||||
|
43
cli/ssh.go
43
cli/ssh.go
@ -65,6 +65,7 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
var (
|
||||
stdio bool
|
||||
hostPrefix string
|
||||
hostnameSuffix string
|
||||
forwardAgent bool
|
||||
forwardGPG bool
|
||||
identityAgent string
|
||||
@ -202,10 +203,14 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
parsedEnv = append(parsedEnv, [2]string{k, v})
|
||||
}
|
||||
|
||||
workspaceInput := strings.TrimPrefix(inv.Args[0], hostPrefix)
|
||||
// convert workspace name format into owner/workspace.agent
|
||||
namedWorkspace := normalizeWorkspaceInput(workspaceInput)
|
||||
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, namedWorkspace)
|
||||
deploymentSSHConfig := codersdk.SSHConfigResponse{
|
||||
HostnamePrefix: hostPrefix,
|
||||
HostnameSuffix: hostnameSuffix,
|
||||
}
|
||||
|
||||
workspace, workspaceAgent, err := findWorkspaceAndAgentByHostname(
|
||||
ctx, inv, client,
|
||||
inv.Args[0], deploymentSSHConfig, disableAutostart)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -564,6 +569,12 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
Description: "Strip this prefix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command.",
|
||||
Value: serpent.StringOf(&hostPrefix),
|
||||
},
|
||||
{
|
||||
Flag: "hostname-suffix",
|
||||
Env: "CODER_SSH_HOSTNAME_SUFFIX",
|
||||
Description: "Strip this suffix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command. The suffix must be specified without a leading . character.",
|
||||
Value: serpent.StringOf(&hostnameSuffix),
|
||||
},
|
||||
{
|
||||
Flag: "forward-agent",
|
||||
FlagShorthand: "A",
|
||||
@ -656,6 +667,30 @@ func (r *RootCmd) ssh() *serpent.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// findWorkspaceAndAgentByHostname parses the hostname from the commandline and finds the workspace and agent it
|
||||
// corresponds to, taking into account any name prefixes or suffixes configured (e.g. myworkspace.coder, or
|
||||
// vscode-coder--myusername--myworkspace).
|
||||
func findWorkspaceAndAgentByHostname(
|
||||
ctx context.Context, inv *serpent.Invocation, client *codersdk.Client,
|
||||
hostname string, config codersdk.SSHConfigResponse, disableAutostart bool,
|
||||
) (
|
||||
codersdk.Workspace, codersdk.WorkspaceAgent, error,
|
||||
) {
|
||||
// for suffixes, we don't explicitly get the . and must add it. This is to ensure that the suffix is always
|
||||
// interpreted as a dotted label in DNS names, not just any string suffix. That is, a suffix of 'coder' will
|
||||
// match a hostname like 'en.coder', but not 'encoder'.
|
||||
qualifiedSuffix := "." + config.HostnameSuffix
|
||||
|
||||
switch {
|
||||
case config.HostnamePrefix != "" && strings.HasPrefix(hostname, config.HostnamePrefix):
|
||||
hostname = strings.TrimPrefix(hostname, config.HostnamePrefix)
|
||||
case config.HostnameSuffix != "" && strings.HasSuffix(hostname, qualifiedSuffix):
|
||||
hostname = strings.TrimSuffix(hostname, qualifiedSuffix)
|
||||
}
|
||||
hostname = normalizeWorkspaceInput(hostname)
|
||||
return getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, hostname)
|
||||
}
|
||||
|
||||
// watchAndClose ensures closer is called if the context is canceled or
|
||||
// the workspace reaches the stopped state.
|
||||
//
|
||||
|
@ -1690,8 +1690,21 @@ func TestSSH(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SSHHostPrefix", func(t *testing.T) {
|
||||
t.Run("SSHHost", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name, hostnameFormat string
|
||||
flags []string
|
||||
}{
|
||||
{"Prefix", "coder.dummy.com--%s--%s", []string{"--ssh-host-prefix", "coder.dummy.com--"}},
|
||||
{"Suffix", "%s--%s.coder", []string{"--hostname-suffix", "coder"}},
|
||||
{"Both", "%s--%s.coder", []string{"--hostname-suffix", "coder", "--ssh-host-prefix", "coder.dummy.com--"}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, workspace, agentToken := setupWorkspaceForAgent(t)
|
||||
_, _ = tGoContext(t, func(ctx context.Context) {
|
||||
// Run this async so the SSH command has to wait for
|
||||
@ -1714,7 +1727,10 @@ func TestSSH(t *testing.T) {
|
||||
user, err := client.User(ctx, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
|
||||
inv, root := clitest.New(t, "ssh", "--stdio", "--ssh-host-prefix", "coder.dummy.com--", fmt.Sprintf("coder.dummy.com--%s--%s", user.Username, workspace.Name))
|
||||
args := []string{"ssh", "--stdio"}
|
||||
args = append(args, tc.flags...)
|
||||
args = append(args, fmt.Sprintf(tc.hostnameFormat, user.Username, workspace.Name))
|
||||
inv, root := clitest.New(t, args...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
inv.Stdin = clientOutput
|
||||
inv.Stdout = serverInput
|
||||
@ -1753,6 +1769,8 @@ func TestSSH(t *testing.T) {
|
||||
<-cmdDone
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:paralleltest // This test uses t.Setenv, parent test MUST NOT be parallel.
|
||||
func TestSSH_ForwardGPG(t *testing.T) {
|
||||
|
5
cli/testdata/coder_ssh_--help.golden
vendored
5
cli/testdata/coder_ssh_--help.golden
vendored
@ -23,6 +23,11 @@ OPTIONS:
|
||||
locally and will not be started for you. If a GPG agent is already
|
||||
running in the workspace, it will be attempted to be killed.
|
||||
|
||||
--hostname-suffix string, $CODER_SSH_HOSTNAME_SUFFIX
|
||||
Strip this suffix from the provided hostname to determine the
|
||||
workspace name. This is useful when used as part of an OpenSSH proxy
|
||||
command. The suffix must be specified without a leading . character.
|
||||
|
||||
--identity-agent string, $CODER_SSH_IDENTITY_AGENT
|
||||
Specifies which identity agent to use (overrides $SSH_AUTH_SOCK),
|
||||
forward agent must also be enabled.
|
||||
|
9
docs/reference/cli/ssh.md
generated
9
docs/reference/cli/ssh.md
generated
@ -29,6 +29,15 @@ Specifies whether to emit SSH output over stdin/stdout.
|
||||
|
||||
Strip this prefix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command.
|
||||
|
||||
### --hostname-suffix
|
||||
|
||||
| | |
|
||||
|-------------|-----------------------------------------|
|
||||
| Type | <code>string</code> |
|
||||
| Environment | <code>$CODER_SSH_HOSTNAME_SUFFIX</code> |
|
||||
|
||||
Strip this suffix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command. The suffix must be specified without a leading . character.
|
||||
|
||||
### -A, --forward-agent
|
||||
|
||||
| | |
|
||||
|
Reference in New Issue
Block a user