From 82945cfb1667deef65cc53f4f43db356c46c0b1c Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 15 Oct 2025 19:34:33 +0300 Subject: [PATCH] fix(coderd/database): add missing columns to tasks with status (#20311) Updates coder/internal#976 --- cli/exp_task_status_test.go | 1 + coderd/aitasks.go | 10 + coderd/apidoc/docs.go | 11 + coderd/apidoc/swagger.json | 11 + coderd/database/dump.sql | 5 +- ..._add_columns_to_tasks_with_status.down.sql | 72 +++ ...82_add_columns_to_tasks_with_status.up.sql | 74 +++ coderd/database/models.go | 25 +- coderd/database/querier_test.go | 426 +++++++++++------- coderd/database/queries.sql.go | 15 +- coderd/database/sqlc.yaml | 3 + codersdk/aitasks.go | 2 + docs/reference/api/schemas.md | 12 + site/src/api/typesGenerated.ts | 2 + site/src/testHelpers/entities.ts | 2 + 15 files changed, 502 insertions(+), 169 deletions(-) create mode 100644 coderd/database/migrations/000382_add_columns_to_tasks_with_status.down.sql create mode 100644 coderd/database/migrations/000382_add_columns_to_tasks_with_status.up.sql diff --git a/cli/exp_task_status_test.go b/cli/exp_task_status_test.go index be62a76476..1e255c05b9 100644 --- a/cli/exp_task_status_test.go +++ b/cli/exp_task_status_test.go @@ -193,6 +193,7 @@ STATE CHANGED STATUS HEALTHY STATE MESSAGE "workspace_agent_id": null, "workspace_agent_lifecycle": null, "workspace_agent_health": null, + "workspace_app_id": null, "initial_prompt": "", "status": "running", "current_state": { diff --git a/coderd/aitasks.go b/coderd/aitasks.go index 6bb706d1a5..95ad8008e8 100644 --- a/coderd/aitasks.go +++ b/coderd/aitasks.go @@ -268,6 +268,14 @@ func taskFromWorkspace(ws codersdk.Workspace, initialPrompt string) codersdk.Tas } } + var appID uuid.NullUUID + if ws.LatestBuild.AITaskSidebarAppID != nil { + appID = uuid.NullUUID{ + Valid: true, + UUID: *ws.LatestBuild.AITaskSidebarAppID, + } + } + return codersdk.Task{ ID: ws.ID, OrganizationID: ws.OrganizationID, @@ -279,9 +287,11 @@ func taskFromWorkspace(ws codersdk.Workspace, initialPrompt string) codersdk.Tas TemplateDisplayName: ws.TemplateDisplayName, TemplateIcon: ws.TemplateIcon, WorkspaceID: uuid.NullUUID{Valid: true, UUID: ws.ID}, + WorkspaceBuildNumber: ws.LatestBuild.BuildNumber, WorkspaceAgentID: taskAgentID, WorkspaceAgentLifecycle: taskAgentLifecycle, WorkspaceAgentHealth: taskAgentHealth, + WorkspaceAppID: appID, CreatedAt: ws.CreatedAt, UpdatedAt: ws.UpdatedAt, InitialPrompt: initialPrompt, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0fc64ef9b0..b3bad21329 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -17613,6 +17613,17 @@ const docTemplate = `{ "workspace_agent_lifecycle": { "$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle" }, + "workspace_app_id": { + "format": "uuid", + "allOf": [ + { + "$ref": "#/definitions/uuid.NullUUID" + } + ] + }, + "workspace_build_number": { + "type": "integer" + }, "workspace_id": { "format": "uuid", "allOf": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 867e5e5818..a3319a9471 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -16121,6 +16121,17 @@ "workspace_agent_lifecycle": { "$ref": "#/definitions/codersdk.WorkspaceAgentLifecycle" }, + "workspace_app_id": { + "format": "uuid", + "allOf": [ + { + "$ref": "#/definitions/uuid.NullUUID" + } + ] + }, + "workspace_build_number": { + "type": "integer" + }, "workspace_id": { "format": "uuid", "allOf": [ diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 71e2845d1c..8b5700e58d 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1971,7 +1971,10 @@ CREATE VIEW tasks_with_status AS ELSE 'unknown'::task_status END ELSE 'unknown'::task_status - END AS status + END AS status, + task_app.workspace_build_number, + task_app.workspace_agent_id, + task_app.workspace_app_id FROM ((((tasks LEFT JOIN LATERAL ( SELECT task_app_1.workspace_build_number, task_app_1.workspace_agent_id, diff --git a/coderd/database/migrations/000382_add_columns_to_tasks_with_status.down.sql b/coderd/database/migrations/000382_add_columns_to_tasks_with_status.down.sql new file mode 100644 index 0000000000..c9cd9c8665 --- /dev/null +++ b/coderd/database/migrations/000382_add_columns_to_tasks_with_status.down.sql @@ -0,0 +1,72 @@ +DROP VIEW IF EXISTS tasks_with_status; + +-- Restore from 00037_add_columns_to_tasks_with_status.up.sql. +CREATE VIEW + tasks_with_status +AS + SELECT + tasks.*, + CASE + WHEN tasks.workspace_id IS NULL OR latest_build.job_status IS NULL THEN 'pending'::task_status + + WHEN latest_build.job_status = 'failed' THEN 'error'::task_status + + WHEN latest_build.transition IN ('stop', 'delete') + AND latest_build.job_status = 'succeeded' THEN 'paused'::task_status + + WHEN latest_build.transition = 'start' + AND latest_build.job_status = 'pending' THEN 'initializing'::task_status + + WHEN latest_build.transition = 'start' AND latest_build.job_status IN ('running', 'succeeded') THEN + CASE + WHEN agent_status.none THEN 'initializing'::task_status + WHEN agent_status.connecting THEN 'initializing'::task_status + WHEN agent_status.connected THEN + CASE + WHEN app_status.any_unhealthy THEN 'error'::task_status + WHEN app_status.any_initializing THEN 'initializing'::task_status + WHEN app_status.all_healthy_or_disabled THEN 'active'::task_status + ELSE 'unknown'::task_status + END + ELSE 'unknown'::task_status + END + + ELSE 'unknown'::task_status + END AS status + FROM + tasks + LEFT JOIN LATERAL ( + SELECT workspace_build_number, workspace_agent_id, workspace_app_id + FROM task_workspace_apps task_app + WHERE task_id = tasks.id + ORDER BY workspace_build_number DESC + LIMIT 1 + ) task_app ON TRUE + LEFT JOIN LATERAL ( + SELECT + workspace_build.transition, + provisioner_job.job_status, + workspace_build.job_id + FROM workspace_builds workspace_build + JOIN provisioner_jobs provisioner_job ON provisioner_job.id = workspace_build.job_id + WHERE workspace_build.workspace_id = tasks.workspace_id + AND workspace_build.build_number = task_app.workspace_build_number + ) latest_build ON TRUE + CROSS JOIN LATERAL ( + SELECT + COUNT(*) = 0 AS none, + bool_or(workspace_agent.lifecycle_state IN ('created', 'starting')) AS connecting, + bool_and(workspace_agent.lifecycle_state = 'ready') AS connected + FROM workspace_agents workspace_agent + WHERE workspace_agent.id = task_app.workspace_agent_id + ) agent_status + CROSS JOIN LATERAL ( + SELECT + bool_or(workspace_app.health = 'unhealthy') AS any_unhealthy, + bool_or(workspace_app.health = 'initializing') AS any_initializing, + bool_and(workspace_app.health IN ('healthy', 'disabled')) AS all_healthy_or_disabled + FROM workspace_apps workspace_app + WHERE workspace_app.id = task_app.workspace_app_id + ) app_status + WHERE + tasks.deleted_at IS NULL; diff --git a/coderd/database/migrations/000382_add_columns_to_tasks_with_status.up.sql b/coderd/database/migrations/000382_add_columns_to_tasks_with_status.up.sql new file mode 100644 index 0000000000..4d949384c0 --- /dev/null +++ b/coderd/database/migrations/000382_add_columns_to_tasks_with_status.up.sql @@ -0,0 +1,74 @@ +-- Drop view from 00037_add_columns_to_tasks_with_status.up.sql. +DROP VIEW IF EXISTS tasks_with_status; + +-- Add task_app columns. +CREATE VIEW + tasks_with_status +AS + SELECT + tasks.*, + CASE + WHEN tasks.workspace_id IS NULL OR latest_build.job_status IS NULL THEN 'pending'::task_status + + WHEN latest_build.job_status = 'failed' THEN 'error'::task_status + + WHEN latest_build.transition IN ('stop', 'delete') + AND latest_build.job_status = 'succeeded' THEN 'paused'::task_status + + WHEN latest_build.transition = 'start' + AND latest_build.job_status = 'pending' THEN 'initializing'::task_status + + WHEN latest_build.transition = 'start' AND latest_build.job_status IN ('running', 'succeeded') THEN + CASE + WHEN agent_status.none THEN 'initializing'::task_status + WHEN agent_status.connecting THEN 'initializing'::task_status + WHEN agent_status.connected THEN + CASE + WHEN app_status.any_unhealthy THEN 'error'::task_status + WHEN app_status.any_initializing THEN 'initializing'::task_status + WHEN app_status.all_healthy_or_disabled THEN 'active'::task_status + ELSE 'unknown'::task_status + END + ELSE 'unknown'::task_status + END + + ELSE 'unknown'::task_status + END AS status, + task_app.* + FROM + tasks + LEFT JOIN LATERAL ( + SELECT workspace_build_number, workspace_agent_id, workspace_app_id + FROM task_workspace_apps task_app + WHERE task_id = tasks.id + ORDER BY workspace_build_number DESC + LIMIT 1 + ) task_app ON TRUE + LEFT JOIN LATERAL ( + SELECT + workspace_build.transition, + provisioner_job.job_status, + workspace_build.job_id + FROM workspace_builds workspace_build + JOIN provisioner_jobs provisioner_job ON provisioner_job.id = workspace_build.job_id + WHERE workspace_build.workspace_id = tasks.workspace_id + AND workspace_build.build_number = task_app.workspace_build_number + ) latest_build ON TRUE + CROSS JOIN LATERAL ( + SELECT + COUNT(*) = 0 AS none, + bool_or(workspace_agent.lifecycle_state IN ('created', 'starting')) AS connecting, + bool_and(workspace_agent.lifecycle_state = 'ready') AS connected + FROM workspace_agents workspace_agent + WHERE workspace_agent.id = task_app.workspace_agent_id + ) agent_status + CROSS JOIN LATERAL ( + SELECT + bool_or(workspace_app.health = 'unhealthy') AS any_unhealthy, + bool_or(workspace_app.health = 'initializing') AS any_initializing, + bool_and(workspace_app.health IN ('healthy', 'disabled')) AS all_healthy_or_disabled + FROM workspace_apps workspace_app + WHERE workspace_app.id = task_app.workspace_app_id + ) app_status + WHERE + tasks.deleted_at IS NULL; diff --git a/coderd/database/models.go b/coderd/database/models.go index 741d70f9d2..8b0b6a97aa 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -4200,17 +4200,20 @@ type TailnetTunnel struct { } type Task struct { - ID uuid.UUID `db:"id" json:"id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - Name string `db:"name" json:"name"` - WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` - TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` - TemplateParameters json.RawMessage `db:"template_parameters" json:"template_parameters"` - Prompt string `db:"prompt" json:"prompt"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"` - Status TaskStatus `db:"status" json:"status"` + ID uuid.UUID `db:"id" json:"id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + Name string `db:"name" json:"name"` + WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` + TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` + TemplateParameters json.RawMessage `db:"template_parameters" json:"template_parameters"` + Prompt string `db:"prompt" json:"prompt"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"` + Status TaskStatus `db:"status" json:"status"` + WorkspaceBuildNumber sql.NullInt32 `db:"workspace_build_number" json:"workspace_build_number"` + WorkspaceAgentID uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"` + WorkspaceAppID uuid.NullUUID `db:"workspace_app_id" json:"workspace_app_id"` } type TaskTable struct { diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 9db6f585fb..aca6156904 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -6813,193 +6813,284 @@ func TestTasksWithStatusView(t *testing.T) { } tests := []struct { - name string - buildStatus database.ProvisionerJobStatus - buildTransition database.WorkspaceTransition - agentState database.WorkspaceAgentLifecycleState - appHealths []database.WorkspaceAppHealth - expectedStatus database.TaskStatus - description string + name string + buildStatus database.ProvisionerJobStatus + buildTransition database.WorkspaceTransition + agentState database.WorkspaceAgentLifecycleState + appHealths []database.WorkspaceAppHealth + expectedStatus database.TaskStatus + description string + expectBuildNumberValid bool + expectBuildNumber int32 + expectWorkspaceAgentValid bool + expectWorkspaceAppValid bool }{ { - name: "NoWorkspace", - expectedStatus: "pending", - description: "Task with no workspace assigned", + name: "NoWorkspace", + expectedStatus: "pending", + description: "Task with no workspace assigned", + expectBuildNumberValid: false, + expectWorkspaceAgentValid: false, + expectWorkspaceAppValid: false, }, { - name: "FailedBuild", - buildStatus: database.ProvisionerJobStatusFailed, - buildTransition: database.WorkspaceTransitionStart, - expectedStatus: database.TaskStatusError, - description: "Latest workspace build failed", + name: "FailedBuild", + buildStatus: database.ProvisionerJobStatusFailed, + buildTransition: database.WorkspaceTransitionStart, + expectedStatus: database.TaskStatusError, + description: "Latest workspace build failed", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: false, + expectWorkspaceAppValid: false, }, { - name: "StoppedWorkspace", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStop, - expectedStatus: database.TaskStatusPaused, - description: "Workspace is stopped", + name: "StoppedWorkspace", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStop, + expectedStatus: database.TaskStatusPaused, + description: "Workspace is stopped", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: false, + expectWorkspaceAppValid: false, }, { - name: "DeletedWorkspace", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionDelete, - expectedStatus: database.TaskStatusPaused, - description: "Workspace is deleted", + name: "DeletedWorkspace", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionDelete, + expectedStatus: database.TaskStatusPaused, + description: "Workspace is deleted", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: false, + expectWorkspaceAppValid: false, }, { - name: "PendingStart", - buildStatus: database.ProvisionerJobStatusPending, - buildTransition: database.WorkspaceTransitionStart, - expectedStatus: database.TaskStatusInitializing, - description: "Workspace build is starting (pending)", + name: "PendingStart", + buildStatus: database.ProvisionerJobStatusPending, + buildTransition: database.WorkspaceTransitionStart, + expectedStatus: database.TaskStatusInitializing, + description: "Workspace build is starting (pending)", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: false, + expectWorkspaceAppValid: false, }, { - name: "RunningStart", - buildStatus: database.ProvisionerJobStatusRunning, - buildTransition: database.WorkspaceTransitionStart, - expectedStatus: database.TaskStatusInitializing, - description: "Workspace build is starting (running)", + name: "RunningStart", + buildStatus: database.ProvisionerJobStatusRunning, + buildTransition: database.WorkspaceTransitionStart, + expectedStatus: database.TaskStatusInitializing, + description: "Workspace build is starting (running)", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: false, + expectWorkspaceAppValid: false, }, { - name: "StartingAgent", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateStarting, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, - expectedStatus: database.TaskStatusInitializing, - description: "Workspace is running but agent is starting", + name: "StartingAgent", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateStarting, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, + expectedStatus: database.TaskStatusInitializing, + description: "Workspace is running but agent is starting", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "CreatedAgent", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateCreated, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, - expectedStatus: database.TaskStatusInitializing, - description: "Workspace is running but agent is created", + name: "CreatedAgent", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateCreated, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, + expectedStatus: database.TaskStatusInitializing, + description: "Workspace is running but agent is created", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "ReadyAgentInitializingApp", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, - expectedStatus: database.TaskStatusInitializing, - description: "Agent is ready but app is initializing", + name: "ReadyAgentInitializingApp", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, + expectedStatus: database.TaskStatusInitializing, + description: "Agent is ready but app is initializing", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "ReadyAgentHealthyApp", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy}, - expectedStatus: database.TaskStatusActive, - description: "Agent is ready and app is healthy", + name: "ReadyAgentHealthyApp", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy}, + expectedStatus: database.TaskStatusActive, + description: "Agent is ready and app is healthy", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "ReadyAgentDisabledApp", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthDisabled}, - expectedStatus: database.TaskStatusActive, - description: "Agent is ready and app health checking is disabled", + name: "ReadyAgentDisabledApp", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthDisabled}, + expectedStatus: database.TaskStatusActive, + description: "Agent is ready and app health checking is disabled", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "ReadyAgentUnhealthyApp", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthUnhealthy}, - expectedStatus: database.TaskStatusError, - description: "Agent is ready but app is unhealthy", + name: "ReadyAgentUnhealthyApp", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthUnhealthy}, + expectedStatus: database.TaskStatusError, + description: "Agent is ready but app is unhealthy", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "AgentStartTimeout", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateStartTimeout, - expectedStatus: database.TaskStatusUnknown, - description: "Agent start timed out", + name: "AgentStartTimeout", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateStartTimeout, + expectedStatus: database.TaskStatusUnknown, + description: "Agent start timed out", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: false, }, { - name: "AgentStartError", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateStartError, - expectedStatus: database.TaskStatusUnknown, - description: "Agent failed to start", + name: "AgentStartError", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateStartError, + expectedStatus: database.TaskStatusUnknown, + description: "Agent failed to start", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: false, }, { - name: "AgentShuttingDown", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateShuttingDown, - expectedStatus: database.TaskStatusUnknown, - description: "Agent is shutting down", + name: "AgentShuttingDown", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateShuttingDown, + expectedStatus: database.TaskStatusUnknown, + description: "Agent is shutting down", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: false, }, { - name: "AgentOff", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateOff, - expectedStatus: database.TaskStatusUnknown, - description: "Agent is off", + name: "AgentOff", + buildStatus: database.ProvisionerJobStatusSucceeded, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateOff, + expectedStatus: database.TaskStatusUnknown, + description: "Agent is off", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: false, }, { - name: "RunningJobReadyAgentHealthyApp", - buildStatus: database.ProvisionerJobStatusRunning, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy}, - expectedStatus: database.TaskStatusActive, - description: "Running job with ready agent and healthy app should be active", + name: "RunningJobReadyAgentHealthyApp", + buildStatus: database.ProvisionerJobStatusRunning, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy}, + expectedStatus: database.TaskStatusActive, + description: "Running job with ready agent and healthy app should be active", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "RunningJobReadyAgentInitializingApp", - buildStatus: database.ProvisionerJobStatusRunning, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, - expectedStatus: database.TaskStatusInitializing, - description: "Running job with ready agent but initializing app should be initializing", + name: "RunningJobReadyAgentInitializingApp", + buildStatus: database.ProvisionerJobStatusRunning, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, + expectedStatus: database.TaskStatusInitializing, + description: "Running job with ready agent but initializing app should be initializing", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "RunningJobReadyAgentUnhealthyApp", - buildStatus: database.ProvisionerJobStatusRunning, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthUnhealthy}, - expectedStatus: database.TaskStatusError, - description: "Running job with ready agent but unhealthy app should be error", + name: "RunningJobReadyAgentUnhealthyApp", + buildStatus: database.ProvisionerJobStatusRunning, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthUnhealthy}, + expectedStatus: database.TaskStatusError, + description: "Running job with ready agent but unhealthy app should be error", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "RunningJobConnectingAgent", - buildStatus: database.ProvisionerJobStatusRunning, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateStarting, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, - expectedStatus: database.TaskStatusInitializing, - description: "Running job with connecting agent should be initializing", + name: "RunningJobConnectingAgent", + buildStatus: database.ProvisionerJobStatusRunning, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateStarting, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing}, + expectedStatus: database.TaskStatusInitializing, + description: "Running job with connecting agent should be initializing", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "RunningJobReadyAgentDisabledApp", - buildStatus: database.ProvisionerJobStatusRunning, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthDisabled}, - expectedStatus: database.TaskStatusActive, - description: "Running job with ready agent and disabled app health checking should be active", + name: "RunningJobReadyAgentDisabledApp", + buildStatus: database.ProvisionerJobStatusRunning, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthDisabled}, + expectedStatus: database.TaskStatusActive, + description: "Running job with ready agent and disabled app health checking should be active", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, { - name: "RunningJobReadyAgentHealthyTaskAppUnhealthyOtherAppIsOK", - buildStatus: database.ProvisionerJobStatusRunning, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateReady, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy, database.WorkspaceAppHealthUnhealthy}, - expectedStatus: database.TaskStatusActive, - description: "Running job with ready agent and multiple healthy apps should be active", + name: "RunningJobReadyAgentHealthyTaskAppUnhealthyOtherAppIsOK", + buildStatus: database.ProvisionerJobStatusRunning, + buildTransition: database.WorkspaceTransitionStart, + agentState: database.WorkspaceAgentLifecycleStateReady, + appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy, database.WorkspaceAppHealthUnhealthy}, + expectedStatus: database.TaskStatusActive, + description: "Running job with ready agent and multiple healthy apps should be active", + expectBuildNumberValid: true, + expectBuildNumber: 1, + expectWorkspaceAgentValid: true, + expectWorkspaceAppValid: true, }, } @@ -7018,7 +7109,22 @@ func TestTasksWithStatusView(t *testing.T) { got, err := db.GetTaskByID(ctx, task.ID) require.NoError(t, err) - require.Equal(t, tt.expectedStatus, got.Status, "unexpected status for test case: %s", tt.description) + require.Equal(t, tt.expectedStatus, got.Status) + + require.Equal(t, tt.expectBuildNumberValid, got.WorkspaceBuildNumber.Valid) + if tt.expectBuildNumberValid { + require.Equal(t, tt.expectBuildNumber, got.WorkspaceBuildNumber.Int32) + } + + require.Equal(t, tt.expectWorkspaceAgentValid, got.WorkspaceAgentID.Valid) + if tt.expectWorkspaceAgentValid { + require.NotEqual(t, uuid.Nil, got.WorkspaceAgentID.UUID) + } + + require.Equal(t, tt.expectWorkspaceAppValid, got.WorkspaceAppID.Valid) + if tt.expectWorkspaceAppValid { + require.NotEqual(t, uuid.Nil, got.WorkspaceAppID.UUID) + } }) } } @@ -7095,11 +7201,14 @@ func TestGetTaskByWorkspaceID(t *testing.T) { ctx := testutil.Context(t, testutil.WaitLong) - _, err := db.GetTaskByWorkspaceID(ctx, workspace.ID) + task, err := db.GetTaskByWorkspaceID(ctx, workspace.ID) if tt.wantErr { require.Error(t, err) } else { require.NoError(t, err) + require.False(t, task.WorkspaceBuildNumber.Valid) + require.False(t, task.WorkspaceAgentID.Valid) + require.False(t, task.WorkspaceAppID.Valid) } }) } @@ -7354,7 +7463,7 @@ func TestListTasks(t *testing.T) { }) pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{}) sidebarAppID := uuid.New() - _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + wb := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ JobID: pj.ID, TemplateVersionID: tv.ID, WorkspaceID: ws.ID, @@ -7377,9 +7486,10 @@ func TestListTasks(t *testing.T) { WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, }) _ = dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{ - TaskID: tsk.ID, - WorkspaceAgentID: uuid.NullUUID{Valid: true, UUID: agt.ID}, - WorkspaceAppID: uuid.NullUUID{Valid: true, UUID: wa.ID}, + TaskID: tsk.ID, + WorkspaceBuildNumber: wb.BuildNumber, + WorkspaceAgentID: uuid.NullUUID{Valid: true, UUID: agt.ID}, + WorkspaceAppID: uuid.NullUUID{Valid: true, UUID: wa.ID}, }) t.Logf("task_id:%s owner_id:%s org_id:%s", tsk.ID, ownerID, orgID) return tsk @@ -7441,11 +7551,19 @@ func TestListTasks(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitShort) tasks, err := db.ListTasks(ctx, tc.filter) - if assert.NoError(t, err) { - require.Len(t, tasks, len(tc.expectIDs)) - for idx, eid := range tc.expectIDs { - assert.Equal(t, eid.String(), tasks[idx].ID.String()) - } + require.NoError(t, err) + require.Len(t, tasks, len(tc.expectIDs)) + + for idx, eid := range tc.expectIDs { + task := tasks[idx] + assert.Equal(t, eid, task.ID, "task ID mismatch at index %d", idx) + + require.True(t, task.WorkspaceBuildNumber.Valid) + require.Greater(t, task.WorkspaceBuildNumber.Int32, int32(0)) + require.True(t, task.WorkspaceAgentID.Valid) + require.NotEqual(t, uuid.Nil, task.WorkspaceAgentID.UUID) + require.True(t, task.WorkspaceAppID.Valid) + require.NotEqual(t, uuid.Nil, task.WorkspaceAppID.UUID) } }) } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 558ace2fbe..9319416c56 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12508,7 +12508,7 @@ func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetT } const getTaskByID = `-- name: GetTaskByID :one -SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status FROM tasks_with_status WHERE id = $1::uuid +SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status, workspace_build_number, workspace_agent_id, workspace_app_id FROM tasks_with_status WHERE id = $1::uuid ` func (q *sqlQuerier) GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error) { @@ -12526,12 +12526,15 @@ func (q *sqlQuerier) GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error &i.CreatedAt, &i.DeletedAt, &i.Status, + &i.WorkspaceBuildNumber, + &i.WorkspaceAgentID, + &i.WorkspaceAppID, ) return i, err } const getTaskByWorkspaceID = `-- name: GetTaskByWorkspaceID :one -SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status FROM tasks_with_status WHERE workspace_id = $1::uuid +SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status, workspace_build_number, workspace_agent_id, workspace_app_id FROM tasks_with_status WHERE workspace_id = $1::uuid ` func (q *sqlQuerier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (Task, error) { @@ -12549,6 +12552,9 @@ func (q *sqlQuerier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid. &i.CreatedAt, &i.DeletedAt, &i.Status, + &i.WorkspaceBuildNumber, + &i.WorkspaceAgentID, + &i.WorkspaceAppID, ) return i, err } @@ -12600,7 +12606,7 @@ func (q *sqlQuerier) InsertTask(ctx context.Context, arg InsertTaskParams) (Task } const listTasks = `-- name: ListTasks :many -SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status FROM tasks_with_status tws +SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status, workspace_build_number, workspace_agent_id, workspace_app_id FROM tasks_with_status tws WHERE tws.deleted_at IS NULL AND CASE WHEN $1::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.owner_id = $1::UUID ELSE TRUE END AND CASE WHEN $2::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.organization_id = $2::UUID ELSE TRUE END @@ -12633,6 +12639,9 @@ func (q *sqlQuerier) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task &i.CreatedAt, &i.DeletedAt, &i.Status, + &i.WorkspaceBuildNumber, + &i.WorkspaceAgentID, + &i.WorkspaceAppID, ); err != nil { return nil, err } diff --git a/coderd/database/sqlc.yaml b/coderd/database/sqlc.yaml index d12a7d399c..af700c1451 100644 --- a/coderd/database/sqlc.yaml +++ b/coderd/database/sqlc.yaml @@ -103,6 +103,9 @@ sql: - column: "user_links.claims" go_type: type: "UserLinkClaims" + # Workaround for sqlc not interpreting the left join correctly. + - column: "tasks_with_status.workspace_build_number" + go_type: "database/sql.NullInt32" rename: group_member: GroupMemberTable group_members_expanded: GroupMember diff --git a/codersdk/aitasks.go b/codersdk/aitasks.go index 21efb15b53..67e68daa70 100644 --- a/codersdk/aitasks.go +++ b/codersdk/aitasks.go @@ -116,9 +116,11 @@ type Task struct { TemplateDisplayName string `json:"template_display_name" table:"template display name"` TemplateIcon string `json:"template_icon" table:"template icon"` WorkspaceID uuid.NullUUID `json:"workspace_id" format:"uuid" table:"workspace id"` + WorkspaceBuildNumber int32 `json:"workspace_build_number,omitempty" table:"workspace build number"` WorkspaceAgentID uuid.NullUUID `json:"workspace_agent_id" format:"uuid" table:"workspace agent id"` WorkspaceAgentLifecycle *WorkspaceAgentLifecycle `json:"workspace_agent_lifecycle" table:"workspace agent lifecycle"` WorkspaceAgentHealth *WorkspaceAgentHealth `json:"workspace_agent_health" table:"workspace agent health"` + WorkspaceAppID uuid.NullUUID `json:"workspace_app_id" format:"uuid" table:"workspace app id"` InitialPrompt string `json:"initial_prompt" table:"initial prompt"` Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted" table:"status"` CurrentState *TaskStateEntry `json:"current_state" table:"cs,recursive_inline"` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 526deff931..8d16d63a5e 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -312,6 +312,11 @@ "valid": true }, "workspace_agent_lifecycle": "created", + "workspace_app_id": { + "uuid": "string", + "valid": true + }, + "workspace_build_number": 0, "workspace_id": { "uuid": "string", "valid": true @@ -7705,6 +7710,11 @@ Only certain features set these fields: - FeatureManagedAgentLimit| "valid": true }, "workspace_agent_lifecycle": "created", + "workspace_app_id": { + "uuid": "string", + "valid": true + }, + "workspace_build_number": 0, "workspace_id": { "uuid": "string", "valid": true @@ -7733,6 +7743,8 @@ Only certain features set these fields: - FeatureManagedAgentLimit| | `workspace_agent_health` | [codersdk.WorkspaceAgentHealth](#codersdkworkspaceagenthealth) | false | | | | `workspace_agent_id` | [uuid.NullUUID](#uuidnulluuid) | false | | | | `workspace_agent_lifecycle` | [codersdk.WorkspaceAgentLifecycle](#codersdkworkspaceagentlifecycle) | false | | | +| `workspace_app_id` | [uuid.NullUUID](#uuidnulluuid) | false | | | +| `workspace_build_number` | integer | false | | | | `workspace_id` | [uuid.NullUUID](#uuidnulluuid) | false | | | #### Enumerated Values diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 71329fb318..1deb951b79 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3348,9 +3348,11 @@ export interface Task { readonly template_display_name: string; readonly template_icon: string; readonly workspace_id: string | null; + readonly workspace_build_number?: number; readonly workspace_agent_id: string | null; readonly workspace_agent_lifecycle: WorkspaceAgentLifecycle | null; readonly workspace_agent_health: WorkspaceAgentHealth | null; + readonly workspace_app_id: string | null; readonly initial_prompt: string; readonly status: WorkspaceStatus; readonly current_state: TaskStateEntry | null; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 0b8d87f811..17e8c68315 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -5028,9 +5028,11 @@ export const MockTask: TypesGen.Task = { template_display_name: MockTemplate.display_name, template_icon: MockTemplate.icon, workspace_id: MockWorkspace.id, + workspace_build_number: MockWorkspaceBuild.build_number, workspace_agent_id: MockWorkspaceAgent.id, workspace_agent_lifecycle: MockWorkspaceAgent.lifecycle_state, workspace_agent_health: MockWorkspaceAgent.health, + workspace_app_id: MockWorkspaceApp.id, initial_prompt: "Perform some task", status: "running", current_state: {