@@ -32,6 +32,7 @@ import (
3232"github.com/coder/coder/v2/agent/agentexec"
3333"github.com/coder/coder/v2/agent/usershell"
3434"github.com/coder/coder/v2/coderd/httpapi"
35+ "github.com/coder/coder/v2/coderd/httpapi/httperror"
3536"github.com/coder/coder/v2/codersdk"
3637"github.com/coder/coder/v2/codersdk/agentsdk"
3738"github.com/coder/coder/v2/provisioner"
@@ -743,6 +744,7 @@ func (api *API) Routes() http.Handler {
743744// /-route was dropped. We can drop the /devcontainers prefix here too.
744745r .Route ("/devcontainers/{devcontainer}" ,func (r chi.Router ) {
745746r .Post ("/recreate" ,api .handleDevcontainerRecreate )
747+ r .Delete ("/" ,api .handleDevcontainerDelete )
746748})
747749
748750return r
@@ -853,26 +855,24 @@ func (api *API) updateContainers(ctx context.Context) error {
853855listCtx ,listCancel := context .WithTimeout (ctx ,defaultOperationTimeout )
854856defer listCancel ()
855857
858+ api .mu .Lock ()
859+ defer api .mu .Unlock ()
860+
856861updated ,err := api .ccli .List (listCtx )
857862if err != nil {
858863// If the context was canceled, we hold off on clearing the
859864// containers cache. This is to avoid clearing the cache if
860865// the update was canceled due to a timeout. Hopefully this
861866// will clear up on the next update.
862867if ! errors .Is (err ,context .Canceled ) {
863- api .mu .Lock ()
864868api .containersErr = err
865- api .mu .Unlock ()
866869}
867870
868871return xerrors .Errorf ("list containers failed: %w" ,err )
869872}
870873// Clone to avoid test flakes due to data manipulation.
871874updated .Containers = slices .Clone (updated .Containers )
872875
873- api .mu .Lock ()
874- defer api .mu .Unlock ()
875-
876876var previouslyKnownDevcontainers map [string ]codersdk.WorkspaceAgentDevcontainer
877877if len (api .updateChans )> 0 {
878878previouslyKnownDevcontainers = maps .Clone (api .knownDevcontainers )
@@ -1019,6 +1019,9 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
10191019case dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting :
10201020continue // This state is handled by the recreation routine.
10211021
1022+ case dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStopping :
1023+ continue // This state is handled by the delete routine.
1024+
10221025case dc .Status == codersdk .WorkspaceAgentDevcontainerStatusError && (dc .Container == nil || dc .Container .CreatedAt .Before (api .recreateErrorTimes [dc .WorkspaceFolder ])):
10231026continue // The devcontainer needs to be recreated.
10241027
@@ -1220,6 +1223,140 @@ func (api *API) getContainers() (codersdk.WorkspaceAgentListContainersResponse,
12201223},nil
12211224}
12221225
1226+ func (api * API )devcontainerByIDLocked (devcontainerID string ) (codersdk.WorkspaceAgentDevcontainer ,error ) {
1227+ for _ ,knownDC := range api .knownDevcontainers {
1228+ if knownDC .ID .String ()== devcontainerID {
1229+ return knownDC ,nil
1230+ }
1231+ }
1232+
1233+ return codersdk.WorkspaceAgentDevcontainer {},httperror .NewResponseError (http .StatusNotFound , codersdk.Response {
1234+ Message :"Devcontainer not found." ,
1235+ Detail :fmt .Sprintf ("Could not find devcontainer with ID: %q" ,devcontainerID ),
1236+ })
1237+ }
1238+
1239+ func (api * API )handleDevcontainerDelete (w http.ResponseWriter ,r * http.Request ) {
1240+ var (
1241+ ctx = r .Context ()
1242+ devcontainerID = chi .URLParam (r ,"devcontainer" )
1243+ )
1244+
1245+ if devcontainerID == "" {
1246+ httpapi .Write (ctx ,w ,http .StatusBadRequest , codersdk.Response {
1247+ Message :"Missing devcontainer ID" ,
1248+ Detail :"Devcontainer ID is required to delete a devcontainer." ,
1249+ })
1250+ return
1251+ }
1252+
1253+ api .mu .Lock ()
1254+
1255+ dc ,err := api .devcontainerByIDLocked (devcontainerID )
1256+ if err != nil {
1257+ api .mu .Unlock ()
1258+ httperror .WriteResponseError (ctx ,w ,err )
1259+ return
1260+ }
1261+
1262+ // Check if the devcontainer is currently starting - if so, we can't delete it.
1263+ if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting {
1264+ api .mu .Unlock ()
1265+ httpapi .Write (ctx ,w ,http .StatusConflict , codersdk.Response {
1266+ Message :"Devcontainer is starting" ,
1267+ Detail :fmt .Sprintf ("Devcontainer %q is currently starting and cannot be deleted." ,dc .Name ),
1268+ })
1269+ return
1270+ }
1271+
1272+ // Similarly, if already stopping, don't allow another delete.
1273+ if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStopping {
1274+ api .mu .Unlock ()
1275+ httpapi .Write (ctx ,w ,http .StatusConflict , codersdk.Response {
1276+ Message :"Devcontainer is stopping" ,
1277+ Detail :fmt .Sprintf ("Devcontainer %q is currently stopping." ,dc .Name ),
1278+ })
1279+ return
1280+ }
1281+
1282+ dc .Status = codersdk .WorkspaceAgentDevcontainerStatusStopping
1283+ api .knownDevcontainers [dc .WorkspaceFolder ]= dc
1284+ api .broadcastUpdatesLocked ()
1285+
1286+ // Gather the information we need before unlocking.
1287+ workspaceFolder := dc .WorkspaceFolder
1288+ dcName := dc .Name
1289+ var containerID string
1290+ if dc .Container != nil {
1291+ containerID = dc .Container .ID
1292+ }
1293+ proc ,hasSubAgent := api .injectedSubAgentProcs [workspaceFolder ]
1294+ var subAgentID uuid.UUID
1295+ if hasSubAgent && proc .agent .ID != uuid .Nil {
1296+ subAgentID = proc .agent .ID
1297+ // Stop the subagent process context to ensure it stops.
1298+ proc .stop ()
1299+ }
1300+
1301+ // Unlock the mutex while we perform potentially slow operations
1302+ // (network calls, docker commands) to avoid blocking other operations.
1303+ api .mu .Unlock ()
1304+
1305+ // Delete the subagent if it exists.
1306+ if subAgentID != uuid .Nil {
1307+ client := * api .subAgentClient .Load ()
1308+ if err := client .Delete (ctx ,subAgentID );err != nil {
1309+ api .logger .Error (ctx ,"unable to delete agent" ,slog .Error (err ))
1310+
1311+ httpapi .Write (ctx ,w ,http .StatusInternalServerError , codersdk.Response {
1312+ Message :"An internal error occurred deleting the agent" ,
1313+ Detail :err .Error (),
1314+ })
1315+ return
1316+ }
1317+ }
1318+
1319+ // Stop and remove the container if it exists.
1320+ if containerID != "" {
1321+ if err := api .ccli .Stop (ctx ,containerID );err != nil {
1322+ api .logger .Error (ctx ,"unable to stop container" ,slog .Error (err ))
1323+
1324+ httpapi .Write (ctx ,w ,http .StatusInternalServerError , codersdk.Response {
1325+ Message :"An internal error occurred stopping the container" ,
1326+ Detail :err .Error (),
1327+ })
1328+ return
1329+ }
1330+
1331+ if err := api .ccli .Remove (ctx ,containerID );err != nil {
1332+ api .logger .Error (ctx ,"unable to remove container" ,slog .Error (err ))
1333+
1334+ httpapi .Write (ctx ,w ,http .StatusInternalServerError , codersdk.Response {
1335+ Message :"An internal error occurred removing the container" ,
1336+ Detail :err .Error (),
1337+ })
1338+ return
1339+ }
1340+ }
1341+
1342+ // Re-lock to clean up the state.
1343+ api .mu .Lock ()
1344+ delete (api .devcontainerNames ,dcName )
1345+ delete (api .knownDevcontainers ,workspaceFolder )
1346+ delete (api .devcontainerLogSourceIDs ,workspaceFolder )
1347+ delete (api .recreateSuccessTimes ,workspaceFolder )
1348+ delete (api .recreateErrorTimes ,workspaceFolder )
1349+ delete (api .injectedSubAgentProcs ,workspaceFolder )
1350+ delete (api .usingWorkspaceFolderName ,workspaceFolder )
1351+ api .broadcastUpdatesLocked ()
1352+ api .mu .Unlock ()
1353+
1354+ httpapi .Write (ctx ,w ,http .StatusInternalServerError , codersdk.Response {
1355+ Message :"An internal error occurred" ,
1356+ Detail :err .Error (),
1357+ })
1358+ }
1359+
12231360// handleDevcontainerRecreate handles the HTTP request to recreate a
12241361// devcontainer by referencing the container.
12251362func (api * API )handleDevcontainerRecreate (w http.ResponseWriter ,r * http.Request ) {
@@ -1236,20 +1373,10 @@ func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Reques
12361373
12371374api .mu .Lock ()
12381375
1239- var dc codersdk.WorkspaceAgentDevcontainer
1240- for _ ,knownDC := range api .knownDevcontainers {
1241- if knownDC .ID .String ()== devcontainerID {
1242- dc = knownDC
1243- break
1244- }
1245- }
1246- if dc .ID == uuid .Nil {
1376+ dc ,err := api .devcontainerByIDLocked (devcontainerID )
1377+ if err != nil {
12471378api .mu .Unlock ()
1248-
1249- httpapi .Write (ctx ,w ,http .StatusNotFound , codersdk.Response {
1250- Message :"Devcontainer not found." ,
1251- Detail :fmt .Sprintf ("Could not find devcontainer with ID: %q" ,devcontainerID ),
1252- })
1379+ httperror .WriteResponseError (ctx ,w ,err )
12531380return
12541381}
12551382if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting {