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:
Cian Johnston 2024-01-08 17:14:09 +00:00 committed by GitHub
parent 6096af77c8
commit 220e95dd5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 278 additions and 4 deletions

View File

@ -26,6 +26,7 @@ var HealthSections = []HealthSection{
HealthSectionWebsocket,
HealthSectionDatabase,
HealthSectionWorkspaceProxy,
HealthSectionProvisionerDaemons,
}
type HealthSettings struct {

View File

@ -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_

View File

@ -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

View File

@ -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);

View File

@ -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 = {};

View File

@ -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;

View File

@ -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",
},
],
},
],
},