mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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   - Enabled cancel action for pending workspaces (`expect_status=pending` is sent if workspace is in pending status)  --------- Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
co-authored by
Dean Sheather
parent
2f42b64182
commit
8202514ce0
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user