Skip to content

Commit 00b5f56

Browse files
authored
feat(agent/agentcontainers): add devcontainers list endpoint (#17389)
This change allows listing both predefined and runtime-detected devcontainers, as well as showing whether or not the devcontainer is running and which container represents it. Fixes coder/internal#478
1 parent c8c4de5 commit 00b5f56

File tree

5 files changed

+487
-19
lines changed

5 files changed

+487
-19
lines changed

agent/agentcontainers/api.go

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ package agentcontainers
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"net/http"
8+
"path"
79
"slices"
10+
"strings"
811
"time"
912

1013
"github.com/go-chi/chi/v5"
14+
"github.com/google/uuid"
1115
"golang.org/x/xerrors"
1216

1317
"cdr.dev/slog"
@@ -31,11 +35,13 @@ type API struct {
3135
dccli DevcontainerCLI
3236
clock quartz.Clock
3337

34-
// lockCh protects the below fields. We use a channel instead of a mutex so we
35-
// can handle cancellation properly.
36-
lockCh chan struct{}
37-
containers codersdk.WorkspaceAgentListContainersResponse
38-
mtime time.Time
38+
// lockCh protects the below fields. We use a channel instead of a
39+
// mutex so we can handle cancellation properly.
40+
lockCh chan struct{}
41+
containers codersdk.WorkspaceAgentListContainersResponse
42+
mtime time.Time
43+
devcontainerNames map[string]struct{} // Track devcontainer names to avoid duplicates.
44+
knownDevcontainers []codersdk.WorkspaceAgentDevcontainer // Track predefined and runtime-detected devcontainers.
3945
}
4046

4147
// Option is a functional option for API.
@@ -55,12 +61,29 @@ func WithDevcontainerCLI(dccli DevcontainerCLI) Option {
5561
}
5662
}
5763

