mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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
@@ -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,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user