Skip to content

Commit 220e95d

Browse files
authored
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
1 parent 6096af7 commit 220e95d

File tree

8 files changed

+277
-3
lines changed

8 files changed

+277
-3
lines changed

codersdk/health.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var HealthSections = []HealthSection{
2626
HealthSectionWebsocket,
2727
HealthSectionDatabase,
2828
HealthSectionWorkspaceProxy,
29+
HealthSectionProvisionerDaemons,
2930
}
3031

3132
type HealthSettings struct {

docs/admin/healthcheck.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,54 @@ _One or more Workspace Proxies Unhealthy_
267267
**Solution:** Ensure that Coder can establish a connection to the configured
268268
workspace proxies.
269269

270+
### EPD01
271+
272+
_No Provisioner Daemons Available_
273+
274+
**Problem:** No provisioner daemons are registered with Coder. No workspaces can
275+
be built until there is at least one provisioner daemon running.
276+
277+
**Solution:**
278+
279+
If you are using
280+
[External Provisioner Daemons](./provisioners.md#external-provisioners), ensure
281+
that they are able to successfully connect to Coder. Otherwise, ensure
282+
[`--provisioner-daemons`](../cli/server.md#provisioner-daemons) is set to a
283+
value greater than 0.
284+
285+
> Note: This may be a transient issue if you are currently in the process of
286+
> updating your deployment.
287+
288+
### EPD02
289+
290+
_Provisioner Daemon Version Mismatch_
291+
292+
**Problem:** One or more provisioner daemons are more than one major or minor
293+
version out of date with the main deployment. It is important that provisioner
294+
daemons are updated at the same time as the main deployment to minimize the risk
295+
of API incompatibility.
296+
297+
**Solution:** Update the provisioner daemon to match the currently running
298+
version of Coder.
299+
300+
> Note: This may be a transient issue if you are currently in the process of
301+
> updating your deployment.
302+
303+
### EPD03
304+
305+
_Provisioner Daemon API Version Mismatch_
306+
307+
**Problem:** One or more provisioner daemons are using APIs that are marked as
308+
deprecated. These deprecated APIs may be removed in a future release of Coder,
309+
at which point the affected provisioner daemons will no longer be able to
310+
connect to Coder.
311+
312+
**Solution:** Update the provisioner daemon to match the currently running
313+
version of Coder.
314+
315+
> Note: This may be a transient issue if you are currently in the process of
316+
> updating your deployment.
317+
270318
## EUNKNOWN
271319

272320
_Unknown Error_

site/src/AppRouter.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ const WebsocketPage = lazy(() => import("./pages/HealthPage/WebsocketPage"));
234234
const WorkspaceProxyHealthPage = lazy(
235235
() => import("./pages/HealthPage/WorkspaceProxyPage"),
236236
);
237+
const ProvisionerDaemonsHealthPage = lazy(
238+
() => import("./pages/HealthPage/ProvisionerDaemonsPage"),
239+
);
237240

238241
export const AppRouter: FC = () => {
239242
return (
@@ -400,6 +403,10 @@ export const AppRouter: FC = () => {
400403
path="workspace-proxy"
401404
element={<WorkspaceProxyHealthPage />}
402405
/>
406+
<Route
407+
path="provisioner-daemons"
408+
element={<ProvisionerDaemonsHealthPage />}
409+
/>
403410
</Route>
404411
{/* Using path="*"" means "match anything", so this route
405412
acts like a catch-all for URLs that we don't have explicit

site/src/pages/HealthPage/HealthLayout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function HealthLayout() {
3434
websocket: "Websocket",
3535
database: "Database",
3636
workspace_proxy: "Workspace Proxy",
37+
provisioner_daemons: "Provisioner Daemons",
3738
} as const;
3839
const visibleSections = filterVisibleSections(sections);
3940

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { StoryObj, Meta } from "@storybook/react";
2+
import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage";
3+
import { generateMeta } from "./storybook";
4+
5+
const meta: Meta = {
6+
title: "pages/Health/ProvisionerDaemons",
7+
...generateMeta({
8+
path: "/health/provisioner-daemons",
9+
element: <ProvisionerDaemonsPage />,
10+
}),
11+
};
12+
13+
export default meta;
14+
type Story = StoryObj;
15+
16+
export const Default: Story = {};
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Header, HeaderTitle, HealthyDot, Main, Pill } from "./Content";
2+
import { Helmet } from "react-helmet-async";
3+
import { pageTitle } from "utils/page";
4+
import { useTheme } from "@mui/material/styles";
5+
import { DismissWarningButton } from "./DismissWarningButton";
6+
import { Alert } from "components/Alert/Alert";
7+
import { HealthcheckReport } from "api/typesGenerated";
8+
import { createDayString } from "utils/createDayString";
9+
10+
import { useOutletContext } from "react-router-dom";
11+
import Business from "@mui/icons-material/Business";
12+
import Person from "@mui/icons-material/Person";
13+
import SwapHoriz from "@mui/icons-material/SwapHoriz";
14+
import Tooltip from "@mui/material/Tooltip";
15+
import Sell from "@mui/icons-material/Sell";
16+
17+
export const ProvisionerDaemonsPage = () => {
18+
const healthStatus = useOutletContext<HealthcheckReport>();
19+
const { provisioner_daemons: daemons } = healthStatus;
20+
const theme = useTheme();
21+
return (
22+
<>
23+
<Helmet>
24+
<title>{pageTitle("Provisioner Daemons - Health")}</title>
25+
</Helmet>
26+
27+
<Header>
28+
<HeaderTitle>
29+
<HealthyDot severity={daemons.severity} />
30+
Provisioner Daemons
31+
</HeaderTitle>
32+
<DismissWarningButton healthcheck="ProvisionerDaemons" />
33+
</Header>
34+
35+
<Main>
36+
{daemons.warnings.map((warning) => {
37+
return (
38+
<Alert key={warning.code} severity="warning">
39+
{warning.message}
40+
</Alert>
41+
);
42+
})}
43+
44+
{daemons.items.map(({ provisioner_daemon: daemon, warnings }) => {
45+
const daemonScope = daemon.tags["scope"] || "organization";
46+
const iconScope =
47+
daemonScope === "organization" ? <Business /> : <Person />;
48+
const extraTags = Object.keys(daemon.tags)
49+
.filter((key) => key !== "scope" && key !== "owner")
50+
.reduce(
51+
(acc, key) => {
52+
acc[key] = daemon.tags[key];
53+
return acc;
54+
},
55+
{} as Record<string, string>,
56+
);
57+
const isWarning = warnings.length > 0;
58+
return (
59+
<div
60+
key={daemon.name}
61+
css={{
62+
borderRadius: 8,
63+
border: `1px solid ${
64+
isWarning
65+
? theme.palette.warning.light
66+
: theme.palette.divider
67+
}`,
68+
fontSize: 14,
69+
}}
70+
>
71+
<header
72+
css={{
73+
padding: 24,
74+
display: "flex",
75+
alignItems: "center",
76+
justifyContenxt: "space-between",
77+
gap: 24,
78+
}}
79+
>
80+
<div
81+
css={{
82+
display: "flex",
83+
alignItems: "center",
84+
gap: 24,
85+
objectFit: "fill",
86+
}}
87+
>
88+
<div css={{ lineHeight: "160%" }}>
89+
<h4 css={{ fontWeight: 500, margin: 0 }}>{daemon.name}</h4>
90+
<span css={{ color: theme.palette.text.secondary }}>
91+
<code>{daemon.version}</code>
92+
</span>
93+
</div>
94+
</div>
95+
<div
96+
css={{
97+
marginLeft: "auto",
98+
display: "flex",
99+
flexWrap: "wrap",
100+
gap: 12,
101+
}}
102+
>
103+
<Tooltip title="API Version">
104+
<Pill icon={<SwapHoriz />}>
105+
<code>{daemon.api_version}</code>
106+
</Pill>
107+
</Tooltip>
108+
<Tooltip title="Scope">
109+
<Pill icon={iconScope}>
110+
<span
111+
css={{
112+
":first-letter": { textTransform: "uppercase" },
113+
}}
114+
>
115+
{daemonScope}
116+
</span>
117+
</Pill>
118+
</Tooltip>
119+
{Object.keys(extraTags).map((k) => (
120+
<Tooltip key={k} title={k}>
121+
<Pill key={k} icon={<Sell />}>
122+
{extraTags[k]}
123+
</Pill>
124+
</Tooltip>
125+
))}
126+
</div>
127+
</header>
128+
129+
<div
130+
css={{
131+
borderTop: `1px solid ${theme.palette.divider}`,
132+
display: "flex",
133+
alignItems: "center",
134+
justifyContent: "space-between",
135+
padding: "8px 24px",
136+
fontSize: 12,
137+
color: theme.palette.text.secondary,
138+
}}
139+
>
140+
{warnings.length > 0 ? (
141+
<div css={{ display: "flex", flexDirection: "column" }}>
142+
{warnings.map((warning, i) => (
143+
<span key={i}>{warning.message}</span>
144+
))}
145+
</div>
146+
) : (
147+
<span>No warnings</span>
148+
)}
149+
{daemon.last_seen_at && (
150+
<span
151+
css={{ color: theme.palette.text.secondary }}
152+
data-chromatic="ignore"
153+
>
154+
Last seen {createDayString(daemon.last_seen_at)}
155+
</span>
156+
)}
157+
</div>
158+
</div>
159+
);
160+
})}
161+
</Main>
162+
</>
163+
);
164+
};
165+
166+
export default ProvisionerDaemonsPage;

site/src/testHelpers/entities.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3103,25 +3103,60 @@ export const MockHealth: TypesGen.HealthcheckReport = {
31033103
},
31043104
provisioner_daemons: {
31053105
severity: "ok",
3106-
warnings: [],
3106+
warnings: [
3107+
{
3108+
message: "Something is wrong!",
3109+
code: "EUNKNOWN",
3110+
},
3111+
{
3112+
message: "This is also bad.",
3113+
code: "EPD01",
3114+
},
3115+
],
31073116
dismissed: false,
31083117
items: [
31093118
{
31103119
provisioner_daemon: {
31113120
id: "e455b582-ac04-4323-9ad6-ab71301fa006",
31123121
created_at: "2024-01-04T15:53:03.21563Z",
31133122
last_seen_at: "2024-01-04T16:05:03.967551Z",
3114-
name: "vvuurrkk-2",
3115-
version: "v2.6.0-devel+965ad5e96",
3123+
name: "ok",
3124+
version: "v2.3.4-devel+abcd1234",
31163125
api_version: "1.0",
31173126
provisioners: ["echo", "terraform"],
31183127
tags: {
31193128
owner: "",
31203129
scope: "organization",
3130+
custom_tag_name: "custom_tag_value",
31213131
},
31223132
},
31233133
warnings: [],
31243134
},
3135+
{
3136+
provisioner_daemon: {
3137+
id: "e455b582-ac04-4323-9ad6-ab71301fa006",
3138+
created_at: "2024-01-04T15:53:03.21563Z",
3139+
last_seen_at: "2024-01-04T16:05:03.967551Z",
3140+
name: "unhappy",
3141+
version: "v0.0.1",
3142+
api_version: "0.1",
3143+
provisioners: ["echo", "terraform"],
3144+
tags: {
3145+
owner: "",
3146+
scope: "organization",
3147+
},
3148+
},
3149+
warnings: [
3150+
{
3151+
message: "Something specific is wrong with this daemon.",
3152+
code: "EUNKNOWN",
3153+
},
3154+
{
3155+
message: "And now for something completely different.",
3156+
code: "EUNKNOWN",
3157+
},
3158+
],
3159+
},
31253160
],
31263161
},
31273162
coder_version: "v2.5.0-devel+5fad61102",

0 commit comments

Comments
 (0)