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: add one shot commands to the coder ssh command#17779

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
brettkolodny merged 11 commits intomainfrombett-feat/add-ssh-one-shot-command
May 16, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
11 commits
Select commitHold shift + click to select a range
594e4b5
feat: allow one shot commands using coder ssh
brettkolodnyMay 12, 2025
bed4103
chore: improve ssh missing args error message
brettkolodnyMay 12, 2025
4026c07
fix: lint
brettkolodnyMay 12, 2025
55735fc
fix: gen
brettkolodnyMay 12, 2025
aaaab01
fix: update command to work on both unix and windows
brettkolodnyMay 13, 2025
d4feb6c
Merge branch 'main' into bett-feat/add-ssh-one-shot-command
brettkolodnyMay 13, 2025
19fa5f9
Merge branch 'main' into bett-feat/add-ssh-one-shot-command
brettkolodnyMay 14, 2025
c25d27f
Merge branch 'main' into bett-feat/add-ssh-one-shot-command
brettkolodnyMay 15, 2025
ca640b5
chore: add example to ssh help documentation
brettkolodnyMay 16, 2025
e0a3e7a
chore: make gen
brettkolodnyMay 16, 2025
bfd941d
Merge branch 'main' into bett-feat/add-ssh-one-shot-command
brettkolodnyMay 16, 2025
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
88 changes: 56 additions & 32 deletionscli/ssh.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -90,15 +90,33 @@ func (r *RootCmd) ssh() *serpent.Command {
wsClient := workspacesdk.New(client)
cmd := &serpent.Command{
Annotations: workspaceCommand,
Use: "ssh <workspace>",
Short: "Start a shell into a workspace",
Long: "This command does not have full parity with the standard SSH command. For users who need the full functionality of SSH, create an ssh configuration with `coder config-ssh`.",
Use: "ssh <workspace> [command]",
Short: "Start a shell into a workspace or run a command",
Long: "This command does not have full parity with the standard SSH command. For users who need the full functionality of SSH, create an ssh configuration with `coder config-ssh`.\n\n" +
FormatExamples(
Example{
Description: "Use `--` to separate and pass flags directly to the command executed via SSH.",
Command: "coder ssh <workspace> -- ls -la",
},
),
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
// Require at least one arg for the workspace name
func(next serpent.HandlerFunc) serpent.HandlerFunc {
return func(i *serpent.Invocation) error {
got := len(i.Args)
if got < 1 {
return xerrors.New("expected the name of a workspace")
}

return next(i)
}
},
Comment on lines +104 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Keep this here, but we should add a serpent MW forRequireMinArgs(n). I also wanted it at some point.

brettkolodny reacted with thumbs up emoji
r.InitClient(client),
initAppearance(client, &appearanceConfig),
),
Handler: func(inv *serpent.Invocation) (retErr error) {
command := strings.Join(inv.Args[1:], " ")

// Before dialing the SSH server over TCP, capture Interrupt signals
// so that if we are interrupted, we have a chance to tear down the
// TCP session cleanly before exiting. If we don't, then the TCP
Expand DownExpand Up@@ -548,40 +566,46 @@ func (r *RootCmd) ssh() *serpent.Command {
sshSession.Stdout = inv.Stdout
sshSession.Stderr = inv.Stderr

err = sshSession.Shell()
if err != nil {
return xerrors.Errorf("start shell: %w", err)
}
if command != "" {
err := sshSession.Run(command)
if err != nil {
return xerrors.Errorf("run command: %w", err)
}
} else {
err = sshSession.Shell()
if err != nil {
return xerrors.Errorf("start shell: %w", err)
}

// Put cancel at the top of the defer stack to initiate
// shutdown of services.
defer cancel()
// Put cancel at the top of the defer stack to initiate
// shutdown of services.
defer cancel()

if validOut {
// Set initial window size.
width, height, err := term.GetSize(int(stdoutFile.Fd()))
if err == nil {
_ = sshSession.WindowChange(height, width)
if validOut {
// Set initial window size.
width, height, err := term.GetSize(int(stdoutFile.Fd()))
if err == nil {
_ = sshSession.WindowChange(height, width)
}
}
}

err = sshSession.Wait()
conn.SendDisconnectedTelemetry()
if err != nil {
if exitErr := (&gossh.ExitError{}); errors.As(err, &exitErr) {
// Clear the error since it's not useful beyond
// reporting status.
return ExitError(exitErr.ExitStatus(), nil)
}
// If the connection drops unexpectedly, we get an
// ExitMissingError but no other error details, so try to at
// least give the user a better message
if errors.Is(err, &gossh.ExitMissingError{}) {
return ExitError(255, xerrors.New("SSH connection ended unexpectedly"))
err = sshSession.Wait()
conn.SendDisconnectedTelemetry()
if err != nil {
if exitErr := (&gossh.ExitError{}); errors.As(err, &exitErr) {
// Clear the error since it's not useful beyond
// reporting status.
return ExitError(exitErr.ExitStatus(), nil)
}
// If the connection drops unexpectedly, we get an
// ExitMissingError but no other error details, so try to at
// least give the user a better message
if errors.Is(err, &gossh.ExitMissingError{}) {
return ExitError(255, xerrors.New("SSH connection ended unexpectedly"))
}
return xerrors.Errorf("session ended: %w", err)
}
return xerrors.Errorf("session ended: %w", err)
}

return nil
},
}
Expand Down
121 changes: 121 additions & 0 deletionscli/ssh_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2200,6 +2200,127 @@ func TestSSH_CoderConnect(t *testing.T) {

<-cmdDone
})

t.Run("OneShot", func(t *testing.T) {
t.Parallel()

client, workspace, agentToken := setupWorkspaceForAgent(t)
inv, root := clitest.New(t, "ssh", workspace.Name, "echo 'hello world'")
clitest.SetupConfig(t, client, root)

// Capture command output
output := new(bytes.Buffer)
inv.Stdout = output

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

cmdDone := tGo(t, func() {
err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})

_ = agenttest.New(t, client.URL, agentToken)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)

<-cmdDone

// Verify command output
assert.Contains(t, output.String(), "hello world")
})

t.Run("OneShotExitCode", func(t *testing.T) {
t.Parallel()

client, workspace, agentToken := setupWorkspaceForAgent(t)

// Setup agent first to avoid race conditions
_ = agenttest.New(t, client.URL, agentToken)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

// Test successful exit code
t.Run("Success", func(t *testing.T) {
inv, root := clitest.New(t, "ssh", workspace.Name, "exit 0")
clitest.SetupConfig(t, client, root)

err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})

// Test error exit code
t.Run("Error", func(t *testing.T) {
inv, root := clitest.New(t, "ssh", workspace.Name, "exit 1")
clitest.SetupConfig(t, client, root)

err := inv.WithContext(ctx).Run()
assert.Error(t, err)
var exitErr *ssh.ExitError
assert.True(t, errors.As(err, &exitErr))
assert.Equal(t, 1, exitErr.ExitStatus())
})
})

t.Run("OneShotStdio", 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()
}
}()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

inv, root := clitest.New(t, "ssh", "--stdio", workspace.Name, "echo 'hello stdio'")
clitest.SetupConfig(t, client, root)
inv.Stdin = clientOutput
inv.Stdout = serverInput
inv.Stderr = io.Discard

cmdDone := tGo(t, func() {
err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})

conn, channels, requests, err := ssh.NewClientConn(&testutil.ReaderWriterConn{
Reader: serverOutput,
Writer: clientInput,
}, "", &ssh.ClientConfig{
// #nosec
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
require.NoError(t, err)
defer conn.Close()

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

// Capture and verify command output
output, err := session.Output("echo 'hello back'")
require.NoError(t, err)
assert.Contains(t, string(output), "hello back")

err = sshClient.Close()
require.NoError(t, err)
_ = clientOutput.Close()

<-cmdDone
})
}

