fix(coderd): wake dormant workspace when attempting to start it (#21306)

Relates to #20925

This PR modifies the `postWorkspaceBuild` handler to automatically unset
dormancy on a workspace when a start transition is requested.
Previously, the client was responsible for unsetting the dormancy on the
workspace prior to posting a workspace build.
This commit is contained in:
Cian Johnston
2025-12-18 10:35:04 +00:00
committed by GitHub
parent cac6d4ce98
commit 8248fa3b84
2 changed files with 69 additions and 10 deletions
+34
View File
@@ -399,6 +399,40 @@ func (api *API) postWorkspaceBuildsInternal(
err := api.Database.InTx(func(tx database.Store) error {
var err error
// #20925: if the workspace is dormant and we are starting the workspace,
// we need to unset that status before inserting a new build.
// This is done inside the transaction for consistency, but it could also be
// done outside the transaction so that an attempt to start a workspace will
// also unset dormancy.
if workspace.DormantAt.Valid && transition == database.WorkspaceTransitionStart {
if _, err := tx.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
ID: workspace.ID,
DormantAt: sql.NullTime{Valid: false},
}); err != nil {
return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
Message: "Internal error unsetting workspace dormant status",
Detail: err.Error(),
})
}
// We need to audit this change separately.
updatedWorkspace := workspace.WorkspaceTable()
updatedWorkspace.DormantAt = sql.NullTime{Valid: false}
auditor := api.Auditor.Load()
bag := audit.BaggageFromContext(ctx)
audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.WorkspaceTable]{
Audit: *auditor,
Old: workspace.WorkspaceTable(),
New: updatedWorkspace,
Log: api.Logger,
UserID: apiKey.UserID,
OrganizationID: workspace.OrganizationID,
RequestID: workspace.ID,
IP: bag.IP,
Action: database.AuditActionWrite,
Status: http.StatusOK,
})
}
previousWorkspaceBuild, err = tx.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
api.Logger.Error(ctx, "failed fetching previous workspace build", slog.F("workspace_id", workspace.ID), slog.Error(err))
+35 -10
View File
@@ -4244,10 +4244,16 @@ func TestWorkspaceDormant(t *testing.T) {
require.True(t, workspace.LastUsedAt.After(lastUsedAt))
})
t.Run("CannotStart", func(t *testing.T) {
// #20925: this test originally validated that you could **not** start a dormant workspace.
// The client was required to explicitly update the dormancy status before starting.
// This led to a 'whack-a-mole' situation where various code paths that create a workspace build
// would need to special case dormant workspaces.
// Now, a dormant workspace will automatically 'wake up' on start.
t.Run("StartWakesUpDormantWorkspace", func(t *testing.T) {
t.Parallel()
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
auditor = audit.NewMock()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
@@ -4267,18 +4273,37 @@ func TestWorkspaceDormant(t *testing.T) {
// Should be able to stop a workspace while it is dormant.
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Should not be able to start a workspace while it is dormant.
_, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
// Reset the auditor
auditor.ResetLogs()
// Assert test invariant: workspace is dormant.
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch dormant workspace")
if assert.NotNil(t, workspace.DormantAt, "workspace must be dormant") {
require.WithinDuration(t, *workspace.DormantAt, time.Now(), 10*time.Second)
}
// Starting a dormant workspace should 'wake' it.
wb, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransition(database.WorkspaceTransitionStart),
})
require.Error(t, err)
err = client.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
Dormant: false,
})
require.NoError(t, err)
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionStart)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, wb.ID)
// After starting, the workspace should no longer be dormant.
updatedWs, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Nil(t, updatedWs.DormantAt)
// There should be an audit log for both the dormancy update and the start.
require.Len(t, auditor.AuditLogs(), 2)
require.True(t, auditor.Contains(t, database.AuditLog{
Action: database.AuditActionWrite,
ResourceType: database.ResourceTypeWorkspace,
}))
require.True(t, auditor.Contains(t, database.AuditLog{
Action: database.AuditActionStart,
ResourceType: database.ResourceTypeWorkspaceBuild,
}))
})
}