mirror of https://github.com/coder/coder.git
feat: Add support for VS Code and JetBrains Gateway via SSH (#956)
* Improve CLI documentation * feat: Allow workspace resources to attach multiple agents This enables a "kubernetes_pod" to attach multiple agents that could be for multiple services. Each agent is required to have a unique name, so SSH syntax is: `coder ssh <workspace>.<agent>` A resource can have zero agents too, they aren't required. * Add tree view * Improve table UI * feat: Allow workspace resources to attach multiple agents This enables a "kubernetes_pod" to attach multiple agents that could be for multiple services. Each agent is required to have a unique name, so SSH syntax is: `coder ssh <workspace>.<agent>` A resource can have zero agents too, they aren't required. * Rename `tunnel` to `skip-tunnel` This command was `true` by default, which causes a confusing user experience. * Add disclaimer about editing templates * Add help to template create * Improve workspace create flow * Add end-to-end test for config-ssh * Improve testing of config-ssh * Fix workspace list * feat: Add support for VS Code and JetBrains Gateway via SSH This fixes various bugs that made this not work: - Incorrect max message size in `peer` - Incorrect reader buffer size in `peer` - Lack of SFTP support in `agent` - Lack of direct-tcpip support in `agent` - Misuse of command from session. It should always use the shell - Blocking on SSH session, only allowing one at a time Fixes #833 too. * Fix config-ssh command with socat
This commit is contained in:
parent
fb9dc4f346
commit
e8b1a57929
|
@ -158,6 +158,10 @@ jobs:
|
|||
terraform_version: 1.1.2
|
||||
terraform_wrapper: false
|
||||
|
||||
- name: Install socat
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get install -y socat
|
||||
|
||||
- name: Test with Mock Database
|
||||
shell: bash
|
||||
env:
|
||||
|
|
|
@ -21,6 +21,8 @@ import (
|
|||
"github.com/coder/coder/pty"
|
||||
"github.com/coder/retry"
|
||||
|
||||
"github.com/pkg/sftp"
|
||||
|
||||
"github.com/gliderlabs/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"golang.org/x/xerrors"
|
||||
|
@ -121,7 +123,7 @@ func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) {
|
|||
|
||||
switch channel.Protocol() {
|
||||
case "ssh":
|
||||
a.sshServer.HandleConn(channel.NetConn())
|
||||
go a.sshServer.HandleConn(channel.NetConn())
|
||||
default:
|
||||
a.options.Logger.Warn(ctx, "unhandled protocol from channel",
|
||||
slog.F("protocol", channel.Protocol()),
|
||||
|
@ -146,7 +148,10 @@ func (a *agent) init(ctx context.Context) {
|
|||
sshLogger := a.options.Logger.Named("ssh-server")
|
||||
forwardHandler := &ssh.ForwardedTCPHandler{}
|
||||
a.sshServer = &ssh.Server{
|
||||
ChannelHandlers: ssh.DefaultChannelHandlers,
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"direct-tcpip": ssh.DirectTCPIPHandler,
|
||||
"session": ssh.DefaultSessionHandler,
|
||||
},
|
||||
ConnectionFailedCallback: func(conn net.Conn, err error) {
|
||||
sshLogger.Info(ctx, "ssh connection ended", slog.Error(err))
|
||||
},
|
||||
|
@ -185,61 +190,54 @@ func (a *agent) init(ctx context.Context) {
|
|||
NoClientAuth: true,
|
||||
}
|
||||
},
|
||||
SubsystemHandlers: map[string]ssh.SubsystemHandler{
|
||||
"sftp": func(session ssh.Session) {
|
||||
server, err := sftp.NewServer(session)
|
||||
if err != nil {
|
||||
a.options.Logger.Debug(session.Context(), "initialize sftp server", slog.Error(err))
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
err = server.Serve()
|
||||
if errors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
a.options.Logger.Debug(session.Context(), "sftp server exited with error", slog.Error(err))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
go a.run(ctx)
|
||||
}
|
||||
|
||||
func (a *agent) handleSSHSession(session ssh.Session) error {
|
||||
var (
|
||||
command string
|
||||
args = []string{}
|
||||
err error
|
||||
)
|
||||
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get current user: %w", err)
|
||||
}
|
||||
username := currentUser.Username
|
||||
|
||||
// gliderlabs/ssh returns a command slice of zero
|
||||
// when a shell is requested.
|
||||
if len(session.Command()) == 0 {
|
||||
command, err = usershell.Get(username)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get user shell: %w", err)
|
||||
}
|
||||
} else {
|
||||
command = session.Command()[0]
|
||||
if len(session.Command()) > 1 {
|
||||
args = session.Command()[1:]
|
||||
}
|
||||
shell, err := usershell.Get(username)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get user shell: %w", err)
|
||||
}
|
||||
|
||||
signals := make(chan ssh.Signal)
|
||||
breaks := make(chan bool)
|
||||
defer close(signals)
|
||||
defer close(breaks)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-session.Context().Done():
|
||||
return
|
||||
// Ignore signals and breaks for now!
|
||||
case <-signals:
|
||||
case <-breaks:
|
||||
}
|
||||
}
|
||||
}()
|
||||
// gliderlabs/ssh returns a command slice of zero
|
||||
// when a shell is requested.
|
||||
command := session.RawCommand()
|
||||
if len(session.Command()) == 0 {
|
||||
command = shell
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(session.Context(), command, args...)
|
||||
// OpenSSH executes all commands with the users current shell.
|
||||
// We replicate that behavior for IDE support.
|
||||
cmd := exec.CommandContext(session.Context(), shell, "-c", command)
|
||||
cmd.Env = append(os.Environ(), session.Environ()...)
|
||||
executablePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("getting os executable: %w", err)
|
||||
}
|
||||
cmd.Env = append(session.Environ(), fmt.Sprintf(`GIT_SSH_COMMAND="%s gitssh --"`, executablePath))
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_SSH_COMMAND="%s gitssh --"`, executablePath))
|
||||
|
||||
sshPty, windowSize, isPty := session.Pty()
|
||||
if isPty {
|
||||
|
@ -268,7 +266,7 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
|
|||
}
|
||||
|
||||
cmd.Stdout = session
|
||||
cmd.Stderr = session
|
||||
cmd.Stderr = session.Stderr()
|
||||
// This blocks forever until stdin is received if we don't
|
||||
// use StdinPipe. It's unknown what causes this.
|
||||
stdinPipe, err := cmd.StdinPipe()
|
||||
|
@ -282,8 +280,7 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
|
|||
if err != nil {
|
||||
return xerrors.Errorf("start: %w", err)
|
||||
}
|
||||
_ = cmd.Wait()
|
||||
return nil
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
// isClosed returns whether the API is closed or not.
|
||||
|
|
|
@ -5,13 +5,16 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -94,6 +97,7 @@ func TestAgent(t *testing.T) {
|
|||
|
||||
local, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer local.Close()
|
||||
tcpAddr, valid = local.Addr().(*net.TCPAddr)
|
||||
require.True(t, valid)
|
||||
localPort := tcpAddr.Port
|
||||
|
@ -113,6 +117,21 @@ func TestAgent(t *testing.T) {
|
|||
conn.Close()
|
||||
<-done
|
||||
})
|
||||
|
||||
t.Run("SFTP", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
sshClient, err := setupAgent(t).SSHClient()
|
||||
require.NoError(t, err)
|
||||
client, err := sftp.NewClient(sshClient)
|
||||
require.NoError(t, err)
|
||||
tempFile := filepath.Join(t.TempDir(), "sftp")
|
||||
file, err := client.Create(tempFile)
|
||||
require.NoError(t, err)
|
||||
err = file.Close()
|
||||
require.NoError(t, err)
|
||||
_, err = os.Stat(tempFile)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {
|
||||
|
|
|
@ -4,11 +4,12 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/cli/cliui"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/pty/ptytest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWorkspaceResources(t *testing.T) {
|
||||
|
|
16
go.mod
16
go.mod
|
@ -48,6 +48,7 @@ require (
|
|||
github.com/fatih/color v1.13.0
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/httprate v0.5.3
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/go-playground/validator/v10 v10.10.1
|
||||
github.com/gohugoio/hugo v0.96.0
|
||||
|
@ -60,6 +61,7 @@ require (
|
|||
github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f
|
||||
github.com/hashicorp/terraform-exec v0.15.0
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87
|
||||
github.com/jedib0t/go-pretty/v6 v6.3.0
|
||||
github.com/justinas/nosurf v1.1.1
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
|
||||
github.com/lib/pq v1.10.5
|
||||
|
@ -72,7 +74,9 @@ require (
|
|||
github.com/pion/transport v0.13.0
|
||||
github.com/pion/webrtc/v3 v3.1.27
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/pkg/sftp v1.13.4
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.19
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
|
@ -83,20 +87,19 @@ require (
|
|||
go.uber.org/atomic v1.9.0
|
||||
go.uber.org/goleak v1.1.12
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
|
||||
golang.org/x/mod v0.5.1
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
|
||||
google.golang.org/api v0.74.0
|
||||
google.golang.org/protobuf v1.28.0
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.37.1
|
||||
nhooyr.io/websocket v1.8.7
|
||||
storj.io/drpc v0.0.30
|
||||
)
|
||||
|
||||
require github.com/go-chi/httprate v0.5.3
|
||||
|
||||
require github.com/jedib0t/go-pretty/v6 v6.3.0
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/BurntSushi/toml v1.0.0 // indirect
|
||||
|
@ -171,6 +174,7 @@ require (
|
|||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.15.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.25.1-0.20220307142123-ad1cb27c1b64 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
|
@ -222,7 +226,6 @@ require (
|
|||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/afero v1.8.1 // indirect
|
||||
|
@ -235,16 +238,13 @@ require (
|
|||
github.com/zclconf/go-cty v1.10.0 // indirect
|
||||
github.com/zeebo/errs v1.2.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/mod v0.5.1
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
golang.org/x/tools v0.1.9 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb // indirect
|
||||
google.golang.org/grpc v1.45.0 // indirect
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.37.1
|
||||
gopkg.in/coreos/go-oidc.v2 v2.2.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
|
|
3
go.sum
3
go.sum
|
@ -1103,6 +1103,7 @@ github.com/klauspost/crc32 v1.2.0/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3H
|
|||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
|
@ -1438,6 +1439,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg=
|
||||
github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
|
|
@ -21,7 +21,7 @@ const (
|
|||
// For some reason messages larger just don't work...
|
||||
// This shouldn't be a huge deal for real-world usage.
|
||||
// See: https://github.com/pion/datachannel/issues/59
|
||||
maxMessageLength = 32 * 1024 // 32 KB
|
||||
maxMessageLength = 64 * 1024 // 64 KB
|
||||
)
|
||||
|
||||
// newChannel creates a new channel and initializes it.
|
||||
|
@ -145,7 +145,9 @@ func (c *Channel) init() {
|
|||
if c.opts.Unordered {
|
||||
c.reader = c.rwc
|
||||
} else {
|
||||
c.reader = bufio.NewReader(c.rwc)
|
||||
// This must be the max message length otherwise a short
|
||||
// buffer error can occur.
|
||||
c.reader = bufio.NewReaderSize(c.rwc, maxMessageLength)
|
||||
}
|
||||
close(c.opened)
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue