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

Commite5ac640

Browse files
authored
feat(coderd): add tasks delete endpoint (#19638)
This change adds a DELETE endpoint for tasks (for now, alias ofworkspace build delete transition).Fixescoder/internal#903
1 parent605dad8 commite5ac640

File tree

5 files changed

+258
-18
lines changed

5 files changed

+258
-18
lines changed

‎coderd/aitasks.go‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,78 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) {
472472

473473
httpapi.Write(ctx,rw,http.StatusOK,tasks[0])
474474
}
475+
476+
// taskDelete is an experimental endpoint to delete a task by ID (workspace ID).
477+
// It creates a delete workspace build and returns 202 Accepted if the build was
478+
// created.
479+
func (api*API)taskDelete(rw http.ResponseWriter,r*http.Request) {
480+
ctx:=r.Context()
481+
apiKey:=httpmw.APIKey(r)
482+
483+
idStr:=chi.URLParam(r,"id")
484+
taskID,err:=uuid.Parse(idStr)
485+
iferr!=nil {
486+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
487+
Message:fmt.Sprintf("Invalid UUID %q for task ID.",idStr),
488+
})
489+
return
490+
}
491+
492+
// For now, taskID = workspaceID, once we have a task data model in
493+
// the DB, we can change this lookup.
494+
workspaceID:=taskID
495+
workspace,err:=api.Database.GetWorkspaceByID(ctx,workspaceID)
496+
ifhttpapi.Is404Error(err) {
497+
httpapi.ResourceNotFound(rw)
498+
return
499+
}
500+
iferr!=nil {
501+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
502+
Message:"Internal error fetching workspace.",
503+
Detail:err.Error(),
504+
})
505+
return
506+
}
507+
508+
data,err:=api.workspaceData(ctx, []database.Workspace{workspace})
509+
iferr!=nil {
510+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
511+
Message:"Internal error fetching workspace resources.",
512+
Detail:err.Error(),
513+
})
514+
return
515+
}
516+
iflen(data.builds)==0||len(data.templates)==0 {
517+
httpapi.ResourceNotFound(rw)
518+
return
519+
}
520+
ifdata.builds[0].HasAITask==nil||!*data.builds[0].HasAITask {
521+
httpapi.ResourceNotFound(rw)
522+
return
523+
}
524+
525+
// Construct a request to the workspace build creation handler to
526+
// initiate deletion.
527+
buildReq:= codersdk.CreateWorkspaceBuildRequest{
528+
Transition:codersdk.WorkspaceTransitionDelete,
529+
Reason:"Deleted via tasks API",
530+
}
531+
532+
_,err=api.postWorkspaceBuildsInternal(
533+
ctx,
534+
apiKey,
535+
workspace,
536+
buildReq,
537+
func(action policy.Action,object rbac.Objecter)bool {
538+
returnapi.Authorize(r,action,object)
539+
},
540+
audit.WorkspaceBuildBaggageFromRequest(r),
541+
)
542+
iferr!=nil {
543+
httperror.WriteWorkspaceBuildError(ctx,rw,err)
544+
return
545+
}
546+
547+
// Delete build created successfully.
548+
rw.WriteHeader(http.StatusAccepted)
549+
}

