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

Commitaa46321

Browse files
committed
feat(coderd): add tasks delete endpoint
1 parentfcef2ec commitaa46321

File tree

4 files changed

+251
-0
lines changed

4 files changed

+251
-0
lines changed

‎coderd/aitasks.go‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package coderd
22

33
import (
4+
"bytes"
45
"context"
56
"database/sql"
7+
"encoding/json"
68
"errors"
79
"fmt"
810
"net/http"
@@ -440,3 +442,122 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) {
440442

441443
httpapi.Write(ctx,rw,http.StatusOK,tasks[0])
442444
}
445+
446+
// taskDelete is an experimental endpoint to delete a task by ID (workspace ID).
447+
// It creates a delete workspace build and returns 202 Accepted if the build was created.
448+
func (api*API)taskDelete(rw http.ResponseWriter,r*http.Request) {
449+
ctx:=r.Context()
450+
451+
idStr:=chi.URLParam(r,"id")
452+
taskID,err:=uuid.Parse(idStr)
453+
iferr!=nil {
454+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
455+
Message:fmt.Sprintf("Invalid UUID %q for task ID.",idStr),
456+
})
457+
return
458+
}
459+
460+
// For now, taskID = workspaceID, once we have a task data model in
461+
// the DB, we can change this lookup.
462+
workspaceID:=taskID
463+
workspace,err:=api.Database.GetWorkspaceByID(ctx,workspaceID)
464+
ifhttpapi.Is404Error(err) {
465+
httpapi.ResourceNotFound(rw)
466+
return
467+
}
468+
iferr!=nil {
469+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
470+
Message:"Internal error fetching workspace.",
471+
Detail:err.Error(),
472+
})
473+
return
474+
}
475+
476+
data,err:=api.workspaceData(ctx, []database.Workspace{workspace})
477+
iferr!=nil {
478+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
479+
Message:"Internal error fetching workspace resources.",
480+
Detail:err.Error(),
481+
})
482+
return
483+
}
484+
iflen(data.builds)==0||len(data.templates)==0 {
485+
httpapi.ResourceNotFound(rw)
486+
return
487+
}
488+
ifdata.builds[0].HasAITask==nil||!*data.builds[0].HasAITask {
489+
httpapi.ResourceNotFound(rw)
490+
return
491+
}
492+
493+
// Construct a request to the workspace build creation handler to initiate deletion.
494+
buildReq:= codersdk.CreateWorkspaceBuildRequest{
495+
Transition:codersdk.WorkspaceTransitionDelete,
496+
}
497+
body,err:=json.Marshal(buildReq)
498+
iferr!=nil {
499+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
500+
Message:"Internal error marshaling delete request.",
501+
Detail:err.Error(),
502+
})
503+
return
504+
}
505+
506+
req,err:=http.NewRequestWithContext(ctx,http.MethodPost,fmt.Sprintf("/api/v2/workspaces/%s/builds",workspace.ID.String()),bytes.NewReader(body))
507+
iferr!=nil {
508+
httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{
509+
Message:"Internal error creating request.",
510+
Detail:err.Error(),
511+
})
512+
return
513+
}
514+
req.Header.Set("Content-Type","application/json")
515+
516+
// Inject the "workspace" URL param so ExtractWorkspaceParam can
517+
// resolve the workspace.
518+
rctx:=chi.NewRouteContext()
519+
rctx.URLParams.Add("workspace",workspace.ID.String())
520+
req=req.WithContext(context.WithValue(req.Context(),chi.RouteCtxKey,rctx))
521+
522+
// Call the existing workspace build handler via middleware.
523+
rc:=&responseWriterCapture{}
524+
handler:=httpmw.ExtractWorkspaceParam(api.Database)(http.HandlerFunc(api.postWorkspaceBuilds))
525+
handler.ServeHTTP(rc,req)
526+
527+
status:=rc.status
528+
ifstatus==0 {
529+
status=http.StatusOK
530+
}
531+
532+
ifstatus!=http.StatusCreated {
533+
// Propagate the error response from the workspace build handler.
534+
fork,vs:=rangerc.Header() {
535+
for_,v:=rangevs {
536+
rw.Header().Add(k,v)
537+
}
538+
}
539+
rw.WriteHeader(status)
540+
_,_=rw.Write(rc.body.Bytes())
541+
return
542+
}
543+
544+
// Delete build created successfully.
545+
rw.WriteHeader(http.StatusAccepted)
546+
}
547+
548+
typeresponseWriterCapturestruct {
549+
header http.Header
550+
statusint
551+
body bytes.Buffer
552+
}
553+
554+
func (w*responseWriterCapture)Header() http.Header {
555+
ifw.header==nil {
556+
w.header=make(http.Header)
557+
}
558+
returnw.header
559+
}
560+
561+
func (w*responseWriterCapture)Write(b []byte) (int,error) {returnw.body.Write(b) }
562+
563+
func (w*responseWriterCapture)WriteHeader(statusCodeint) {w.status=statusCode }

‎coderd/aitasks_test.go‎

Lines changed: 114 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,119 @@ 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+
ws:=coderdtest.CreateWorkspace(t,client,template.ID,func(req*codersdk.CreateWorkspaceRequest) {
281+
req.RichParameterValues= []codersdk.WorkspaceBuildParameter{
282+
{Name:codersdk.AITaskPromptParameterName,Value:"delete-me"},
283+
}
284+
})
285+
coderdtest.AwaitWorkspaceBuildJobCompleted(t,client,ws.LatestBuild.ID)
286+
287+
ctx:=testutil.Context(t,testutil.WaitLong)
288+
289+
exp:=codersdk.NewExperimentalClient(client)
290+
err:=exp.DeleteTask(ctx,"me",ws.ID)
291+
require.NoError(t,err,"delete task request should be accepted")
292+
293+
// Poll until the workspace is deleted.
294+
for {
295+
dws,derr:=client.DeletedWorkspace(ctx,ws.ID)
296+
ifderr==nil&&dws.LatestBuild.Status==codersdk.WorkspaceStatusDeleted {
297+
break
298+
}
299+
ifctx.Err()!=nil {
300+
require.NoError(t,derr,"expected to fetch deleted workspace before deadline")
301+
require.Equal(t,codersdk.WorkspaceStatusDeleted,dws.LatestBuild.Status,"workspace should be deleted before deadline")
302+
break
303+
}
304+
time.Sleep(testutil.IntervalMedium)
305+
}
306+
})
307+
308+
t.Run("NotFound",func(t*testing.T) {
309+
t.Parallel()
310+
311+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerDaemon:true})
312+
_=coderdtest.CreateFirstUser(t,client)
313+
314+
ctx:=testutil.Context(t,testutil.WaitShort)
315+
316+
exp:=codersdk.NewExperimentalClient(client)
317+
err:=exp.DeleteTask(ctx,"me",uuid.New())
318+
319+
varsdkErr*codersdk.Error
320+
require.Error(t,err,"expected an error for non-existent task")
321+
require.ErrorAs(t,err,&sdkErr)
322+
require.Equal(t,404,sdkErr.StatusCode())
323+
})
324+
325+
t.Run("NotTaskWorkspace",func(t*testing.T) {
326+
t.Parallel()
327+
328+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerDaemon:true})
329+
user:=coderdtest.CreateFirstUser(t,client)
330+
331+
ctx:=testutil.Context(t,testutil.WaitShort)
332+
333+
// Create a template without AI tasks support and a workspace from it.
334+
version:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,nil)
335+
coderdtest.AwaitTemplateVersionJobCompleted(t,client,version.ID)
336+
template:=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
337+
ws:=coderdtest.CreateWorkspace(t,client,template.ID)
338+
coderdtest.AwaitWorkspaceBuildJobCompleted(t,client,ws.LatestBuild.ID)
339+
340+
exp:=codersdk.NewExperimentalClient(client)
341+
err:=exp.DeleteTask(ctx,"me",ws.ID)
342+
343+
varsdkErr*codersdk.Error
344+
require.Error(t,err,"expected an error for non-task workspace delete via tasks endpoint")
345+
require.ErrorAs(t,err,&sdkErr)
346+
require.Equal(t,404,sdkErr.StatusCode())
347+
})
348+
349+
t.Run("UnauthorizedUserCannotDeleteOthersTask",func(t*testing.T) {
350+
t.Parallel()
351+
352+
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerDaemon:true})
353+
owner:=coderdtest.CreateFirstUser(t,client)
354+
355+
// Owner's AI-capable template and workspace (task).
356+
template:=createAITemplate(t,client,owner)
357+
ws:=coderdtest.CreateWorkspace(t,client,template.ID,func(req*codersdk.CreateWorkspaceRequest) {
358+
req.RichParameterValues= []codersdk.WorkspaceBuildParameter{
359+
{Name:codersdk.AITaskPromptParameterName,Value:"secure"},
360+
}
361+
})
362+
coderdtest.AwaitWorkspaceBuildJobCompleted(t,client,ws.LatestBuild.ID)
363+
364+
ctx:=testutil.Context(t,testutil.WaitShort)
365+
366+
// Another regular org member without elevated permissions.
367+
otherClient,_:=coderdtest.CreateAnotherUser(t,client,owner.OrganizationID)
368+
expOther:=codersdk.NewExperimentalClient(otherClient)
369+
370+
// Attempt to delete the owner's task as a non-owner without permissions.
371+
err:=expOther.DeleteTask(ctx,"me",ws.ID)
372+
373+
varauthErr*codersdk.Error
374+
require.Error(t,err,"expected an authorization error when deleting another user's task")
375+
require.ErrorAs(t,err,&authErr)
376+
// Accept either 403 or 404 depending on authz behavior.
377+
ifauthErr.StatusCode()!=403&&authErr.StatusCode()!=404 {
378+
t.Fatalf("unexpected status code: %d (expected 403 or 404)",authErr.StatusCode())
379+
}
380+
})
381+
})
268382
}
269383

270384
funcTestTasksCreate(t*testing.T) {

‎coderd/coderd.go‎

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

‎codersdk/aitasks.go‎

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

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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp