Skip to content

Commit c4ee5b6

Browse files
committed
Use single transaction for canceling workspace build
1 parent ba41ae8 commit c4ee5b6

File tree

6 files changed

+82
-83
lines changed

6 files changed

+82
-83
lines changed

coderd/apidoc/docs.go

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

coderd/apidoc/swagger.json

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

coderd/workspacebuilds.go

Lines changed: 49 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -581,12 +581,12 @@ func (api *API) notifyWorkspaceUpdated(
581581
// @Produce json
582582
// @Tags Builds
583583
// @Param workspacebuild path string true "Workspace build ID"
584-
// @Param expect_state query string false "Expected state of the job"
584+
// @Param expect_status query string false "Expected status of the job" Enums(running, pending)
585585
// @Success 200 {object} codersdk.Response
586586
// @Router /workspacebuilds/{workspacebuild}/cancel [patch]
587587
func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
588588
ctx := r.Context()
589-
expectState := r.URL.Query().Get("expect_state")
589+
expectStatus := r.URL.Query().Get("expect_status")
590590
workspaceBuild := httpmw.WorkspaceBuildParam(r)
591591
workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
592592
if err != nil {
@@ -596,90 +596,60 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
596596
return
597597
}
598598

599-
valid, err := api.verifyUserCanCancelWorkspaceBuilds(ctx, httpmw.APIKey(r).UserID, workspace.TemplateID)
600-
if err != nil {
601-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
602-
Message: "Internal error verifying permission to cancel workspace build.",
603-
Detail: err.Error(),
604-
})
605-
return
606-
}
607-
if !valid {
608-
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
609-
Message: "User is not allowed to cancel workspace builds. Owner role is required.",
610-
})
611-
return
612-
}
613-
614-
if expectState != "" {
615-
jobStatus := database.ProvisionerJobStatus(expectState)
616-
if !jobStatus.Valid() {
617-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
618-
Message: "Invalid expect_state. Only 'pending', 'running', 'succeeded', 'canceling', 'canceled', 'failed', or 'unknown' are allowed.",
619-
})
620-
return
621-
}
622-
623-
// use local error type to detect if the error is due to job state mismatch
624-
var errJobStateMismatch = xerrors.New("job is not in the expected state")
625-
626-
err := api.Database.InTx(func(store database.Store) error {
627-
job, err := store.GetProvisionerJobByIDForUpdate(ctx, workspaceBuild.JobID)
628-
if err != nil {
629-
return err
630-
}
631-
632-
if job.JobStatus != jobStatus {
633-
return errJobStateMismatch
634-
}
635-
636-
return store.UpdateProvisionerJobWithCancelByID(ctx, database.UpdateProvisionerJobWithCancelByIDParams{
637-
ID: job.ID,
638-
CanceledAt: sql.NullTime{
639-
Time: dbtime.Now(),
640-
Valid: true,
641-
},
642-
CompletedAt: sql.NullTime{
643-
Time: dbtime.Now(),
644-
Valid: !job.WorkerID.Valid,
645-
},
646-
})
647-
}, nil)
599+
err = api.Database.InTx(func(store database.Store) error {
600+
valid, err := api.verifyUserCanCancelWorkspaceBuilds(ctx, store, httpmw.APIKey(r).UserID, workspace.TemplateID)
648601
if err != nil {
649-
if errors.Is(err, errJobStateMismatch) {
650-
httpapi.Write(ctx, rw, http.StatusPreconditionFailed, codersdk.Response{
651-
Message: "Job is not in the expected state.",
652-
})
653-
return
654-
}
655602
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
656-
Message: "Internal error fetching provisioner job.",
603+
Message: "Internal error verifying permission to cancel workspace build.",
657604
Detail: err.Error(),
658605
})
659-
return
606+
return nil
607+
}
608+
if !valid {
609+
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
610+
Message: "User is not allowed to cancel workspace builds. Owner role is required.",
611+
})
612+
return nil
660613
}
661-
} else {
662-
job, err := api.Database.GetProvisionerJobByID(ctx, workspaceBuild.JobID)
614+
615+
job, err := store.GetProvisionerJobByID(ctx, workspaceBuild.JobID)
663616
if err != nil {
664617
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
665618
Message: "Internal error fetching provisioner job.",
666619
Detail: err.Error(),
667620
})
668-
return
621+
return nil
669622
}
670623
if job.CompletedAt.Valid {
671624
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
672625
Message: "Job has already completed!",
673626
})
674-
return
627+
return nil
675628
}
676629
if job.CanceledAt.Valid {
677630
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
678631
Message: "Job has already been marked as canceled!",
679632
})
680-
return
633+
return nil
634+
}
635+
636+
if expectStatus != "" {
637+
if expectStatus != "running" && expectStatus != "pending" {
638+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
639+
Message: "Invalid expect_status. Only 'running' or 'pending' are allowed.",
640+
})
641+
return nil
642+
}
643+
644+
if job.JobStatus != database.ProvisionerJobStatus(expectStatus) {
645+
httpapi.Write(ctx, rw, http.StatusPreconditionFailed, codersdk.Response{
646+
Message: "Job is not in the expected state.",
647+
})
648+
return nil
649+
}
681650
}
682-
err = api.Database.UpdateProvisionerJobWithCancelByID(ctx, database.UpdateProvisionerJobWithCancelByIDParams{
651+
652+
err = store.UpdateProvisionerJobWithCancelByID(ctx, database.UpdateProvisionerJobWithCancelByIDParams{
683653
ID: job.ID,
684654
CanceledAt: sql.NullTime{
685655
Time: dbtime.Now(),
@@ -696,9 +666,19 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
696666
Message: "Internal error updating provisioner job.",
697667
Detail: err.Error(),
698668
})
699-
return
669+
return nil
700670
}
671+
672+
return nil
673+
}, nil)
674+
if err != nil {
675+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
676+
Message: "Internal error updating provisioner job.",
677+
Detail: err.Error(),
678+
})
679+
return
701680
}
681+
702682
api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{
703683
Kind: wspubsub.WorkspaceEventKindStateChange,
704684
WorkspaceID: workspace.ID,
@@ -709,8 +689,8 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
709689
})
710690
}
711691

