-
Notifications
You must be signed in to change notification settings - Fork 874
feat(cli): allow SSH command to connect to running container #16726
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3dc994a
56538a9
7437e49
a1d0d00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally dislike the |
||
) | ||
|
||
// 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 | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
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,30 +537,34 @@ 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. review: think it might be time to consider an opts struct? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be done, but no need to do that yet IMO. If this grows more then probably. I'd actually want us to refactor the whole session handling and manage our own |
||
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 | ||
} | ||
|
||
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) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be more specific about the container here? Is it only Devcontainer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not forcing it to be :) It can be any container but the UI and
coder show
should only show devcontainers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, so in theory a smart Coder user can run any container this way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, in theory. This is still 'early access' so I'm choosing to not restrict things like this until it makes sense to do so.