Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: support --hostname-suffix flag on coder ssh#17279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletionscli/server.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand Down
43 changes: 39 additions & 4 deletionscli/ssh.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -65,6 +65,7 @@ func (r *RootCmd) ssh() *serpent.Command {
var (
stdio bool
hostPrefix string
hostnameSuffix string
forwardAgent bool
forwardGPG bool
identityAgent string
Expand DownExpand Up@@ -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
}
Expand DownExpand Up@@ -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",
Expand DownExpand Up@@ -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.
//
Expand Down
120 changes: 69 additions & 51 deletionscli/ssh_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1690,67 +1690,85 @@ func TestSSH(t *testing.T) {
}
})

t.Run("SSHHostPrefix", func(t *testing.T) {
t.Run("SSHHost", 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
// the build and agent to connect!
_ = agenttest.New(t, client.URL, agentToken)
<-ctx.Done()
})

clientOutput, clientInput := io.Pipe()
serverOutput, serverInput := io.Pipe()
defer func() {
for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} {
_ = c.Close()
}
}()
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()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
client, workspace, agentToken := setupWorkspaceForAgent(t)
_, _ = tGoContext(t, func(ctx context.Context) {
// Run this async so the SSH command has to wait for
// the build and agent to connect!
_ = agenttest.New(t, client.URL, agentToken)
<-ctx.Done()
})

user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
clientOutput, clientInput := io.Pipe()
serverOutput, serverInput := io.Pipe()
defer func() {
for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} {
_ = c.Close()
}
}()

inv, root := clitest.New(t, "ssh", "--stdio", "--ssh-host-prefix", "coder.dummy.com--", fmt.Sprintf("coder.dummy.com--%s--%s", user.Username, workspace.Name))
clitest.SetupConfig(t, client, root)
inv.Stdin = clientOutput
inv.Stdout = serverInput
inv.Stderr = io.Discard
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

cmdDone := tGo(t, func() {
err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)

conn, channels, requests, err := ssh.NewClientConn(&stdioConn{
Reader: serverOutput,
Writer: clientInput,
}, "", &ssh.ClientConfig{
// #nosec
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
require.NoError(t, err)
defer conn.Close()
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
inv.Stderr = io.Discard

sshClient :=ssh.NewClient(conn, channels, requests)
session,err :=sshClient.NewSession()
require.NoError(t, err)
defer session.Close()
cmdDone :=tGo(t, func() {
err :=inv.WithContext(ctx).Run()
assert.NoError(t, err)
})

command := "sh -c exit"
if runtime.GOOS == "windows" {
command = "cmd.exe /c exit"
}
err = session.Run(command)
require.NoError(t, err)
err = sshClient.Close()
require.NoError(t, err)
_ = clientOutput.Close()
conn, channels, requests, err := ssh.NewClientConn(&stdioConn{
Reader: serverOutput,
Writer: clientInput,
}, "", &ssh.ClientConfig{
// #nosec
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
require.NoError(t, err)
defer conn.Close()

<-cmdDone
sshClient := ssh.NewClient(conn, channels, requests)
session, err := sshClient.NewSession()
require.NoError(t, err)
defer session.Close()

command := "sh -c exit"
if runtime.GOOS == "windows" {
command = "cmd.exe /c exit"
}
err = session.Run(command)
require.NoError(t, err)
err = sshClient.Close()
require.NoError(t, err)
_ = clientOutput.Close()

<-cmdDone
})
}
})
}

Expand Down
5 changes: 5 additions & 0 deletionscli/testdata/coder_ssh_--help.golden
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
9 changes: 9 additions & 0 deletionsdocs/reference/cli/ssh.md
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

Loading

[8]ページ先頭

©2009-2025 Movatter.jp