@@ -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
@@ -1019,6 +1021,9 @@ func (api *API) processUpdatedContainersLocked(ctx context.Context, updated code
10191021 case dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting :
10201022 continue // This state is handled by the recreation routine.
10211023
1024+ case dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStopping :
1025+ continue // This state is handled by the delete routine.
1026+
10221027 case dc .Status == codersdk .WorkspaceAgentDevcontainerStatusError && (dc .Container == nil || dc .Container .CreatedAt .Before (api .recreateErrorTimes [dc .WorkspaceFolder ])):
10231028 continue // The devcontainer needs to be recreated.
10241029
@@ -1224,6 +1229,137 @@ func (api *API) getContainers() (codersdk.WorkspaceAgentListContainersResponse,
12241229 }, nil
12251230}
12261231
1232+ func (api * API ) devcontainerByIDLocked (devcontainerID string ) (codersdk.WorkspaceAgentDevcontainer , error ) {
1233+ for _ , knownDC := range api .knownDevcontainers {
1234+ if knownDC .ID .String () == devcontainerID {
1235+ return knownDC , nil
1236+ }
1237+ }
1238+
1239+ return codersdk.WorkspaceAgentDevcontainer {}, httperror .NewResponseError (http .StatusNotFound , codersdk.Response {
1240+ Message : "Devcontainer not found." ,
1241+ Detail : fmt .Sprintf ("Could not find devcontainer with ID: %q" , devcontainerID ),
1242+ })
1243+ }
1244+
1245+ func (api * API ) handleDevcontainerDelete (w http.ResponseWriter , r * http.Request ) {
1246+ var (
1247+ ctx = r .Context ()
1248+ devcontainerID = chi .URLParam (r , "devcontainer" )
1249+ )
1250+
1251+ if devcontainerID == "" {
1252+ httpapi .Write (ctx , w , http .StatusBadRequest , codersdk.Response {
1253+ Message : "Missing devcontainer ID" ,
1254+ Detail : "Devcontainer ID is required to delete a devcontainer." ,
1255+ })
1256+ return
1257+ }
1258+
1259+ api .mu .Lock ()
1260+
1261+ dc , err := api .devcontainerByIDLocked (devcontainerID )
1262+ if err != nil {
1263+ api .mu .Unlock ()
1264+ httperror .WriteResponseError (ctx , w , err )
1265+ return
1266+ }
1267+
1268+ // Check if the devcontainer is currently starting - if so, we can't delete it.
1269+ if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting {
1270+ api .mu .Unlock ()
1271+ httpapi .Write (ctx , w , http .StatusConflict , codersdk.Response {
1272+ Message : "Devcontainer is starting" ,
1273+ Detail : fmt .Sprintf ("Devcontainer %q is currently starting and cannot be deleted." , dc .Name ),
1274+ })
1275+ return
1276+ }
1277+
1278+ // Similarly, if already stopping, don't allow another delete.
1279+ if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStopping {
1280+ api .mu .Unlock ()
1281+ httpapi .Write (ctx , w , http .StatusConflict , codersdk.Response {
1282+ Message : "Devcontainer is stopping" ,
1283+ Detail : fmt .Sprintf ("Devcontainer %q is currently stopping." , dc .Name ),
1284+ })
1285+ return
1286+ }
1287+
1288+ dc .Status = codersdk .WorkspaceAgentDevcontainerStatusStopping
1289+ dc .Error = ""
1290+ api .knownDevcontainers [dc .WorkspaceFolder ] = dc
1291+ api .broadcastUpdatesLocked ()
1292+
1293+ // Gather the information we need before unlocking.
1294+ workspaceFolder := dc .WorkspaceFolder
1295+ dcName := dc .Name
1296+ var containerID string
1297+ if dc .Container != nil {
1298+ containerID = dc .Container .ID
1299+ }
1300+ proc , hasSubAgent := api .injectedSubAgentProcs [workspaceFolder ]
1301+ var subAgentID uuid.UUID
1302+ if hasSubAgent && proc .agent .ID != uuid .Nil {
1303+ subAgentID = proc .agent .ID
1304+ // Stop the subagent process context to ensure it stops.
1305+ proc .stop ()
1306+ }
1307+
1308+ // Unlock the mutex while we perform potentially slow operations
1309+ // (network calls, docker commands) to avoid blocking other operations.
1310+ api .mu .Unlock ()
1311+
1312+ // Stop and remove the container if it exists.
1313+ if containerID != "" {
1314+ if err := api .ccli .Stop (ctx , containerID ); err != nil {
1315+ api .logger .Error (ctx , "unable to stop container" , slog .Error (err ))
1316+
1317+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
1318+ Message : "An internal error occurred stopping the container" ,
1319+ Detail : err .Error (),
1320+ })
1321+ return
1322+ }
1323+
1324+ if err := api .ccli .Remove (ctx , containerID ); err != nil {
1325+ api .logger .Error (ctx , "unable to remove container" , slog .Error (err ))
1326+
1327+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
1328+ Message : "An internal error occurred removing the container" ,
1329+ Detail : err .Error (),
1330+ })
1331+ return
1332+ }
1333+ }
1334+
1335+ // Delete the subagent if it exists.
1336+ if subAgentID != uuid .Nil {
1337+ client := * api .subAgentClient .Load ()
1338+ if err := client .Delete (ctx , subAgentID ); err != nil {
1339+ api .logger .Error (ctx , "unable to delete agent" , slog .Error (err ))
1340+
1341+ httpapi .Write (ctx , w , http .StatusInternalServerError , codersdk.Response {
1342+ Message : "An internal error occurred deleting the agent" ,
1343+ Detail : err .Error (),
1344+ })
1345+ return
1346+ }
1347+ }
1348+
1349+ api .mu .Lock ()
1350+ delete (api .devcontainerNames , dcName )
1351+ delete (api .knownDevcontainers , workspaceFolder )
1352+ delete (api .devcontainerLogSourceIDs , workspaceFolder )
1353+ delete (api .recreateSuccessTimes , workspaceFolder )
1354+ delete (api .recreateErrorTimes , workspaceFolder )
1355+ delete (api .usingWorkspaceFolderName , workspaceFolder )
1356+ delete (api .injectedSubAgentProcs , workspaceFolder )
1357+ api .broadcastUpdatesLocked ()
1358+ api .mu .Unlock ()
1359+
1360+ httpapi .Write (ctx , w , http .StatusNoContent , nil )
1361+ }
1362+
12271363// handleDevcontainerRecreate handles the HTTP request to recreate a
12281364// devcontainer by referencing the container.
12291365func (api * API ) handleDevcontainerRecreate (w http.ResponseWriter , r * http.Request ) {
@@ -1240,20 +1376,10 @@ func (api *API) handleDevcontainerRecreate(w http.ResponseWriter, r *http.Reques
12401376
12411377 api .mu .Lock ()
12421378
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 {
1379+ dc , err := api .devcontainerByIDLocked (devcontainerID )
1380+ if err != nil {
12511381 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- })
1382+ httperror .WriteResponseError (ctx , w , err )
12571383 return
12581384 }
12591385 if dc .Status == codersdk .WorkspaceAgentDevcontainerStatusStarting {
0 commit comments