Skip to content

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

Merged
merged 4 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
address PR comments
  • Loading branch information
johnstcn committed Feb 27, 2025
commit 7437e49b7c30cb946922271ffd1be66da7c2b282
12 changes: 6 additions & 6 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ type Options struct {
Execer agentexec.Execer
ContainerLister agentcontainers.Lister

ExperimentalContainersEnabled bool
ExperimentalConnectionReports bool
ExperimentalConnectionReports bool
ExperimentalDevcontainersEnabled bool
}

type Client interface {
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -308,7 +308,7 @@ func (a *agent) init() {
return a.reportConnection(id, connectionType, ip)
},

ExperimentalContainersEnabled: a.experimentalDevcontainersEnabled,
ExperimentalDevContainersEnabled: a.experimentalDevcontainersEnabled,
})
if err != nil {
panic(err)
Expand Down Expand Up @@ -337,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()
Expand Down
2 changes: 1 addition & 1 deletion agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions agent/agentssh/agentssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ type Config struct {
ReportConnection reportConnectionFunc
// Experimental: allow connecting to running containers if
// CODER_AGENT_DEVCONTAINERS_ENABLE=true.
ExperimentalContainersEnabled bool
ExperimentalDevContainersEnabled bool
}

type Server struct {
Expand Down Expand Up @@ -425,16 +425,19 @@ func (s *Server) sessionHandler(session ssh.Session) {
}

container, containerUser, env := extractContainerInfo(env)
s.logger.Debug(ctx, "container info",
slog.F("container", container),
slog.F("container_user", containerUser),
)
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.ExperimentalContainersEnabled && container != "" {
if s.config.ExperimentalDevContainersEnabled && container != "" {
closeCause("sftp not yet supported with containers")
_ = session.Exit(1)
return
}
err := s.sftpHandler(logger, session)
Expand Down Expand Up @@ -546,7 +549,7 @@ func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []str

var ei usershell.EnvInfoer
var err error
if s.config.ExperimentalContainersEnabled && container != "" {
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)
Expand Down
4 changes: 2 additions & 2 deletions agent/reconnectingpty/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Server struct {
reconnectingPTYs sync.Map
timeout time.Duration

ExperimentalContainersEnabled bool
ExperimentalDevcontainersEnabled bool
}

// NewServer returns a new ReconnectingPTY server
Expand Down Expand Up @@ -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)
Expand Down
44 changes: 22 additions & 22 deletions cli/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,24 @@ import (

func (r *RootCmd) workspaceAgent() *serpent.Command {
var (
auth string
logDir string
scriptDataDir string
pprofAddress string
noReap bool
sshMaxTimeout time.Duration
tailnetListenPort int64
prometheusAddress string
debugAddress string
slogHumanPath string
slogJSONPath string
slogStackdriverPath string
blockFileTransfer bool
agentHeaderCommand string
agentHeader []string
devcontainersEnabled bool

experimentalConnectionReports bool
auth string
logDir string
scriptDataDir string
pprofAddress string
noReap bool
sshMaxTimeout time.Duration
tailnetListenPort int64
prometheusAddress string
debugAddress string
slogHumanPath string
slogJSONPath string
slogStackdriverPath string
blockFileTransfer bool
agentHeaderCommand string
agentHeader []string

experimentalConnectionReports bool
experimentalDevcontainersEnabled bool
)
cmd := &serpent.Command{
Use: "agent",
Expand Down Expand Up @@ -319,7 +319,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
}

var containerLister agentcontainers.Lister
if !devcontainersEnabled {
if !experimentalDevcontainersEnabled {
logger.Info(ctx, "agent devcontainer detection not enabled")
containerLister = &agentcontainers.NoopLister{}
} else {
Expand Down Expand Up @@ -358,8 +358,8 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
Execer: execer,
ContainerLister: containerLister,

ExperimentalContainersEnabled: devcontainersEnabled,
ExperimentalConnectionReports: experimentalConnectionReports,
ExperimentalDevcontainersEnabled: experimentalDevcontainersEnabled,
ExperimentalConnectionReports: experimentalConnectionReports,
})

promHandler := agent.PrometheusMetricsHandler(prometheusRegistry, logger)
Expand Down Expand Up @@ -487,7 +487,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
Default: "false",
Env: "CODER_AGENT_DEVCONTAINERS_ENABLE",
Description: "Allow the agent to automatically detect running devcontainers.",
Value: serpent.BoolOf(&devcontainersEnabled),
Value: serpent.BoolOf(&experimentalDevcontainersEnabled),
},
{
Flag: "experimental-connection-reports-enable",
Expand Down
2 changes: 1 addition & 1 deletion cli/exp_rpty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TestExpRpty(t *testing.T) {
})

_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.ExperimentalContainersEnabled = true
o.ExperimentalDevcontainersEnabled = true
})
_ = coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait()

Expand Down
34 changes: 22 additions & 12 deletions cli/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (r *RootCmd) ssh() *serpent.Command {
networkInfoDir string
networkInfoInterval time.Duration

container string
containerName string
containerUser string
)
client := new(codersdk.Client)
Expand Down Expand Up @@ -286,20 +286,31 @@ func (r *RootCmd) ssh() *serpent.Command {
}
conn.AwaitReachable(ctx)

if container != "" {
if containerName != "" {
cts, err := client.WorkspaceAgentListContainers(ctx, workspaceAgent.ID, nil)
if err != nil {
return xerrors.Errorf("list containers: %w", err)
}
if len(cts.Containers) == 0 {
cliui.Info(inv.Stderr, "No containers found!")
cliui.Info(inv.Stderr, "Tip: Agent container integration is experimental and not enabled by default.")
cliui.Info(inv.Stderr, " To enable it, set CODER_AGENT_DEVCONTAINERS_ENABLE=true in your template.")
return nil
}
var found bool
for _, c := range cts.Containers {
if c.FriendlyName == container || c.ID == container {
if c.FriendlyName == containerName || c.ID == containerName {
found = true
break
}
}
if !found {
return xerrors.Errorf("container not found: %q", container)
availableContainers := make([]string, len(cts.Containers))
for i, c := range cts.Containers {
availableContainers[i] = c.FriendlyName
}
cliui.Errorf(inv.Stderr, "Container not found: %q\nAvailable containers: %v", containerName, availableContainers)
return nil
}
}

Expand Down Expand Up @@ -475,9 +486,9 @@ func (r *RootCmd) ssh() *serpent.Command {
}
}

if container != "" {
if containerName != "" {
for k, v := range map[string]string{
agentssh.ContainerEnvironmentVariable: container,
agentssh.ContainerEnvironmentVariable: containerName,
agentssh.ContainerUserEnvironmentVariable: containerUser,
} {
if err := sshSession.Setenv(k, v); err != nil {
Expand Down Expand Up @@ -630,15 +641,14 @@ func (r *RootCmd) ssh() *serpent.Command {
Flag: "container",
FlagShorthand: "c",
Description: "Specifies a container inside the workspace to connect to.",
Value: serpent.StringOf(&container),
Value: serpent.StringOf(&containerName),
Hidden: true, // Hidden until this features is at least in beta.
},
{
Flag: "container-user",
FlagShorthand: "u",
Description: "When connecting to a container, specifies the user to connect as.",
Value: serpent.StringOf(&containerUser),
Hidden: true, // Hidden until this features is at least in beta.
Flag: "container-user",
Description: "When connecting to a container, specifies the user to connect as.",
Value: serpent.StringOf(&containerUser),
Hidden: true, // Hidden until this features is at least in beta.
},
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
}
Expand Down
30 changes: 24 additions & 6 deletions cli/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"golang.org/x/xerrors"

"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agentcontainers"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/agenttest"
agentproto "github.com/coder/coder/v2/agent/proto"
Expand Down Expand Up @@ -1959,7 +1960,8 @@ func TestSSH_Container(t *testing.T) {
})

_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.ExperimentalContainersEnabled = true
o.ExperimentalDevcontainersEnabled = true
o.ContainerLister = agentcontainers.NewDocker(o.Execer)
})
_ = coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait()

Expand All @@ -1985,14 +1987,22 @@ func TestSSH_Container(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
client, workspace, agentToken := setupWorkspaceForAgent(t)
_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.ExperimentalContainersEnabled = true
o.ExperimentalDevcontainersEnabled = true
o.ContainerLister = agentcontainers.NewDocker(o.Execer)
})
_ = coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait()

inv, root := clitest.New(t, "ssh", workspace.Name, "-c", uuid.NewString())
clitest.SetupConfig(t, client, root)
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "container not found:")
ptty := ptytest.New(t).Attach(inv)

cmdDone := tGo(t, func() {
err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})

ptty.ExpectMatch("Container not found:")
<-cmdDone
})

t.Run("NotEnabled", func(t *testing.T) {
Expand All @@ -2005,8 +2015,16 @@ func TestSSH_Container(t *testing.T) {

inv, root := clitest.New(t, "ssh", workspace.Name, "-c", uuid.NewString())
clitest.SetupConfig(t, client, root)
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "container not found:")
ptty := ptytest.New(t).Attach(inv)

cmdDone := tGo(t, func() {
err := inv.WithContext(ctx).Run()
assert.NoError(t, err)
})

ptty.ExpectMatch("No containers found!")
ptty.ExpectMatch("Tip: Agent container integration is experimental and not enabled by default.")
<-cmdDone
})
}

Expand Down
Loading