- 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 fromall commits
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
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,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 | ||
} | ||
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() | ||
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, code, resp) | ||
deansheather marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
return | ||
} | ||
@@ -659,8 +693,14 @@ 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 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 :=store.GetUserByID(ctx, userID) | ||
if err != nil { | ||
return false, xerrors.New("user does not exist") | ||
} | ||
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.