‎coderd/aitasks_test.go‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package coderd_test
33
import (
44
"net/http"
55
"testing"
6+
"time"
67

78
"github.com/google/uuid"
89
"github.com/stretchr/testify/assert"
@@ -265,6 +266,125 @@ func TestTasks(t *testing.T) {
265266
assert.Equal(t,workspace.ID,task.WorkspaceID.UUID,"workspace id should match")
266267
assert.NotEmpty(t,task.Status,"task status should not be empty")
267268
})
269+
270+
t.Run("Delete",func(t*testing.T) {
271+
t.Parallel()
272+
273+
t.Run("OK",func(t*testing.T) {
274+
t.Parallel()
275+
276+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerDaemon:true})
277+
user:=coderdtest.CreateFirstUser(t,client)
278+
template:=createAITemplate(t,client,user)
279+
280+
ctx:=testutil.Context(t,testutil.WaitLong)
281+
282+
exp:=codersdk.NewExperimentalClient(client)
283+
task,err:=exp.CreateTask(ctx,"me", codersdk.CreateTaskRequest{
284+
TemplateVersionID:template.ActiveVersionID,
285+
Prompt:"delete me",
286+
})
287+
require.NoError(t,err)
288+
ws,err:=client.Workspace(ctx,task.ID)
289+
require.NoError(t,err)
290+
coderdtest.AwaitWorkspaceBuildJobCompleted(t,client,ws.LatestBuild.ID)
291+
292+
err=exp.DeleteTask(ctx,"me",task.ID)
293+
require.NoError(t,err,"delete task request should be accepted")
294+
295+
// Poll until the workspace is deleted.
296+
for {
297+
dws,derr:=client.DeletedWorkspace(ctx,task.ID)
298+
ifderr==nil&&dws.LatestBuild.Status==codersdk.WorkspaceStatusDeleted {
299+
break
300+
}
301+
ifctx.Err()!=nil {
302+
require.NoError(t,derr,"expected to fetch deleted workspace before deadline")
303+
require.Equal(t,codersdk.WorkspaceStatusDeleted,dws.LatestBuild.Status,"workspace should be deleted before deadline")
304+
break
305+
}
306+
time.Sleep(testutil.IntervalMedium)
307+
}
308+
})
309+
310+
t.Run("NotFound",func(t*testing.T) {
311+
t.Parallel()
312+
313+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerDaemon:true})
314+
_=coderdtest.CreateFirstUser(t,client)
315+
316+
ctx:=testutil.Context(t,testutil.WaitShort)
317+
318+
exp:=codersdk.NewExperimentalClient(client)
319+
err:=exp.DeleteTask(ctx,"me",uuid.New())
320+
321+
varsdkErr*codersdk.Error
322+
require.Error(t,err,"expected an error for non-existent task")
323+
require.ErrorAs(t,err,&sdkErr)
324+
require.Equal(t,404,sdkErr.StatusCode())
325+
})
326+
327+
t.Run("NotTaskWorkspace",func(t*testing.T) {
328+
t.Parallel()
329+
330+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerDaemon:true})
331+
user:=coderdtest.CreateFirstUser(t,client)
332+
333+
ctx:=testutil.Context(t,testutil.WaitShort)
334+
335+
// Create a template without AI tasks support and a workspace from it.
336+
version:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,nil)
337+
coderdtest.AwaitTemplateVersionJobCompleted(t,client,version.ID)
338+
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
339+
ws:=coderdtest.CreateWorkspace(t,client,template.ID)
340+
coderdtest.AwaitWorkspaceBuildJobCompleted(t,client,ws.LatestBuild.ID)
341+
342+
exp:=codersdk.NewExperimentalClient(client)
343+
err:=exp.DeleteTask(ctx,"me",ws.ID)
344+
345+
varsdkErr*codersdk.Error
346+
require.Error(t,err,"expected an error for non-task workspace delete via tasks endpoint")
347+
require.ErrorAs(t,err,&sdkErr)
348+
require.Equal(t,404,sdkErr.StatusCode())
349+
})
350+
351+
t.Run("UnauthorizedUserCannotDeleteOthersTask",func(t*testing.T) {
352+
t.Parallel()
353+
354+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerDaemon:true})
355+
owner:=coderdtest.CreateFirstUser(t,client)
356+
357+
// Owner's AI-capable template and workspace (task).
358+
template:=createAITemplate(t,client,owner)
359+
360+
ctx:=testutil.Context(t,testutil.WaitShort)
361+
362+
exp:=codersdk.NewExperimentalClient(client)
363+
task,err:=exp.CreateTask(ctx,"me", codersdk.CreateTaskRequest{
364+
TemplateVersionID:template.ActiveVersionID,
365+
Prompt:"delete me not",
366+
})
367+
require.NoError(t,err)
368+
ws,err:=client.Workspace(ctx,task.ID)
369+
require.NoError(t,err)
370+
coderdtest.AwaitWorkspaceBuildJobCompleted(t,client,ws.LatestBuild.ID)
371+
372+
// Another regular org member without elevated permissions.
373+
otherClient,_:=coderdtest.CreateAnotherUser(t,client,owner.OrganizationID)
374+
expOther:=codersdk.NewExperimentalClient(otherClient)
375+
376+
// Attempt to delete the owner's task as a non-owner without permissions.
377+
err=expOther.DeleteTask(ctx,"me",task.ID)
378+
379+
varauthErr*codersdk.Error
380+
require.Error(t,err,"expected an authorization error when deleting another user's task")
381+
require.ErrorAs(t,err,&authErr)
382+
// Accept either 403 or 404 depending on authz behavior.
383+
ifauthErr.StatusCode()!=403&&authErr.StatusCode()!=404 {
384+
t.Fatalf("unexpected status code: %d (expected 403 or 404)",authErr.StatusCode())
385+
}
386+
})
387+
})
268388
}
269389

