Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion coderd/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,12 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
}, nil)
if errors.Is(err, errRunningWorkspaceLimitExceeded) {
maxRunning := api.DeploymentValues.MaxRunningWorkspacesPerUser.Value()
message := fmt.Sprintf("Running workspace limit reached (max %d per user). Stop one or more workspaces to start another.", maxRunning)
if createBuild.TemplateVersionID != uuid.Nil {
message = fmt.Sprintf("Running workspace limit reached (max %d per user). Stop one or more workspaces to update this workspace.", maxRunning)
}
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: fmt.Sprintf("Running workspace limit reached (max %d per user). Stop one or more workspaces to start another.", maxRunning),
Message: message,
})
return
}
Expand Down
13 changes: 13 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,19 @@ class ApiMethods {
throw new MissingBuildParameters(missingParameters, activeVersionId);
}

// If the workspace is currently running, stop it first so that the
// running workspace limit check on the server passes when we update
// with the new version.
if (workspace.latest_build.status === "running") {
const stopBuild = await this.stopWorkspace(workspace.id);
const awaitedStopBuild = await this.waitForBuild(stopBuild);
if (awaitedStopBuild?.status === "canceled") {
throw new Error(
"Workspace stop was canceled before the update could be applied.",
);
}
}

return this.postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: activeVersionId,
Expand Down
36 changes: 27 additions & 9 deletions site/src/pages/WorkspacePage/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface WorkspaceProps {
isOwner: boolean;
timings?: TypesGen.WorkspaceBuildTimings;
startWorkspaceError?: unknown;
updateWorkspaceError?: unknown;
}

/**
Expand Down Expand Up @@ -89,6 +90,7 @@ export const Workspace: FC<WorkspaceProps> = ({
isOwner,
timings,
startWorkspaceError,
updateWorkspaceError,
}) => {
const navigate = useNavigate();
const theme = useTheme();
Expand Down Expand Up @@ -125,14 +127,10 @@ export const Workspace: FC<WorkspaceProps> = ({
const { shouldShow: shouldShowWorkspaceReadyDelayAlert } =
useWorkspaceReadyDelayAlert(timings, workspaceRunning);

const isRunningWorkspaceLimitError = Boolean(
startWorkspaceError &&
isApiError(startWorkspaceError) &&
startWorkspaceError.response?.status === 409 &&
startWorkspaceError.response?.data?.message?.includes(
"Running workspace limit",
),
);
const isStartRunningWorkspaceLimitError =
isRunningWorkspaceLimitError(startWorkspaceError);
const isUpdateRunningWorkspaceLimitError =
isRunningWorkspaceLimitError(updateWorkspaceError);

return (
<div
Expand Down Expand Up @@ -255,7 +253,7 @@ export const Workspace: FC<WorkspaceProps> = ({
</Alert>
)}

{isRunningWorkspaceLimitError && (
{isStartRunningWorkspaceLimitError && (
<Alert severity="warning">
<AlertTitle>Running workspace limit reached</AlertTitle>
<AlertDetail>
Expand All @@ -267,6 +265,18 @@ export const Workspace: FC<WorkspaceProps> = ({
</Alert>
)}

{isUpdateRunningWorkspaceLimitError && (
<Alert severity="warning">
<AlertTitle>Running workspace limit reached</AlertTitle>
<AlertDetail>
{getErrorMessage(
updateWorkspaceError,
"Running workspace limit reached (max 1 per user). Stop one or more workspaces to update this workspace.",
)}
</AlertDetail>
</Alert>
)}

{workspace.latest_build.job.error && (
<Alert severity="error">
<AlertTitle>Workspace build failed</AlertTitle>
Expand Down Expand Up @@ -342,6 +352,14 @@ const countAgents = (resource: TypesGen.WorkspaceResource) => {
return resource.agents ? resource.agents.length : 0;
};

const isRunningWorkspaceLimitError = (error: unknown): boolean =>
Boolean(
error &&
isApiError(error) &&
error.response?.status === 409 &&
error.response?.data?.message?.includes("Running workspace limit"),
);

const styles = {
content: {
padding: 32,
Expand Down
27 changes: 27 additions & 0 deletions site/src/pages/WorkspacePage/WorkspacePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,33 @@ describe("WorkspacePage", () => {
});
});

it("shows a running workspace limit warning when update fails with 409", async () => {
jest
.spyOn(API, "getWorkspaceByOwnerAndName")
.mockResolvedValueOnce(MockOutdatedWorkspace);
jest.spyOn(API, "updateWorkspace").mockRejectedValueOnce({
isAxiosError: true,
response: {
status: 409,
data: {
message:
"Running workspace limit reached (max 1 per user). Stop one or more workspaces to update this workspace.",
},
},
});

await renderWorkspacePage(MockWorkspace);

const user = userEvent.setup();
await user.click(screen.getByTestId("workspace-update-button"));
const confirmButton = await screen.findByTestId("confirm-button");
await user.click(confirmButton);

await screen.findByText(
/Stop one or more workspaces to update this workspace/i,
);
});

it("restart the workspace with one time parameters when having the confirmation dialog", async () => {
localStorage.removeItem(`${MockUser.id}_ignoredWarnings`);
jest.spyOn(API, "getWorkspaceParameters").mockResolvedValue({
Expand Down
1 change: 1 addition & 0 deletions site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
isOwner={isOwner}
timings={timingsQuery.data}
startWorkspaceError={startWorkspaceMutation.error}
updateWorkspaceError={updateWorkspaceMutation.error}
/>

<WorkspaceDeleteDialog
Expand Down