From 9a872f903e7a1b1dde485b301b4ef4757f31eb7e Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 21 Aug 2025 20:22:25 -0400 Subject: [PATCH 1/3] feat: show workspace health error alert above agents in WorkspacePage (#19400) closes #19338 image --- .../pages/WorkspacePage/Workspace.stories.tsx | 18 +++++++++ site/src/pages/WorkspacePage/Workspace.tsx | 39 +++++++++++++++++++ .../WorkspaceNotifications.stories.tsx | 2 +- .../WorkspaceNotifications.tsx | 2 +- site/src/testHelpers/entities.ts | 23 +++++++++++ 5 files changed, 82 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index df07c59c1c660..5a49e0fa57091 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -9,6 +9,7 @@ import type { ProvisionerJobLog } from "api/typesGenerated"; import { action } from "storybook/actions"; import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { Workspace } from "./Workspace"; +import { defaultPermissions } from "./WorkspaceNotifications/WorkspaceNotifications.stories"; // Helper function to create timestamps easily - Copied from AppStatuses.stories.tsx const createTimestamp = ( @@ -349,6 +350,23 @@ export const Stopping: Story = { }, }; +export const Unhealthy: Story = { + args: { + ...Running.args, + workspace: Mocks.MockUnhealthyWorkspace, + }, +}; + +export const UnhealthyWithoutUpdatePermission: Story = { + args: { + ...Unhealthy.args, + permissions: { + ...defaultPermissions, + updateWorkspace: false, + }, + }, +}; + export const FailedWithLogs: Story = { args: { ...Running.args, diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index b81605dc239e9..b1eda1618038b 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -21,6 +21,8 @@ import { WorkspaceBuildProgress, } from "./WorkspaceBuildProgress"; import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; +import { NotificationActionButton } from "./WorkspaceNotifications/Notifications"; +import { findTroubleshootingURL } from "./WorkspaceNotifications/WorkspaceNotifications"; import { WorkspaceTopbar } from "./WorkspaceTopbar"; interface WorkspaceProps { @@ -97,6 +99,8 @@ export const Workspace: FC = ({ (workspace.latest_build.matched_provisioners?.available ?? 1) > 0; const shouldShowProvisionerAlert = workspacePending && !haveBuildLogs && !provisionersHealthy && !isRestarting; + const troubleshootingURL = findTroubleshootingURL(workspace.latest_build); + const hasActions = permissions.updateWorkspace || troubleshootingURL; return (
@@ -194,6 +198,41 @@ export const Workspace: FC = ({ )} + {!workspace.health.healthy && ( + + Workspace is unhealthy + +

+ Your workspace is running but{" "} + {workspace.health.failing_agents.length > 1 + ? `${workspace.health.failing_agents.length} agents are unhealthy` + : "1 agent is unhealthy"} + . +

+ {hasActions && ( +
+ {permissions.updateWorkspace && ( + handleRestart()} + > + Restart + + )} + {troubleshootingURL && ( + + window.open(troubleshootingURL, "_blank") + } + > + Troubleshooting + + )} +
+ )} +
+
+ )} + {transitionStats !== undefined && ( >; -const findTroubleshootingURL = ( +export const findTroubleshootingURL = ( workspaceBuild: WorkspaceBuild, ): string | undefined => { for (const resource of workspaceBuild.resources) { diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index c130c952185fd..993b012bc09e2 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -994,6 +994,15 @@ export const MockWorkspaceSubAgent: TypesGen.WorkspaceAgent = { ], }; +const MockWorkspaceUnhealthyAgent: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: "test-workspace-unhealthy-agent", + name: "a-workspace-unhealthy-agent", + status: "timeout", + lifecycle_state: "start_error", + health: { healthy: false }, +}; + export const MockWorkspaceAppStatus: TypesGen.WorkspaceAppStatus = { id: "test-app-status", created_at: "2022-05-17T17:39:01.382927298Z", @@ -1445,6 +1454,20 @@ export const MockStoppingWorkspace: TypesGen.Workspace = { status: "stopping", }, }; +export const MockUnhealthyWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: "test-unhealthy-workspace", + health: { + healthy: false, + failing_agents: [MockWorkspaceUnhealthyAgent.id], + }, + latest_build: { + ...MockWorkspace.latest_build, + resources: [ + { ...MockWorkspaceResource, agents: [MockWorkspaceUnhealthyAgent] }, + ], + }, +}; export const MockStartingWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-starting-workspace", From a71e5cc8b0bf05f96c65b5320973fe848f48f294 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:20:03 +1000 Subject: [PATCH 2/3] test: add increasing integer to GetRandomNameHyphenated (#19481) Fixes flakes like the following: ``` workspaces_test.go:4938: Error Trace: /home/runner/work/coder/coder/coderd/coderdtest/coderdtest.go:1279 /home/runner/work/coder/coder/coderd/workspaces_test.go:4938 /home/runner/work/coder/coder/coderd/workspaces_test.go:5044 Error: Received unexpected error: POST http://127.0.0.1:42597/api/v2/users/me/workspaces: unexpected status code 409: Workspace "romantic-mcclintock" already exists. name: This value is already in use and should be unique. Test: TestWorkspaceCreateWithImplicitPreset/SinglePresetWithParameters ``` https://github.com/coder/coder/actions/runs/17142665868/job/48633017007?pr=19464 Which are caused by insufficient randomness when creating multiple workspaces with random names. Two words is not enough to avoid flakes. We have a `testutil.GetRandomName` function that appends a monotonically increasing integer, but this alternative function that uses hyphens doesn't add that integer. This PR fixes that by just `testutil.GetRandomName` --- testutil/names.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutil/names.go b/testutil/names.go index e53e854fae239..bb804ba2cf400 100644 --- a/testutil/names.go +++ b/testutil/names.go @@ -30,7 +30,7 @@ func GetRandomName(t testing.TB) string { // an underscore. func GetRandomNameHyphenated(t testing.TB) string { t.Helper() - name := namesgenerator.GetRandomName(0) + name := GetRandomName(t) return strings.ReplaceAll(name, "_", "-") } From b90bc7c398d7d878d537011896f345afbc162faa Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 22 Aug 2025 07:41:49 +0200 Subject: [PATCH 3/3] feat: use cloud secret for DNS token in scaletest TF (#19466) Removes the requirement to obtain a Cloudflare DNS token from our scaletest/terraform/action builds. Instead, by default, we pull the token from Google Secrets Manager and use the `scaletest.dev` DNS domain. Removes cloudflare_email as this was unneeded. Removes the cloudflare_zone_id and instead pulls it from a data source via the Cloudflare API. closes https://github.com/coder/internal/issues/839 --- scaletest/terraform/action/cf_dns.tf | 6 +++++- scaletest/terraform/action/main.tf | 7 ++++++- scaletest/terraform/action/vars.tf | 14 +++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/scaletest/terraform/action/cf_dns.tf b/scaletest/terraform/action/cf_dns.tf index eaaff28ce03a0..664b909ae90b2 100644 --- a/scaletest/terraform/action/cf_dns.tf +++ b/scaletest/terraform/action/cf_dns.tf @@ -1,6 +1,10 @@ +data "cloudflare_zone" "domain" { + name = var.cloudflare_domain +} + resource "cloudflare_record" "coder" { for_each = local.deployments - zone_id = var.cloudflare_zone_id + zone_id = data.cloudflare_zone.domain.zone_id name = each.value.subdomain content = google_compute_address.coder[each.key].address type = "A" diff --git a/scaletest/terraform/action/main.tf b/scaletest/terraform/action/main.tf index c5e22ff9f03ad..cd26c7ec1ccd2 100644 --- a/scaletest/terraform/action/main.tf +++ b/scaletest/terraform/action/main.tf @@ -46,8 +46,13 @@ terraform { provider "google" { } +data "google_secret_manager_secret_version_access" "cloudflare_api_token_dns" { + secret = "cloudflare-api-token-dns" + project = var.project_id +} + provider "cloudflare" { - api_token = var.cloudflare_api_token + api_token = coalesce(var.cloudflare_api_token, data.google_secret_manager_secret_version_access.cloudflare_api_token_dns.secret_data) } provider "kubernetes" { diff --git a/scaletest/terraform/action/vars.tf b/scaletest/terraform/action/vars.tf index 6788e843d8b6f..3952baab82b80 100644 --- a/scaletest/terraform/action/vars.tf +++ b/scaletest/terraform/action/vars.tf @@ -13,6 +13,7 @@ variable "scenario" { // GCP variable "project_id" { description = "The project in which to provision resources" + default = "coder-scaletest" } variable "k8s_version" { @@ -24,19 +25,14 @@ variable "k8s_version" { variable "cloudflare_api_token" { description = "Cloudflare API token." sensitive = true -} - -variable "cloudflare_email" { - description = "Cloudflare email address." - sensitive = true + # only override if you want to change the cloudflare_domain; pulls the token for scaletest.dev from Google Secrets + # Manager if null. + default = null } variable "cloudflare_domain" { description = "Cloudflare coder domain." -} - -variable "cloudflare_zone_id" { - description = "Cloudflare zone ID." + default = "scaletest.dev" } // Coder