2022-02-10 14:33:27 +00:00
|
|
|
package cli
|
2022-03-22 19:17:50 +00:00
|
|
|
|
|
|
|
import (
|
2022-03-29 00:19:28 +00:00
|
|
|
"context"
|
2022-03-30 22:59:54 +00:00
|
|
|
"io"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"time"
|
2022-03-22 19:17:50 +00:00
|
|
|
|
2022-04-11 21:06:15 +00:00
|
|
|
"github.com/google/uuid"
|
2022-03-30 22:59:54 +00:00
|
|
|
"github.com/mattn/go-isatty"
|
2022-03-29 00:19:28 +00:00
|
|
|
"github.com/pion/webrtc/v3"
|
2022-03-22 19:17:50 +00:00
|
|
|
"github.com/spf13/cobra"
|
2022-03-29 00:19:28 +00:00
|
|
|
gossh "golang.org/x/crypto/ssh"
|
2022-04-01 21:25:46 +00:00
|
|
|
"golang.org/x/term"
|
2022-03-25 19:48:08 +00:00
|
|
|
"golang.org/x/xerrors"
|
2022-03-22 19:17:50 +00:00
|
|
|
|
2022-03-30 22:59:54 +00:00
|
|
|
"github.com/coder/coder/cli/cliflag"
|
2022-03-29 00:19:28 +00:00
|
|
|
"github.com/coder/coder/cli/cliui"
|
2022-03-25 21:07:45 +00:00
|
|
|
"github.com/coder/coder/coderd/database"
|
2022-03-25 19:48:08 +00:00
|
|
|
"github.com/coder/coder/codersdk"
|
2022-03-22 19:17:50 +00:00
|
|
|
)
|
|
|
|
|
2022-03-29 00:19:28 +00:00
|
|
|
func ssh() *cobra.Command {
|
2022-03-30 22:59:54 +00:00
|
|
|
var (
|
|
|
|
stdio bool
|
|
|
|
)
|
2022-03-22 19:17:50 +00:00
|
|
|
cmd := &cobra.Command{
|
2022-04-11 21:06:15 +00:00
|
|
|
Use: "ssh <workspace> [agent]",
|
2022-03-22 19:17:50 +00:00
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
client, err := createClient(cmd)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
|
|
|
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
|
2022-03-22 19:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-03-30 22:59:54 +00:00
|
|
|
if workspace.LatestBuild.Transition != database.WorkspaceTransitionStart {
|
|
|
|
return xerrors.New("workspace must be in start transition to ssh")
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-03-29 00:19:28 +00:00
|
|
|
if workspace.LatestBuild.Job.CompletedAt == nil {
|
2022-03-30 22:59:54 +00:00
|
|
|
err = cliui.WorkspaceBuild(cmd.Context(), cmd.ErrOrStderr(), client, workspace.LatestBuild.ID, workspace.CreatedAt)
|
2022-03-29 00:19:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-03-25 19:48:08 +00:00
|
|
|
if workspace.LatestBuild.Transition == database.WorkspaceTransitionDelete {
|
|
|
|
return xerrors.New("workspace is deleting...")
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-03-22 19:17:50 +00:00
|
|
|
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-04-11 21:06:15 +00:00
|
|
|
agents := make([]codersdk.WorkspaceAgent, 0)
|
2022-03-22 19:17:50 +00:00
|
|
|
for _, resource := range resources {
|
2022-04-11 21:06:15 +00:00
|
|
|
agents = append(agents, resource.Agents...)
|
2022-03-25 19:48:08 +00:00
|
|
|
}
|
2022-04-11 21:06:15 +00:00
|
|
|
if len(agents) == 0 {
|
|
|
|
return xerrors.New("workspace has no agents")
|
|
|
|
}
|
|
|
|
var agent codersdk.WorkspaceAgent
|
2022-03-25 19:48:08 +00:00
|
|
|
if len(args) >= 2 {
|
2022-04-11 21:06:15 +00:00
|
|
|
for _, otherAgent := range agents {
|
|
|
|
if otherAgent.Name != args[1] {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
agent = otherAgent
|
2022-03-25 19:48:08 +00:00
|
|
|
break
|
2022-03-22 19:17:50 +00:00
|
|
|
}
|
2022-04-11 21:06:15 +00:00
|
|
|
if agent.ID == uuid.Nil {
|
|
|
|
return xerrors.Errorf("agent not found by name %q", args[1])
|
|
|
|
}
|
2022-03-25 19:48:08 +00:00
|
|
|
}
|
2022-04-11 21:06:15 +00:00
|
|
|
if agent.ID == uuid.Nil {
|
|
|
|
if len(agents) > 1 {
|
|
|
|
return xerrors.New("you must specify the name of an agent")
|
2022-03-22 19:17:50 +00:00
|
|
|
}
|
2022-04-11 21:06:15 +00:00
|
|
|
agent = agents[0]
|
2022-03-25 19:48:08 +00:00
|
|
|
}
|
2022-03-30 22:59:54 +00:00
|
|
|
// OpenSSH passes stderr directly to the calling TTY.
|
|
|
|
// This is required in "stdio" mode so a connecting indicator can be displayed.
|
|
|
|
err = cliui.Agent(cmd.Context(), cmd.ErrOrStderr(), cliui.AgentOptions{
|
2022-03-29 00:19:28 +00:00
|
|
|
WorkspaceName: workspace.Name,
|
2022-04-11 21:06:15 +00:00
|
|
|
Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
|
|
|
|
return client.WorkspaceAgent(ctx, agent.ID)
|
2022-03-29 00:19:28 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("await agent: %w", err)
|
2022-03-25 19:48:08 +00:00
|
|
|
}
|
2022-03-22 19:17:50 +00:00
|
|
|
|
2022-04-11 21:06:15 +00:00
|
|
|
conn, err := client.DialWorkspaceAgent(cmd.Context(), agent.ID, []webrtc.ICEServer{{
|
2022-03-29 00:19:28 +00:00
|
|
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
|
|
|
}}, nil)
|
2022-03-25 19:48:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-03-29 00:19:28 +00:00
|
|
|
defer conn.Close()
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-03-30 22:59:54 +00:00
|
|
|
if stdio {
|
|
|
|
rawSSH, err := conn.SSH()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
_, _ = io.Copy(cmd.OutOrStdout(), rawSSH)
|
|
|
|
}()
|
|
|
|
_, _ = io.Copy(rawSSH, cmd.InOrStdin())
|
|
|
|
return nil
|
|
|
|
}
|
2022-03-25 19:48:08 +00:00
|
|
|
sshClient, err := conn.SSHClient()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sshSession, err := sshClient.NewSession()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-03-29 00:19:28 +00:00
|
|
|
|
2022-03-30 22:59:54 +00:00
|
|
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
2022-04-01 21:25:46 +00:00
|
|
|
state, err := term.MakeRaw(int(os.Stdin.Fd()))
|
2022-03-30 22:59:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer func() {
|
2022-04-01 21:25:46 +00:00
|
|
|
_ = term.Restore(int(os.Stdin.Fd()), state)
|
2022-03-30 22:59:54 +00:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
err = sshSession.RequestPty("xterm-256color", 128, 128, gossh.TerminalModes{})
|
2022-03-25 19:48:08 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-03-29 00:19:28 +00:00
|
|
|
sshSession.Stdin = cmd.InOrStdin()
|
|
|
|
sshSession.Stdout = cmd.OutOrStdout()
|
|
|
|
sshSession.Stderr = cmd.OutOrStdout()
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-03-25 19:48:08 +00:00
|
|
|
err = sshSession.Shell()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-04-01 19:42:36 +00:00
|
|
|
|
2022-03-25 19:48:08 +00:00
|
|
|
err = sshSession.Wait()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2022-03-22 19:17:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
2022-03-30 22:59:54 +00:00
|
|
|
cliflag.BoolVarP(cmd.Flags(), &stdio, "stdio", "", "CODER_SSH_STDIO", false, "Specifies whether to emit SSH output over stdin/stdout.")
|
2022-03-22 19:17:50 +00:00
|
|
|
|
|
|
|
return cmd
|
|
|
|
}
|
2022-03-30 22:59:54 +00:00
|
|
|
|
|
|
|
type stdioConn struct {
|
|
|
|
io.Reader
|
|
|
|
io.Writer
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*stdioConn) Close() (err error) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*stdioConn) LocalAddr() net.Addr {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*stdioConn) RemoteAddr() net.Addr {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*stdioConn) SetDeadline(_ time.Time) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*stdioConn) SetReadDeadline(_ time.Time) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (*stdioConn) SetWriteDeadline(_ time.Time) error {
|
|
|
|
return nil
|
|
|
|
}
|