@@ -3,11 +3,15 @@ package agentcontainers
33import (
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 {
3135dccli DevcontainerCLI
3236clock 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.
5980func NewAPI (logger slog.Logger ,options ... Option )* API {
6081api := & 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}
6588for _ ,opt := range options {
6689opt (api )
@@ -79,6 +102,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
79102func (api * API )Routes () http.Handler {
80103r := chi .NewRouter ()
81104r .Get ("/" ,api .handleList )
105+ r .Get ("/devcontainers" ,api .handleListDevcontainers )
82106r .Post ("/{id}/recreate" ,api .handleRecreate )
83107return r
84108}
@@ -121,12 +145,11 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC
121145select {
122146case <- ctx .Done ():
123147return 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
131154now := api .clock .Now ()
132155if now .Sub (api .mtime )< api .cacheDuration {
@@ -142,6 +165,53 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC
142165api .containers = updated
143166api .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+
145215return copyListContainersResponse (api .containers ),nil
146216}
147217
@@ -158,7 +228,7 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
158228return
159229}
160230
161- containers ,err := api .cl . List (ctx )
231+ containers ,err := api .getContainers (ctx )
162232if err != nil {
163233httpapi .Write (ctx ,w ,http .StatusInternalServerError , codersdk.Response {
164234Message :"Could not list containers" ,
@@ -203,3 +273,39 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) {
203273
204274w .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+ }