270390
funcTestTasksCreate(t*testing.T) {

‎coderd/coderd.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,7 @@ func New(options *Options) *API {
10151015
r.Route("/{user}",func(r chi.Router) {
10161016
r.Use(httpmw.ExtractOrganizationMembersParam(options.Database,api.HTTPAuth.Authorize))
10171017
r.Get("/{id}",api.taskGet)
1018+
r.Delete("/{id}",api.taskDelete)
10181019
r.Post("/",api.tasksCreate)
10191020
})
10201021
})

‎coderd/workspacebuilds.go‎

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,44 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
329329
func (api*API)postWorkspaceBuilds(rw http.ResponseWriter,r*http.Request) {
330330
ctx:=r.Context()
331331
apiKey:=httpmw.APIKey(r)
332-
333332
workspace:=httpmw.WorkspaceParam(r)
334333
varcreateBuild codersdk.CreateWorkspaceBuildRequest
335334
if!httpapi.Read(ctx,rw,r,&createBuild) {
336335
return
337336
}
338337

338+
apiBuild,err:=api.postWorkspaceBuildsInternal(
339+
ctx,
340+
apiKey,
341+
workspace,
342+
createBuild,
343+
func(action policy.Action,object rbac.Objecter)bool {
344+
returnapi.Authorize(r,action,object)
345+
},
346+
audit.WorkspaceBuildBaggageFromRequest(r),
347+
)
348+
iferr!=nil {
349+
httperror.WriteWorkspaceBuildError(ctx,rw,err)
350+
return
351+
}
352+
353+
httpapi.Write(ctx,rw,http.StatusCreated,apiBuild)
354+
}
355+
356+
// postWorkspaceBuildsInternal handles the internal logic for creating
357+
// workspace builds, can be called by other handlers and must not
358+
// reference httpmw.
359+
func (api*API)postWorkspaceBuildsInternal(
360+
ctx context.Context,
361+
apiKey database.APIKey,
362+
workspace database.Workspace,
363+
createBuild codersdk.CreateWorkspaceBuildRequest,
364+
authorizefunc(action policy.Action,object rbac.Objecter)bool,
365+
workspaceBuildBaggage audit.WorkspaceBuildBaggage,
366+
) (
367+
codersdk.WorkspaceBuild,
368+
error,
369+
) {
339370
transition:=database.WorkspaceTransition(createBuild.Transition)
340371
builder:=wsbuilder.New(workspace,transition,*api.BuildUsageChecker.Load()).
341372
Initiator(apiKey.UserID).
@@ -362,11 +393,10 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
362393
previousWorkspaceBuild,err=tx.GetLatestWorkspaceBuildByWorkspaceID(ctx,workspace.ID)
363394
iferr!=nil&&!xerrors.Is(err,sql.ErrNoRows) {
364395
api.Logger.Error(ctx,"failed fetching previous workspace build",slog.F("workspace_id",workspace.ID),slog.Error(err))
365-
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
396+
returnhttperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
366397
Message:"Internal error fetching previous workspace build",
367398
Detail:err.Error(),
368399
})
369-
returnnil
370400
}
371401

