Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit6eeae3c

Browse files
feat(coderd): support deleting dev containers
1 parenta8ff575 commit6eeae3c

File tree

11 files changed

+484
-4
lines changed

11 files changed

+484
-4
lines changed

‎agent/agentcontainers/api.go‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,10 +1351,7 @@ func (api *API) handleDevcontainerDelete(w http.ResponseWriter, r *http.Request)
13511351
api.broadcastUpdatesLocked()
13521352
api.mu.Unlock()
13531353

1354-
httpapi.Write(ctx,w,http.StatusInternalServerError, codersdk.Response{
1355-
Message:"An internal error occurred",
1356-
Detail:err.Error(),
1357-
})
1354+
httpapi.Write(ctx,w,http.StatusNoContent,nil)
13581355
}
13591356

13601357
// handleDevcontainerRecreate handles the HTTP request to recreate a

‎coderd/apidoc/docs.go‎

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

‎coderd/apidoc/swagger.json‎

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

‎coderd/coderd.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,7 @@ func New(options *Options) *API {
14341434
r.Get("/connection",api.workspaceAgentConnection)
14351435
r.Get("/containers",api.workspaceAgentListContainers)
14361436
r.Get("/containers/watch",api.watchWorkspaceAgentContainers)
1437+
r.Delete("/containers/devcontainers/{devcontainer}",api.workspaceAgentDeleteDevcontainer)
14371438
r.Post("/containers/devcontainers/{devcontainer}/recreate",api.workspaceAgentRecreateDevcontainer)
14381439
r.Get("/coordinate",api.workspaceAgentClientCoordinate)
14391440

‎coderd/workspaceagents.go‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,91 @@ func (api *API) workspaceAgentListContainers(rw http.ResponseWriter, r *http.Req
11221122
httpapi.Write(ctx,rw,http.StatusOK,cts)
11231123
}
11241124

1125+
// @Summary Delete devcontainer for workspace agent
1126+
// @ID delete-devcontainer-for-workspace-agent
1127+
// @Security CoderSessionToken
1128+
// @Tags Agents
1129+
// @Produce json
1130+
// @Param workspaceagent path string true "Workspace agent ID" format(uuid)
1131+
// @Param devcontainer path string true "Devcontainer ID"
1132+
// @Success 204
1133+
// @Router /workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer} [delete]
1134+
func (api*API)workspaceAgentDeleteDevcontainer(rw http.ResponseWriter,r*http.Request) {
1135+
ctx:=r.Context()
1136+
workspaceAgent:=httpmw.WorkspaceAgentParam(r)
1137+
1138+
devcontainer:=chi.URLParam(r,"devcontainer")
1139+
ifdevcontainer=="" {
1140+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
1141+
Message:"Devcontainer ID is required.",
1142+
Validations: []codersdk.ValidationError{
1143+
{Field:"devcontainer",Detail:"Devcontainer ID is required."},
1144+
},
1145+
})
1146+
return
1147+
}
1148+
1149+
apiAgent,err:=db2sdk.WorkspaceAgent(
1150+
api.DERPMap(),
1151+
*api.TailnetCoordinator.Load(),
1152+
workspaceAgent,
1153+
nil,
1154+
nil,
1155+
nil,
1156+
api.AgentInactiveDisconnectTimeout,
1157+
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
1158+
)
1159+
iferr!=nil {
1160+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
1161+
Message:"Internal error reading workspace agent.",
1162+
Detail:err.Error(),
1163+
})
1164+
return
1165+
}
1166+
ifapiAgent.Status!=codersdk.WorkspaceAgentConnected {
1167+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
1168+
Message:fmt.Sprintf("Agent state is %q, it must be in the %q state.",apiAgent.Status,codersdk.WorkspaceAgentConnected),
1169+
})
1170+
return
1171+
}
1172+
1173+
// If the agent is unreachable, the request will hang. Assume that if we
1174+
// don't get a response after 30s that the agent is unreachable.
1175+
dialCtx,dialCancel:=context.WithTimeout(ctx,30*time.Second)
1176+
deferdialCancel()
1177+
agentConn,release,err:=api.agentProvider.AgentConn(dialCtx,workspaceAgent.ID)
1178+
iferr!=nil {
1179+
httpapi.Write(dialCtx,rw,http.StatusInternalServerError, codersdk.Response{
1180+
Message:"Internal error dialing workspace agent.",
1181+
Detail:err.Error(),
1182+
})
1183+
return
1184+
}
1185+
deferrelease()
1186+
1187+
iferr=agentConn.DeleteDevcontainer(ctx,devcontainer);err!=nil {
1188+
iferrors.Is(err,context.Canceled) {
1189+
httpapi.Write(ctx,rw,http.StatusRequestTimeout, codersdk.Response{
1190+
Message:"Failed to delete devcontainer from agent.",
1191+
Detail:"Request timed out.",
1192+
})
1193+
return
1194+
}
1195+
// If the agent returns a codersdk.Error, we can return that directly.
1196+
ifcerr,ok:=codersdk.AsError(err);ok {
1197+
httpapi.Write(ctx,rw,cerr.StatusCode(),cerr.Response)
1198+
return
1199+
}
1200+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
1201+
Message:"Internal error deleting devcontainer.",
1202+
Detail:err.Error(),
1203+
})
1204+
return
1205+
}
1206+
1207+
httpapi.Write(ctx,rw,http.StatusNoContent,nil)
1208+
}
1209+
11251210
// @Summary Recreate devcontainer for workspace agent
11261211
// @ID recreate-devcontainer-for-workspace-agent
11271212
// @Security CoderSessionToken

