From 885259a0b532f2cf1c0092bb4acc58a1b97461bc Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 24 Jun 2025 14:56:07 +0000 Subject: [PATCH 1/2] feat(agent): enable devcontainers by default --- agent/agent.go | 25 ++++++++++++------------- agent/agent_test.go | 17 ++++++++--------- agent/agentssh/agentssh.go | 11 ++++++----- agent/api.go | 2 +- agent/reconnectingpty/server.go | 8 +++++--- cli/agent.go | 19 +++++++++---------- cli/exp_rpty_test.go | 4 ++-- cli/open_test.go | 8 ++++---- cli/ssh_test.go | 8 ++++---- coderd/workspaceagents_test.go | 14 +++++++------- 10 files changed, 58 insertions(+), 58 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index e142f8662f641..833b4032d491b 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -89,9 +89,8 @@ type Options struct { ServiceBannerRefreshInterval time.Duration BlockFileTransfer bool Execer agentexec.Execer - - ExperimentalDevcontainersEnabled bool - ContainerAPIOptions []agentcontainers.Option // Enable ExperimentalDevcontainersEnabled for these to be effective. + Devcontainers bool + DevcontainerAPIOptions []agentcontainers.Option // Enable Devcontainers for these to be effective. } type Client interface { @@ -190,8 +189,8 @@ func New(options Options) Agent { metrics: newAgentMetrics(prometheusRegistry), execer: options.Execer, - experimentalDevcontainersEnabled: options.ExperimentalDevcontainersEnabled, - containerAPIOptions: options.ContainerAPIOptions, + devcontainers: options.Devcontainers, + containerAPIOptions: options.DevcontainerAPIOptions, } // Initially, we have a closed channel, reflecting the fact that we are not initially connected. // Each time we connect we replace the channel (while holding the closeMutex) with a new one @@ -272,9 +271,9 @@ type agent struct { metrics *agentMetrics execer agentexec.Execer - experimentalDevcontainersEnabled bool - containerAPIOptions []agentcontainers.Option - containerAPI atomic.Pointer[agentcontainers.API] // Set by apiHandler. + devcontainers bool + containerAPIOptions []agentcontainers.Option + containerAPI atomic.Pointer[agentcontainers.API] // Set by apiHandler. } func (a *agent) TailnetConn() *tailnet.Conn { @@ -311,7 +310,7 @@ func (a *agent) init() { return a.reportConnection(id, connectionType, ip) }, - ExperimentalDevContainersEnabled: a.experimentalDevcontainersEnabled, + ExperimentalContainers: a.devcontainers, }) if err != nil { panic(err) @@ -340,7 +339,7 @@ func (a *agent) init() { a.metrics.connectionsTotal, a.metrics.reconnectingPTYErrors, a.reconnectingPTYTimeout, func(s *reconnectingpty.Server) { - s.ExperimentalDevcontainersEnabled = a.experimentalDevcontainersEnabled + s.ExperimentalContainers = a.devcontainers }, ) go a.runLoop() @@ -1087,9 +1086,9 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, slog.F("parent_id", manifest.ParentID), slog.F("agent_id", manifest.AgentID), ) - if a.experimentalDevcontainersEnabled { + if a.devcontainers { a.logger.Info(ctx, "devcontainers are not supported on sub agents, disabling feature") - a.experimentalDevcontainersEnabled = false + a.devcontainers = false } } a.client.RewriteDERPMap(manifest.DERPMap) @@ -1145,7 +1144,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context, scripts = manifest.Scripts scriptRunnerOpts []agentscripts.InitOption ) - if a.experimentalDevcontainersEnabled { + if a.devcontainers { var dcScripts []codersdk.WorkspaceAgentScript scripts, dcScripts = agentcontainers.ExtractAndInitializeDevcontainerScripts(manifest.Devcontainers, scripts) // See ExtractAndInitializeDevcontainerScripts for motivation diff --git a/agent/agent_test.go b/agent/agent_test.go index 8ee15e563f3ce..1b24520e45cc5 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -1954,8 +1954,8 @@ func TestAgent_ReconnectingPTYContainer(t *testing.T) { // nolint: dogsled conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(_ *agenttest.Client, o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"), ) }) @@ -2161,9 +2161,9 @@ func TestAgent_DevcontainerAutostart(t *testing.T) { //nolint:dogsled _, agentClient, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append( - o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append( + o.DevcontainerAPIOptions, // Only match this specific dev container. agentcontainers.WithClock(mClock), agentcontainers.WithContainerLabelIncludeFilter("devcontainer.local_folder", tempWorkspaceFolder), @@ -2312,8 +2312,8 @@ func TestAgent_DevcontainerRecreate(t *testing.T) { //nolint:dogsled conn, client, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerLabelIncludeFilter("devcontainer.local_folder", workspaceFolder), ) }) @@ -2438,8 +2438,7 @@ func TestAgent_DevcontainersDisabledForSubAgent(t *testing.T) { // Setup the agent with devcontainers enabled initially. //nolint:dogsled - conn, _, _, _, _ := setupAgent(t, manifest, 0, func(_ *agenttest.Client, o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true + conn, _, _, _, _ := setupAgent(t, manifest, 0, func(*agenttest.Client, *agent.Options) { }) // Query the containers API endpoint. This should fail because diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index fb4cc055e31da..f49a64924bd36 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -113,9 +113,10 @@ type Config struct { BlockFileTransfer bool // ReportConnection. ReportConnection reportConnectionFunc - // Experimental: allow connecting to running containers if - // CODER_AGENT_DEVCONTAINERS_ENABLE=true. - ExperimentalDevContainersEnabled bool + // Experimental: allow connecting to running containers via Docker exec. + // Note that this is different from the devcontainers feature, which uses + // subagents. + ExperimentalContainers bool } type Server struct { @@ -435,7 +436,7 @@ func (s *Server) sessionHandler(session ssh.Session) { switch ss := session.Subsystem(); ss { case "": case "sftp": - if s.config.ExperimentalDevContainersEnabled && container != "" { + if s.config.ExperimentalContainers && container != "" { closeCause("sftp not yet supported with containers") _ = session.Exit(1) return @@ -549,7 +550,7 @@ func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, env []str var ei usershell.EnvInfoer var err error - if s.config.ExperimentalDevContainersEnabled && container != "" { + if s.config.ExperimentalContainers && container != "" { ei, err = agentcontainers.EnvInfo(ctx, s.Execer, container, containerUser) if err != nil { s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "container_env_info").Add(1) diff --git a/agent/api.go b/agent/api.go index fa761988b2f21..52c2c0fbb3094 100644 --- a/agent/api.go +++ b/agent/api.go @@ -40,7 +40,7 @@ func (a *agent) apiHandler(aAPI proto.DRPCAgentClient26) (http.Handler, func() e cacheDuration: cacheDuration, } - if a.experimentalDevcontainersEnabled { + if a.devcontainers { containerAPIOpts := []agentcontainers.Option{ agentcontainers.WithExecer(a.execer), agentcontainers.WithCommandEnv(a.sshServer.CommandEnv), diff --git a/agent/reconnectingpty/server.go b/agent/reconnectingpty/server.go index 04bbdc7efb7b2..19a2853c9d47f 100644 --- a/agent/reconnectingpty/server.go +++ b/agent/reconnectingpty/server.go @@ -31,8 +31,10 @@ type Server struct { connCount atomic.Int64 reconnectingPTYs sync.Map timeout time.Duration - - ExperimentalDevcontainersEnabled bool + // Experimental: allow connecting to running containers via Docker exec. + // Note that this is different from the devcontainers feature, which uses + // subagents. + ExperimentalContainers bool } // NewServer returns a new ReconnectingPTY server @@ -187,7 +189,7 @@ func (s *Server) handleConn(ctx context.Context, logger slog.Logger, conn net.Co }() var ei usershell.EnvInfoer - if s.ExperimentalDevcontainersEnabled && msg.Container != "" { + if s.ExperimentalContainers && 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) diff --git a/cli/agent.go b/cli/agent.go index 5d6037f9930ec..2285d44fc3584 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -55,8 +55,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { blockFileTransfer bool agentHeaderCommand string agentHeader []string - - experimentalDevcontainersEnabled bool + devcontainers bool ) cmd := &serpent.Command{ Use: "agent", @@ -321,7 +320,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { return xerrors.Errorf("create agent execer: %w", err) } - if experimentalDevcontainersEnabled { + if devcontainers { logger.Info(ctx, "agent devcontainer detection enabled") } else { logger.Info(ctx, "agent devcontainer detection not enabled") @@ -359,11 +358,11 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { SSHMaxTimeout: sshMaxTimeout, Subsystems: subsystems, - PrometheusRegistry: prometheusRegistry, - BlockFileTransfer: blockFileTransfer, - Execer: execer, - ExperimentalDevcontainersEnabled: experimentalDevcontainersEnabled, - ContainerAPIOptions: []agentcontainers.Option{ + PrometheusRegistry: prometheusRegistry, + BlockFileTransfer: blockFileTransfer, + Execer: execer, + Devcontainers: devcontainers, + DevcontainerAPIOptions: []agentcontainers.Option{ agentcontainers.WithSubAgentURL(r.agentURL.String()), }, }) @@ -506,10 +505,10 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { }, { Flag: "devcontainers-enable", - Default: "false", + Default: "true", Env: "CODER_AGENT_DEVCONTAINERS_ENABLE", Description: "Allow the agent to automatically detect running devcontainers.", - Value: serpent.BoolOf(&experimentalDevcontainersEnabled), + Value: serpent.BoolOf(&devcontainers), }, } diff --git a/cli/exp_rpty_test.go b/cli/exp_rpty_test.go index 04c8798c166af..213764bb40113 100644 --- a/cli/exp_rpty_test.go +++ b/cli/exp_rpty_test.go @@ -116,8 +116,8 @@ func TestExpRpty(t *testing.T) { }) _ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerLabelIncludeFilter(wantLabel, "true"), ) }) diff --git a/cli/open_test.go b/cli/open_test.go index 698a4d777984b..b76b603d35b1e 100644 --- a/cli/open_test.go +++ b/cli/open_test.go @@ -334,8 +334,8 @@ func TestOpenVSCodeDevContainer(t *testing.T) { }) _ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerCLI(mccli), agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"), ) @@ -509,8 +509,8 @@ func TestOpenVSCodeDevContainer_NoAgentDirectory(t *testing.T) { }) _ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerCLI(mccli), agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"), ) diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 127d57b22ae75..582f8a3fdf691 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -2029,8 +2029,8 @@ func TestSSH_Container(t *testing.T) { }) _ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"), ) }) @@ -2069,8 +2069,8 @@ func TestSSH_Container(t *testing.T) { Warnings: nil, }, nil).AnyTimes() _ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerCLI(mLister), agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"), ) diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index b3fb53c228ef8..8bb9d87961117 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -1250,8 +1250,8 @@ func TestWorkspaceAgentContainers(t *testing.T) { return agents }).Do() _ = agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) { - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"), ) }) @@ -1358,8 +1358,8 @@ func TestWorkspaceAgentContainers(t *testing.T) { }).Do() _ = agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) { o.Logger = logger.Named("agent") - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append(o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append(o.DevcontainerAPIOptions, agentcontainers.WithContainerCLI(mcl), agentcontainers.WithContainerLabelIncludeFilter("this.label.does.not.exist.ignore.devcontainers", "true"), ) @@ -1473,9 +1473,9 @@ func TestWorkspaceAgentRecreateDevcontainer(t *testing.T) { }).Do() _ = agenttest.New(t, client.URL, r.AgentToken, func(o *agent.Options) { o.Logger = logger.Named("agent") - o.ExperimentalDevcontainersEnabled = true - o.ContainerAPIOptions = append( - o.ContainerAPIOptions, + o.Devcontainers = true + o.DevcontainerAPIOptions = append( + o.DevcontainerAPIOptions, agentcontainers.WithContainerCLI(mccli), agentcontainers.WithDevcontainerCLI(mdccli), agentcontainers.WithWatcher(watcher.NewNoop()), From 167122b39459d4497d3cb6a1f37d4733caa57b17 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 24 Jun 2025 16:36:35 +0000 Subject: [PATCH 2/2] gen --- cli/testdata/coder_agent_--help.golden | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/testdata/coder_agent_--help.golden b/cli/testdata/coder_agent_--help.golden index 6548a2fadbe49..3dcbb343149d3 100644 --- a/cli/testdata/coder_agent_--help.golden +++ b/cli/testdata/coder_agent_--help.golden @@ -33,7 +33,7 @@ OPTIONS: --debug-address string, $CODER_AGENT_DEBUG_ADDRESS (default: 127.0.0.1:2113) The bind address to serve a debug HTTP server. - --devcontainers-enable bool, $CODER_AGENT_DEVCONTAINERS_ENABLE (default: false) + --devcontainers-enable bool, $CODER_AGENT_DEVCONTAINERS_ENABLE (default: true) Allow the agent to automatically detect running devcontainers. --log-dir string, $CODER_AGENT_LOG_DIR (default: /tmp)