372402
ifcreateBuild.TemplateVersionID!=uuid.Nil {
@@ -375,16 +405,14 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
375405

376406
ifcreateBuild.Orphan {
377407
ifcreateBuild.Transition!=codersdk.WorkspaceTransitionDelete {
378-
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
408+
returnhttperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
379409
Message:"Orphan is only permitted when deleting a workspace.",
380410
})
381-
returnnil
382411
}
383412
iflen(createBuild.ProvisionerState)>0 {
384-
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
413+
returnhttperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
385414
Message:"ProvisionerState cannot be set alongside Orphan since state intent is unclear.",
386415
})
387-
returnnil
388416
}
389417
builder=builder.Orphan()
390418
}
@@ -397,24 +425,23 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
397425
tx,
398426
api.FileCache,
399427
func(action policy.Action,object rbac.Objecter)bool {
400-
ifauth:=api.Authorize(r,action,object);auth {
428+
ifauth:=authorize(action,object);auth {
401429
returntrue
402430
}
403431
// Special handling for prebuilt workspace deletion
404432
ifaction==policy.ActionDelete {
405433
ifworkspaceObj,ok:=object.(database.PrebuiltWorkspaceResource);ok&&workspaceObj.IsPrebuild() {
406-
returnapi.Authorize(r,action,workspaceObj.AsPrebuild())
434+
returnauthorize(action,workspaceObj.AsPrebuild())
407435
}
408436
}
409437
returnfalse
410438
},
411-
audit.WorkspaceBuildBaggageFromRequest(r),
439+
workspaceBuildBaggage,
412440
)
413441
returnerr
414442
},nil)
415443
iferr!=nil {
416-
httperror.WriteWorkspaceBuildError(ctx,rw,err)
417-
return
444+
return codersdk.WorkspaceBuild{},err
418445
}
419446

420447
varqueuePos database.GetProvisionerJobsByIDsWithQueuePositionRow
@@ -478,11 +505,13 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
478505
provisionerDaemons,
479506
)
480507
iferr!=nil {
481-
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
482-
Message:"Internal error converting workspace build.",
483-
Detail:err.Error(),
484-
})
485-
return
508+
return codersdk.WorkspaceBuild{},httperror.NewResponseError(
509+
http.StatusInternalServerError,
510+
codersdk.Response{
511+
Message:"Internal error converting workspace build.",
512+
Detail:err.Error(),
513+
},
514+
)
486515
}
487516

488517
// If this workspace build has a different template version ID to the previous build
@@ -509,7 +538,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
509538
WorkspaceID:workspace.ID,
510539
})
511540

512-
httpapi.Write(ctx,rw,http.StatusCreated,apiBuild)
541+
returnapiBuild,nil
513542
}
514543

515544
func (api*API)notifyWorkspaceUpdated(

‎codersdk/aitasks.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,18 @@ func (c *ExperimentalClient) TaskByID(ctx context.Context, id uuid.UUID) (Task,
190190

191191
returntask,nil
192192
}
193+
194+
// DeleteTask deletes a task by its ID.
195+
//
196+
// Experimental: This method is experimental and may change in the future.
197+
func (c*ExperimentalClient)DeleteTask(ctx context.Context,userstring,id uuid.UUID)error {
198+
res,err:=c.Request(ctx,http.MethodDelete,fmt.Sprintf("/api/experimental/tasks/%s/%s",user,id.String()),nil)
199+
iferr!=nil {
200+
returnerr
201+
}
202+
deferres.Body.Close()
203+
ifres.StatusCode!=http.StatusAccepted {
204+
returnReadBodyAsError(res)
205+
}
206+
returnnil
207+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp