Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitec44f06

Browse files
authored
feat(cli): allow SSH command to connect to running container (#16726)
Fixes#16709 and#16420Adds 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
1 parent64fec8b commitec44f06

File tree

8 files changed

+253
-43
lines changed

8 files changed

+253
-43
lines changed

‎agent/agent.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ type Options struct {
9191
Execer agentexec.Execer
9292
ContainerLister agentcontainers.Lister
9393

94-
ExperimentalContainersEnabledbool
95-
ExperimentalConnectionReportsbool
94+
ExperimentalConnectionReportsbool
95+
ExperimentalDevcontainersEnabledbool
9696
}
9797

9898
typeClientinterface {
@@ -156,7 +156,7 @@ func New(options Options) Agent {
156156
options.Execer=agentexec.DefaultExecer
157157
}
158158
ifoptions.ContainerLister==nil {
159-
options.ContainerLister=agentcontainers.NewDocker(options.Execer)
159+
options.ContainerLister= agentcontainers.NoopLister{}
160160
}
161161

162162
hardCtx,hardCancel:=context.WithCancel(context.Background())
@@ -195,7 +195,7 @@ func New(options Options) Agent {
195195
execer:options.Execer,
196196
lister:options.ContainerLister,
197197

198-
experimentalDevcontainersEnabled:options.ExperimentalContainersEnabled,
198+
experimentalDevcontainersEnabled:options.ExperimentalDevcontainersEnabled,
199199
experimentalConnectionReports:options.ExperimentalConnectionReports,
200200
}
201201
// Initially, we have a closed channel, reflecting the fact that we are not initially connected.
@@ -307,6 +307,8 @@ func (a *agent) init() {
307307

308308
returna.reportConnection(id,connectionType,ip)
309309
},
310+
311+
ExperimentalDevContainersEnabled:a.experimentalDevcontainersEnabled,
310312
})
311313
iferr!=nil {
312314
panic(err)
@@ -335,7 +337,7 @@ func (a *agent) init() {
335337
a.metrics.connectionsTotal,a.metrics.reconnectingPTYErrors,
336338
a.reconnectingPTYTimeout,
337339
func(s*reconnectingpty.Server) {
338-
s.ExperimentalContainersEnabled=a.experimentalDevcontainersEnabled
340+
s.ExperimentalDevcontainersEnabled=a.experimentalDevcontainersEnabled
339341
},
340342
)
341343
goa.runLoop()

‎agent/agent_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1841,7 +1841,7 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) {
18411841

18421842
// nolint: dogsled
18431843
conn,_,_,_,_:=setupAgent(t, agentsdk.Manifest{},0,func(_*agenttest.Client,o*agent.Options) {
1844-
o.ExperimentalContainersEnabled=true
1844+
o.ExperimentalDevcontainersEnabled=true
18451845
})
18461846
ac,err:=conn.ReconnectingPTY(ctx,uuid.New(),80,80,"/bin/sh",func(arp*workspacesdk.AgentReconnectingPTYInit) {
18471847
arp.Container=ct.Container.ID

‎agent/agentssh/agentssh.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
"cdr.dev/slog"
3131

32+
"github.com/coder/coder/v2/agent/agentcontainers"
3233
"github.com/coder/coder/v2/agent/agentexec"
3334
"github.com/coder/coder/v2/agent/agentrsa"
3435
"github.com/coder/coder/v2/agent/usershell"
@@ -60,6 +61,14 @@ const (
6061
// MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection.
6162
// This is stripped from any commands being executed, and is counted towards connection stats.
6263
MagicSessionTypeEnvironmentVariable="CODER_SSH_SESSION_TYPE"
64+
// ContainerEnvironmentVariable is used to specify the target container for an SSH connection.
65+
// This is stripped from any commands being executed.
66+
// Only available if CODER_AGENT_DEVCONTAINERS_ENABLE=true.
67+
ContainerEnvironmentVariable="CODER_CONTAINER"
68+
// ContainerUserEnvironmentVariable is used to specify the container user for
69+
// an SSH connection.
70+
// Only available if CODER_AGENT_DEVCONTAINERS_ENABLE=true.
71+
ContainerUserEnvironmentVariable="CODER_CONTAINER_USER"
6372
)
6473

6574
// MagicSessionType enums.
@@ -104,6 +113,9 @@ type Config struct {
104113
BlockFileTransferbool
105114
// ReportConnection.
106115
ReportConnectionreportConnectionFunc
116+
// Experimental: allow connecting to running containers if
117+
// CODER_AGENT_DEVCONTAINERS_ENABLE=true.
118+
ExperimentalDevContainersEnabledbool
107119
}
108120

109121
typeServerstruct {
@@ -324,6 +336,22 @@ func (s *sessionCloseTracker) Close() error {
324336
returns.Session.Close()
325337
}
326338

339+
funcextractContainerInfo(env []string) (container,containerUserstring,filteredEnv []string) {
340+
for_,kv:=rangeenv {
341+
ifstrings.HasPrefix(kv,ContainerEnvironmentVariable+"=") {
342+
container=strings.TrimPrefix(kv,ContainerEnvironmentVariable+"=")
343+
}
344+
345+
ifstrings.HasPrefix(kv,ContainerUserEnvironmentVariable+"=") {
346+
containerUser=strings.TrimPrefix(kv,ContainerUserEnvironmentVariable+"=")
347+
}
348+
}
349+
350+
returncontainer,containerUser,slices.DeleteFunc(env,func(kvstring)bool {
351+
returnstrings.HasPrefix(kv,ContainerEnvironmentVariable+"=")||strings.HasPrefix(kv,ContainerUserEnvironmentVariable+"=")
352+
})
353+
}
354+
327355
func (s*Server)sessionHandler(session ssh.Session) {
328356
ctx:=session.Context()
329357
id:=uuid.New()
@@ -353,6 +381,7 @@ func (s *Server) sessionHandler(session ssh.Session) {
353381
defers.trackSession(session,false)
354382

355383
reportSession:=true
384+
356385
switchmagicType {
357386
caseMagicSessionTypeVSCode:
358387
s.connCountVSCode.Add(1)
@@ -395,9 +424,22 @@ func (s *Server) sessionHandler(session ssh.Session) {
395424
return
396425
}
397426

427+
container,containerUser,env:=extractContainerInfo(env)
428+
ifcontainer!="" {
429+
s.logger.Debug(ctx,"container info",
430+
slog.F("container",container),
431+
slog.F("container_user",containerUser),
432+
)
433+
}
434+
398435
switchss:=session.Subsystem();ss {
399436
case"":
400437
case"sftp":
438+
ifs.config.ExperimentalDevContainersEnabled&&container!="" {
439+
closeCause("sftp not yet supported with containers")
440+
_=session.Exit(1)
441+
return
442+
}
401443
err:=s.sftpHandler(logger,session)
402444
iferr!=nil {
403445
closeCause(err.Error())
@@ -422,7 +464,7 @@ func (s *Server) sessionHandler(session ssh.Session) {
422464
env=append(env,fmt.Sprintf("DISPLAY=localhost:%d.%d",display,x11.ScreenNumber))
423465
}
424466

425-
err:=s.sessionStart(logger,session,env,magicType)
467+
err:=s.sessionStart(logger,session,env,magicType,container,containerUser)
426468
varexitError*exec.ExitError
427469
ifxerrors.As(err,&exitError) {
428470
code:=exitError.ExitCode()
@@ -495,30 +537,34 @@ func (s *Server) fileTransferBlocked(session ssh.Session) bool {
495537
returnfalse
496538
}
497539

498-
func (s*Server)sessionStart(logger slog.Logger,session ssh.Session,env []string,magicTypeMagicSessionType) (retErrerror) {
540+
func (s*Server)sessionStart(logger slog.Logger,session ssh.Session,env []string,magicTypeMagicSessionType,container,containerUserstring) (retErrerror) {
499541
ctx:=session.Context()
500542

501543
magicTypeLabel:=magicTypeMetricLabel(magicType)
502544
sshPty,windowSize,isPty:=session.Pty()
545+
ptyLabel:="no"
546+
ifisPty {
547+
ptyLabel="yes"
548+
}
503549

504-
cmd,err:=s.CreateCommand(ctx,session.RawCommand(),env,nil)
505-
iferr!=nil {
506-
ptyLabel:="no"
507-
ifisPty {
508-
ptyLabel="yes"
550+
varei usershell.EnvInfoer
551+
varerrerror
552+
ifs.config.ExperimentalDevContainersEnabled&&container!="" {
553+
ei,err=agentcontainers.EnvInfo(ctx,s.Execer,container,containerUser)
554+
iferr!=nil {
555+
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel,ptyLabel,"container_env_info").Add(1)
556+
returnerr
509557
}
558+
}
559+
cmd,err:=s.CreateCommand(ctx,session.RawCommand(),env,ei)
560+
iferr!=nil {
510561
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel,ptyLabel,"create_command").Add(1)
511562
returnerr
512563
}
513564

514565
ifssh.AgentRequested(session) {
515566
l,err:=ssh.NewAgentListener()
516567
iferr!=nil {
517-
ptyLabel:="no"
518-
ifisPty {
519-
ptyLabel="yes"
520-
}
521-
522568
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel,ptyLabel,"listener").Add(1)
523569
returnxerrors.Errorf("new agent listener: %w",err)
524570
}

‎agent/reconnectingpty/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type Server struct {
3232
reconnectingPTYs sync.Map
3333
timeout time.Duration
3434

35-
ExperimentalContainersEnabledbool
35+
ExperimentalDevcontainersEnabledbool
3636
}
3737

3838
// NewServer returns a new ReconnectingPTY server
@@ -187,7 +187,7 @@ func (s *Server) handleConn(ctx context.Context, logger slog.Logger, conn net.Co
187187
}()
188188

189189
varei usershell.EnvInfoer
190-
ifs.ExperimentalContainersEnabled&&msg.Container!="" {
190+
ifs.ExperimentalDevcontainersEnabled&&msg.Container!="" {
191191
dei,err:=agentcontainers.EnvInfo(ctx,s.commandCreator.Execer,msg.Container,msg.ContainerUser)
192192
iferr!=nil {
193193
returnxerrors.Errorf("get container env info: %w",err)

‎cli/agent.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,24 @@ import (
3838

3939
func (r*RootCmd)workspaceAgent()*serpent.Command {
4040
var (
41-
authstring
42-
logDirstring
43-
scriptDataDirstring
44-
pprofAddressstring
45-
noReapbool
46-
sshMaxTimeouttime.Duration
47-
tailnetListenPortint64
48-
prometheusAddressstring
49-
debugAddressstring
50-
slogHumanPathstring
51-
slogJSONPathstring
52-
slogStackdriverPathstring
53-
blockFileTransferbool
54-
agentHeaderCommandstring
55-
agentHeader[]string
56-
devcontainersEnabledbool
57-
58-
experimentalConnectionReportsbool
41+
authstring
42+
logDirstring
43+
scriptDataDirstring
44+
pprofAddressstring
45+
noReapbool
46+
sshMaxTimeout time.Duration
47+
tailnetListenPortint64
48+
prometheusAddressstring
49+
debugAddressstring
50+
slogHumanPathstring
51+
slogJSONPathstring
52+
slogStackdriverPathstring
53+
blockFileTransferbool
54+
agentHeaderCommandstring
55+
agentHeader []string
56+
57+
experimentalConnectionReportsbool
58+
experimentalDevcontainersEnabledbool
5959
)
6060
cmd:=&serpent.Command{
6161
Use:"agent",
@@ -319,7 +319,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
319319
}
320320

321321
varcontainerLister agentcontainers.Lister
322-
if!devcontainersEnabled {
322+
if!experimentalDevcontainersEnabled {
323323
logger.Info(ctx,"agent devcontainer detection not enabled")
324324
containerLister=&agentcontainers.NoopLister{}
325325
}else {
@@ -358,8 +358,8 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
358358
Execer:execer,
359359
ContainerLister:containerLister,
360360

361-
ExperimentalContainersEnabled:devcontainersEnabled,
362-
ExperimentalConnectionReports:experimentalConnectionReports,
361+
ExperimentalDevcontainersEnabled:experimentalDevcontainersEnabled,
362+
ExperimentalConnectionReports:experimentalConnectionReports,
363363
})
364364

365365
promHandler:=agent.PrometheusMetricsHandler(prometheusRegistry,logger)
@@ -487,7 +487,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
487487
Default:"false",
488488
Env:"CODER_AGENT_DEVCONTAINERS_ENABLE",
489489
Description:"Allow the agent to automatically detect running devcontainers.",
490-
Value:serpent.BoolOf(&devcontainersEnabled),
490+
Value:serpent.BoolOf(&experimentalDevcontainersEnabled),
491491
},
492492
{
493493
Flag:"experimental-connection-reports-enable",

‎cli/exp_rpty_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/ory/dockertest/v3/docker"
1010

1111
"github.com/coder/coder/v2/agent"
12+
"github.com/coder/coder/v2/agent/agentcontainers"
1213
"github.com/coder/coder/v2/agent/agenttest"
1314
"github.com/coder/coder/v2/cli/clitest"
1415
"github.com/coder/coder/v2/coderd/coderdtest"
@@ -88,7 +89,8 @@ func TestExpRpty(t *testing.T) {
8889
})
8990

9091
_=agenttest.New(t,client.URL,agentToken,func(o*agent.Options) {
91-
o.ExperimentalContainersEnabled=true
92+
o.ExperimentalDevcontainersEnabled=true
93+
o.ContainerLister=agentcontainers.NewDocker(o.Execer)
9294
})
9395
_=coderdtest.NewWorkspaceAgentWaiter(t,client,workspace.ID).Wait()
9496

‎cli/ssh.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434

3535
"cdr.dev/slog"
3636
"cdr.dev/slog/sloggers/sloghuman"
37+
"github.com/coder/coder/v2/agent/agentssh"
3738
"github.com/coder/coder/v2/cli/cliui"
3839
"github.com/coder/coder/v2/cli/cliutil"
3940
"github.com/coder/coder/v2/coderd/autobuild/notify"
@@ -76,6 +77,9 @@ func (r *RootCmd) ssh() *serpent.Command {
7677
appearanceConfig codersdk.AppearanceConfig
7778
networkInfoDirstring
7879
networkInfoInterval time.Duration
80+
81+
containerNamestring
82+
containerUserstring
7983
)
8084
client:=new(codersdk.Client)
8185
cmd:=&serpent.Command{
@@ -282,6 +286,34 @@ func (r *RootCmd) ssh() *serpent.Command {
282286
}
283287
conn.AwaitReachable(ctx)
284288

289+
ifcontainerName!="" {
290+
cts,err:=client.WorkspaceAgentListContainers(ctx,workspaceAgent.ID,nil)
291+
iferr!=nil {
292+
returnxerrors.Errorf("list containers: %w",err)
293+
}
294+
iflen(cts.Containers)==0 {
295+
cliui.Info(inv.Stderr,"No containers found!")
296+
cliui.Info(inv.Stderr,"Tip: Agent container integration is experimental and not enabled by default.")
297+
cliui.Info(inv.Stderr," To enable it, set CODER_AGENT_DEVCONTAINERS_ENABLE=true in your template.")
298+
returnnil
299+
}
300+
varfoundbool
301+
for_,c:=rangects.Containers {
302+
ifc.FriendlyName==containerName||c.ID==containerName {
303+
found=true
304+
break
305+
}
306+
}
307+
if!found {
308+
availableContainers:=make([]string,len(cts.Containers))
309+
fori,c:=rangects.Containers {
310+
availableContainers[i]=c.FriendlyName
311+
}
312+
cliui.Errorf(inv.Stderr,"Container not found: %q\nAvailable containers: %v",containerName,availableContainers)
313+
returnnil
314+
}
315+
}
316+
285317
stopPolling:=tryPollWorkspaceAutostop(ctx,client,workspace)
286318
deferstopPolling()
287319

@@ -454,6 +486,17 @@ func (r *RootCmd) ssh() *serpent.Command {
454486
}
455487
}
456488

489+
ifcontainerName!="" {
490+
fork,v:=rangemap[string]string{
491+
agentssh.ContainerEnvironmentVariable:containerName,
492+
agentssh.ContainerUserEnvironmentVariable:containerUser,
493+
} {
494+
iferr:=sshSession.Setenv(k,v);err!=nil {
495+
returnxerrors.Errorf("setenv: %w",err)
496+
}
497+
}
498+
}
499+
457500
err=sshSession.RequestPty("xterm-256color",128,128, gossh.TerminalModes{})
458501
iferr!=nil {
459502
returnxerrors.Errorf("request pty: %w",err)
@@ -594,6 +637,19 @@ func (r *RootCmd) ssh() *serpent.Command {
594637
Default:"5s",
595638
Value:serpent.DurationOf(&networkInfoInterval),
596639
},
640+
{
641+
Flag:"container",
642+
FlagShorthand:"c",
643+
Description:"Specifies a container inside the workspace to connect to.",
644+
Value:serpent.StringOf(&containerName),
645+
Hidden:true,// Hidden until this features is at least in beta.
646+
},
647+
{
648+
Flag:"container-user",
649+
Description:"When connecting to a container, specifies the user to connect as.",
650+
Value:serpent.StringOf(&containerUser),
651+
Hidden:true,// Hidden until this features is at least in beta.
652+
},
597653
sshDisableAutostartOption(serpent.BoolOf(&disableAutostart)),
598654
}
599655
returncmd

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp