diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 48ade4bb195f4..fb459804646ae 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -77,7 +77,8 @@ type API struct { subAgentURL string subAgentEnv []string - projectDiscovery bool // If we should perform project discovery or not. + projectDiscovery bool // If we should perform project discovery or not. + discoveryAutostart bool // If we should autostart discovered projects. ownerName string workspaceName string @@ -144,7 +145,8 @@ func WithCommandEnv(ce CommandEnv) Option { strings.HasPrefix(s, "CODER_AGENT_TOKEN=") || strings.HasPrefix(s, "CODER_AGENT_AUTH=") || strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_ENABLE=") || - strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE=") + strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE=") || + strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE=") }) return shell, dir, env, nil } @@ -287,6 +289,14 @@ func WithProjectDiscovery(projectDiscovery bool) Option { } } +// WithDiscoveryAutostart sets if the API should attempt to autostart +// projects that have been discovered +func WithDiscoveryAutostart(discoveryAutostart bool) Option { + return func(api *API) { + api.discoveryAutostart = discoveryAutostart + } +} + // ScriptLogger is an interface for sending devcontainer logs to the // controlplane. type ScriptLogger interface { @@ -542,11 +552,13 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error { Container: nil, } - config, err := api.dccli.ReadConfig(api.ctx, workspaceFolder, path, []string{}) - if err != nil { - logger.Error(api.ctx, "read project configuration", slog.Error(err)) - } else if config.Configuration.Customizations.Coder.AutoStart { - dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting + if api.discoveryAutostart { + config, err := api.dccli.ReadConfig(api.ctx, workspaceFolder, path, []string{}) + if err != nil { + logger.Error(api.ctx, "read project configuration", slog.Error(err)) + } else if config.Configuration.Customizations.Coder.AutoStart { + dc.Status = codersdk.WorkspaceAgentDevcontainerStatusStarting + } } api.knownDevcontainers[workspaceFolder] = dc diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 340853a91d360..a9ce12b892602 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -3792,6 +3792,7 @@ func TestDevcontainerDiscovery(t *testing.T) { agentcontainers.WithContainerCLI(&fakeContainerCLI{}), agentcontainers.WithDevcontainerCLI(mDCCLI), agentcontainers.WithProjectDiscovery(true), + agentcontainers.WithDiscoveryAutostart(true), ) api.Start() defer api.Close() @@ -3813,6 +3814,75 @@ func TestDevcontainerDiscovery(t *testing.T) { // Then: We expect the mock infra to not fail. }) } + + t.Run("Disabled", func(t *testing.T) { + t.Parallel() + var ( + ctx = testutil.Context(t, testutil.WaitShort) + logger = testutil.Logger(t) + mClock = quartz.NewMock(t) + mDCCLI = acmock.NewMockDevcontainerCLI(gomock.NewController(t)) + + fs = map[string]string{ + "/home/coder/.git/HEAD": "", + "/home/coder/.devcontainer/devcontainer.json": "", + } + + r = chi.NewRouter() + ) + + // We expect that neither `ReadConfig`, nor `Up` are called as we + // have explicitly disabled the agentcontainers API from attempting + // to autostart devcontainers that it discovers. + mDCCLI.EXPECT().ReadConfig(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + []string{}, + ).Return(agentcontainers.DevcontainerConfig{ + Configuration: agentcontainers.DevcontainerConfiguration{ + Customizations: agentcontainers.DevcontainerCustomizations{ + Coder: agentcontainers.CoderCustomization{ + AutoStart: true, + }, + }, + }, + }, nil).Times(0) + + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + gomock.Any(), + ).Return("", nil).Times(0) + + api := agentcontainers.NewAPI(logger, + agentcontainers.WithClock(mClock), + agentcontainers.WithWatcher(watcher.NewNoop()), + agentcontainers.WithFileSystem(initFS(t, fs)), + agentcontainers.WithManifestInfo("owner", "workspace", "parent-agent", "/home/coder"), + agentcontainers.WithContainerCLI(&fakeContainerCLI{}), + agentcontainers.WithDevcontainerCLI(mDCCLI), + agentcontainers.WithProjectDiscovery(true), + agentcontainers.WithDiscoveryAutostart(false), + ) + api.Start() + defer api.Close() + r.Mount("/", api.Routes()) + + // When: All expected dev containers have been found. + require.Eventuallyf(t, func() bool { + req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + got := codersdk.WorkspaceAgentListContainersResponse{} + err := json.NewDecoder(rec.Body).Decode(&got) + require.NoError(t, err) + + return len(got.Devcontainers) >= 1 + }, testutil.WaitShort, testutil.IntervalFast, "dev containers never found") + + // Then: We expect the mock infra to not fail. + }) }) } diff --git a/cli/agent.go b/cli/agent.go index 4f50fbfe88942..c192d4429ccaf 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -40,23 +40,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 - devcontainers bool - devcontainerProjectDiscovery 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 + devcontainers bool + devcontainerProjectDiscovery bool + devcontainerDiscoveryAutostart bool ) cmd := &serpent.Command{ Use: "agent", @@ -366,6 +367,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { DevcontainerAPIOptions: []agentcontainers.Option{ agentcontainers.WithSubAgentURL(r.agentURL.String()), agentcontainers.WithProjectDiscovery(devcontainerProjectDiscovery), + agentcontainers.WithDiscoveryAutostart(devcontainerDiscoveryAutostart), }, }) @@ -519,6 +521,13 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { Description: "Allow the agent to search the filesystem for devcontainer projects.", Value: serpent.BoolOf(&devcontainerProjectDiscovery), }, + { + Flag: "devcontainers-discovery-autostart-enable", + Default: "false", + Env: "CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE", + Description: "Allow the agent to autostart devcontainer projects it discovers based on their configuration.", + Value: serpent.BoolOf(&devcontainerDiscoveryAutostart), + }, } return cmd diff --git a/cli/testdata/coder_agent_--help.golden b/cli/testdata/coder_agent_--help.golden index 0627016855e08..c6d75705a6eb4 100644 --- a/cli/testdata/coder_agent_--help.golden +++ b/cli/testdata/coder_agent_--help.golden @@ -33,6 +33,10 @@ OPTIONS: --debug-address string, $CODER_AGENT_DEBUG_ADDRESS (default: 127.0.0.1:2113) The bind address to serve a debug HTTP server. + --devcontainers-discovery-autostart-enable bool, $CODER_AGENT_DEVCONTAINERS_DISCOVERY_AUTOSTART_ENABLE (default: false) + Allow the agent to autostart devcontainer projects it discovers based + on their configuration. + --devcontainers-enable bool, $CODER_AGENT_DEVCONTAINERS_ENABLE (default: true) Allow the agent to automatically detect running devcontainers.