mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
fix: show accurate error message when startup script fails instead of misleading "agents not connected" (#22843)
Fixes #21946 When a startup script fails (exits with non-zero code), the UI displayed a misleading "Workspace agents are not connected" error even though the agent is actually connected and functional (SSH works, web terminal works). - Extracts the `WorkspaceAlert` component from `Workspace.tsx` to its own component - Updates the `WorkspaceAlert` component in `Workspace.tsx` distinguish correctly between agent disconnection, timeout, shutdown, and startup script failures. - Fixes double period bug in the alert description ("the agent has not connected yet.." Created on behalf of @kylecarbs --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: Cian Johnston <cian@coder.com>
This commit is contained in:
co-authored by
blink-so[bot]
Cian Johnston
parent
4b8a5e2b10
commit
4ce9fbeaf0
@@ -1,8 +1,6 @@
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import type { WorkspaceAgentStatus } from "api/typesGenerated";
|
||||
import { Alert, AlertDescription, AlertTitle } from "components/Alert/Alert";
|
||||
import { SidebarIconButton } from "components/FullPageLayout/Sidebar";
|
||||
import { Link } from "components/Link/Link";
|
||||
import { useSearchParamsKey } from "hooks/useSearchParamsKey";
|
||||
import { BlocksIcon, HistoryIcon } from "lucide-react";
|
||||
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
|
||||
@@ -15,6 +13,7 @@ import { HistorySidebar } from "./HistorySidebar";
|
||||
import { ResourceMetadata } from "./ResourceMetadata";
|
||||
import { ResourcesSidebar } from "./ResourcesSidebar";
|
||||
import { resourceOptionValue, useResourcesNav } from "./useResourcesNav";
|
||||
import { WorkspaceAlert } from "./WorkspaceAlert";
|
||||
import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection";
|
||||
import {
|
||||
getActiveTransitionStats,
|
||||
@@ -193,7 +192,7 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
)}
|
||||
|
||||
{!workspace.health.healthy && (
|
||||
<UnhealthyWorkspaceAlert
|
||||
<WorkspaceAlert
|
||||
workspace={workspace}
|
||||
troubleshootingURL={troubleshootingURL}
|
||||
/>
|
||||
@@ -256,65 +255,6 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface UnhealthyWorkspaceAlertProps {
|
||||
workspace: TypesGen.Workspace;
|
||||
troubleshootingURL: string | undefined;
|
||||
}
|
||||
|
||||
const UnhealthyWorkspaceAlert: FC<UnhealthyWorkspaceAlertProps> = ({
|
||||
workspace,
|
||||
troubleshootingURL,
|
||||
}) => {
|
||||
const failingAgentCount = workspace.health.failing_agents.length;
|
||||
const failureSet = new Set<WorkspaceAgentStatus>();
|
||||
|
||||
workspace.latest_build.resources.forEach((resource) => {
|
||||
resource.agents?.forEach((agent) => {
|
||||
failureSet.add(agent.status);
|
||||
});
|
||||
});
|
||||
|
||||
var title = "Workspace agents are not connected";
|
||||
var message =
|
||||
"Your workspace cannot be used until an agent connects. Continue to wait and check the log output of your workspace for any errors.";
|
||||
|
||||
// Disconnected is a more serious failure than timeout, so we can
|
||||
// prioritize handling it first.
|
||||
if (failureSet.has("disconnected")) {
|
||||
title = "Workspace agents have disconnected";
|
||||
message =
|
||||
"Continue to wait and check the log output of your workspace for any errors. If the agent does not reconnect, restarting the workspace can be used to try again.";
|
||||
} else if (failureSet.has("timeout")) {
|
||||
// Handle timeout case
|
||||
title = "Your workspace is starting, but the agent has not yet connected.";
|
||||
message =
|
||||
"The agent is taking longer than expected to connect. Continue to wait and check the log output of your workspace for any errors. If the agent does not connect, restarting the workspace can be used to try again.";
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert severity="warning" prominent>
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
Your workspace is running but{" "}
|
||||
{failingAgentCount > 1
|
||||
? `${failingAgentCount} agents have not connected yet.`
|
||||
: "the agent has not connected yet."}
|
||||
.{" "}
|
||||
</p>
|
||||
<p>{message}</p>
|
||||
<p>
|
||||
{troubleshootingURL && (
|
||||
<Link href={troubleshootingURL} target="_blank">
|
||||
View docs to troubleshoot
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
const countAgents = (resource: TypesGen.WorkspaceResource) => {
|
||||
return resource.agents ? resource.agents.length : 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import {
|
||||
MockWorkspace,
|
||||
MockWorkspaceAgent,
|
||||
MockWorkspaceResource,
|
||||
} from "testHelpers/entities";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
|
||||
import { WorkspaceAlert } from "./WorkspaceAlert";
|
||||
|
||||
const createUnhealthyWorkspace = (
|
||||
agentOverrides: Partial<WorkspaceAgent>,
|
||||
agentCount = 1,
|
||||
): Workspace => {
|
||||
const agents = Array.from({ length: agentCount }, (_, i) => ({
|
||||
...MockWorkspaceAgent,
|
||||
id: `test-agent-${i}`,
|
||||
name: `agent-${i}`,
|
||||
health: { healthy: false },
|
||||
...agentOverrides,
|
||||
}));
|
||||
return {
|
||||
...MockWorkspace,
|
||||
health: {
|
||||
healthy: false,
|
||||
failing_agents: agents.map((a) => a.id),
|
||||
},
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
resources: [{ ...MockWorkspaceResource, agents }],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const meta: Meta<typeof WorkspaceAlert> = {
|
||||
title: "pages/WorkspacePage/WorkspaceAlert",
|
||||
component: WorkspaceAlert,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkspaceAlert>;
|
||||
|
||||
export const Disconnected: Story = {
|
||||
args: {
|
||||
workspace: createUnhealthyWorkspace({ status: "disconnected" }),
|
||||
troubleshootingURL: "https://coder.com/docs/troubleshoot",
|
||||
},
|
||||
};
|
||||
|
||||
export const DisconnectedMultipleAgents: Story = {
|
||||
args: {
|
||||
workspace: createUnhealthyWorkspace({ status: "disconnected" }, 3),
|
||||
troubleshootingURL: "https://coder.com/docs/troubleshoot",
|
||||
},
|
||||
};
|
||||
|
||||
export const TimeoutWarning: Story = {
|
||||
args: {
|
||||
workspace: createUnhealthyWorkspace({ status: "timeout" }),
|
||||
troubleshootingURL: "https://coder.com/docs/troubleshoot",
|
||||
},
|
||||
};
|
||||
|
||||
export const StartupScriptFailed: Story = {
|
||||
args: {
|
||||
workspace: createUnhealthyWorkspace({
|
||||
status: "connected",
|
||||
lifecycle_state: "start_error",
|
||||
}),
|
||||
troubleshootingURL: "https://coder.com/docs/troubleshoot",
|
||||
},
|
||||
};
|
||||
|
||||
export const StartupScriptFailedMultipleAgents: Story = {
|
||||
args: {
|
||||
workspace: createUnhealthyWorkspace(
|
||||
{
|
||||
status: "connected",
|
||||
lifecycle_state: "start_error",
|
||||
},
|
||||
2,
|
||||
),
|
||||
troubleshootingURL: "https://coder.com/docs/troubleshoot",
|
||||
},
|
||||
};
|
||||
|
||||
export const ShuttingDownInformational: Story = {
|
||||
args: {
|
||||
workspace: createUnhealthyWorkspace({
|
||||
status: "connected",
|
||||
lifecycle_state: "shutting_down",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const NotConnected: Story = {
|
||||
args: {
|
||||
workspace: createUnhealthyWorkspace({ status: "connecting" }),
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutTroubleshootingURL: Story = {
|
||||
args: {
|
||||
workspace: createUnhealthyWorkspace({ status: "disconnected" }),
|
||||
troubleshootingURL: undefined,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import type { WorkspaceAgentStatus } from "api/typesGenerated";
|
||||
import { Alert, AlertDescription, AlertTitle } from "components/Alert/Alert";
|
||||
import { Link } from "components/Link/Link";
|
||||
import type { FC } from "react";
|
||||
|
||||
interface WorkspaceAlertProps {
|
||||
workspace: TypesGen.Workspace;
|
||||
troubleshootingURL: string | undefined;
|
||||
}
|
||||
|
||||
export const WorkspaceAlert: FC<WorkspaceAlertProps> = ({
|
||||
workspace,
|
||||
troubleshootingURL,
|
||||
}) => {
|
||||
const failingAgentCount = workspace.health.failing_agents.length;
|
||||
const statusSet = new Set<WorkspaceAgentStatus>();
|
||||
let hasStartError = false;
|
||||
let hasShuttingDown = false;
|
||||
|
||||
for (const resource of workspace.latest_build.resources) {
|
||||
for (const agent of resource.agents ?? []) {
|
||||
statusSet.add(agent.status);
|
||||
if (agent.lifecycle_state === "start_error") {
|
||||
hasStartError = true;
|
||||
}
|
||||
if (
|
||||
agent.lifecycle_state === "shutting_down" ||
|
||||
agent.lifecycle_state === "shutdown_error" ||
|
||||
agent.lifecycle_state === "shutdown_timeout"
|
||||
) {
|
||||
hasShuttingDown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const plural = failingAgentCount > 1;
|
||||
|
||||
let title: string;
|
||||
let message: string;
|
||||
let severity: "info" | "warning" = "warning";
|
||||
let prominent = true;
|
||||
|
||||
if (statusSet.has("disconnected")) {
|
||||
title = plural
|
||||
? `${failingAgentCount} workspace agents have disconnected`
|
||||
: "Workspace agent has disconnected";
|
||||
message =
|
||||
"Check the log output for errors. If the agent does not reconnect, try restarting the workspace.";
|
||||
} else if (statusSet.has("timeout")) {
|
||||
title = plural
|
||||
? `${failingAgentCount} agents are taking longer than expected to connect`
|
||||
: "Agent is taking longer than expected to connect";
|
||||
message =
|
||||
"Continue to wait and check the log output for errors. If the agent does not connect, try restarting the workspace.";
|
||||
severity = "warning";
|
||||
prominent = false;
|
||||
} else if (hasShuttingDown) {
|
||||
title = plural
|
||||
? `${failingAgentCount} workspace agents are shutting down`
|
||||
: "Workspace agent is shutting down";
|
||||
message = "The workspace is not available while the agent shuts down.";
|
||||
severity = "info";
|
||||
prominent = false;
|
||||
} else if (hasStartError) {
|
||||
title = plural
|
||||
? `Startup scripts failed on ${failingAgentCount} agents`
|
||||
: "Startup script failed";
|
||||
message =
|
||||
"The workspace is running but a startup script exited with an error. Check the agent logs for details.";
|
||||
} else {
|
||||
title = plural
|
||||
? `${failingAgentCount} workspace agents are still connecting`
|
||||
: "Workspace agent is still connecting";
|
||||
message =
|
||||
"The workspace agent is still connecting. Check the log output if the connection does not complete.";
|
||||
severity = "info";
|
||||
prominent = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert severity={severity} prominent={prominent}>
|
||||
<AlertTitle>{title}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>{message}</p>
|
||||
<p>
|
||||
{troubleshootingURL && (
|
||||
<Link href={troubleshootingURL} target="_blank">
|
||||
View docs to troubleshoot
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user