- Notifications
You must be signed in to change notification settings - Fork1k
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
Uh oh!
There was an error while loading.Please reload this page.
Changes from1 commit
ba41ae8
c4ee5b6
c49c33e
ba1dbf3
acffda6
b672d76
86a34df
42170ab
5db9d71
1ede20c
2597615
6c2d0cf
c800494
c5cb203
1de84cc
17fb6a3
1b7b614
634f556
4deace0
43430fa
6272d93
4d4a01d
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
- Loading branch information
Uh oh!
There was an error while loading.Please reload this page.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. 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() | ||
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 { | ||
@@ -596,8 +608,10 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques | ||
return | ||
} | ||
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 { | ||
@@ -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 != "" && job.JobStatus != 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) | ||
} | ||
err = db.UpdateProvisionerJobWithCancelByID(ctx, database.UpdateProvisionerJobWithCancelByIDParams{ | ||
@@ -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,jobStatus database.ProvisionerJobStatus) (bool, error) { | ||
// If thejobStatus is pending, we can cancel it. | ||
kacpersaw marked this conversation as resolved. OutdatedShow resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
ifjobStatus ==database.ProvisionerJobStatusPending { | ||
return true, nil | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -682,7 +682,7 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) { | ||
require.Equal(t, codersdk.ProvisionerJobCanceled, build.Job.Status) | ||
}) | ||
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}) | ||
@@ -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) | ||
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) | ||
@@ -722,6 +722,64 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) { | ||
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode()) | ||
}) | ||
deansheather marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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() | ||
Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.
Uh oh!
There was an error while loading.Please reload this page.
Original file line number | Diff line number | Diff 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, | ||
@@ -266,12 +267,12 @@ export const startWorkspace = ( | ||
export const cancelBuild = (workspace: Workspace, queryClient: QueryClient) => { | ||
return { | ||
mutationFn: () => { | ||
const {status} = workspace.latest_build; | ||
const params: CancelWorkspaceBuildParams = { | ||
expect_status: | ||
status === "pending" || status === "running" ? status : undefined, | ||
}; | ||
return API.cancelWorkspaceBuild(workspace.latest_build.id, params); | ||
Collaborator
| ||
}, | ||
onSuccess: async () => { | ||
await queryClient.invalidateQueries({ | ||
Uh oh!
There was an error while loading.Please reload this page.