diff --git a/codersdk/health.go b/codersdk/health.go index a53ca73192..a54b65762e 100644 --- a/codersdk/health.go +++ b/codersdk/health.go @@ -26,6 +26,7 @@ var HealthSections = []HealthSection{ HealthSectionWebsocket, HealthSectionDatabase, HealthSectionWorkspaceProxy, + HealthSectionProvisionerDaemons, } type HealthSettings struct { diff --git a/docs/admin/healthcheck.md b/docs/admin/healthcheck.md index a85d6f50ec..62a7de6197 100644 --- a/docs/admin/healthcheck.md +++ b/docs/admin/healthcheck.md @@ -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_ diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 5d19dd3b88..245e263586 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -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={} /> + } + /> {/* Using path="*"" means "match anything", so this route acts like a catch-all for URLs that we don't have explicit diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx index 0b80dc01b4..0aef42e457 100644 --- a/site/src/pages/HealthPage/HealthLayout.tsx +++ b/site/src/pages/HealthPage/HealthLayout.tsx @@ -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); diff --git a/site/src/pages/HealthPage/ProvisionerDaemonsPage.stories.tsx b/site/src/pages/HealthPage/ProvisionerDaemonsPage.stories.tsx new file mode 100644 index 0000000000..7117aa8869 --- /dev/null +++ b/site/src/pages/HealthPage/ProvisionerDaemonsPage.stories.tsx @@ -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: , + }), +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx b/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx new file mode 100644 index 0000000000..6c797b9815 --- /dev/null +++ b/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx @@ -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(); + const { provisioner_daemons: daemons } = healthStatus; + const theme = useTheme(); + return ( + <> + + {pageTitle("Provisioner Daemons - Health")} + + +
+ + + Provisioner Daemons + + +
+ +
+ {daemons.warnings.map((warning) => { + return ( + + {warning.message} + + ); + })} + + {daemons.items.map(({ provisioner_daemon: daemon, warnings }) => { + const daemonScope = daemon.tags["scope"] || "organization"; + const iconScope = + daemonScope === "organization" ? : ; + const extraTags = Object.keys(daemon.tags) + .filter((key) => key !== "scope" && key !== "owner") + .reduce( + (acc, key) => { + acc[key] = daemon.tags[key]; + return acc; + }, + {} as Record, + ); + const isWarning = warnings.length > 0; + return ( +
+
+
+
+

{daemon.name}

+ + {daemon.version} + +
+
+
+ + }> + {daemon.api_version} + + + + + + {daemonScope} + + + + {Object.keys(extraTags).map((k) => ( + + }> + {extraTags[k]} + + + ))} +
+
+ +
+ {warnings.length > 0 ? ( +
+ {warnings.map((warning, i) => ( + {warning.message} + ))} +
+ ) : ( + No warnings + )} + {daemon.last_seen_at && ( + + Last seen {createDayString(daemon.last_seen_at)} + + )} +
+
+ ); + })} +
+ + ); +}; + +export default ProvisionerDaemonsPage; diff --git a/site/src/pages/HealthPage/WebsocketProxyPage.stories.tsx b/site/src/pages/HealthPage/WorkspaceProxyPage.stories.tsx similarity index 100% rename from site/src/pages/HealthPage/WebsocketProxyPage.stories.tsx rename to site/src/pages/HealthPage/WorkspaceProxyPage.stories.tsx diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index dc2d107802..1f303f4ee8 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -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", + }, + ], }, ], },