712-
func (api *API) verifyUserCanCancelWorkspaceBuilds(ctx context.Context, userID uuid.UUID, templateID uuid.UUID) (bool, error) {
713-
template, err := api.Database.GetTemplateByID(ctx, templateID)
692+
func (*API) verifyUserCanCancelWorkspaceBuilds(ctx context.Context, store database.Store, userID uuid.UUID, templateID uuid.UUID) (bool, error) {
693+
template, err := store.GetTemplateByID(ctx, templateID)
714694
if err != nil {
715695
return false, xerrors.New("no template exists for this workspace")
716696
}
@@ -719,7 +699,7 @@ func (api *API) verifyUserCanCancelWorkspaceBuilds(ctx context.Context, userID u
719699
return true, nil // all users can cancel workspace builds
720700
}
721701

722-
user, err := api.Database.GetUserByID(ctx, userID)
702+
user, err := store.GetUserByID(ctx, userID)
723703
if err != nil {
724704
return false, xerrors.New("user does not exist")
725705
}

coderd/workspacebuilds_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
672672

673673
// When: the workspace build is canceled
674674
err = client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildRequest{
675-
ExpectState: codersdk.ProvisionerJobPending,
675+
ExpectStatus: codersdk.CancelWorkspaceBuildStatusPending,
676676
})
677677
require.NoError(t, err)
678678

@@ -734,7 +734,7 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
734734

735735
// When: a cancel request is made with expect_state=pending
736736
err = client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildRequest{
737-
ExpectState: codersdk.ProvisionerJobPending,
737+
ExpectStatus: codersdk.CancelWorkspaceBuildStatusPending,
738738
})
739739
// Then: the request should fail with 412.
740740
require.Error(t, err)
@@ -765,13 +765,13 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
765765

766766
// When: a cancel request is made with invalid expect_state
767767
err := client.CancelWorkspaceBuild(ctx, workspace.LatestBuild.ID, codersdk.CancelWorkspaceBuildRequest{
768-
ExpectState: "invalid_status",
768+
ExpectStatus: "invalid_status",
769769
})
770770
// Then: the request should fail with 400.
771771
var apiErr *codersdk.Error
772772
require.ErrorAs(t, err, &apiErr)
773773
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
774-
require.Contains(t, apiErr.Message, "Invalid expect_state")
774+
require.Contains(t, apiErr.Message, "Invalid expect_status")
775775
})
776776
}
777777

codersdk/workspacebuilds.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,21 @@ func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBui
123123
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
124124
}
125125

126+
type CancelWorkspaceBuildStatus string
127+
128+
const (
129+
CancelWorkspaceBuildStatusRunning CancelWorkspaceBuildStatus = "running"
130+
CancelWorkspaceBuildStatusPending CancelWorkspaceBuildStatus = "pending"
131+
)
132+
126133
type CancelWorkspaceBuildRequest struct {
127-
ExpectState ProvisionerJobStatus `json:"expect_state,omitempty"`
134+
ExpectStatus CancelWorkspaceBuildStatus `json:"expect_status,omitempty"`
128135
}
129136

130137
func (c *CancelWorkspaceBuildRequest) asRequestOption() RequestOption {
131138
return func(r *http.Request) {
132139
q := r.URL.Query()
133-
q.Set("expect_state", string(c.ExpectState))
140+
q.Set("expect_status", string(c.ExpectStatus))
134141
r.URL.RawQuery = q.Encode()
135142
}
136143
}

docs/reference/api/builds.md

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

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy