@@ -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.
744745 r .Route ("/devcontainers/{devcontainer}" , func (r chi.Router ) {
745746 r .Post ("/recreate" , api .handleDevcontainerRecreate )
747+ r .Delete ("/" , api .handleDevcontainerDelete )
746748 })
747749
748750 return r
@@ -853,26 +855,24 @@ func (api *API) updateContainers(ctx context.Context) error {
853855 listCtx , listCancel := context .WithTimeout (ctx , defaultOperationTimeout )
854856 defer listCancel ()
855857
858+ api .mu .Lock ()
859+ defer api .mu .Unlock ()
860+
856861 updated , err := api .ccli .List (listCtx )
857862 if 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.
862867 if ! errors .Is (err , context .Canceled ) {
863- api .mu .Lock ()
864868 api .containersErr = err
865- api .mu .Unlock ()
866869 }
867870
868871 return xerrors .Errorf ("list containers failed: %w" , err )
869872 }
870873 // Clone to avoid test flakes due to data manipulation.
871874 updated .Containers = slices .Clone (updated .Containers )
872875
873- api .mu .Lock ()
874- defer api .mu .Unlock ()
875-
876876 var previouslyKnownDevcontainers map [string ]codersdk.WorkspaceAgentDevcontainer
877877 if len (api .updateChans ) > 0 {
878878 previouslyKnownDevcontainers = maps .Clone (api .knownDevcontainers )
@@ -1019,6 +1019,9 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
10191019 case dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting :
10201020 continue // 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+
10221025 case dc .Status == codersdk .WorkspaceAgentDevcontainerStatusError && (dc .Container == nil || dc .Container .CreatedAt .Before (api .recreateErrorTimes [dc .WorkspaceFolder ])):
10231026 continue // The devcontainer needs to be recreated.
10241027
@@ -1224,6 +1227,141 @@ func (api *API) getContainers() (codersdk.WorkspaceAgentListContainersResponse,
12241227 }, nil
12251228}
12261229
1230+ func (api * API ) devcontainerByIDLocked (devcontainerID string ) (codersdk.WorkspaceAgentDevcontainer , error ) {
1231+ for _ , knownDC := range api .knownDevcontainers {
1232+ if knownDC .ID .String () == devcontainerID {
1233+ return knownDC , nil
1234+ }
1235+ }
1236+
1237+ return codersdk.WorkspaceAgentDevcontainer {}, httperror .NewResponseError (http .StatusNotFound , codersdk.Response {
1238+ Message : "Devcontainer not found." ,
1239+ Detail : fmt .Sprintf ("Could not find devcontainer with ID: %q" , devcontainerID ),
1240+ })
1241+ }
1242+
1243+ func (api * API ) handleDevcontainerDelete (w http.ResponseWriter , r * http.Request ) {
1244+ var (
1245+ ctx = r .Context ()
1246+ devcontainerID = chi .URLParam (r , "devcontainer" )
1247+ )
1248+
1249+ if devcontainerID == "" {
1250+ httpapi .Write (ctx , w , http .StatusBadRequest , codersdk.Response {
1251+ Message : "Missing devcontainer ID" ,
1252+ Detail : "Devcontainer ID is required to delete a devcontainer." ,
1253+ })
1254+ return
1255+ }
1256+
1257+ api .mu .Lock ()
1258+
1259+ dc , err := api .devcontainerByIDLocked (devcontainerID )
1260+ if err != nil {
1261+ api .mu .Unlock ()
1262+ httperror .WriteResponseError (ctx , w , err )
1263+ return
1264+ }
1265+
1266+ // Check if the devcontainer is currently starting - if so, we can't delete it.
1267+ if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting {
1268+ api .mu .Unlock ()
1269+ httpapi .Write (ctx , w , http .StatusConflict , codersdk.Response {
1270+ Message : "Devcontainer is starting" ,
1271+ Detail : fmt .Sprintf ("Devcontainer %q is currently starting and cannot be deleted." , dc .Name ),
1272+ })
1273+ return
1274+ }
1275+
1276+ // Similarly, if already stopping, don't allow another delete.
1277+ if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStopping {
1278+ api .mu .Unlock ()
1279+ httpapi .Write (ctx , w , http .StatusConflict , codersdk.Response {
1280+ Message : "Devcontainer is stopping" ,
1281+ Detail : fmt .Sprintf ("Devcontainer %q is currently stopping." , dc .Name ),
1282+ })
1283+ return
1284+ }
1285+
1286+ dc .Status = codersdk .WorkspaceAgentDevcontainerStatusStopping
1287+ dc .Error = ""
1288+ api .knownDevcontainers [dc .WorkspaceFolder ] = dc
1289+ api .broadcastUpdatesLocked ()
1290+
1291+ // Gather the information we need before unlocking.
1292+ workspaceFolder := dc .WorkspaceFolder
1293+ dcName := dc .Name
1294+ var containerID string
1295+ if dc .Container != nil {
1296+ containerID = dc .Container .ID
1297+ }
1298+ proc , hasSubAgent := api .injectedSubAgentProcs [workspaceFolder ]
1299+ var subAgentID uuid.UUID
1300+ if hasSubAgent && proc .agent .ID != uuid .Nil {
1301+ subAgentID = proc .agent .ID
1302+ // Stop the subagent process context to ensure it stops.
1303+ proc .stop ()
1304+ }
1305+
1306+ // Unlock the mutex while we perform potentially slow operations
1307+ // (network calls, docker commands) to avoid blocking other operations.
1308+ api .mu .Unlock ()
1309+
1310+ // Delete the subagent if it exists.
1311+ if subAgentID != uuid .Nil {
1312+ client := * api .subAgentClient .Load ()
1313+ if err := client .Delete (ctx , subAgentID ); err != nil {
1314+ api .logger .Error (ctx , "unable to delete agent" , slog .Error (err ))
1315+
1316+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
1317+ Message : "An internal error occurred deleting the agent" ,
1318+ Detail : err .Error (),
1319+ })
1320+ return
1321+ }
1322+
1323+ api .mu .Lock ()
1324+ delete (api .injectedSubAgentProcs , workspaceFolder )
1325+ api .broadcastUpdatesLocked ()
1326+ api .mu .Unlock ()
1327+ }
1328+
1329+ // Stop and remove the container if it exists.
1330+ if containerID != "" {
1331+ if err := api .ccli .Stop (ctx , containerID ); err != nil {
1332+ api .logger .Error (ctx , "unable to stop container" , slog .Error (err ))
1333+
1334+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
1335+ Message : "An internal error occurred stopping the container" ,
1336+ Detail : err .Error (),
1337+ })
1338+ return
1339+ }
1340+
1341+ if err := api .ccli .Remove (ctx , containerID ); err != nil {
1342+ api .logger .Error (ctx , "unable to remove container" , slog .Error (err ))
1343+
1344+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
1345+ Message : "An internal error occurred removing the container" ,
1346+ Detail : err .Error (),
1347+ })
1348+ return
1349+ }
1350+ }
1351+
1352+ api .mu .Lock ()
1353+ delete (api .devcontainerNames , dcName )
1354+ delete (api .knownDevcontainers , workspaceFolder )
1355+ delete (api .devcontainerLogSourceIDs , workspaceFolder )
1356+ delete (api .recreateSuccessTimes , workspaceFolder )
1357+ delete (api .recreateErrorTimes , workspaceFolder )
1358+ delete (api .usingWorkspaceFolderName , workspaceFolder )
1359+ api .broadcastUpdatesLocked ()
1360+ api .mu .Unlock ()
1361+
1362+ httpapi .Write (ctx , w , http .StatusNoContent , nil )
1363+ }
1364+
12271365// handleDevcontainerRecreate handles the HTTP request to recreate a
12281366// devcontainer by referencing the container.
12291367func (api * API ) handleDevcontainerRecreate (w http.ResponseWriter , r * http.Request ) {
@@ -1240,20 +1378,10 @@ func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Reques
12401378
12411379 api .mu .Lock ()
12421380
1243- var dc codersdk.WorkspaceAgentDevcontainer
1244- for _ , knownDC := range api .knownDevcontainers {
1245- if knownDC .ID .String () == devcontainerID {
1246- dc = knownDC
1247- break
1248- }
1249- }
1250- if dc .ID == uuid .Nil {
1381+ dc , err := api .devcontainerByIDLocked (devcontainerID )
1382+ if err != nil {
12511383 api .mu .Unlock ()
1252-
1253- httpapi .Write (ctx , w , http .StatusNotFound , codersdk.Response {
1254- Message : "Devcontainer not found." ,
1255- Detail : fmt .Sprintf ("Could not find devcontainer with ID: %q" , devcontainerID ),
1256- })
1384+ httperror .WriteResponseError (ctx , w , err )
12571385 return
12581386 }
12591387 if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting {
0 commit comments