mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add workspace restart functionality to API (#25757)
This models restart as durable orchestration of existing stop and start workspace builds instead of adding a new restart transition. Keeping restart as two existing transitions preserves the current build/provisioner model. The child start build is created only after the parent stop build succeeds, rather than being inserted immediately in a pending state. That keeps `workspace_builds` aligned with actual provisioner-ready work and avoids introducing a second pending-build lifecycle that the provisioner and build acquisition paths would need to understand. Refs: https://linear.app/codercom/issue/PLAT-143
This commit is contained in:
+126
-1
@@ -375,6 +375,30 @@ func (api *API) postWorkspaceBuildsInternal(
|
||||
codersdk.WorkspaceBuild,
|
||||
error,
|
||||
) {
|
||||
if err := validateCreateWorkspaceBuildOnSuccess(createBuild); err != nil {
|
||||
return codersdk.WorkspaceBuild{}, err
|
||||
}
|
||||
|
||||
var childParameterValuesJSON json.RawMessage
|
||||
if createBuild.OnSuccess != nil {
|
||||
childParameterValues := createBuild.OnSuccess.RichParameterValues
|
||||
if childParameterValues == nil {
|
||||
childParameterValues = []codersdk.WorkspaceBuildParameter{}
|
||||
}
|
||||
|
||||
var err error
|
||||
childParameterValuesJSON, err = json.Marshal(childParameterValues)
|
||||
if err != nil {
|
||||
return codersdk.WorkspaceBuild{}, httperror.NewResponseError(
|
||||
http.StatusInternalServerError,
|
||||
codersdk.Response{
|
||||
Message: "Internal error preparing follow-up workspace build parameters",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
transition := database.WorkspaceTransition(createBuild.Transition)
|
||||
builder := wsbuilder.New(workspace, transition, *api.BuildUsageChecker.Load()).
|
||||
Initiator(apiKey.UserID).
|
||||
@@ -481,7 +505,62 @@ func (api *API) postWorkspaceBuildsInternal(
|
||||
},
|
||||
workspaceBuildBaggage,
|
||||
)
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if createBuild.OnSuccess != nil {
|
||||
onSuccessReq := createBuild.OnSuccess
|
||||
// Reuse the parent build's timestamps so the orchestration row and
|
||||
// the parent build agree on created_at.
|
||||
_, err = tx.InsertWorkspaceBuildOrchestration(ctx, database.InsertWorkspaceBuildOrchestrationParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: workspaceBuild.CreatedAt,
|
||||
UpdatedAt: workspaceBuild.UpdatedAt,
|
||||
ParentBuildID: workspaceBuild.ID,
|
||||
ChildTransition: database.WorkspaceTransition(onSuccessReq.Transition),
|
||||
ChildTemplateVersionID: uuid.NullUUID{
|
||||
UUID: onSuccessReq.TemplateVersionID,
|
||||
Valid: onSuccessReq.TemplateVersionID != uuid.Nil,
|
||||
},
|
||||
ChildTemplateVersionPresetID: uuid.NullUUID{
|
||||
UUID: onSuccessReq.TemplateVersionPresetID,
|
||||
Valid: onSuccessReq.TemplateVersionPresetID != uuid.Nil,
|
||||
},
|
||||
ChildRichParameterValues: childParameterValuesJSON,
|
||||
ChildLogLevel: string(createBuild.LogLevel),
|
||||
ChildReason: database.NullBuildReason{
|
||||
BuildReason: database.BuildReason(createBuild.Reason),
|
||||
Valid: createBuild.Reason != "",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if dbauthz.IsNotAuthorizedError(err) {
|
||||
detail := "Queuing the follow-up workspace build requires permission to start the workspace."
|
||||
if onSuccessReq.TemplateVersionID != uuid.Nil {
|
||||
detail = "Pinning a template version on the follow-up build requires template update permission. Omit template_version_id to use the active version."
|
||||
}
|
||||
return httperror.NewResponseError(http.StatusForbidden, codersdk.Response{
|
||||
Message: "Unauthorized to queue follow-up workspace build.",
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
api.Logger.Error(ctx, "failed to queue follow-up workspace build",
|
||||
slog.F("workspace_id", workspace.ID),
|
||||
slog.F("workspace_build_id", workspaceBuild.ID),
|
||||
slog.Error(err),
|
||||
)
|
||||
return httperror.NewResponseError(
|
||||
http.StatusInternalServerError,
|
||||
codersdk.Response{
|
||||
Message: "Internal error queueing follow-up workspace build",
|
||||
Detail: err.Error(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return codersdk.WorkspaceBuild{}, err
|
||||
@@ -584,6 +663,48 @@ func (api *API) postWorkspaceBuildsInternal(
|
||||
return apiBuild, nil
|
||||
}
|
||||
|
||||
// validateCreateWorkspaceBuildOnSuccess enforces the subset of build options
|
||||
// that currently has well-defined stop-then-start semantics.
|
||||
func validateCreateWorkspaceBuildOnSuccess(createBuild codersdk.CreateWorkspaceBuildRequest) error {
|
||||
onSuccess := createBuild.OnSuccess
|
||||
if onSuccess == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if createBuild.Transition != codersdk.WorkspaceTransitionStop {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "OnSuccess is only permitted when stopping a workspace.",
|
||||
})
|
||||
}
|
||||
if onSuccess.Transition != codersdk.WorkspaceTransitionStart {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "OnSuccess transition must be start.",
|
||||
})
|
||||
}
|
||||
if createBuild.DryRun {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "OnSuccess cannot be set alongside DryRun.",
|
||||
})
|
||||
}
|
||||
if createBuild.Orphan {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "OnSuccess cannot be set alongside Orphan.",
|
||||
})
|
||||
}
|
||||
if len(createBuild.ProvisionerState) > 0 {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "OnSuccess cannot be set alongside ProvisionerState.",
|
||||
})
|
||||
}
|
||||
if onSuccess.TemplateVersionPresetID != uuid.Nil && onSuccess.TemplateVersionID == uuid.Nil {
|
||||
return httperror.NewResponseError(http.StatusBadRequest, codersdk.Response{
|
||||
Message: "OnSuccess TemplateVersionPresetID requires TemplateVersionID.",
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) notifyWorkspaceUpdated(
|
||||
ctx context.Context,
|
||||
initiatorID uuid.UUID,
|
||||
@@ -765,6 +886,10 @@ func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
|
||||
Kind: wspubsub.WorkspaceEventKindStateChange,
|
||||
WorkspaceID: workspace.ID,
|
||||
})
|
||||
err = wspubsub.PublishWorkspaceBuildOrchestrationWake(ctx, api.Pubsub)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "failed to publish workspace build orchestration wake", slog.Error(err))
|
||||
}
|
||||
|
||||
// Publish workspace build update to the all builds channel if the experiment is enabled.
|
||||
if api.Experiments.Enabled(codersdk.ExperimentWorkspaceBuildUpdates) {
|
||||
|
||||
Reference in New Issue
Block a user