Content-Length: 14247 | pFad | http://github.com/coder/coder/pull/19040.patch

thub.com From df5255b24249e91b8cc6b99151615caf9a832b76 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 23 Jul 2025 11:48:10 +0000 Subject: [PATCH 1/3] feat(agent/agentcontainers): allow auto starting discovered dev containers --- agent/agentcontainers/api.go | 25 ++- agent/agentcontainers/api_test.go | 246 +++++++++++++++++++++++ agent/agentcontainers/devcontainercli.go | 1 + 3 files changed, 268 insertions(+), 4 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 4f9287713fcfc..ef80b5e9146f6 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -143,7 +143,8 @@ func WithCommandEnv(ce CommandEnv) Option { strings.HasPrefix(s, "CODER_WORKSPACE_AGENT_URL=") || 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_ENABLE=") || + strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_PROJECT_DISCOVERY_ENABLE=") }) return shell, dir, env, nil } @@ -524,23 +525,39 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error { workspaceFolder := strings.TrimSuffix(path, relativeConfigPath) - logger.Debug(api.ctx, "discovered dev container project", slog.F("workspace_folder", workspaceFolder)) + logger := logger.With(slog.F("workspace_folder", workspaceFolder)) + logger.Debug(api.ctx, "discovered dev container project") api.mu.Lock() if _, found := api.knownDevcontainers[workspaceFolder]; !found { - logger.Debug(api.ctx, "adding dev container project", slog.F("workspace_folder", workspaceFolder)) + logger.Debug(api.ctx, "adding dev container project") dc := codersdk.WorkspaceAgentDevcontainer{ ID: uuid.New(), Name: "", // Updated later based on container state. WorkspaceFolder: workspaceFolder, ConfigPath: path, - Status: "", // Updated later based on container state. + Status: codersdk.WorkspaceAgentDevcontainerStatusStopped, Dirty: false, // Updated later based on config file changes. 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 + } + api.knownDevcontainers[workspaceFolder] = dc + api.broadcastUpdatesLocked() + + if dc.Status == codersdk.WorkspaceAgentDevcontainerStatusStarting { + go func() { + _ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath) + }() + } + } api.mu.Unlock() } diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 5714027960a7b..6ebfff03853d4 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -3568,4 +3568,250 @@ func TestDevcontainerDiscovery(t *testing.T) { // This is implicitly handled by `testutil.Logger` failing when it // detects an error has been logged. }) + + t.Run("AutoStart", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + agentDir string + fs map[string]string + expectDevcontainerCount int + setupMocks func(mDCCLI *acmock.MockDevcontainerCLI) + }{ + { + name: "SingleEnabled", + agentDir: "/home/coder", + expectDevcontainerCount: 1, + fs: map[string]string{ + "/home/coder/.git/HEAD": "", + "/home/coder/.devcontainer/devcontainer.json": "", + }, + setupMocks: func(mDCCLI *acmock.MockDevcontainerCLI) { + gomock.InOrder( + // Given: This dev container has auto start enabled. + 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), + + // Then: We expect it to be started. + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + gomock.Any(), + ).Return("", nil), + ) + }, + }, + { + name: "SingleDisabled", + agentDir: "/home/coder", + expectDevcontainerCount: 1, + fs: map[string]string{ + "/home/coder/.git/HEAD": "", + "/home/coder/.devcontainer/devcontainer.json": "", + }, + setupMocks: func(mDCCLI *acmock.MockDevcontainerCLI) { + gomock.InOrder( + // Given: This dev container has auto start disabled. + 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: false, + }, + }, + }, + }, nil), + + // Then: We expect it to _not_ be started. + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + gomock.Any(), + ).Return("", nil).Times(0), + ) + }, + }, + { + name: "OneEnabledOneDisabled", + agentDir: "/home/coder", + expectDevcontainerCount: 2, + fs: map[string]string{ + "/home/coder/.git/HEAD": "", + "/home/coder/.devcontainer/devcontainer.json": "", + "/home/coder/project/.devcontainer.json": "", + }, + setupMocks: func(mDCCLI *acmock.MockDevcontainerCLI) { + gomock.InOrder( + // Given: This dev container has auto start enabled. + 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), + + // Then: We expect it to be started. + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + gomock.Any(), + ).Return("", nil), + ) + + gomock.InOrder( + // Given: This dev container has auto start disbaled. + mDCCLI.EXPECT().ReadConfig(gomock.Any(), + "/home/coder/project", + "/home/coder/project/.devcontainer.json", + []string{}, + ).Return(agentcontainers.DevcontainerConfig{ + Configuration: agentcontainers.DevcontainerConfiguration{ + Customizations: agentcontainers.DevcontainerCustomizations{ + Coder: agentcontainers.CoderCustomization{ + AutoStart: false, + }, + }, + }, + }, nil), + + // Then: We expect it to _not_ be started. + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder/project", + "/home/coder/project/.devcontainer.json", + gomock.Any(), + ).Return("", nil).Times(0), + ) + }, + }, + { + name: "MultipleEnabled", + agentDir: "/home/coder", + expectDevcontainerCount: 2, + fs: map[string]string{ + "/home/coder/.git/HEAD": "", + "/home/coder/.devcontainer/devcontainer.json": "", + "/home/coder/project/.devcontainer.json": "", + }, + setupMocks: func(mDCCLI *acmock.MockDevcontainerCLI) { + gomock.InOrder( + // Given: This dev container has auto start enabled. + 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), + + // Then: We expect it to be started. + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder", + "/home/coder/.devcontainer/devcontainer.json", + gomock.Any(), + ).Return("", nil), + ) + + gomock.InOrder( + // Given: This dev container has auto start enabled. + mDCCLI.EXPECT().ReadConfig(gomock.Any(), + "/home/coder/project", + "/home/coder/project/.devcontainer.json", + []string{}, + ).Return(agentcontainers.DevcontainerConfig{ + Configuration: agentcontainers.DevcontainerConfiguration{ + Customizations: agentcontainers.DevcontainerCustomizations{ + Coder: agentcontainers.CoderCustomization{ + AutoStart: true, + }, + }, + }, + }, nil), + + // Then: We expect it to be started. + mDCCLI.EXPECT().Up(gomock.Any(), + "/home/coder/project", + "/home/coder/project/.devcontainer.json", + gomock.Any(), + ).Return("", nil), + ) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, 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)) + + r = chi.NewRouter() + ) + + // Given: We setup our mocks. These mocks handle our expectations for these + // tests. If there are missing/unexpected mock calls, the test will fail. + tt.setupMocks(mDCCLI) + + api := agentcontainers.NewAPI(logger, + agentcontainers.WithClock(mClock), + agentcontainers.WithWatcher(watcher.NewNoop()), + agentcontainers.WithFileSystem(initFS(t, tt.fs)), + agentcontainers.WithManifestInfo("owner", "workspace", "parent-agent", "/home/coder"), + agentcontainers.WithContainerCLI(&fakeContainerCLI{}), + agentcontainers.WithDevcontainerCLI(mDCCLI), + agentcontainers.WithProjectDiscovery(true), + ) + 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) >= tt.expectDevcontainerCount + }, testutil.WaitShort, testutil.IntervalFast, "dev containers never found") + + // Then: We expect the mock infra to not fail. + }) + } + }) } diff --git a/agent/agentcontainers/devcontainercli.go b/agent/agentcontainers/devcontainercli.go index d7cd25f85a7b3..2242e62f602e8 100644 --- a/agent/agentcontainers/devcontainercli.go +++ b/agent/agentcontainers/devcontainercli.go @@ -91,6 +91,7 @@ type CoderCustomization struct { Apps []SubAgentApp `json:"apps,omitempty"` Name string `json:"name,omitempty"` Ignore bool `json:"ignore,omitempty"` + AutoStart bool `json:"autoStart,omitempty"` } type DevcontainerWorkspace struct { From 02e606e6c9e83bbbff36883181ca3aaaffdb3fd7 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 28 Jul 2025 09:36:39 +0000 Subject: [PATCH 2/3] chore: appease linter --- agent/agentcontainers/api.go | 1 - agent/agentcontainers/api_test.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index ef80b5e9146f6..c9b72a4738c94 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -557,7 +557,6 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error { _ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath) }() } - } api.mu.Unlock() } diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 6ebfff03853d4..e6e25ed92558e 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -3682,7 +3682,7 @@ func TestDevcontainerDiscovery(t *testing.T) { ) gomock.InOrder( - // Given: This dev container has auto start disbaled. + // Given: This dev container has auto start disabled. mDCCLI.EXPECT().ReadConfig(gomock.Any(), "/home/coder/project", "/home/coder/project/.devcontainer.json", From d5d9edc3115ba713a92535b3b4d96da459af1b33 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 28 Jul 2025 10:39:31 +0000 Subject: [PATCH 3/3] chore: add some asyncWg logic --- agent/agentcontainers/api.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index c9b72a4738c94..c8cba67cea73f 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -553,7 +553,10 @@ func (api *API) discoverDevcontainersInProject(projectPath string) error { api.broadcastUpdatesLocked() if dc.Status == codersdk.WorkspaceAgentDevcontainerStatusStarting { + api.asyncWg.Add(1) go func() { + defer api.asyncWg.Done() + _ = api.CreateDevcontainer(dc.WorkspaceFolder, dc.ConfigPath) }() }








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/coder/coder/pull/19040.patch

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy