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

Closes #17791 

This PR adds ability to cancel workspace builds that are in "pending"
status.

Breaking changes:
- CancelWorkspaceBuild method in codersdk now accepts an optional
request parameter

API:
- Added `expect_status` query parameter to the cancel workspace build
endpoint
- This parameter ensures the job hasn't changed state before canceling
- API returns `412 Precondition Failed` if the job is not in the
expected status
- Valid values: `running` or `pending`
- Wrapped the entire cancel method in a database transaction

UI:
- Added confirmation dialog to the `Cancel` button, since it's a
destructive operation

![image](https://github.com/user-attachments/assets/437aa5f4-5669-45b6-82a0-e46f277114bf)

![image](https://github.com/user-attachments/assets/423b5cb1-a4fb-4a10-933b-c1c73f4b838c)


- Enabled cancel action for pending workspaces (`expect_status=pending`
is sent if workspace is in pending status)

![image](https://github.com/user-attachments/assets/32d35ff1-12e6-4f7b-9f6c-fde9da9de6cf)

---------

Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
Kacper Sawicki
2025-07-08 11:02:58 +02:00
committed by GitHub
co-authored by Dean Sheather
parent 2f42b64182
commit 8202514ce0
20 changed files with 556 additions and 93 deletions
+10
View File
@@ -9122,6 +9122,16 @@ const docTemplate = `{
"name": "workspacebuild",
"in": "path",
"required": true
},
{
"enum": [
"running",
"pending"
],
"type": "string",
"description": "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.",
"name": "expect_status",
"in": "query"
}
],
"responses": {
+7
View File
@@ -8065,6 +8065,13 @@
"name": "workspacebuild",
"in": "path",
"required": true
},
{
"enum": ["running", "pending"],
"type": "string",
"description": "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.",
"name": "expect_status",
"in": "query"
}
],
"responses": {
+31 -18
View File
@@ -1182,6 +1182,27 @@ func (q *querier) customRoleCheck(ctx context.Context, role database.CustomRole)
return nil
}
func (q *querier) authorizeProvisionerJob(ctx context.Context, job database.ProvisionerJob) error {
switch job.Type {
case database.ProvisionerJobTypeWorkspaceBuild:
// Authorized call to get workspace build. If we can read the build, we
// can read the job.
_, err := q.GetWorkspaceBuildByJobID(ctx, job.ID)
if err != nil {
return xerrors.Errorf("fetch related workspace build: %w", err)
}
case database.ProvisionerJobTypeTemplateVersionDryRun, database.ProvisionerJobTypeTemplateVersionImport:
// Authorized call to get template version.
_, err := authorizedTemplateVersionFromJob(ctx, q, job)
if err != nil {
return xerrors.Errorf("fetch related template version: %w", err)
}
default:
return xerrors.Errorf("unknown job type: %q", job.Type)
}
return nil
}
func (q *querier) AcquireLock(ctx context.Context, id int64) error {
return q.db.AcquireLock(ctx, id)
}
@@ -2445,32 +2466,24 @@ func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (data
return database.ProvisionerJob{}, err
}
switch job.Type {
case database.ProvisionerJobTypeWorkspaceBuild:
// Authorized call to get workspace build. If we can read the build, we
// can read the job.
_, err := q.GetWorkspaceBuildByJobID(ctx, id)
if err != nil {
return database.ProvisionerJob{}, xerrors.Errorf("fetch related workspace build: %w", err)
}
case database.ProvisionerJobTypeTemplateVersionDryRun, database.ProvisionerJobTypeTemplateVersionImport:
// Authorized call to get template version.
_, err := authorizedTemplateVersionFromJob(ctx, q, job)
if err != nil {
return database.ProvisionerJob{}, xerrors.Errorf("fetch related template version: %w", err)
}
default:
return database.ProvisionerJob{}, xerrors.Errorf("unknown job type: %q", job.Type)
if err := q.authorizeProvisionerJob(ctx, job); err != nil {
return database.ProvisionerJob{}, err
}
return job, nil
}
func (q *querier) GetProvisionerJobByIDForUpdate(ctx context.Context, id uuid.UUID) (database.ProvisionerJob, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceProvisionerJobs); err != nil {
job, err := q.db.GetProvisionerJobByIDForUpdate(ctx, id)
if err != nil {
return database.ProvisionerJob{}, err
}
return q.db.GetProvisionerJobByIDForUpdate(ctx, id)
if err := q.authorizeProvisionerJob(ctx, job); err != nil {
return database.ProvisionerJob{}, err
}
return job, nil
}
func (q *querier) GetProvisionerJobTimingsByJobID(ctx context.Context, jobID uuid.UUID) ([]database.ProvisionerJobTiming, error) {
+53 -2
View File
@@ -4655,8 +4655,59 @@ func (s *MethodTestSuite) TestSystemFunctions() {
VapidPrivateKey: "test",
}).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
}))
s.Run("GetProvisionerJobByIDForUpdate", s.Subtest(func(db database.Store, check *expects) {
check.Args(uuid.New()).Asserts(rbac.ResourceProvisionerJobs, policy.ActionRead).Errors(sql.ErrNoRows)
s.Run("Build/GetProvisionerJobByIDForUpdate", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
o := dbgen.Organization(s.T(), db, database.Organization{})
tpl := dbgen.Template(s.T(), db, database.Template{
OrganizationID: o.ID,
CreatedBy: u.ID,
})
w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{
OwnerID: u.ID,
OrganizationID: o.ID,
TemplateID: tpl.ID,
})
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
})
tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
JobID: j.ID,
OrganizationID: o.ID,
CreatedBy: u.ID,
})
_ = dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{
JobID: j.ID,
WorkspaceID: w.ID,
TemplateVersionID: tv.ID,
})
check.Args(j.ID).Asserts(w, policy.ActionRead).Returns(j)
}))
s.Run("TemplateVersion/GetProvisionerJobByIDForUpdate", s.Subtest(func(db database.Store, check *expects) {
dbtestutil.DisableForeignKeysAndTriggers(s.T(), db)
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeTemplateVersionImport,
})
tpl := dbgen.Template(s.T(), db, database.Template{})
v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
JobID: j.ID,
})
check.Args(j.ID).Asserts(v.RBACObject(tpl), policy.ActionRead).Returns(j)
}))
s.Run("TemplateVersionDryRun/GetProvisionerJobByIDForUpdate", s.Subtest(func(db database.Store, check *expects) {
dbtestutil.DisableForeignKeysAndTriggers(s.T(), db)
tpl := dbgen.Template(s.T(), db, database.Template{})
v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
})
j := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
Type: database.ProvisionerJobTypeTemplateVersionDryRun,
Input: must(json.Marshal(struct {
TemplateVersionID uuid.UUID `json:"template_version_id"`
}{TemplateVersionID: v.ID})),
})
check.Args(j.ID).Asserts(v.RBACObject(tpl), policy.ActionRead).Returns(j)
}))
s.Run("HasTemplateVersionsWithAITask", s.Subtest(func(db database.Store, check *expects) {
check.Args().Asserts()
+92 -52
View File
@@ -581,10 +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 {
@@ -594,58 +608,78 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
return
}
valid, err := api.verifyUserCanCancelWorkspaceBuilds(ctx, httpmw.APIKey(r).UserID, workspace.TemplateID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error verifying permission to cancel workspace build.",
Detail: err.Error(),
})
return
}
if !valid {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: "User is not allowed to cancel workspace builds. Owner role is required.",
})
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 {
code = http.StatusInternalServerError
resp.Message = "Internal error verifying permission to cancel workspace build."
resp.Detail = err.Error()
job, err := api.Database.GetProvisionerJobByID(ctx, workspaceBuild.JobID)
return xerrors.Errorf("verify user can cancel workspace builds: %w", err)
}
if !valid {
code = http.StatusForbidden
resp.Message = "User is not allowed to cancel workspace builds. Owner role is required."
return xerrors.New("user is not allowed to cancel workspace builds")
}
job, err := db.GetProvisionerJobByIDForUpdate(ctx, workspaceBuild.JobID)
if err != nil {
code = http.StatusInternalServerError
resp.Message = "Internal error fetching provisioner job."
resp.Detail = err.Error()
return xerrors.Errorf("get provisioner job: %w", err)
}
if job.CompletedAt.Valid {
code = http.StatusBadRequest
resp.Message = "Job has already completed!"
return xerrors.New("job has already completed")
}
if job.CanceledAt.Valid {
code = http.StatusBadRequest
resp.Message = "Job has already been marked as canceled!"
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{
ID: job.ID,
CanceledAt: sql.NullTime{
Time: dbtime.Now(),
Valid: true,
},
CompletedAt: sql.NullTime{
Time: dbtime.Now(),
// If the job is running, don't mark it completed!
Valid: !job.WorkerID.Valid,
},
})
if err != nil {
code = http.StatusInternalServerError
resp.Message = "Internal error updating provisioner job."
resp.Detail = err.Error()
return xerrors.Errorf("update provisioner job: %w", err)
}
return nil
}, nil)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job.",
Detail: err.Error(),
})
return
}
if job.CompletedAt.Valid {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Job has already completed!",
})
return
}
if job.CanceledAt.Valid {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Job has already been marked as canceled!",
})
return
}
err = api.Database.UpdateProvisionerJobWithCancelByID(ctx, database.UpdateProvisionerJobWithCancelByIDParams{
ID: job.ID,
CanceledAt: sql.NullTime{
Time: dbtime.Now(),
Valid: true,
},
CompletedAt: sql.NullTime{
Time: dbtime.Now(),
// If the job is running, don't mark it completed!
Valid: !job.WorkerID.Valid,
},
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating provisioner job.",
Detail: err.Error(),
})
httpapi.Write(ctx, rw, code, resp)
return
}
@@ -659,8 +693,14 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
})
}
func (api *API) verifyUserCanCancelWorkspaceBuilds(ctx context.Context, userID uuid.UUID, templateID uuid.UUID) (bool, error) {
template, err := api.Database.GetTemplateByID(ctx, templateID)
func verifyUserCanCancelWorkspaceBuilds(ctx context.Context, store database.Store, userID uuid.UUID, templateID uuid.UUID, jobStatus database.ProvisionerJobStatus) (bool, error) {
// If the jobStatus is pending, we always allow cancellation regardless of
// the template setting as it's non-destructive to Terraform resources.
if jobStatus == database.ProvisionerJobStatusPending {
return true, nil
}
template, err := store.GetTemplateByID(ctx, templateID)
if err != nil {
return false, xerrors.New("no template exists for this workspace")
}
@@ -669,7 +709,7 @@ func (api *API) verifyUserCanCancelWorkspaceBuilds(ctx context.Context, userID u
return true, nil // all users can cancel workspace builds
}
user, err := api.Database.GetUserByID(ctx, userID)
user, err := store.GetUserByID(ctx, userID)
if err != nil {
return false, xerrors.New("user does not exist")
}
+191 -3
View File
@@ -573,7 +573,7 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
build, err = client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
return assert.NoError(t, err) && build.Job.Status == codersdk.ProvisionerJobRunning
}, testutil.WaitShort, testutil.IntervalFast)
err := client.CancelWorkspaceBuild(ctx, build.ID)
err := client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{})
require.NoError(t, err)
require.Eventually(t, func() bool {
var err error
@@ -618,11 +618,199 @@ func TestPatchCancelWorkspaceBuild(t *testing.T) {
build, err = userClient.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
return assert.NoError(t, err) && build.Job.Status == codersdk.ProvisionerJobRunning
}, testutil.WaitShort, testutil.IntervalFast)
err := userClient.CancelWorkspaceBuild(ctx, build.ID)
err := userClient.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
})
t.Run("Cancel with expect_state=pending", 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: the workspace build is canceled
err = client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{
ExpectStatus: codersdk.CancelWorkspaceBuildStatusPending,
})
require.NoError(t, err)
// Then: the workspace build should be canceled.
build, err = client.WorkspaceBuild(ctx, build.ID)
require.NoError(t, err)
require.Equal(t, codersdk.ProvisionerJobCanceled, build.Job.Status)
})
t.Run("Cancel with expect_state=pending when job is running - should fail with 412", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
ProvisionPlan: echo.PlanComplete,
})
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)
return assert.NoError(t, err) && build.Job.Status == codersdk.ProvisionerJobRunning
}, testutil.WaitShort, testutil.IntervalFast)
// When: a cancel request is made with expect_state=pending
err := client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{
ExpectStatus: codersdk.CancelWorkspaceBuildStatusPending,
})
// 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=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()
// Given: a coderd instance with a provisioner daemon
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Log{
Log: &proto.Log{},
},
}},
ProvisionPlan: echo.PlanComplete,
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
ctx := testutil.Context(t, testutil.WaitLong)
// When: a cancel request is made with invalid expect_state
err := client.CancelWorkspaceBuild(ctx, workspace.LatestBuild.ID, codersdk.CancelWorkspaceBuildParams{
ExpectStatus: "invalid_status",
})
// Then: the request should fail with 400.
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
require.Contains(t, apiErr.Message, "Invalid expect_status")
})
}
func TestWorkspaceBuildResources(t *testing.T) {
@@ -968,7 +1156,7 @@ func TestWorkspaceBuildStatus(t *testing.T) {
_ = closeDaemon.Close()
// after successful cancel is "canceled"
build = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
err = client.CancelWorkspaceBuild(ctx, build.ID)
err = client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{})
require.NoError(t, err)
workspace, err = client.Workspace(ctx, workspace.ID)
+1 -1
View File
@@ -3245,7 +3245,7 @@ func TestWorkspaceWatcher(t *testing.T) {
closeFunc.Close()
build := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
wait("first is for the workspace build itself", nil)
err = client.CancelWorkspaceBuild(ctx, build.ID)
err = client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{})
require.NoError(t, err)
wait("second is for the build cancel", nil)
}