feat(cli): add `--env` flag for `coder ssh` (#12991)

This allows environment variables to be set on the SSH session.

Example:

   coder ssh myworkspace --env VAR1=val1,VAR2=val2
This commit is contained in:
Aaron Lehmann 2024-04-22 03:13:48 -07:00 committed by GitHub
parent e17e8aa3c9
commit 8a1216254e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 85 additions and 9 deletions

View File

@ -55,6 +55,7 @@ func (r *RootCmd) ssh() *serpent.Command {
noWait bool noWait bool
logDirPath string logDirPath string
remoteForwards []string remoteForwards []string
env []string
disableAutostart bool disableAutostart bool
) )
client := new(codersdk.Client) client := new(codersdk.Client)
@ -144,16 +145,23 @@ func (r *RootCmd) ssh() *serpent.Command {
stack := newCloserStack(ctx, logger) stack := newCloserStack(ctx, logger)
defer stack.close(nil) defer stack.close(nil)
if len(remoteForwards) > 0 { for _, remoteForward := range remoteForwards {
for _, remoteForward := range remoteForwards { isValid := validateRemoteForward(remoteForward)
isValid := validateRemoteForward(remoteForward) if !isValid {
if !isValid { return xerrors.Errorf(`invalid format of remote-forward, expected: remote_port:local_address:local_port`)
return xerrors.Errorf(`invalid format of remote-forward, expected: remote_port:local_address:local_port`)
}
if isValid && stdio {
return xerrors.Errorf(`remote-forward can't be enabled in the stdio mode`)
}
} }
if isValid && stdio {
return xerrors.Errorf(`remote-forward can't be enabled in the stdio mode`)
}
}
var parsedEnv [][2]string
for _, e := range env {
k, v, ok := strings.Cut(e, "=")
if !ok {
return xerrors.Errorf("invalid environment variable setting %q", e)
}
parsedEnv = append(parsedEnv, [2]string{k, v})
} }
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0]) workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
@ -375,6 +383,12 @@ func (r *RootCmd) ssh() *serpent.Command {
}() }()
} }
for _, kv := range parsedEnv {
if err := sshSession.Setenv(kv[0], kv[1]); err != nil {
return xerrors.Errorf("setenv: %w", err)
}
}
err = sshSession.RequestPty("xterm-256color", 128, 128, gossh.TerminalModes{}) err = sshSession.RequestPty("xterm-256color", 128, 128, gossh.TerminalModes{})
if err != nil { if err != nil {
return xerrors.Errorf("request pty: %w", err) return xerrors.Errorf("request pty: %w", err)
@ -483,6 +497,13 @@ func (r *RootCmd) ssh() *serpent.Command {
FlagShorthand: "R", FlagShorthand: "R",
Value: serpent.StringArrayOf(&remoteForwards), Value: serpent.StringArrayOf(&remoteForwards),
}, },
{
Flag: "env",
Description: "Set environment variable(s) for session (key1=value1,key2=value2,...).",
Env: "CODER_SSH_ENV",
FlagShorthand: "e",
Value: serpent.StringArrayOf(&env),
},
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)), sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
} }
return cmd return cmd

View File

@ -968,6 +968,49 @@ func TestSSH(t *testing.T) {
<-cmdDone <-cmdDone
}) })
t.Run("Env", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Test not supported on windows")
}
t.Parallel()
client, workspace, agentToken := setupWorkspaceForAgent(t)
_ = agenttest.New(t, client.URL, agentToken)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
inv, root := clitest.New(t,
"ssh",
workspace.Name,
"--env",
"foo=bar,baz=qux",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
inv.Stderr = pty.Output()
// Wait super long so this doesn't flake on -race test.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
defer cancel()
w := clitest.StartWithWaiter(t, inv.WithContext(ctx))
defer w.Wait() // We don't care about any exit error (exit code 255: SSH connection ended unexpectedly).
// Since something was output, it should be safe to write input.
// This could show a prompt or "running startup scripts", so it's
// not indicative of the SSH connection being ready.
_ = pty.Peek(ctx, 1)
// Ensure the SSH connection is ready by testing the shell
// input/output.
pty.WriteLine("echo $foo $baz")
pty.ExpectMatchContext(ctx, "bar qux")
// And we're done.
pty.WriteLine("exit")
})
t.Run("RemoteForwardUnixSocket", func(t *testing.T) { t.Run("RemoteForwardUnixSocket", func(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("Test not supported on windows") t.Skip("Test not supported on windows")

View File

@ -9,6 +9,9 @@ OPTIONS:
--disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false) --disable-autostart bool, $CODER_SSH_DISABLE_AUTOSTART (default: false)
Disable starting the workspace automatically when connecting via SSH. Disable starting the workspace automatically when connecting via SSH.
-e, --env string-array, $CODER_SSH_ENV
Set environment variable(s) for session (key1=value1,key2=value2,...).
-A, --forward-agent bool, $CODER_SSH_FORWARD_AGENT -A, --forward-agent bool, $CODER_SSH_FORWARD_AGENT
Specifies whether to forward the SSH agent specified in Specifies whether to forward the SSH agent specified in
$SSH_AUTH_SOCK. $SSH_AUTH_SOCK.

9
docs/cli/ssh.md generated
View File

@ -95,6 +95,15 @@ Specify the directory containing SSH diagnostic log files.
Enable remote port forwarding (remote_port:local_address:local_port). Enable remote port forwarding (remote_port:local_address:local_port).
### -e, --env
| | |
| ----------- | --------------------------- |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_SSH_ENV</code> |
Set environment variable(s) for session (key1=value1,key2=value2,...).
### --disable-autostart ### --disable-autostart
| | | | | |