type fakeCoderConnectDialer struct{}
Expand Down
2 changes: 1 addition & 1 deletioncli/testdata/coder_--help.golden
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,7 +46,7 @@ SUBCOMMANDS:
show Display details of a workspace's resources and agents
speedtest Run upload and download tests from your machine to a
workspace
ssh Start a shell into a workspace
ssh Start a shell into a workspace or run a command
start Start a workspace
stat Show resource usage for the current workspace.
state Manually manage Terraform state to fix broken workspaces
Expand Down
9 changes: 7 additions & 2 deletionscli/testdata/coder_ssh_--help.golden
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
coder v0.0.0-devel

USAGE:
coder ssh [flags] <workspace>
coder ssh [flags] <workspace> [command]

Start a shell into a workspace
Start a shell into a workspace or run a command

This command does not have full parity with the standard SSH command. For
users who need the full functionality of SSH, create an ssh configuration with
`coder config-ssh`.

- Use `--` to separate and pass flags directly to the command executed via
SSH.:

$ coder ssh <workspace> -- ls -la

OPTIONS:
--disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false)
Expand Down
2 changes: 1 addition & 1 deletiondocs/manifest.json
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1460,7 +1460,7 @@
},
{
"title": "ssh",
"description": "Start a shell into a workspace",
"description": "Start a shell into a workspace or run a command",
"path": "reference/cli/ssh.md"
},
{
Expand Down
2 changes: 1 addition & 1 deletiondocs/reference/cli/index.md
View file
Open in desktop

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

8 changes: 6 additions & 2 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