64+
// WithDevcontainers sets the known devcontainers for the API. This
65+
// allows the API to be aware of devcontainers defined in the workspace
66+
// agent manifest.
67+
func WithDevcontainers(devcontainers []codersdk.WorkspaceAgentDevcontainer) Option {
68+
return func(api *API) {
69+
if len(devcontainers) > 0 {
70+
api.knownDevcontainers = slices.Clone(devcontainers)
71+
api.devcontainerNames = make(map[string]struct{}, len(devcontainers))
72+
for _, devcontainer := range devcontainers {
73+
api.devcontainerNames[devcontainer.Name] = struct{}{}
74+
}
75+
}
76+
}
77+
}
78+
5879
// NewAPI returns a new API with the given options applied.
5980
func NewAPI(logger slog.Logger, options ...Option) *API {
6081
api := &API{
61-
clock: quartz.NewReal(),
62-
cacheDuration: defaultGetContainersCacheDuration,
63-
lockCh: make(chan struct{}, 1),
82+
clock: quartz.NewReal(),
83+
cacheDuration: defaultGetContainersCacheDuration,
84+
lockCh: make(chan struct{}, 1),
85+
devcontainerNames: make(map[string]struct{}),
86+
knownDevcontainers: []codersdk.WorkspaceAgentDevcontainer{},
6487
}
6588
for _, opt := range options {
6689
opt(api)
@@ -79,6 +102,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
79102
func (api *API) Routes() http.Handler {
80103
r := chi.NewRouter()
81104
r.Get("/", api.handleList)
105+
r.Get("/devcontainers", api.handleListDevcontainers)
82106
r.Post("/{id}/recreate", api.handleRecreate)
83107
return r
84108
}
@@ -121,12 +145,11 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC
121145
select {
122146
case <-ctx.Done():
123147
return codersdk.WorkspaceAgentListContainersResponse{}, ctx.Err()
124-
default:
125-
api.lockCh <- struct{}{}
148+
case api.lockCh <- struct{}{}:
149+
defer func() {
150+
<-api.lockCh
151+
}()
126152
}
127-
defer func() {
128-
<-api.lockCh
129-
}()
130153

131154
now := api.clock.Now()
132155
if now.Sub(api.mtime) < api.cacheDuration {
@@ -142,6 +165,53 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC
142165
api.containers = updated
143166
api.mtime = now
144167

168+
// Reset all known devcontainers to not running.
169+
for i := range api.knownDevcontainers {
170+
api.knownDevcontainers[i].Running = false
171+
api.knownDevcontainers[i].Container = nil
172+
}
173+
174+
// Check if the container is running and update the known devcontainers.
175+
for _, container := range updated.Containers {
176+
workspaceFolder := container.Labels[DevcontainerLocalFolderLabel]
177+
if workspaceFolder != "" {
178+
// Check if this is already in our known list.
179+
if knownIndex := slices.IndexFunc(api.knownDevcontainers, func(dc codersdk.WorkspaceAgentDevcontainer) bool {
180+
return dc.WorkspaceFolder == workspaceFolder
181+
}); knownIndex != -1 {
182+
// Update existing entry with runtime information.
183+
if api.knownDevcontainers[knownIndex].ConfigPath == "" {
184+
api.knownDevcontainers[knownIndex].ConfigPath = container.Labels[DevcontainerConfigFileLabel]
185+
}
186+
api.knownDevcontainers[knownIndex].Running = container.Running
187+
api.knownDevcontainers[knownIndex].Container = &container
188+
continue
189+
}
190+
191+
// If not in our known list, add as a runtime detected entry.
192+
name := path.Base(workspaceFolder)
193+
if _, ok := api.devcontainerNames[name]; ok {
194+
// Try to find a unique name by appending a number.
195+
for i := 2; ; i++ {
196+
newName := fmt.Sprintf("%s-%d", name, i)
197+
if _, ok := api.devcontainerNames[newName]; !ok {
198+
name = newName
199+
break
200+
}
201+
}
202+
}
203+
api.devcontainerNames[name] = struct{}{}
204+
api.knownDevcontainers = append(api.knownDevcontainers, codersdk.WorkspaceAgentDevcontainer{
205+
ID: uuid.New(),
206+
Name: name,
207+
WorkspaceFolder: workspaceFolder,
208+
ConfigPath: container.Labels[DevcontainerConfigFileLabel],
209+
Running: container.Running,
210+
Container: &container,
211+
})
212+
}
213+
}
214+
145215
return copyListContainersResponse(api.containers), nil
146216
}
147217

@@ -158,7 +228,7 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
158228
return
159229
}
160230

161-
containers, err := api.cl.List(ctx)
231+
containers, err := api.getContainers(ctx)
162232
if err != nil {
163233
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
164234
Message: "Could not list containers",
@@ -203,3 +273,39 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
203273

204274
w.WriteHeader(http.StatusNoContent)
205275
}
276+
277+
// handleListDevcontainers handles the HTTP request to list known devcontainers.
278+
func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request) {
279+
ctx := r.Context()
280+
281+
// Run getContainers to detect the latest devcontainers and their state.
282+
_, err := api.getContainers(ctx)
283+
if err != nil {
284+
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
285+
Message: "Could not list containers",
286+
Detail: err.Error(),
287+
})
288+
return
289+
}
290+
291+
select {
292+
case <-ctx.Done():
293+
return
294+
case api.lockCh <- struct{}{}:
295+
}
296+
devcontainers := slices.Clone(api.knownDevcontainers)
297+
<-api.lockCh
298+
299+
slices.SortFunc(devcontainers, func(a, b codersdk.WorkspaceAgentDevcontainer) int {
300+
if cmp := strings.Compare(a.WorkspaceFolder, b.WorkspaceFolder); cmp != 0 {
301+
return cmp
302+
}
303+
return strings.Compare(a.ConfigPath, b.ConfigPath)
304+
})
305+
306+
response := codersdk.WorkspaceAgentDevcontainersResponse{
307+
Devcontainers: devcontainers,
308+
}
309+
310+
httpapi.Write(ctx, w, http.StatusOK, response)
311+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy