mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(cli): allow SSH command to connect to running container (#16726)
Fixes https://github.com/coder/coder/issues/16709 and https://github.com/coder/coder/issues/16420 Adds the capability to`coder ssh` into a running container if `CODER_AGENT_DEVCONTAINERS_ENABLE=true`. Notes: * SFTP is currently not supported * Haven't tested X11 container forwarding * Haven't tested agent forwarding
This commit is contained in:
+7
-5
@@ -91,8 +91,8 @@ type Options struct {
|
||||
Execer agentexec.Execer
|
||||
ContainerLister agentcontainers.Lister
|
||||
|
||||
ExperimentalContainersEnabled bool
|
||||
ExperimentalConnectionReports bool
|
||||
ExperimentalConnectionReports bool
|
||||
ExperimentalDevcontainersEnabled bool
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
@@ -156,7 +156,7 @@ func New(options Options) Agent {
|
||||
options.Execer = agentexec.DefaultExecer
|
||||
}
|
||||
if options.ContainerLister == nil {
|
||||
options.ContainerLister = agentcontainers.NewDocker(options.Execer)
|
||||
options.ContainerLister = agentcontainers.NoopLister{}
|
||||
}
|
||||
|
||||
hardCtx, hardCancel := context.WithCancel(context.Background())
|
||||
@@ -195,7 +195,7 @@ func New(options Options) Agent {
|
||||
execer: options.Execer,
|
||||
lister: options.ContainerLister,
|
||||
|
||||
experimentalDevcontainersEnabled: options.ExperimentalContainersEnabled,
|
||||
experimentalDevcontainersEnabled: options.ExperimentalDevcontainersEnabled,
|
||||
experimentalConnectionReports: options.ExperimentalConnectionReports,
|
||||
}
|
||||
// Initially, we have a closed channel, reflecting the fact that we are not initially connected.
|
||||
@@ -307,6 +307,8 @@ func (a *agent) init() {
|
||||
|
||||
return a.reportConnection(id, connectionType, ip)
|
||||
},
|
||||
|
||||
ExperimentalDevContainersEnabled: a.experimentalDevcontainersEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -335,7 +337,7 @@ func (a *agent) init() {
|
||||
a.metrics.connectionsTotal, a.metrics.reconnectingPTYErrors,
|
||||
a.reconnectingPTYTimeout,
|
||||
func(s *reconnectingpty.Server) {
|
||||
s.ExperimentalContainersEnabled = a.experimentalDevcontainersEnabled
|
||||
s.ExperimentalDevcontainersEnabled = a.experimentalDevcontainersEnabled
|
||||
},
|
||||
)
|
||||
go a.runLoop()
|
||||
|
||||
+1
-1
@@ -1841,7 +1841,7 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
|
||||
|
||||
// nolint: dogsled
|
||||
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) {
|
||||
o.ExperimentalContainersEnabled = true
|
||||
o.ExperimentalDevcontainersEnabled = true
|
||||
})
|
||||
ac, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "/bin/sh", func(arp *workspacesdk.AgentReconnectingPTYInit) {
|
||||
arp.Container = ct.Container.ID
|
||||
|
||||
+58
-12
@@ -29,6 +29,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/v2/agent/agentcontainers"
|
||||
"github.com/coder/coder/v2/agent/agentexec"
|
||||
"github.com/coder/coder/v2/agent/agentrsa"
|
||||
"github.com/coder/coder/v2/agent/usershell"
|
||||
@@ -60,6 +61,14 @@ const (
|
||||
// MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection.
|
||||
// This is stripped from any commands being executed, and is counted towards connection stats.
|
||||
MagicSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE"
|
||||
// ContainerEnvironmentVariable is used to specify the target container for an SSH connection.
|
||||
// This is stripped from any commands being executed.
|
||||
// Only available if CODER_AGENT_DEVCONTAINERS_ENABLE=true.
|
||||
ContainerEnvironmentVariable = "CODER_CONTAINER"
|
||||
// ContainerUserEnvironmentVariable is used to specify the container user for
|
||||
// an SSH connection.
|
||||
// Only available if CODER_AGENT_DEVCONTAINERS_ENABLE=true.
|
||||
ContainerUserEnvironmentVariable = "CODER_CONTAINER_USER"
|
||||
)
|
||||
|
||||
// MagicSessionType enums.
|
||||
@@ -104,6 +113,9 @@ type Config struct {
|
||||
BlockFileTransfer bool
|
||||
// ReportConnection.
|
||||
ReportConnection reportConnectionFunc
|
||||
// Experimental: allow connecting to running containers if
|
||||
// CODER_AGENT_DEVCONTAINERS_ENABLE=true.
|
||||
ExperimentalDevContainersEnabled bool
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
@@ -324,6 +336,22 @@ func (s *sessionCloseTracker) Close() error {
|
||||
return s.Session.Close()
|
||||
}
|
||||
|
||||
func extractContainerInfo(env []string) (container, containerUser string, filteredEnv []string) {
|
||||
for _, kv := range env {
|
||||
if strings.HasPrefix(kv, ContainerEnvironmentVariable+"=") {
|
||||
container = strings.TrimPrefix(kv, ContainerEnvironmentVariable+"=")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(kv, ContainerUserEnvironmentVariable+"=") {
|
||||
containerUser = strings.TrimPrefix(kv, ContainerUserEnvironmentVariable+"=")
|
||||
}
|
||||
}
|
||||
|
||||
return container, containerUser, slices.DeleteFunc(env, func(kv string) bool {
|
||||
return strings.HasPrefix(kv, ContainerEnvironmentVariable+"=") || strings.HasPrefix(kv, ContainerUserEnvironmentVariable+"=")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) sessionHandler(session ssh.Session) {
|
||||
ctx := session.Context()
|
||||
id := uuid.New()
|
||||
@@ -353,6 +381,7 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
||||
defer s.trackSession(session, false)
|
||||
|
||||
reportSession := true
|
||||
|
||||
switch magicType {
|
||||
case MagicSessionTypeVSCode:
|
||||
s.connCountVSCode.Add(1)
|
||||
@@ -395,9 +424,22 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
||||
return
|
||||
}
|
||||
|
||||
container, containerUser, env := extractContainerInfo(env)
|
||||
if container != "" {
|
||||
s.logger.Debug(ctx, "container info",
|
||||
slog.F("container", container),
|
||||
slog.F("container_user", containerUser),
|
||||
)
|
||||
}
|
||||
|
||||
switch ss := session.Subsystem(); ss {
|
||||
case "":
|
||||
case "sftp":
|
||||
if s.config.ExperimentalDevContainersEnabled && container != "" {
|
||||
closeCause("sftp not yet supported with containers")
|
||||
_ = session.Exit(1)
|
||||
return
|
||||
}
|
||||
err := s.sftpHandler(logger, session)
|
||||
if err != nil {
|
||||
closeCause(err.Error())
|
||||
@@ -422,7 +464,7 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
||||
env = append(env, fmt.Sprintf("DISPLAY=localhost:%d.%d", display, x11.ScreenNumber))
|
||||
}
|
||||
|
||||
err := s.sessionStart(logger, session, env, magicType)
|
||||
err := s.sessionStart(logger, session, env, magicType, container, containerUser)
|
||||
var exitError *exec.ExitError
|
||||
if xerrors.As(err, &exitError) {
|
||||
code := exitError.ExitCode()
|
||||
@@ -495,18 +537,27 @@ func (s *Server) fileTransferBlocked(session ssh.Session) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []string, magicType MagicSessionType) (retErr error) {
|
||||
func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []string, magicType MagicSessionType, container, containerUser string) (retErr error) {
|
||||
ctx := session.Context()
|
||||
|
||||
magicTypeLabel := magicTypeMetricLabel(magicType)
|
||||
sshPty, windowSize, isPty := session.Pty()
|
||||
ptyLabel := "no"
|
||||
if isPty {
|
||||
ptyLabel = "yes"
|
||||
}
|
||||
|
||||
cmd, err := s.CreateCommand(ctx, session.RawCommand(), env, nil)
|
||||
if err != nil {
|
||||
ptyLabel := "no"
|
||||
if isPty {
|
||||
ptyLabel = "yes"
|
||||
var ei usershell.EnvInfoer
|
||||
var err error
|
||||
if s.config.ExperimentalDevContainersEnabled && container != "" {
|
||||
ei, err = agentcontainers.EnvInfo(ctx, s.Execer, container, containerUser)
|
||||
if err != nil {
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "container_env_info").Add(1)
|
||||
return err
|
||||
}
|
||||
}
|
||||
cmd, err := s.CreateCommand(ctx, session.RawCommand(), env, ei)
|
||||
if err != nil {
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "create_command").Add(1)
|
||||
return err
|
||||
}
|
||||
@@ -514,11 +565,6 @@ func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []str
|
||||
if ssh.AgentRequested(session) {
|
||||
l, err := ssh.NewAgentListener()
|
||||
if err != nil {
|
||||
ptyLabel := "no"
|
||||
if isPty {
|
||||
ptyLabel = "yes"
|
||||
}
|
||||
|
||||
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "listener").Add(1)
|
||||
return xerrors.Errorf("new agent listener: %w", err)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ type Server struct {
|
||||
reconnectingPTYs sync.Map
|
||||
timeout time.Duration
|
||||
|
||||
ExperimentalContainersEnabled bool
|
||||
ExperimentalDevcontainersEnabled bool
|
||||
}
|
||||
|
||||
// NewServer returns a new ReconnectingPTY server
|
||||
@@ -187,7 +187,7 @@ func (s *Server) handleConn(ctx context.Context, logger slog.Logger, conn net.Co
|
||||
}()
|
||||
|
||||
var ei usershell.EnvInfoer
|
||||
if s.ExperimentalContainersEnabled && msg.Container != "" {
|
||||
if s.ExperimentalDevcontainersEnabled && msg.Container != "" {
|
||||
dei, err := agentcontainers.EnvInfo(ctx, s.commandCreator.Execer, msg.Container, msg.ContainerUser)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get container env info: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user