Skip to content

Commit 4504755

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

File tree

12 files changed

+573
-21
lines changed

12 files changed

+573
-21
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: 146 additions & 18 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
@@ -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.
12291367
func (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

Comments
 (0)