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

feat!: add ability to cancel pending workspace build#18713

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
kacpersaw merged 22 commits intomainfromkacpersaw/cancel-pending-provisioner-jobs
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
Show all changes
22 commits
Select commitHold shift + click to select a range
ba41ae8
add support for canceling workspace builds with expect_state param (l…
kacpersawJul 1, 2025
c4ee5b6
Use single transaction for canceling workspace build
kacpersawJul 2, 2025
c49c33e
Fix lint problem in ut
kacpersawJul 2, 2025
ba1dbf3
add cancel confirmation dialog for workspace builds and add expect_st…
kacpersawJul 2, 2025
acffda6
Fix lint
kacpersawJul 2, 2025
b672d76
Merge branch 'main' into kacpersaw/cancel-pending-provisioner-jobs
kacpersawJul 2, 2025
86a34df
Apply review suggestions
kacpersawJul 3, 2025
42170ab
Fix unit test
kacpersawJul 3, 2025
5db9d71
Apply review suggestions
kacpersawJul 3, 2025
1ede20c
Fix typo
kacpersawJul 3, 2025
2597615
Regenerate api types
kacpersawJul 3, 2025
6c2d0cf
Fix typo
kacpersawJul 3, 2025
c800494
Merge branch 'main' into kacpersaw/cancel-pending-provisioner-jobs
kacpersawJul 3, 2025
c5cb203
Apply a new authorization check for GetProvisionerJobByIDForUpdate
kacpersawJul 6, 2025
1de84cc
Apply a new authorization check for GetProvisionerJobByIDForUpdate
kacpersawJul 6, 2025
17fb6a3
Apply FE review suggestions
kacpersawJul 6, 2025
1b7b614
Apply review suggestions
kacpersawJul 7, 2025
634f556
Refactor cancelWorkspaceBuild parameter handling
kacpersawJul 7, 2025
4deace0
Fix lint
kacpersawJul 7, 2025
43430fa
Update coderd/workspacebuilds.go
kacpersawJul 7, 2025
6272d93
Extract cancel confirm dialog to a separate component
kacpersawJul 7, 2025
4d4a01d
Merge branch 'main' into kacpersaw/cancel-pending-provisioner-jobs
kacpersawJul 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
PrevPrevious commit
NextNext commit
Apply review suggestions
  • Loading branch information
@kacpersaw
kacpersaw committedJul 7, 2025
commit1b7b614da1f82ea2b29b688c1d099075b5a5de32
2 changes: 1 addition & 1 deletioncoderd/apidoc/docs.go
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

2 changes: 1 addition & 1 deletioncoderd/apidoc/swagger.json
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

45 changes: 25 additions & 20 deletionscoderd/workspacebuilds.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -581,12 +581,24 @@ func (api *API) notifyWorkspaceUpdated(
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Param expect_status query string false "Expected status of the job" Enums(running, pending)
// @Param expect_status query string false "Expected status of the job. If expect_status is supplied, the request will be rejected with 412 Precondition Failed if the job doesn't match the state when performing the cancellation." Enums(running, pending)
// @Success 200 {object} codersdk.Response
// @Router /workspacebuilds/{workspacebuild}/cancel [patch]
func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
expectStatus := r.URL.Query().Get("expect_status")

var expectStatus database.ProvisionerJobStatus
expectStatusParam := r.URL.Query().Get("expect_status")
if expectStatusParam != "" {
if expectStatusParam != "running" && expectStatusParam != "pending" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: fmt.Sprintf("Invalid expect_status %q. Only 'running' or 'pending' are allowed.", expectStatusParam),
})
return
}
expectStatus = database.ProvisionerJobStatus(expectStatusParam)
}

workspaceBuild := httpmw.WorkspaceBuildParam(r)
workspace, err := api.Database.GetWorkspaceByID(ctx, workspaceBuild.WorkspaceID)
if err != nil {
Expand All@@ -596,8 +608,10 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
return
}

code := http.StatusOK
resp := codersdk.Response{}
code := http.StatusInternalServerError
resp := codersdk.Response{
Message: "Internal error canceling workspace build.",
}
err = api.Database.InTx(func(db database.Store) error {
valid, err := verifyUserCanCancelWorkspaceBuilds(ctx, db, httpmw.APIKey(r).UserID, workspace.TemplateID, expectStatus)
if err != nil {
Expand DownExpand Up@@ -635,20 +649,11 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
return xerrors.New("job has already been marked as canceled")
}

if expectStatus != "" {
if expectStatus != "running" && expectStatus != "pending" {
code = http.StatusBadRequest
resp.Message = fmt.Sprintf("Invalid expect_status %q. Only 'running' or 'pending' are allowed.", expectStatus)

return xerrors.Errorf("invalid expect_status %q", expectStatus)
}
if expectStatus != "" && job.JobStatus != expectStatus {
code = http.StatusPreconditionFailed
resp.Message = "Job is not in the expected state."

if job.JobStatus != database.ProvisionerJobStatus(expectStatus) {
code = http.StatusPreconditionFailed
resp.Message = "Job is not in the expected state."

return xerrors.Errorf("job is not in the expected state: expected: %q, got %q", expectStatus, job.JobStatus)
}
return xerrors.Errorf("job is not in the expected state: expected: %q, got %q", expectStatus, job.JobStatus)
}

err = db.UpdateProvisionerJobWithCancelByID(ctx, database.UpdateProvisionerJobWithCancelByIDParams{
Expand DownExpand Up@@ -688,9 +693,9 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
})
}

func verifyUserCanCancelWorkspaceBuilds(ctx context.Context, store database.Store, userID uuid.UUID, templateID uuid.UUID,expectStatus string) (bool, error) {
// If theexpectStatus is pending, we can cancel it.
ifexpectStatus =="pending" {
func verifyUserCanCancelWorkspaceBuilds(ctx context.Context, store database.Store, userID uuid.UUID, templateID uuid.UUID,jobStatus database.ProvisionerJobStatus) (bool, error) {
// If thejobStatus is pending, we can cancel it.
ifjobStatus ==database.ProvisionerJobStatusPending {
return true, nil
}

Expand Down
62 changes: 60 additions & 2 deletionscoderd/workspacebuilds_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -682,7 +682,7 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
require.Equal(t, codersdk.ProvisionerJobCanceled, build.Job.Status)
})

t.Run("Cancel with expect_state=pending - should fail with 412", func(t *testing.T) {
t.Run("Cancel with expect_state=pendingwhen job is running- should fail with 412", func(t *testing.T) {
t.Parallel()

client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
Expand All@@ -699,11 +699,11 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
var build codersdk.WorkspaceBuild

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

var build codersdk.WorkspaceBuild
require.Eventually(t, func() bool {
var err error
build, err = client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
Expand All@@ -722,6 +722,64 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})

t.Run("Cancel with expect_state=running when job is pending - should fail with 412", func(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}
// Given: a coderd instance with a provisioner daemon
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
IncludeProvisionerDaemon: true,
})
defer closeDaemon.Close()
// Given: a user, template, and workspace
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

// Stop the provisioner daemon.
require.NoError(t, closeDaemon.Close())
ctx := testutil.Context(t, testutil.WaitLong)
// Given: no provisioner daemons exist.
_, err := db.ExecContext(ctx, `DELETE FROM provisioner_daemons;`)
require.NoError(t, err)

// When: a new workspace build is created
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransitionStart,
})
// Then: the request should succeed.
require.NoError(t, err)
// Then: the provisioner job should remain pending.
require.Equal(t, codersdk.ProvisionerJobPending, build.Job.Status)

// Then: the response should indicate no provisioners are available.
if assert.NotNil(t, build.MatchedProvisioners) {
assert.Zero(t, build.MatchedProvisioners.Count)
assert.Zero(t, build.MatchedProvisioners.Available)
assert.Zero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
assert.False(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
}

// When: a cancel request is made with expect_state=running
err = client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{
ExpectStatus: codersdk.CancelWorkspaceBuildStatusRunning,
})
// Then: the request should fail with 412.
require.Error(t, err)

var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
})

t.Run("Cancel with expect_state - invalid status", func(t *testing.T) {
t.Parallel()

Expand Down
8 changes: 4 additions & 4 deletionsdocs/reference/api/builds.md
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

5 changes: 3 additions & 2 deletionssite/src/api/api.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1279,9 +1279,10 @@ class ApiMethods {
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
params?: TypesGen.CancelWorkspaceBuildParams,
): Promise<TypesGen.Response> => {
const queryParams = new URLSearchParams({ ...params });
const response = await this.axios.patch(
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel${queryParams}`,
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
null,
{ params },
);

return response.data;
Expand Down
13 changes: 7 additions & 6 deletionssite/src/api/queries/workspaces.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { API, type DeleteWorkspaceOptions } from "api/api";
import { DetailedError, isApiValidationError } from "api/errors";
import type {
CancelWorkspaceBuildParams,
CreateWorkspaceRequest,
ProvisionerLogLevel,
UsageAppName,
Expand DownExpand Up@@ -266,12 +267,12 @@ export const startWorkspace = (
export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => {
return {
mutationFn: () => {
if (workspace.latest_build.status=== "pending") {
return API.cancelWorkspaceBuild(workspace.latest_build.id, {
expect_status: "pending",
});
}
return API.cancelWorkspaceBuild(workspace.latest_build.id);
const {status} = workspace.latest_build;
const params: CancelWorkspaceBuildParams = {
expect_status:
status === "pending" || status === "running" ? status : undefined,
};
return API.cancelWorkspaceBuild(workspace.latest_build.id, params);
Copy link
Collaborator

@BrunoQuaresmaBrunoQuaresmaJul 7, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Double checking... cancancelWorkspaceBuild being called in any status? I see the params can beundefined, but it would still call thecancelWorkspaceBuild 🤔. This part of the code is quite hard for me to understand why the params are only set when the status is pending or running.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

expect_status is an optional parameter for cancel that ensures the job is in a specific state before proceeding - it only supportspending andrunning. So, cancel can be called without it.

},
onSuccess: async () => {
await queryClient.invalidateQueries({
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp