Skip to content

Commit 0fcab3f

Browse files
feat(agent): support deleting dev containers
1 parent 96fca01 commit 0fcab3f

File tree

12 files changed

+566
-16
lines changed

12 files changed

+566
-16
lines changed

agent/agentcontainers/acmock/acmock.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

agent/agentcontainers/api.go

Lines changed: 139 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
12291365
func (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

Comments
 (0)