‎coderd/workspaceagents_internal_test.go‎

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"database/sql"
7+
"encoding/json"
78
"fmt"
89
"io"
910
"net/http"
@@ -17,6 +18,7 @@ import (
1718
"github.com/google/uuid"
1819
"github.com/stretchr/testify/require"
1920
"go.uber.org/mock/gomock"
21+
"golang.org/x/xerrors"
2022

2123
"cdr.dev/slog"
2224
"cdr.dev/slog/sloggers/slogtest"
@@ -35,6 +37,17 @@ import (
3537
"github.com/coder/websocket"
3638
)
3739

40+
// newSDKError creates a codersdk.Error for testing by simulating an HTTP response.
41+
funcnewSDKError(statusCodeint,resp codersdk.Response)error {
42+
body,_:=json.Marshal(resp)
43+
httpResp:=&http.Response{
44+
StatusCode:statusCode,
45+
Body:io.NopCloser(bytes.NewReader(body)),
46+
Request:&http.Request{URL:&url.URL{}},
47+
}
48+
returncodersdk.ReadBodyAsError(httpResp)
49+
}
50+
3851
typefakeAgentProviderstruct {
3952
agentConnfunc(ctx context.Context,agentID uuid.UUID) (_ workspacesdk.AgentConn,releasefunc(),_error)
4053
}
@@ -319,3 +332,145 @@ func TestWatchAgentContainers(t *testing.T) {
319332
}
320333
})
321334
}
335+
336+
funcTestWorkspaceAgentDeleteDevcontainer(t*testing.T) {
337+
t.Parallel()
338+
339+
tests:= []struct {
340+
namestring
341+
agentConnectedbool// Controls FirstConnectedAt/LastConnectedAt validity
342+
agentConnErrorerror// Error returned by fakeAgentProvider.AgentConn (nil = success)
343+
deleteErrorerror// Error returned by DeleteDevcontainer mock (nil = success)
344+
expectedStatusCodeint
345+
}{
346+
{
347+
name:"OK",
348+
agentConnected:true,
349+
agentConnError:nil,
350+
deleteError:nil,
351+
expectedStatusCode:http.StatusNoContent,
352+
},
353+
{
354+
name:"AgentNotConnected",
355+
agentConnected:false,
356+
expectedStatusCode:http.StatusBadRequest,
357+
},
358+
{
359+
name:"DevcontainerNotFound",
360+
agentConnected:true,
361+
deleteError:newSDKError(http.StatusNotFound, codersdk.Response{
362+
Message:"Devcontainer not found.",
363+
}),
364+
expectedStatusCode:http.StatusNotFound,
365+
},
366+
{
367+
name:"AgentConnectionFailure",
368+
agentConnected:true,
369+
agentConnError:xerrors.New("connection failed"),
370+
expectedStatusCode:http.StatusInternalServerError,
371+
},
372+
{
373+
name:"InternalError",
374+
agentConnected:true,
375+
deleteError:xerrors.New("internal error"),
376+
expectedStatusCode:http.StatusInternalServerError,
377+
},
378+
}
379+
380+
for_,tc:=rangetests {
381+
t.Run(tc.name,func(t*testing.T) {
382+
t.Parallel()
383+
384+
var (
385+
ctx=testutil.Context(t,testutil.WaitShort)
386+
logger=slogtest.Make(t,&slogtest.Options{IgnoreErrors:true}).Leveled(slog.LevelDebug).Named("coderd")
387+
388+
mCtrl=gomock.NewController(t)
389+
mDB=dbmock.NewMockStore(mCtrl)
390+
mCoordinator=tailnettest.NewMockCoordinator(mCtrl)
391+
392+
agentID=uuid.New()
393+
resourceID=uuid.New()
394+
jobID=uuid.New()
395+
buildID=uuid.New()
396+
workspaceID=uuid.New()
397+
devcontainerID=uuid.NewString()
398+
399+
r=chi.NewMux()
400+
401+
api=API{
402+
ctx:ctx,
403+
Options:&Options{
404+
AgentInactiveDisconnectTimeout:testutil.WaitShort,
405+
Database:mDB,
406+
Logger:logger,
407+
DeploymentValues:&codersdk.DeploymentValues{},
408+
TailnetCoordinator:tailnettest.NewFakeCoordinator(),
409+
},
410+
}
411+
)
412+
413+
vartailnetCoordinator tailnet.Coordinator=mCoordinator
414+
api.TailnetCoordinator.Store(&tailnetCoordinator)
415+
416+
// Setup agent provider based on test case.
417+
iftc.agentConnected&&tc.agentConnError==nil {
418+
mAgentConn:=agentconnmock.NewMockAgentConn(mCtrl)
419+
mAgentConn.EXPECT().DeleteDevcontainer(gomock.Any(),devcontainerID).Return(tc.deleteError)
420+
api.agentProvider=fakeAgentProvider{
421+
agentConn:func(_ context.Context,_ uuid.UUID) (_ workspacesdk.AgentConn,releasefunc(),_error) {
422+
returnmAgentConn,func() {},nil
423+
},
424+
}
425+
}elseiftc.agentConnError!=nil {
426+
api.agentProvider=fakeAgentProvider{
427+
agentConn:func(_ context.Context,_ uuid.UUID) (_ workspacesdk.AgentConn,releasefunc(),_error) {
428+
returnnil,nil,tc.agentConnError
429+
},
430+
}
431+
}
432+
433+
// Setup database mocks for ExtractWorkspaceAgentParam middleware.
434+
mDB.EXPECT().GetWorkspaceAgentByID(gomock.Any(),agentID).Return(database.WorkspaceAgent{
435+
ID:agentID,
436+
ResourceID:resourceID,
437+
LifecycleState:database.WorkspaceAgentLifecycleStateReady,
438+
FirstConnectedAt: sql.NullTime{Valid:tc.agentConnected,Time:dbtime.Now()},
439+
LastConnectedAt: sql.NullTime{Valid:tc.agentConnected,Time:dbtime.Now()},
440+
},nil)
441+
mDB.EXPECT().GetWorkspaceResourceByID(gomock.Any(),resourceID).Return(database.WorkspaceResource{
442+
ID:resourceID,
443+
JobID:jobID,
444+
},nil)
445+
mDB.EXPECT().GetProvisionerJobByID(gomock.Any(),jobID).Return(database.ProvisionerJob{
446+
ID:jobID,
447+
Type:database.ProvisionerJobTypeWorkspaceBuild,
448+
},nil)
449+
mDB.EXPECT().GetWorkspaceBuildByJobID(gomock.Any(),jobID).Return(database.WorkspaceBuild{
450+
WorkspaceID:workspaceID,
451+
ID:buildID,
452+
},nil)
453+
454+
// Allow db2sdk.WorkspaceAgent to complete.
455+
mCoordinator.EXPECT().Node(gomock.Any()).Return(nil)
456+
457+
// Mount the HTTP handler and create the test server.
458+
r.With(httpmw.ExtractWorkspaceAgentParam(mDB)).
459+
Delete("/workspaceagents/{workspaceagent}/containers/devcontainers/{devcontainer}",api.workspaceAgentDeleteDevcontainer)
460+
461+
srv:=httptest.NewServer(r)
462+
defersrv.Close()
463+
464+
// Send the DELETE request using the test server's client.
465+
req,err:=http.NewRequestWithContext(ctx,http.MethodDelete,
466+
fmt.Sprintf("%s/workspaceagents/%s/containers/devcontainers/%s",srv.URL,agentID,devcontainerID),nil)
467+
require.NoError(t,err)
468+
469+
resp,err:=srv.Client().Do(req)
470+
require.NoError(t,err)
471+
deferresp.Body.Close()
472+
473+
require.Equal(t,tc.expectedStatusCode,resp.StatusCode)
474+
})
475+
}
476+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp