mirror of https://github.com/coder/coder.git
feat(site): add healthcheck page for provisioner daemons (#11494)
Part of #10676 - Adds a health section for provisioner daemons (mostly cannibalized from the Workspace Proxy section) - Adds a corresponding storybook entry for provisioner daemons health section - Fixed an issue where dismissing the provisioner daemons warnings would result in a 500 error - Adds provisioner daemon error codes to docs
This commit is contained in:
parent
6096af77c8
commit
220e95dd5c
|
@ -26,6 +26,7 @@ var HealthSections = []HealthSection{
|
|||
HealthSectionWebsocket,
|
||||
HealthSectionDatabase,
|
||||
HealthSectionWorkspaceProxy,
|
||||
HealthSectionProvisionerDaemons,
|
||||
}
|
||||
|
||||
type HealthSettings struct {
|
||||
|
|
|
@ -267,6 +267,54 @@ _One or more Workspace Proxies Unhealthy_
|
|||
**Solution:** Ensure that Coder can establish a connection to the configured
|
||||
workspace proxies.
|
||||
|
||||
### EPD01
|
||||
|
||||
_No Provisioner Daemons Available_
|
||||
|
||||
**Problem:** No provisioner daemons are registered with Coder. No workspaces can
|
||||
be built until there is at least one provisioner daemon running.
|
||||
|
||||
**Solution:**
|
||||
|
||||
If you are using
|
||||
[External Provisioner Daemons](./provisioners.md#external-provisioners), ensure
|
||||
that they are able to successfully connect to Coder. Otherwise, ensure
|
||||
[`--provisioner-daemons`](../cli/server.md#provisioner-daemons) is set to a
|
||||
value greater than 0.
|
||||
|
||||
> Note: This may be a transient issue if you are currently in the process of
|
||||
> updating your deployment.
|
||||
|
||||
### EPD02
|
||||
|
||||
_Provisioner Daemon Version Mismatch_
|
||||
|
||||
**Problem:** One or more provisioner daemons are more than one major or minor
|
||||
version out of date with the main deployment. It is important that provisioner
|
||||
daemons are updated at the same time as the main deployment to minimize the risk
|
||||
of API incompatibility.
|
||||
|
||||
**Solution:** Update the provisioner daemon to match the currently running
|
||||
version of Coder.
|
||||
|
||||
> Note: This may be a transient issue if you are currently in the process of
|
||||
> updating your deployment.
|
||||
|
||||
### EPD03
|
||||
|
||||
_Provisioner Daemon API Version Mismatch_
|
||||
|
||||
**Problem:** One or more provisioner daemons are using APIs that are marked as
|
||||
deprecated. These deprecated APIs may be removed in a future release of Coder,
|
||||
at which point the affected provisioner daemons will no longer be able to
|
||||
connect to Coder.
|
||||
|
||||
**Solution:** Update the provisioner daemon to match the currently running
|
||||
version of Coder.
|
||||
|
||||
> Note: This may be a transient issue if you are currently in the process of
|
||||
> updating your deployment.
|
||||
|
||||
## EUNKNOWN
|
||||
|
||||
_Unknown Error_
|
||||
|
|
|
@ -234,6 +234,9 @@ const WebsocketPage = lazy(() => import("./pages/HealthPage/WebsocketPage"));
|
|||
const WorkspaceProxyHealthPage = lazy(
|
||||
() => import("./pages/HealthPage/WorkspaceProxyPage"),
|
||||
);
|
||||
const ProvisionerDaemonsHealthPage = lazy(
|
||||
() => import("./pages/HealthPage/ProvisionerDaemonsPage"),
|
||||
);
|
||||
|
||||
export const AppRouter: FC = () => {
|
||||
return (
|
||||
|
@ -400,6 +403,10 @@ export const AppRouter: FC = () => {
|
|||
path="workspace-proxy"
|
||||
element={<WorkspaceProxyHealthPage />}
|
||||
/>
|
||||
<Route
|
||||
path="provisioner-daemons"
|
||||
element={<ProvisionerDaemonsHealthPage />}
|
||||
/>
|
||||
</Route>
|
||||
{/* Using path="*"" means "match anything", so this route
|
||||
acts like a catch-all for URLs that we don't have explicit
|
||||
|
|
|
@ -34,6 +34,7 @@ export function HealthLayout() {
|
|||
websocket: "Websocket",
|
||||
database: "Database",
|
||||
workspace_proxy: "Workspace Proxy",
|
||||
provisioner_daemons: "Provisioner Daemons",
|
||||
} as const;
|
||||
const visibleSections = filterVisibleSections(sections);
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { StoryObj, Meta } from "@storybook/react";
|
||||
import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage";
|
||||
import { generateMeta } from "./storybook";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "pages/Health/ProvisionerDaemons",
|
||||
...generateMeta({
|
||||
path: "/health/provisioner-daemons",
|
||||
element: <ProvisionerDaemonsPage />,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj;
|
||||
|
||||
export const Default: Story = {};
|
|
@ -0,0 +1,166 @@
|
|||
import { Header, HeaderTitle, HealthyDot, Main, Pill } from "./Content";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { DismissWarningButton } from "./DismissWarningButton";
|
||||
import { Alert } from "components/Alert/Alert";
|
||||
import { HealthcheckReport } from "api/typesGenerated";
|
||||
import { createDayString } from "utils/createDayString";
|
||||
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import Business from "@mui/icons-material/Business";
|
||||
import Person from "@mui/icons-material/Person";
|
||||
import SwapHoriz from "@mui/icons-material/SwapHoriz";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Sell from "@mui/icons-material/Sell";
|
||||
|
||||
export const ProvisionerDaemonsPage = () => {
|
||||
const healthStatus = useOutletContext<HealthcheckReport>();
|
||||
const { provisioner_daemons: daemons } = healthStatus;
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("Provisioner Daemons - Health")}</title>
|
||||
</Helmet>
|
||||
|
||||
<Header>
|
||||
<HeaderTitle>
|
||||
<HealthyDot severity={daemons.severity} />
|
||||
Provisioner Daemons
|
||||
</HeaderTitle>
|
||||
<DismissWarningButton healthcheck="ProvisionerDaemons" />
|
||||
</Header>
|
||||
|
||||
<Main>
|
||||
{daemons.warnings.map((warning) => {
|
||||
return (
|
||||
<Alert key={warning.code} severity="warning">
|
||||
{warning.message}
|
||||
</Alert>
|
||||
);
|
||||
})}
|
||||
|
||||
{daemons.items.map(({ provisioner_daemon: daemon, warnings }) => {
|
||||
const daemonScope = daemon.tags["scope"] || "organization";
|
||||
const iconScope =
|
||||
daemonScope === "organization" ? <Business /> : <Person />;
|
||||
const extraTags = Object.keys(daemon.tags)
|
||||
.filter((key) => key !== "scope" && key !== "owner")
|
||||
.reduce(
|
||||
(acc, key) => {
|
||||
acc[key] = daemon.tags[key];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
const isWarning = warnings.length > 0;
|
||||
return (
|
||||
<div
|
||||
key={daemon.name}
|
||||
css={{
|
||||
borderRadius: 8,
|
||||
border: `1px solid ${
|
||||
isWarning
|
||||
? theme.palette.warning.light
|
||||
: theme.palette.divider
|
||||
}`,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<header
|
||||
css={{
|
||||
padding: 24,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContenxt: "space-between",
|
||||
gap: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 24,
|
||||
objectFit: "fill",
|
||||
}}
|
||||
>
|
||||
<div css={{ lineHeight: "160%" }}>
|
||||
<h4 css={{ fontWeight: 500, margin: 0 }}>{daemon.name}</h4>
|
||||
<span css={{ color: theme.palette.text.secondary }}>
|
||||
<code>{daemon.version}</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
marginLeft: "auto",
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Tooltip title="API Version">
|
||||
<Pill icon={<SwapHoriz />}>
|
||||
<code>{daemon.api_version}</code>
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
<Tooltip title="Scope">
|
||||
<Pill icon={iconScope}>
|
||||
<span
|
||||
css={{
|
||||
":first-letter": { textTransform: "uppercase" },
|
||||
}}
|
||||
>
|
||||
{daemonScope}
|
||||
</span>
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
{Object.keys(extraTags).map((k) => (
|
||||
<Tooltip key={k} title={k}>
|
||||
<Pill key={k} icon={<Sell />}>
|
||||
{extraTags[k]}
|
||||
</Pill>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
css={{
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "8px 24px",
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{warnings.length > 0 ? (
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
{warnings.map((warning, i) => (
|
||||
<span key={i}>{warning.message}</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>No warnings</span>
|
||||
)}
|
||||
{daemon.last_seen_at && (
|
||||
<span
|
||||
css={{ color: theme.palette.text.secondary }}
|
||||
data-chromatic="ignore"
|
||||
>
|
||||
Last seen {createDayString(daemon.last_seen_at)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProvisionerDaemonsPage;
|
|
@ -3103,7 +3103,16 @@ export const MockHealth: TypesGen.HealthcheckReport = {
|
|||
},
|
||||
provisioner_daemons: {
|
||||
severity: "ok",
|
||||
warnings: [],
|
||||
warnings: [
|
||||
{
|
||||
message: "Something is wrong!",
|
||||
code: "EUNKNOWN",
|
||||
},
|
||||
{
|
||||
message: "This is also bad.",
|
||||
code: "EPD01",
|
||||
},
|
||||
],
|
||||
dismissed: false,
|
||||
items: [
|
||||
{
|
||||
|
@ -3111,16 +3120,42 @@ export const MockHealth: TypesGen.HealthcheckReport = {
|
|||
id: "e455b582-ac04-4323-9ad6-ab71301fa006",
|
||||
created_at: "2024-01-04T15:53:03.21563Z",
|
||||
last_seen_at: "2024-01-04T16:05:03.967551Z",
|
||||
name: "vvuurrkk-2",
|
||||
version: "v2.6.0-devel+965ad5e96",
|
||||
name: "ok",
|
||||
version: "v2.3.4-devel+abcd1234",
|
||||
api_version: "1.0",
|
||||
provisioners: ["echo", "terraform"],
|
||||
tags: {
|
||||
owner: "",
|
||||
scope: "organization",
|
||||
custom_tag_name: "custom_tag_value",
|
||||
},
|
||||
},
|
||||
warnings: [],
|
||||
},
|
||||
{
|
||||
provisioner_daemon: {
|
||||
id: "e455b582-ac04-4323-9ad6-ab71301fa006",
|
||||
created_at: "2024-01-04T15:53:03.21563Z",
|
||||
last_seen_at: "2024-01-04T16:05:03.967551Z",
|
||||
name: "unhappy",
|
||||
version: "v0.0.1",
|
||||
api_version: "0.1",
|
||||
provisioners: ["echo", "terraform"],
|
||||
tags: {
|
||||
owner: "",
|
||||
scope: "organization",
|
||||
},
|
||||
},
|
||||
warnings: [],
|
||||
warnings: [
|
||||
{
|
||||
message: "Something specific is wrong with this daemon.",
|
||||
code: "EUNKNOWN",
|
||||
},
|
||||
{
|
||||
message: "And now for something completely different.",
|
||||
code: "EUNKNOWN",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue