feat: show agent version in UI and CLI (#3709)

This commit adds the ability for agents to set their version upon start.
This is then reported in the UI and CLI.
This commit is contained in:
Cian Johnston
2022-08-31 16:33:50 +01:00
committed by GitHub
parent aa9a1c3f56
commit 5362f4636e
30 changed files with 366 additions and 26 deletions
+2
View File
@@ -81,6 +81,7 @@
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/react-helmet": "6.1.5",
"@types/semver": "^7.3.12",
"@types/superagent": "4.1.15",
"@types/uuid": "8.3.4",
"@typescript-eslint/eslint-plugin": "5.31.0",
@@ -112,6 +113,7 @@
"prettier": "2.7.1",
"prettier-plugin-organize-imports": "3.0.0",
"react-hot-loader": "4.13.0",
"semver": "^7.3.7",
"sql-formatter": "8.2.0",
"style-loader": "3.3.1",
"ts-jest": "27.1.4",
+6
View File
@@ -249,6 +249,11 @@ export interface ParameterSchema {
readonly validation_contains?: string[]
}
// From codersdk/workspaceagents.go
export interface PostWorkspaceAgentVersionRequest {
readonly version: string
}
// From codersdk/provisionerdaemons.go
export interface ProvisionerDaemon {
readonly id: string
@@ -479,6 +484,7 @@ export interface WorkspaceAgent {
// Named type "inet.af/netaddr.IPPrefix" unknown, using "any"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly ipv6: any
readonly version: string
}
// From codersdk/workspaceagents.go
+20 -2
View File
@@ -9,14 +9,15 @@ import useTheme from "@material-ui/styles/useTheme"
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
import { FC } from "react"
import { getDisplayAgentStatus } from "util/workspace"
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace"
import { BuildInfoResponse, Workspace, WorkspaceResource } from "../../api/typesGenerated"
import { AppLink } from "../AppLink/AppLink"
import { SSHButton } from "../SSHButton/SSHButton"
import { Stack } from "../Stack/Stack"
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
import { TerminalLink } from "../TerminalLink/TerminalLink"
import { AgentHelpTooltip } from "../Tooltips/AgentHelpTooltip"
import { AgentOutdatedTooltip } from "../Tooltips/AgentOutdatedTooltip"
import { ResourcesHelpTooltip } from "../Tooltips/ResourcesHelpTooltip"
import { ResourceAvatarData } from "./ResourceAvatarData"
@@ -26,6 +27,7 @@ const Language = {
agentsLabel: "Agents",
agentLabel: "Agent",
statusLabel: "status: ",
versionLabel: "version: ",
osLabel: "os: ",
}
@@ -34,6 +36,7 @@ interface ResourcesProps {
getResourcesError?: Error | unknown
workspace: Workspace
canUpdateWorkspace: boolean
buildInfo?: BuildInfoResponse | undefined
}
export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
@@ -41,9 +44,11 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
getResourcesError,
workspace,
canUpdateWorkspace,
buildInfo,
}) => {
const styles = useStyles()
const theme: Theme = useTheme()
const serverVersion = buildInfo?.version || ""
return (
<div aria-label={Language.resources} className={styles.wrapper}>
@@ -90,6 +95,10 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
)
}
const { displayVersion, outdated } = getDisplayVersionStatus(
agent.version,
serverVersion,
)
const agentStatus = getDisplayAgentStatus(theme, agent)
return (
<TableRow key={`${resource.id}-${agent.id}`}>
@@ -114,6 +123,11 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
<strong>{Language.osLabel}</strong>
<span className={styles.operatingSystem}>{agent.operating_system}</span>
</div>
<div className={styles.dataRow}>
<strong>{Language.versionLabel}</strong>
<span className={styles.agentVersion}>{displayVersion}</span>
<AgentOutdatedTooltip outdated={outdated} />
</div>
</div>
</TableCell>
<TableCell>
@@ -188,6 +202,10 @@ const useStyles = makeStyles((theme) => ({
textTransform: "capitalize",
},
agentVersion: {
display: "block",
},
accessLinks: {
display: "flex",
gap: theme.spacing(0.5),
@@ -0,0 +1,23 @@
import { FC } from "react"
import { HelpTooltip, HelpTooltipText, HelpTooltipTitle } from "./HelpTooltip"
export const Language = {
label: "Agent Outdated",
text: "This agent is an older version than the Coder server. This can happen after you update Coder with running workspaces. To fix this, you can stop and start the workspace.",
}
interface TooltipProps {
outdated: boolean
}
export const AgentOutdatedTooltip: FC<React.PropsWithChildren<TooltipProps>> = ({ outdated }) => {
if (!outdated) {
return null
}
return (
<HelpTooltip size="small">
<HelpTooltipTitle>{Language.label}</HelpTooltipTitle>
<HelpTooltipText>{Language.text}</HelpTooltipText>
</HelpTooltip>
)
}
@@ -44,6 +44,7 @@ Started.args = {
builds: [Mocks.MockWorkspaceBuild],
canUpdateWorkspace: true,
workspaceErrors: {},
buildInfo: Mocks.MockBuildInfo,
}
export const WithoutUpdateAccess = Template.bind({})
@@ -44,6 +44,7 @@ export interface WorkspaceProps {
builds?: TypesGen.WorkspaceBuild[]
canUpdateWorkspace: boolean
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
buildInfo?: TypesGen.BuildInfoResponse
}
/**
@@ -62,6 +63,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
builds,
canUpdateWorkspace,
workspaceErrors,
buildInfo,
}) => {
const styles = useStyles()
const navigate = useNavigate()
@@ -128,6 +130,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
workspace={workspace}
canUpdateWorkspace={canUpdateWorkspace}
buildInfo={buildInfo}
/>
)}
@@ -204,7 +204,7 @@ describe("WorkspacePage", () => {
const agent1Status = await screen.findAllByText(
DisplayAgentStatusLanguage[MockWorkspaceAgent.status],
)
expect(agent1Status.length).toEqual(2)
expect(agent1Status.length).toEqual(4)
const agent2Status = await screen.findAllByText(
DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status],
)
@@ -1,5 +1,5 @@
import { makeStyles } from "@material-ui/core/styles"
import { useMachine, useSelector } from "@xstate/react"
import { useActor, useMachine, useSelector } from "@xstate/react"
import dayjs from "dayjs"
import minMax from "dayjs/plugin/minMax"
import { FC, useContext, useEffect } from "react"
@@ -51,6 +51,7 @@ export const WorkspacePage: FC = () => {
const canUpdateWorkspace = !!permissions?.updateWorkspace
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
const [buildInfoState] = useActor(xServices.buildInfoXService)
const styles = useStyles()
@@ -133,6 +134,7 @@ export const WorkspacePage: FC = () => {
[WorkspaceErrors.BUILD_ERROR]: buildError,
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
}}
buildInfo={buildInfoState.context.buildInfo}
/>
<DeleteWorkspaceDialog
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
+11 -1
View File
@@ -305,6 +305,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
wireguard_public_key: "",
disco_public_key: "",
ipv6: "",
version: MockBuildInfo.version,
}
export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
@@ -312,10 +313,19 @@ export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
id: "test-workspace-agent-2",
name: "another-workspace-agent",
status: "disconnected",
version: "",
}
export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = {
...MockWorkspaceAgent,
id: "test-workspace-agent-3",
name: "an-outdated-workspace-agent",
version: "v99.999.9998+abcdef",
operating_system: "Windows",
}
export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
agents: [MockWorkspaceAgent, MockWorkspaceAgentDisconnected],
agents: [MockWorkspaceAgent, MockWorkspaceAgentDisconnected, MockWorkspaceAgentOutdated],
created_at: "",
id: "test-workspace-resource",
job_id: "",
+20
View File
@@ -3,6 +3,7 @@ import * as TypesGen from "../api/typesGenerated"
import * as Mocks from "../testHelpers/entities"
import {
defaultWorkspaceExtension,
getDisplayVersionStatus,
getDisplayWorkspaceBuildInitiatedBy,
isWorkspaceDeleted,
isWorkspaceOn,
@@ -128,4 +129,23 @@ describe("util > workspace", () => {
expect(getDisplayWorkspaceBuildInitiatedBy(build)).toEqual(initiatedBy)
})
})
describe("getDisplayVersionStatus", () => {
it.each<[string, string, string, boolean]>([
["", "", "(unknown)", false],
["", "v1.2.3", "(unknown)", false],
["v1.2.3", "", "v1.2.3", false],
["v1.2.3", "v1.2.3", "v1.2.3", false],
["v1.2.3", "v1.2.4", "v1.2.3 (outdated)", true],
["v1.2.4", "v1.2.3", "v1.2.4", false],
["foo", "bar", "foo", false],
])(
`getDisplayVersionStatus(theme, %p, %p) returns (%p, %p)`,
(agentVersion, serverVersion, expectedVersion, expectedOutdated) => {
const { displayVersion, outdated } = getDisplayVersionStatus(agentVersion, serverVersion)
expect(displayVersion).toEqual(expectedVersion)
expect(expectedOutdated).toEqual(outdated)
},
)
})
})
+28
View File
@@ -3,6 +3,7 @@ import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
import minMax from "dayjs/plugin/minMax"
import utc from "dayjs/plugin/utc"
import semver from "semver"
import { WorkspaceBuildTransition } from "../api/types"
import * as TypesGen from "../api/typesGenerated"
@@ -95,6 +96,11 @@ export const DisplayWorkspaceBuildStatusLanguage = {
failed: "Failed",
}
export const DisplayAgentVersionLanguage = {
unknown: "unknown",
outdated: "outdated",
}
export const getDisplayWorkspaceBuildStatus = (
theme: Theme,
build: TypesGen.WorkspaceBuild,
@@ -211,6 +217,28 @@ export const getDisplayAgentStatus = (
}
}
export const getDisplayVersionStatus = (
agentVersion: string,
serverVersion: string,
): { displayVersion: string; outdated: boolean } => {
if (!semver.valid(serverVersion) || !semver.valid(agentVersion)) {
return {
displayVersion: `${agentVersion}` || `(${DisplayAgentVersionLanguage.unknown})`,
outdated: false,
}
} else if (semver.lt(agentVersion, serverVersion)) {
return {
displayVersion: `${agentVersion} (${DisplayAgentVersionLanguage.outdated})`,
outdated: true,
}
} else {
return {
displayVersion: agentVersion,
outdated: false,
}
}
}
export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {
const transition = workspace.latest_build.transition
const status = workspace.latest_build.job.status
+5
View File
@@ -3330,6 +3330,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/semver@^7.3.12":
version "7.3.12"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c"
integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==
"@types/serve-index@^1.9.1":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"