From 60489f9b812dc89edc80635c1264e4376baf577d Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 30 Jun 2023 16:11:35 +0000 Subject: [PATCH 1/5] feat: add computed workspace and agent health fields to the api --- coderd/apidoc/docs.go | 52 ++++++++++++ coderd/apidoc/swagger.json | 52 ++++++++++++ coderd/workspaceagents.go | 18 ++++ coderd/workspaceagents_test.go | 3 + coderd/workspaces.go | 11 +++ coderd/workspaces_test.go | 1 + codersdk/workspaceagents.go | 14 +++- codersdk/workspaces.go | 24 ++++++ docs/api/agents.md | 4 + docs/api/builds.md | 30 +++++++ docs/api/schemas.md | 136 ++++++++++++++++++++++++++----- docs/api/templates.md | 14 ++++ docs/api/workspaces.md | 72 ++++++++++++++++ site/src/api/typesGenerated.ts | 15 ++++ site/src/testHelpers/entities.ts | 38 +++++++++ 15 files changed, 459 insertions(+), 25 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ebf357593f9cb..60f58610f29ed 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9440,6 +9440,14 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, + "health": { + "description": "Health reports the health of the workspace and its agents.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceHealth" + } + ] + }, "id": { "type": "string", "format": "uuid" @@ -9537,6 +9545,14 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, + "health": { + "description": "Health reports the health of the agent.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentHealth" + } + ] + }, "id": { "type": "string", "format": "uuid" @@ -9631,6 +9647,19 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceAgentHealth": { + "type": "object", + "properties": { + "healthy": { + "description": "Healthy is true if the agent is healthy.", + "type": "boolean" + }, + "reason": { + "description": "Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true.", + "type": "string" + } + } + }, "codersdk.WorkspaceAgentLifecycle": { "type": "string", "enum": [ @@ -9997,6 +10026,29 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceHealth": { + "type": "object", + "properties": { + "agents": { + "description": "Agents is a map of agent IDs to their health.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/codersdk.WorkspaceAgentHealth" + } + }, + "failing_sections": { + "description": "FailingSections is a list of sections that have failed their healthcheck.", + "type": "array", + "items": { + "type": "string" + } + }, + "healthy": { + "description": "Healthy is true if the workspace and all of its agents are healthy.", + "type": "boolean" + } + } + }, "codersdk.WorkspaceProxy": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index d23abeff269b4..6601c02d145f3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8516,6 +8516,14 @@ "type": "string", "format": "date-time" }, + "health": { + "description": "Health reports the health of the workspace and its agents.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceHealth" + } + ] + }, "id": { "type": "string", "format": "uuid" @@ -8613,6 +8621,14 @@ "type": "string", "format": "date-time" }, + "health": { + "description": "Health reports the health of the agent.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceAgentHealth" + } + ] + }, "id": { "type": "string", "format": "uuid" @@ -8707,6 +8723,19 @@ } } }, + "codersdk.WorkspaceAgentHealth": { + "type": "object", + "properties": { + "healthy": { + "description": "Healthy is true if the agent is healthy.", + "type": "boolean" + }, + "reason": { + "description": "Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true.", + "type": "string" + } + } + }, "codersdk.WorkspaceAgentLifecycle": { "type": "string", "enum": [ @@ -9044,6 +9073,29 @@ } } }, + "codersdk.WorkspaceHealth": { + "type": "object", + "properties": { + "agents": { + "description": "Agents is a map of agent IDs to their health.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/codersdk.WorkspaceAgentHealth" + } + }, + "failing_sections": { + "description": "FailingSections is a list of sections that have failed their healthcheck.", + "type": "array", + "items": { + "type": "string" + } + }, + "healthy": { + "description": "Healthy is true if the workspace and all of its agents are healthy.", + "type": "boolean" + } + } + }, "codersdk.WorkspaceProxy": { "type": "object", "properties": { diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 4ae04cba22b72..bfe61b4a180df 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -1262,6 +1262,24 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin workspaceAgent.ReadyAt = &dbAgent.ReadyAt.Time } + switch { + case workspaceAgent.Status != codersdk.WorkspaceAgentConnected && workspaceAgent.LifecycleState == codersdk.WorkspaceAgentLifecycleOff: + workspaceAgent.Health.Reason = "agent is not running" + case workspaceAgent.Status == codersdk.WorkspaceAgentTimeout: + workspaceAgent.Health.Reason = "agent is taking too long to connect" + case workspaceAgent.Status == codersdk.WorkspaceAgentDisconnected: + workspaceAgent.Health.Reason = "agent has lost connection" + // Note: We could also handle codersdk.WorkspaceAgentLifecycleStartTimeout + // here, but it's more of a soft issue, so we don't want to mark the agent + // as unhealthy. + case workspaceAgent.LifecycleState == codersdk.WorkspaceAgentLifecycleStartError: + workspaceAgent.Health.Reason = "agent startup script exited with an error" + case workspaceAgent.LifecycleState.ShuttingDown(): + workspaceAgent.Health.Reason = "agent is shutting down" + default: + workspaceAgent.Health.Healthy = true + } + return workspaceAgent, nil } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 719789e33717e..95d75d16810c8 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -72,6 +72,7 @@ func TestWorkspaceAgent(t *testing.T) { require.Equal(t, tmpDir, workspace.LatestBuild.Resources[0].Agents[0].Directory) _, err = client.WorkspaceAgent(ctx, workspace.LatestBuild.Resources[0].Agents[0].ID) require.NoError(t, err) + require.True(t, workspace.LatestBuild.Resources[0].Agents[0].Health.Healthy) }) t.Run("HasFallbackTroubleshootingURL", func(t *testing.T) { t.Parallel() @@ -167,6 +168,8 @@ func TestWorkspaceAgent(t *testing.T) { }, testutil.IntervalMedium, "agent status timeout") require.Equal(t, wantTroubleshootingURL, workspace.LatestBuild.Resources[0].Agents[0].TroubleshootingURL) + require.False(t, workspace.LatestBuild.Resources[0].Agents[0].Health.Healthy) + require.NotEmpty(t, workspace.LatestBuild.Resources[0].Agents[0].Health.Reason) }) } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 0910a1d62f0c0..f5bf5c60911ce 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1115,6 +1115,16 @@ func convertWorkspace( deletingAt = calculateDeletingAt(workspace, template, workspaceBuild) ) + agentHealth := make(map[uuid.UUID]codersdk.WorkspaceAgentHealth) + for _, r := range workspaceBuild.Resources { + // For now, we only consider agent healths when the workspace is running. + if r.Transition == codersdk.WorkspaceTransitionStart { + for _, a := range r.Agents { + agentHealth[a.ID] = a.Health + } + } + } + return codersdk.Workspace{ ID: workspace.ID, CreatedAt: workspace.CreatedAt, @@ -1135,6 +1145,7 @@ func convertWorkspace( LastUsedAt: workspace.LastUsedAt, DeletingAt: deletingAt, LockedAt: lockedAt, + Health: (codersdk.WorkspaceHealth{Agents: agentHealth}).Complete(), } } diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 0df6a05808c99..c57dfc299fe34 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -60,6 +60,7 @@ func TestWorkspace(t *testing.T) { require.NoError(t, err) require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID) require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason) + require.True(t, ws.Health.Healthy) }) t.Run("Deleted", func(t *testing.T) { diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 1b6c4d76d3a2b..767960267bb4e 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -164,10 +164,16 @@ type WorkspaceAgent struct { ConnectionTimeoutSeconds int32 `json:"connection_timeout_seconds"` TroubleshootingURL string `json:"troubleshooting_url"` // Deprecated: Use StartupScriptBehavior instead. - LoginBeforeReady bool `json:"login_before_ready"` - ShutdownScript string `json:"shutdown_script,omitempty"` - ShutdownScriptTimeoutSeconds int32 `json:"shutdown_script_timeout_seconds"` - Subsystem AgentSubsystem `json:"subsystem"` + LoginBeforeReady bool `json:"login_before_ready"` + ShutdownScript string `json:"shutdown_script,omitempty"` + ShutdownScriptTimeoutSeconds int32 `json:"shutdown_script_timeout_seconds"` + Subsystem AgentSubsystem `json:"subsystem"` + Health WorkspaceAgentHealth `json:"health"` // Health reports the health of the agent. +} + +type WorkspaceAgentHealth struct { + Healthy bool `json:"healthy"` // Healthy is true if the agent is healthy. + Reason string `json:"reason,omitempty"` // Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. } type DERPRegion struct { diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index c076e9e86b6a2..788940986eca3 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -43,6 +43,30 @@ type Workspace struct { // unlocked by an admin. It is subject to deletion if it breaches // the duration of the locked_ttl field on its template. LockedAt *time.Time `json:"locked_at" format:"date-time"` + // Health reports the health of the workspace and its agents. + Health WorkspaceHealth `json:"health"` +} + +type WorkspaceHealth struct { + Healthy bool `json:"healthy"` // Healthy is true if the workspace and all of its agents are healthy. + FailingSections []string `json:"failing_sections"` // FailingSections is a list of sections that have failed their healthcheck. + Agents map[uuid.UUID]WorkspaceAgentHealth `json:"agents"` // Agents is a map of agent IDs to their health. +} + +// Complete returns a new copy with the Healthy flag and FailingSections +// set based on the agent healths. +// +//nolint:revive +func (wh WorkspaceHealth) Complete() WorkspaceHealth { + wh.Healthy = true + wh.FailingSections = []string{} + for id, agent := range wh.Agents { + if !agent.Healthy { + wh.Healthy = false + wh.FailingSections = append(wh.FailingSections, "agents."+id.String()) + } + } + return wh } type WorkspacesRequest struct { diff --git a/docs/api/agents.md b/docs/api/agents.md index b8c73c8ceae95..7e1b5d52201b7 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -450,6 +450,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \ }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", diff --git a/docs/api/builds.md b/docs/api/builds.md index e3b3aa2437327..f5434885effe6 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -88,6 +88,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -246,6 +250,10 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -543,6 +551,10 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -635,6 +647,9 @@ Status Code **200** | `»»» [any property]` | string | false | | | | `»» expanded_directory` | string | false | | | | `»» first_connected_at` | string(date-time) | false | | | +| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | | `»» id` | string(uuid) | false | | | | `»» instance_id` | string | false | | | | `»» last_connected_at` | string(date-time) | false | | | @@ -794,6 +809,10 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -957,6 +976,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -1085,6 +1108,9 @@ Status Code **200** | `»»»» [any property]` | string | false | | | | `»»» expanded_directory` | string | false | | | | `»»» first_connected_at` | string(date-time) | false | | | +| `»»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `»»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `»»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | | `»»» id` | string(uuid) | false | | | | `»»» instance_id` | string | false | | | | `»»» last_connected_at` | string(date-time) | false | | | @@ -1298,6 +1324,10 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 3a3fcc95515d1..2c895dc329070 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -4596,6 +4596,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "agents": { + "property1": { + "healthy": true, + "reason": "string" + }, + "property2": { + "healthy": true, + "reason": "string" + } + }, + "failing_sections": ["string"], + "healthy": true + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -4660,6 +4674,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -4740,27 +4758,28 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------------------------------- | -------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `autostart_schedule` | string | false | | | -| `created_at` | string | false | | | -| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. | -| `id` | string | false | | | -| `last_used_at` | string | false | | | -| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | -| `locked_at` | string | false | | Locked at being non-nil indicates a workspace that has been locked. A locked workspace is no longer accessible by a user and must be unlocked by an admin. It is subject to deletion if it breaches the duration of the locked_ttl field on its template. | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `outdated` | boolean | false | | | -| `owner_id` | string | false | | | -| `owner_name` | string | false | | | -| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | -| `template_display_name` | string | false | | | -| `template_icon` | string | false | | | -| `template_id` | string | false | | | -| `template_name` | string | false | | | -| `ttl_ms` | integer | false | | | -| `updated_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------------- | ---------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `autostart_schedule` | string | false | | | +| `created_at` | string | false | | | +| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. | +| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health reports the health of the workspace and its agents. | +| `id` | string | false | | | +| `last_used_at` | string | false | | | +| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | +| `locked_at` | string | false | | Locked at being non-nil indicates a workspace that has been locked. A locked workspace is no longer accessible by a user and must be unlocked by an admin. It is subject to deletion if it breaches the duration of the locked_ttl field on its template. | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `outdated` | boolean | false | | | +| `owner_id` | string | false | | | +| `owner_name` | string | false | | | +| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | +| `template_display_name` | string | false | | | +| `template_icon` | string | false | | | +| `template_id` | string | false | | | +| `template_name` | string | false | | | +| `ttl_ms` | integer | false | | | +| `updated_at` | string | false | | | ## codersdk.WorkspaceAgent @@ -4796,6 +4815,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -4845,6 +4868,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | » `[any property]` | string | false | | | | `expanded_directory` | string | false | | | | `first_connected_at` | string | false | | | +| `health` | [codersdk.WorkspaceAgentHealth](#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | | `id` | string | false | | | | `instance_id` | string | false | | | | `last_connected_at` | string | false | | | @@ -4936,6 +4960,22 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `derp_map` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | | `disable_direct_connections` | boolean | false | | | +## codersdk.WorkspaceAgentHealth + +```json +{ + "healthy": true, + "reason": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| --------- | ------- | -------- | ------------ | --------------------------------------------------------------------------------------------- | +| `healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | + ## codersdk.WorkspaceAgentLifecycle ```json @@ -5214,6 +5254,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -5387,6 +5431,34 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `stopped` | integer | false | | | | `tx_bytes` | integer | false | | | +## codersdk.WorkspaceHealth + +```json +{ + "agents": { + "property1": { + "healthy": true, + "reason": "string" + }, + "property2": { + "healthy": true, + "reason": "string" + } + }, + "failing_sections": ["string"], + "healthy": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------------------ | -------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------- | +| `agents` | object | false | | Agents is a map of agent IDs to their health. | +| » `[any property]` | [codersdk.WorkspaceAgentHealth](#codersdkworkspaceagenthealth) | false | | | +| `failing_sections` | array of string | false | | Failing sections is a list of sections that have failed their healthcheck. | +| `healthy` | boolean | false | | Healthy is true if the workspace and all of its agents are healthy. | + ## codersdk.WorkspaceProxy ```json @@ -5499,6 +5571,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -5643,6 +5719,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "agents": { + "property1": { + "healthy": true, + "reason": "string" + }, + "property2": { + "healthy": true, + "reason": "string" + } + }, + "failing_sections": ["string"], + "healthy": true + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -5703,6 +5793,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", diff --git a/docs/api/templates.md b/docs/api/templates.md index 8a65ff8e45b08..a70a276bd0249 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -1659,6 +1659,10 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -1751,6 +1755,9 @@ Status Code **200** | `»»» [any property]` | string | false | | | | `»» expanded_directory` | string | false | | | | `»» first_connected_at` | string(date-time) | false | | | +| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | | `»» id` | string(uuid) | false | | | | `»» instance_id` | string | false | | | | `»» last_connected_at` | string(date-time) | false | | | @@ -2044,6 +2051,10 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -2136,6 +2147,9 @@ Status Code **200** | `»»» [any property]` | string | false | | | | `»» expanded_directory` | string | false | | | | `»» first_connected_at` | string(date-time) | false | | | +| `»» health` | [codersdk.WorkspaceAgentHealth](schemas.md#codersdkworkspaceagenthealth) | false | | Health reports the health of the agent. | +| `»»» healthy` | boolean | false | | Healthy is true if the agent is healthy. | +| `»»» reason` | string | false | | Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. | | `»» id` | string(uuid) | false | | | | `»» instance_id` | string | false | | | | `»» last_connected_at` | string(date-time) | false | | | diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 4a9b6b79be138..76c558dba615d 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -48,6 +48,20 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "agents": { + "property1": { + "healthy": true, + "reason": "string" + }, + "property2": { + "healthy": true, + "reason": "string" + } + }, + "failing_sections": ["string"], + "healthy": true + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -112,6 +126,10 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -228,6 +246,20 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "agents": { + "property1": { + "healthy": true, + "reason": "string" + }, + "property2": { + "healthy": true, + "reason": "string" + } + }, + "failing_sections": ["string"], + "healthy": true + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -292,6 +324,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -432,6 +468,20 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "agents": { + "property1": { + "healthy": true, + "reason": "string" + }, + "property2": { + "healthy": true, + "reason": "string" + } + }, + "failing_sections": ["string"], + "healthy": true + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -492,6 +542,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", @@ -609,6 +663,20 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "agents": { + "property1": { + "healthy": true, + "reason": "string" + }, + "property2": { + "healthy": true, + "reason": "string" + } + }, + "failing_sections": ["string"], + "healthy": true + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -673,6 +741,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ }, "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", + "health": { + "healthy": true, + "reason": "string" + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", "last_connected_at": "2019-08-24T14:15:22Z", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index eeca691874616..58c2326a4de71 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1126,6 +1126,7 @@ export interface Workspace { readonly last_used_at: string readonly deleting_at?: string readonly locked_at?: string + readonly health: WorkspaceHealth } // From codersdk/workspaceagents.go @@ -1162,6 +1163,13 @@ export interface WorkspaceAgent { readonly shutdown_script?: string readonly shutdown_script_timeout_seconds: number readonly subsystem: AgentSubsystem + readonly health: WorkspaceAgentHealth +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentHealth { + readonly healthy: boolean + readonly reason?: string } // From codersdk/workspaceagentconn.go @@ -1281,6 +1289,13 @@ export interface WorkspaceFilter { readonly q?: string } +// From codersdk/workspaces.go +export interface WorkspaceHealth { + readonly healthy: boolean + readonly failing_sections: string[] + readonly agents: Record +} + // From codersdk/workspaces.go export interface WorkspaceOptions { readonly include_deleted?: boolean diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index e522c20374fb5..57ef63336d4aa 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -510,6 +510,9 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = { startup_script_timeout_seconds: 120, shutdown_script_timeout_seconds: 120, subsystem: "envbox", + health: { + healthy: true, + }, } export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = { @@ -520,6 +523,10 @@ export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = { version: "", latency: {}, lifecycle_state: "ready", + health: { + healthy: false, + reason: "agent is not connected", + }, } export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = { @@ -564,6 +571,10 @@ export const MockWorkspaceAgentTimeout: TypesGen.WorkspaceAgent = { version: "", latency: {}, lifecycle_state: "created", + health: { + healthy: false, + reason: "agent is taking too long to connect", + }, } export const MockWorkspaceAgentStarting: TypesGen.WorkspaceAgent = { @@ -592,6 +603,10 @@ export const MockWorkspaceAgentStartError: TypesGen.WorkspaceAgent = { id: "test-workspace-agent-start-error", name: "a-workspace-agent-errored-while-running-startup-script", lifecycle_state: "start_error", + health: { + healthy: false, + reason: "agent startup script failed", + }, } export const MockWorkspaceAgentShuttingDown: TypesGen.WorkspaceAgent = { @@ -599,6 +614,10 @@ export const MockWorkspaceAgentShuttingDown: TypesGen.WorkspaceAgent = { id: "test-workspace-agent-shutting-down", name: "a-shutting-down-workspace-agent", lifecycle_state: "shutting_down", + health: { + healthy: false, + reason: "agent is shutting down", + }, } export const MockWorkspaceAgentShutdownTimeout: TypesGen.WorkspaceAgent = { @@ -606,6 +625,10 @@ export const MockWorkspaceAgentShutdownTimeout: TypesGen.WorkspaceAgent = { id: "test-workspace-agent-shutdown-timeout", name: "a-workspace-agent-timed-out-while-running-shutdownup-script", lifecycle_state: "shutdown_timeout", + health: { + healthy: false, + reason: "agent is shutting down", + }, } export const MockWorkspaceAgentShutdownError: TypesGen.WorkspaceAgent = { @@ -613,6 +636,10 @@ export const MockWorkspaceAgentShutdownError: TypesGen.WorkspaceAgent = { id: "test-workspace-agent-shutdown-error", name: "a-workspace-agent-errored-while-running-shutdownup-script", lifecycle_state: "shutdown_error", + health: { + healthy: false, + reason: "agent is shutting down", + }, } export const MockWorkspaceAgentOff: TypesGen.WorkspaceAgent = { @@ -620,6 +647,10 @@ export const MockWorkspaceAgentOff: TypesGen.WorkspaceAgent = { id: "test-workspace-agent-off", name: "a-workspace-agent-is-shut-down", lifecycle_state: "off", + health: { + healthy: false, + reason: "agent is shutting down", + }, } export const MockWorkspaceResource: TypesGen.WorkspaceResource = { @@ -771,6 +802,13 @@ export const MockWorkspace: TypesGen.Workspace = { ttl_ms: 2 * 60 * 60 * 1000, latest_build: MockWorkspaceBuild, last_used_at: "2022-05-16T15:29:10.302441433Z", + health: { + healthy: true, + failing_sections: [], + agents: { + [MockWorkspaceAgent.id]: MockWorkspaceAgent.health, + }, + }, } export const MockStoppedWorkspace: TypesGen.Workspace = { From 16976c082947d58b9d859f175fc3414290286c6f Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 3 Jul 2023 09:03:42 +0000 Subject: [PATCH 2/5] update golden files --- cli/testdata/coder_list_--output_json.golden | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index b1e97296493c0..0ea4324dc6a08 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -52,6 +52,11 @@ "ttl_ms": 28800000, "last_used_at": "[timestamp]", "deleting_at": null, - "locked_at": null + "locked_at": null, + "health": { + "healthy": true, + "failing_sections": [], + "agents": {} + } } ] From 15cfc3fd74919132df688a02312fe78221a853e8 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 3 Jul 2023 15:30:08 +0000 Subject: [PATCH 3/5] remove health property from workspace --- coderd/apidoc/docs.go | 31 ----------- coderd/apidoc/swagger.json | 31 ----------- coderd/workspaces.go | 11 ---- codersdk/workspaces.go | 24 --------- docs/api/schemas.md | 99 ++++++++-------------------------- docs/api/workspaces.md | 56 ------------------- site/src/api/typesGenerated.ts | 8 --- 7 files changed, 21 insertions(+), 239 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 60f58610f29ed..2dddce42b8ac9 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9440,14 +9440,6 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, - "health": { - "description": "Health reports the health of the workspace and its agents.", - "allOf": [ - { - "$ref": "#/definitions/codersdk.WorkspaceHealth" - } - ] - }, "id": { "type": "string", "format": "uuid" @@ -10026,29 +10018,6 @@ const docTemplate = `{ } } }, - "codersdk.WorkspaceHealth": { - "type": "object", - "properties": { - "agents": { - "description": "Agents is a map of agent IDs to their health.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/codersdk.WorkspaceAgentHealth" - } - }, - "failing_sections": { - "description": "FailingSections is a list of sections that have failed their healthcheck.", - "type": "array", - "items": { - "type": "string" - } - }, - "healthy": { - "description": "Healthy is true if the workspace and all of its agents are healthy.", - "type": "boolean" - } - } - }, "codersdk.WorkspaceProxy": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 6601c02d145f3..02e89dcb153bc 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8516,14 +8516,6 @@ "type": "string", "format": "date-time" }, - "health": { - "description": "Health reports the health of the workspace and its agents.", - "allOf": [ - { - "$ref": "#/definitions/codersdk.WorkspaceHealth" - } - ] - }, "id": { "type": "string", "format": "uuid" @@ -9073,29 +9065,6 @@ } } }, - "codersdk.WorkspaceHealth": { - "type": "object", - "properties": { - "agents": { - "description": "Agents is a map of agent IDs to their health.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/codersdk.WorkspaceAgentHealth" - } - }, - "failing_sections": { - "description": "FailingSections is a list of sections that have failed their healthcheck.", - "type": "array", - "items": { - "type": "string" - } - }, - "healthy": { - "description": "Healthy is true if the workspace and all of its agents are healthy.", - "type": "boolean" - } - } - }, "codersdk.WorkspaceProxy": { "type": "object", "properties": { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index f5bf5c60911ce..0910a1d62f0c0 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1115,16 +1115,6 @@ func convertWorkspace( deletingAt = calculateDeletingAt(workspace, template, workspaceBuild) ) - agentHealth := make(map[uuid.UUID]codersdk.WorkspaceAgentHealth) - for _, r := range workspaceBuild.Resources { - // For now, we only consider agent healths when the workspace is running. - if r.Transition == codersdk.WorkspaceTransitionStart { - for _, a := range r.Agents { - agentHealth[a.ID] = a.Health - } - } - } - return codersdk.Workspace{ ID: workspace.ID, CreatedAt: workspace.CreatedAt, @@ -1145,7 +1135,6 @@ func convertWorkspace( LastUsedAt: workspace.LastUsedAt, DeletingAt: deletingAt, LockedAt: lockedAt, - Health: (codersdk.WorkspaceHealth{Agents: agentHealth}).Complete(), } } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 788940986eca3..c076e9e86b6a2 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -43,30 +43,6 @@ type Workspace struct { // unlocked by an admin. It is subject to deletion if it breaches // the duration of the locked_ttl field on its template. LockedAt *time.Time `json:"locked_at" format:"date-time"` - // Health reports the health of the workspace and its agents. - Health WorkspaceHealth `json:"health"` -} - -type WorkspaceHealth struct { - Healthy bool `json:"healthy"` // Healthy is true if the workspace and all of its agents are healthy. - FailingSections []string `json:"failing_sections"` // FailingSections is a list of sections that have failed their healthcheck. - Agents map[uuid.UUID]WorkspaceAgentHealth `json:"agents"` // Agents is a map of agent IDs to their health. -} - -// Complete returns a new copy with the Healthy flag and FailingSections -// set based on the agent healths. -// -//nolint:revive -func (wh WorkspaceHealth) Complete() WorkspaceHealth { - wh.Healthy = true - wh.FailingSections = []string{} - for id, agent := range wh.Agents { - if !agent.Healthy { - wh.Healthy = false - wh.FailingSections = append(wh.FailingSections, "agents."+id.String()) - } - } - return wh } type WorkspacesRequest struct { diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 2c895dc329070..84523db076ebb 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -4596,20 +4596,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", - "health": { - "agents": { - "property1": { - "healthy": true, - "reason": "string" - }, - "property2": { - "healthy": true, - "reason": "string" - } - }, - "failing_sections": ["string"], - "healthy": true - }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -4758,28 +4744,27 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------------------------------- | ---------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `autostart_schedule` | string | false | | | -| `created_at` | string | false | | | -| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. | -| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health reports the health of the workspace and its agents. | -| `id` | string | false | | | -| `last_used_at` | string | false | | | -| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | -| `locked_at` | string | false | | Locked at being non-nil indicates a workspace that has been locked. A locked workspace is no longer accessible by a user and must be unlocked by an admin. It is subject to deletion if it breaches the duration of the locked_ttl field on its template. | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `outdated` | boolean | false | | | -| `owner_id` | string | false | | | -| `owner_name` | string | false | | | -| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | -| `template_display_name` | string | false | | | -| `template_icon` | string | false | | | -| `template_id` | string | false | | | -| `template_name` | string | false | | | -| `ttl_ms` | integer | false | | | -| `updated_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------------- | -------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `autostart_schedule` | string | false | | | +| `created_at` | string | false | | | +| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. | +| `id` | string | false | | | +| `last_used_at` | string | false | | | +| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | +| `locked_at` | string | false | | Locked at being non-nil indicates a workspace that has been locked. A locked workspace is no longer accessible by a user and must be unlocked by an admin. It is subject to deletion if it breaches the duration of the locked_ttl field on its template. | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `outdated` | boolean | false | | | +| `owner_id` | string | false | | | +| `owner_name` | string | false | | | +| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | +| `template_display_name` | string | false | | | +| `template_icon` | string | false | | | +| `template_id` | string | false | | | +| `template_name` | string | false | | | +| `ttl_ms` | integer | false | | | +| `updated_at` | string | false | | | ## codersdk.WorkspaceAgent @@ -5431,34 +5416,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `stopped` | integer | false | | | | `tx_bytes` | integer | false | | | -## codersdk.WorkspaceHealth - -```json -{ - "agents": { - "property1": { - "healthy": true, - "reason": "string" - }, - "property2": { - "healthy": true, - "reason": "string" - } - }, - "failing_sections": ["string"], - "healthy": true -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------------------ | -------------------------------------------------------------- | -------- | ------------ | -------------------------------------------------------------------------- | -| `agents` | object | false | | Agents is a map of agent IDs to their health. | -| » `[any property]` | [codersdk.WorkspaceAgentHealth](#codersdkworkspaceagenthealth) | false | | | -| `failing_sections` | array of string | false | | Failing sections is a list of sections that have failed their healthcheck. | -| `healthy` | boolean | false | | Healthy is true if the workspace and all of its agents are healthy. | - ## codersdk.WorkspaceProxy ```json @@ -5719,20 +5676,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", - "health": { - "agents": { - "property1": { - "healthy": true, - "reason": "string" - }, - "property2": { - "healthy": true, - "reason": "string" - } - }, - "failing_sections": ["string"], - "healthy": true - }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 76c558dba615d..13b8ea944f194 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -48,20 +48,6 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", - "health": { - "agents": { - "property1": { - "healthy": true, - "reason": "string" - }, - "property2": { - "healthy": true, - "reason": "string" - } - }, - "failing_sections": ["string"], - "healthy": true - }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -246,20 +232,6 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", - "health": { - "agents": { - "property1": { - "healthy": true, - "reason": "string" - }, - "property2": { - "healthy": true, - "reason": "string" - } - }, - "failing_sections": ["string"], - "healthy": true - }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -468,20 +440,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", - "health": { - "agents": { - "property1": { - "healthy": true, - "reason": "string" - }, - "property2": { - "healthy": true, - "reason": "string" - } - }, - "failing_sections": ["string"], - "healthy": true - }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -663,20 +621,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", - "health": { - "agents": { - "property1": { - "healthy": true, - "reason": "string" - }, - "property2": { - "healthy": true, - "reason": "string" - } - }, - "failing_sections": ["string"], - "healthy": true - }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 58c2326a4de71..d030e579c9ce5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1126,7 +1126,6 @@ export interface Workspace { readonly last_used_at: string readonly deleting_at?: string readonly locked_at?: string - readonly health: WorkspaceHealth } // From codersdk/workspaceagents.go @@ -1289,13 +1288,6 @@ export interface WorkspaceFilter { readonly q?: string } -// From codersdk/workspaces.go -export interface WorkspaceHealth { - readonly healthy: boolean - readonly failing_sections: string[] - readonly agents: Record -} - // From codersdk/workspaces.go export interface WorkspaceOptions { readonly include_deleted?: boolean From 9774488cbe09532be99d7d66503af51fc79b8360 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 4 Jul 2023 12:28:21 +0000 Subject: [PATCH 4/5] implement proposal, add tests and examples --- cli/testdata/coder_list_--output_json.golden | 3 +- coderd/apidoc/docs.go | 32 ++++++- coderd/apidoc/swagger.json | 32 ++++++- coderd/workspaces.go | 13 +++ coderd/workspaces_test.go | 86 ++++++++++++++++++ codersdk/workspaceagents.go | 4 +- codersdk/workspaces.go | 8 ++ docs/api/agents.md | 4 +- docs/api/builds.md | 24 +++--- docs/api/schemas.md | 91 +++++++++++++------- docs/api/templates.md | 8 +- docs/api/workspaces.md | 32 +++++-- site/src/api/typesGenerated.ts | 7 ++ site/src/testHelpers/entities.ts | 5 +- 14 files changed, 278 insertions(+), 71 deletions(-) diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 0ea4324dc6a08..49e51d408285c 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -55,8 +55,7 @@ "locked_at": null, "health": { "healthy": true, - "failing_sections": [], - "agents": {} + "failing_agents": [] } } ] diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 2dddce42b8ac9..173b8043f710f 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -9440,6 +9440,14 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, + "health": { + "description": "Health shows the health of the workspace and information about\nwhat is causing an unhealthy status.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceHealth" + } + ] + }, "id": { "type": "string", "format": "uuid" @@ -9644,11 +9652,13 @@ const docTemplate = `{ "properties": { "healthy": { "description": "Healthy is true if the agent is healthy.", - "type": "boolean" + "type": "boolean", + "example": false }, "reason": { "description": "Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true.", - "type": "string" + "type": "string", + "example": "agent has lost connection" } } }, @@ -10018,6 +10028,24 @@ const docTemplate = `{ } } }, + "codersdk.WorkspaceHealth": { + "type": "object", + "properties": { + "failing_agents": { + "description": "FailingAgents lists the IDs of the agents that are failing, if any.", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "healthy": { + "description": "Healthy is true if the workspace is healthy.", + "type": "boolean", + "example": false + } + } + }, "codersdk.WorkspaceProxy": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 02e89dcb153bc..b1fa6c4da28ad 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -8516,6 +8516,14 @@ "type": "string", "format": "date-time" }, + "health": { + "description": "Health shows the health of the workspace and information about\nwhat is causing an unhealthy status.", + "allOf": [ + { + "$ref": "#/definitions/codersdk.WorkspaceHealth" + } + ] + }, "id": { "type": "string", "format": "uuid" @@ -8720,11 +8728,13 @@ "properties": { "healthy": { "description": "Healthy is true if the agent is healthy.", - "type": "boolean" + "type": "boolean", + "example": false }, "reason": { "description": "Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true.", - "type": "string" + "type": "string", + "example": "agent has lost connection" } } }, @@ -9065,6 +9075,24 @@ } } }, + "codersdk.WorkspaceHealth": { + "type": "object", + "properties": { + "failing_agents": { + "description": "FailingAgents lists the IDs of the agents that are failing, if any.", + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "healthy": { + "description": "Healthy is true if the workspace is healthy.", + "type": "boolean", + "example": false + } + } + }, "codersdk.WorkspaceProxy": { "type": "object", "properties": { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 0910a1d62f0c0..8324d0afae0b2 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1110,6 +1110,15 @@ func convertWorkspace( lockedAt = &workspace.LockedAt.Time } + failingAgents := []uuid.UUID{} + for _, resource := range workspaceBuild.Resources { + for _, agent := range resource.Agents { + if !agent.Health.Healthy { + failingAgents = append(failingAgents, agent.ID) + } + } + } + var ( ttlMillis = convertWorkspaceTTLMillis(workspace.Ttl) deletingAt = calculateDeletingAt(workspace, template, workspaceBuild) @@ -1135,6 +1144,10 @@ func convertWorkspace( LastUsedAt: workspace.LastUsedAt, DeletingAt: deletingAt, LockedAt: lockedAt, + Health: codersdk.WorkspaceHealth{ + Healthy: len(failingAgents) == 0, + FailingAgents: failingAgents, + }, } } diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index c57dfc299fe34..563cd01d16cb7 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -165,6 +165,92 @@ func TestWorkspace(t *testing.T) { assert.Equal(t, templateDisplayName, ws.TemplateDisplayName) assert.Equal(t, templateAllowUserCancelWorkspaceJobs, ws.TemplateAllowUserCancelWorkspaceJobs) }) + + t.Run("Health", func(t *testing.T) { + t.Parallel() + + t.Run("Healthy", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{}, + }}, + }}, + }, + }, + }}, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + workspace, err := client.Workspace(ctx, workspace.ID) + require.NoError(t, err) + agent := workspace.LatestBuild.Resources[0].Agents[0] + + assert.True(t, agent.Health.Healthy) + assert.Empty(t, agent.Health.Reason) + }) + + t.Run("Unhealthy", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{}, + ConnectionTimeoutSeconds: 1, + }}, + }}, + }, + }, + }}, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + var err error + testutil.Eventually(ctx, t, func(ctx context.Context) bool { + workspace, err = client.Workspace(ctx, workspace.ID) + t.Logf("%#v", workspace) + return assert.NoError(t, err) && !workspace.Health.Healthy + }, testutil.IntervalMedium) + + agent := workspace.LatestBuild.Resources[0].Agents[0] + + assert.False(t, workspace.Health.Healthy) + assert.Len(t, workspace.Health.FailingAgents, 1) + assert.False(t, agent.Health.Healthy) + assert.NotEmpty(t, agent.Health.Reason) + }) + }) } func TestAdminViewAllWorkspaces(t *testing.T) { diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 767960267bb4e..208c4511d261a 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -172,8 +172,8 @@ type WorkspaceAgent struct { } type WorkspaceAgentHealth struct { - Healthy bool `json:"healthy"` // Healthy is true if the agent is healthy. - Reason string `json:"reason,omitempty"` // Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. + Healthy bool `json:"healthy" example:"false"` // Healthy is true if the agent is healthy. + Reason string `json:"reason,omitempty" example:"agent has lost connection"` // Reason is a human-readable explanation of the agent's health. It is empty if Healthy is true. } type DERPRegion struct { diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index c076e9e86b6a2..a2ef823fcb87e 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -43,6 +43,14 @@ type Workspace struct { // unlocked by an admin. It is subject to deletion if it breaches // the duration of the locked_ttl field on its template. LockedAt *time.Time `json:"locked_at" format:"date-time"` + // Health shows the health of the workspace and information about + // what is causing an unhealthy status. + Health WorkspaceHealth `json:"health"` +} + +type WorkspaceHealth struct { + Healthy bool `json:"healthy" example:"false"` // Healthy is true if the workspace is healthy. + FailingAgents []uuid.UUID `json:"failing_agents" format:"uuid"` // FailingAgents lists the IDs of the agents that are failing, if any. } type WorkspacesRequest struct { diff --git a/docs/api/agents.md b/docs/api/agents.md index 7e1b5d52201b7..69ff2fbe72318 100644 --- a/docs/api/agents.md +++ b/docs/api/agents.md @@ -451,8 +451,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \ "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", diff --git a/docs/api/builds.md b/docs/api/builds.md index f5434885effe6..665b8778b6865 100644 --- a/docs/api/builds.md +++ b/docs/api/builds.md @@ -89,8 +89,8 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -251,8 +251,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -552,8 +552,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -810,8 +810,8 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -977,8 +977,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -1325,8 +1325,8 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 84523db076ebb..893436b582a88 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -4596,6 +4596,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -4661,8 +4665,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -4744,27 +4748,28 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------------------------------- | -------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `autostart_schedule` | string | false | | | -| `created_at` | string | false | | | -| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. | -| `id` | string | false | | | -| `last_used_at` | string | false | | | -| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | -| `locked_at` | string | false | | Locked at being non-nil indicates a workspace that has been locked. A locked workspace is no longer accessible by a user and must be unlocked by an admin. It is subject to deletion if it breaches the duration of the locked_ttl field on its template. | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `outdated` | boolean | false | | | -| `owner_id` | string | false | | | -| `owner_name` | string | false | | | -| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | -| `template_display_name` | string | false | | | -| `template_icon` | string | false | | | -| `template_id` | string | false | | | -| `template_name` | string | false | | | -| `ttl_ms` | integer | false | | | -| `updated_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------------- | ---------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `autostart_schedule` | string | false | | | +| `created_at` | string | false | | | +| `deleting_at` | string | false | | Deleting at indicates the time of the upcoming workspace deletion, if applicable; otherwise it is nil. Workspaces may have impending deletions if Template.InactivityTTL feature is turned on and the workspace is inactive. | +| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. | +| `id` | string | false | | | +| `last_used_at` | string | false | | | +| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | +| `locked_at` | string | false | | Locked at being non-nil indicates a workspace that has been locked. A locked workspace is no longer accessible by a user and must be unlocked by an admin. It is subject to deletion if it breaches the duration of the locked_ttl field on its template. | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `outdated` | boolean | false | | | +| `owner_id` | string | false | | | +| `owner_name` | string | false | | | +| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | +| `template_display_name` | string | false | | | +| `template_icon` | string | false | | | +| `template_id` | string | false | | | +| `template_name` | string | false | | | +| `ttl_ms` | integer | false | | | +| `updated_at` | string | false | | | ## codersdk.WorkspaceAgent @@ -4801,8 +4806,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -4949,8 +4954,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ```json { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" } ``` @@ -5240,8 +5245,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -5416,6 +5421,22 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `stopped` | integer | false | | | | `tx_bytes` | integer | false | | | +## codersdk.WorkspaceHealth + +```json +{ + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ---------------- | --------------- | -------- | ------------ | -------------------------------------------------------------------- | +| `failing_agents` | array of string | false | | Failing agents lists the IDs of the agents that are failing, if any. | +| `healthy` | boolean | false | | Healthy is true if the workspace is healthy. | + ## codersdk.WorkspaceProxy ```json @@ -5529,8 +5550,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -5676,6 +5697,10 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -5737,8 +5762,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", diff --git a/docs/api/templates.md b/docs/api/templates.md index a70a276bd0249..e787e5f9daa2c 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -1660,8 +1660,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -2052,8 +2052,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 13b8ea944f194..65a399b0e594b 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -48,6 +48,10 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -113,8 +117,8 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -232,6 +236,10 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -297,8 +305,8 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -440,6 +448,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -501,8 +513,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", @@ -621,6 +633,10 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "autostart_schedule": "string", "created_at": "2019-08-24T14:15:22Z", "deleting_at": "2019-08-24T14:15:22Z", + "health": { + "failing_agents": ["497f6eca-6276-4993-bfeb-53cbbbba6f08"], + "healthy": false + }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", "latest_build": { @@ -686,8 +702,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "expanded_directory": "string", "first_connected_at": "2019-08-24T14:15:22Z", "health": { - "healthy": true, - "reason": "string" + "healthy": false, + "reason": "agent has lost connection" }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "instance_id": "string", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d030e579c9ce5..69912955e2963 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1126,6 +1126,7 @@ export interface Workspace { readonly last_used_at: string readonly deleting_at?: string readonly locked_at?: string + readonly health: WorkspaceHealth } // From codersdk/workspaceagents.go @@ -1288,6 +1289,12 @@ export interface WorkspaceFilter { readonly q?: string } +// From codersdk/workspaces.go +export interface WorkspaceHealth { + readonly healthy: boolean + readonly failing_agents: string[] +} + // From codersdk/workspaces.go export interface WorkspaceOptions { readonly include_deleted?: boolean diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 57ef63336d4aa..e63841a92f9f2 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -804,10 +804,7 @@ export const MockWorkspace: TypesGen.Workspace = { last_used_at: "2022-05-16T15:29:10.302441433Z", health: { healthy: true, - failing_sections: [], - agents: { - [MockWorkspaceAgent.id]: MockWorkspaceAgent.health, - }, + failing_agents: [], }, } From fba12e220945c5f78fb024038b4a5837ed49caba Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 4 Jul 2023 16:22:02 +0000 Subject: [PATCH 5/5] more tests --- coderd/workspaces_test.go | 61 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 26a2a6bd221ec..3204e76b5261e 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -60,7 +60,6 @@ func TestWorkspace(t *testing.T) { require.NoError(t, err) require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID) require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason) - require.True(t, ws.Health.Healthy) }) t.Run("Deleted", func(t *testing.T) { @@ -200,8 +199,11 @@ func TestWorkspace(t *testing.T) { workspace, err := client.Workspace(ctx, workspace.ID) require.NoError(t, err) + agent := workspace.LatestBuild.Resources[0].Agents[0] + assert.True(t, workspace.Health.Healthy) + assert.Equal(t, []uuid.UUID{}, workspace.Health.FailingAgents) assert.True(t, agent.Health.Healthy) assert.Empty(t, agent.Health.Reason) }) @@ -239,17 +241,70 @@ func TestWorkspace(t *testing.T) { var err error testutil.Eventually(ctx, t, func(ctx context.Context) bool { workspace, err = client.Workspace(ctx, workspace.ID) - t.Logf("%#v", workspace) return assert.NoError(t, err) && !workspace.Health.Healthy }, testutil.IntervalMedium) agent := workspace.LatestBuild.Resources[0].Agents[0] assert.False(t, workspace.Health.Healthy) - assert.Len(t, workspace.Health.FailingAgents, 1) + assert.Equal(t, []uuid.UUID{agent.ID}, workspace.Health.FailingAgents) assert.False(t, agent.Health.Healthy) assert.NotEmpty(t, agent.Health.Reason) }) + + t.Run("Mixed health", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "some", + Type: "example", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Name: "a1", + Auth: &proto.Agent_Token{}, + }, { + Id: uuid.NewString(), + Name: "a2", + Auth: &proto.Agent_Token{}, + ConnectionTimeoutSeconds: 1, + }}, + }}, + }, + }, + }}, + }) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + var err error + testutil.Eventually(ctx, t, func(ctx context.Context) bool { + workspace, err = client.Workspace(ctx, workspace.ID) + return assert.NoError(t, err) && !workspace.Health.Healthy + }, testutil.IntervalMedium) + + assert.False(t, workspace.Health.Healthy) + assert.Len(t, workspace.Health.FailingAgents, 1) + + agent1 := workspace.LatestBuild.Resources[0].Agents[0] + agent2 := workspace.LatestBuild.Resources[0].Agents[1] + + assert.Equal(t, []uuid.UUID{agent2.ID}, workspace.Health.FailingAgents) + assert.True(t, agent1.Health.Healthy) + assert.Empty(t, agent1.Health.Reason) + assert.False(t, agent2.Health.Healthy) + assert.NotEmpty(t, agent2.Health.Reason) + }) }) }