diff --git a/coderd/database/check_constraint.go b/coderd/database/check_constraint.go index 99cc39159d2..018869ffd08 100644 --- a/coderd/database/check_constraint.go +++ b/coderd/database/check_constraint.go @@ -57,11 +57,6 @@ const ( CheckOauth2ProviderAppsClientTypeCheck CheckConstraint = "oauth2_provider_apps_client_type_check" // oauth2_provider_apps CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs CheckNatsPortValidTcp CheckConstraint = "nats_port_valid_tcp" // replicas - CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents - CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents - CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds - CheckGroupAclIsObject CheckConstraint = "group_acl_is_object" // workspaces - CheckUserAclIsObject CheckConstraint = "user_acl_is_object" // workspaces CheckTelemetryLockEventTypeConstraint CheckConstraint = "telemetry_lock_event_type_constraint" // telemetry_locks CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events @@ -73,6 +68,8 @@ const ( CheckUserSkillsDescriptionSize CheckConstraint = "user_skills_description_size" // user_skills CheckUserSkillsNameFormat CheckConstraint = "user_skills_name_format" // user_skills CheckUserSkillsNameSize CheckConstraint = "user_skills_name_size" // user_skills + CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents + CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents CheckWorkspaceBuildOrchestrationsAttemptCountCheck CheckConstraint = "workspace_build_orchestrations_attempt_count_check" // workspace_build_orchestrations CheckWorkspaceBuildOrchestrationsChildLogLevelCheck CheckConstraint = "workspace_build_orchestrations_child_log_level_check" // workspace_build_orchestrations CheckWorkspaceBuildOrchestrationsChildParametersCheck CheckConstraint = "workspace_build_orchestrations_child_parameters_check" // workspace_build_orchestrations @@ -80,4 +77,7 @@ const ( CheckWorkspaceBuildOrchestrationsCompletedChildCheck CheckConstraint = "workspace_build_orchestrations_completed_child_check" // workspace_build_orchestrations CheckWorkspaceBuildOrchestrationsNextRetryAfterCheck CheckConstraint = "workspace_build_orchestrations_next_retry_after_check" // workspace_build_orchestrations CheckWorkspaceBuildOrchestrationsStatusCheck CheckConstraint = "workspace_build_orchestrations_status_check" // workspace_build_orchestrations + CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds + CheckGroupAclIsObject CheckConstraint = "group_acl_is_object" // workspaces + CheckUserAclIsObject CheckConstraint = "user_acl_is_object" // workspaces ) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 4b281f074c8..65a5d8c12c4 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2593,19 +2593,6 @@ func (q *querier) DeleteTailnetTunnel(ctx context.Context, arg database.DeleteTa return q.db.DeleteTailnetTunnel(ctx, arg) } -func (q *querier) DeleteTask(ctx context.Context, arg database.DeleteTaskParams) (uuid.UUID, error) { - task, err := q.db.GetTaskByID(ctx, arg.ID) - if err != nil { - return uuid.UUID{}, err - } - - if err := q.authorizeContext(ctx, policy.ActionDelete, task.RBACObject()); err != nil { - return uuid.UUID{}, err - } - - return q.db.DeleteTask(ctx, arg) -} - func (q *querier) DeleteUnlinkedChatFilesByIDs(ctx context.Context, arg database.DeleteUnlinkedChatFilesByIDsParams) (int64, error) { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return 0, err @@ -4785,32 +4772,6 @@ func (q *querier) GetTailnetTunnelPeerIDsBatch(ctx context.Context, ids []uuid.U return q.db.GetTailnetTunnelPeerIDsBatch(ctx, ids) } -func (q *querier) GetTaskByID(ctx context.Context, id uuid.UUID) (database.Task, error) { - return fetch(q.log, q.auth, q.db.GetTaskByID)(ctx, id) -} - -func (q *querier) GetTaskByOwnerIDAndName(ctx context.Context, arg database.GetTaskByOwnerIDAndNameParams) (database.Task, error) { - return fetch(q.log, q.auth, q.db.GetTaskByOwnerIDAndName)(ctx, arg) -} - -func (q *querier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.Task, error) { - return fetch(q.log, q.auth, q.db.GetTaskByWorkspaceID)(ctx, workspaceID) -} - -func (q *querier) GetTaskSnapshot(ctx context.Context, taskID uuid.UUID) (database.TaskSnapshot, error) { - // Fetch task to build RBAC object for authorization. - task, err := q.GetTaskByID(ctx, taskID) - if err != nil { - return database.TaskSnapshot{}, err - } - - if err := q.authorizeContext(ctx, policy.ActionRead, task.RBACObject()); err != nil { - return database.TaskSnapshot{}, err - } - - return q.db.GetTaskSnapshot(ctx, taskID) -} - func (q *querier) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return database.TelemetryItem{}, err @@ -4825,13 +4786,6 @@ func (q *querier) GetTelemetryItems(ctx context.Context) ([]database.TelemetryIt return q.db.GetTelemetryItems(ctx) } -func (q *querier) GetTelemetryTaskEvents(ctx context.Context, arg database.GetTelemetryTaskEventsParams) ([]database.GetTelemetryTaskEventsRow, error) { - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTask.All()); err != nil { - return nil, err - } - return q.db.GetTelemetryTaskEvents(ctx, arg) -} - func (q *querier) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil { return nil, err @@ -6498,17 +6452,6 @@ func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaP return q.db.InsertReplica(ctx, arg) } -func (q *querier) InsertTask(ctx context.Context, arg database.InsertTaskParams) (database.TaskTable, error) { - // Ensure the actor can access the specified template version (and thus its template). - if _, err := q.GetTemplateVersionByID(ctx, arg.TemplateVersionID); err != nil { - return database.TaskTable{}, err - } - - obj := rbac.ResourceTask.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID) - - return insert(q.log, q.auth, obj, q.db.InsertTask)(ctx, arg) -} - func (q *querier) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { return err @@ -7012,11 +6955,6 @@ func (q *querier) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.C return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganizationExcludeReserved)(ctx, organizationID) } -func (q *querier) ListTasks(ctx context.Context, arg database.ListTasksParams) ([]database.Task, error) { - // TODO(Cian): replace this with a sql filter for improved performance. https://github.com/coder/internal/issues/1061 - return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListTasks)(ctx, arg) -} - func (q *querier) ListUsageEventCreatedAtsByTypeSince(ctx context.Context, arg database.ListUsageEventCreatedAtsByTypeSinceParams) ([]time.Time, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUsageEvent); err != nil { return nil, err @@ -8187,45 +8125,6 @@ func (q *querier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg return q.db.UpdateTailnetPeerStatusByCoordinator(ctx, arg) } -func (q *querier) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) { - // An actor is allowed to update the prompt of a task if they have - // permission to update the task (same as UpdateTaskWorkspaceID). - task, err := q.db.GetTaskByID(ctx, arg.ID) - if err != nil { - return database.TaskTable{}, err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil { - return database.TaskTable{}, err - } - - return q.db.UpdateTaskPrompt(ctx, arg) -} - -func (q *querier) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) { - // An actor is allowed to update the workspace ID of a task if they are the - // owner of the task and workspace or have the appropriate permissions. - task, err := q.db.GetTaskByID(ctx, arg.ID) - if err != nil { - return database.TaskTable{}, err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil { - return database.TaskTable{}, err - } - - ws, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID.UUID) - if err != nil { - return database.TaskTable{}, err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, ws.RBACObject()); err != nil { - return database.TaskTable{}, err - } - - return q.db.UpdateTaskWorkspaceID(ctx, arg) -} - func (q *querier) UpdateTemplateACLByID(ctx context.Context, arg database.UpdateTemplateACLByIDParams) error { fetch := func(ctx context.Context, arg database.UpdateTemplateACLByIDParams) (database.Template, error) { return q.db.GetTemplateByID(ctx, arg.ID) @@ -9316,32 +9215,6 @@ func (q *querier) UpsertTailnetTunnel(ctx context.Context, arg database.UpsertTa return q.db.UpsertTailnetTunnel(ctx, arg) } -func (q *querier) UpsertTaskSnapshot(ctx context.Context, arg database.UpsertTaskSnapshotParams) error { - // Fetch task to build RBAC object for authorization. - task, err := q.GetTaskByID(ctx, arg.TaskID) - if err != nil { - return err - } - - if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil { - return err - } - - return q.db.UpsertTaskSnapshot(ctx, arg) -} - -func (q *querier) UpsertTaskWorkspaceApp(ctx context.Context, arg database.UpsertTaskWorkspaceAppParams) (database.TaskWorkspaceApp, error) { - // Fetch the task to derive the RBAC object and authorize update on it. - task, err := q.db.GetTaskByID(ctx, arg.TaskID) - if err != nil { - return database.TaskWorkspaceApp{}, err - } - if err := q.authorizeContext(ctx, policy.ActionUpdate, task); err != nil { - return database.TaskWorkspaceApp{}, err - } - return q.db.UpsertTaskWorkspaceApp(ctx, arg) -} - func (q *querier) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 9f87cfec008..a76fe1584c8 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3013,11 +3013,6 @@ func (s *MethodTestSuite) TestTemplate() { dbm.EXPECT().GetTemplateInsightsByTemplate(gomock.Any(), arg).Return([]database.GetTemplateInsightsByTemplateRow{}, nil).AnyTimes() check.Args(arg).Asserts(rbac.ResourceTemplate, policy.ActionViewInsights) })) - s.Run("GetTelemetryTaskEvents", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { - arg := database.GetTelemetryTaskEventsParams{} - dbm.EXPECT().GetTelemetryTaskEvents(gomock.Any(), arg).Return([]database.GetTelemetryTaskEventsRow{}, nil).AnyTimes() - check.Args(arg).Asserts(rbac.ResourceTask.All(), policy.ActionRead) - })) s.Run("GetTemplateAppInsights", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { arg := database.GetTemplateAppInsightsParams{} dbm.EXPECT().GetTemplateAppInsights(gomock.Any(), arg).Return([]database.GetTemplateAppInsightsRow{}, nil).AnyTimes() @@ -4602,134 +4597,6 @@ func (s *MethodTestSuite) TestWorkspacePortSharing() { })) } -func (s *MethodTestSuite) TestTasks() { - s.Run("GetTaskByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes() - check.Args(task.ID).Asserts(task, policy.ActionRead).Returns(task) - })) - s.Run("GetTaskByOwnerIDAndName", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - dbm.EXPECT().GetTaskByOwnerIDAndName(gomock.Any(), database.GetTaskByOwnerIDAndNameParams{ - OwnerID: task.OwnerID, - Name: task.Name, - }).Return(task, nil).AnyTimes() - check.Args(database.GetTaskByOwnerIDAndNameParams{ - OwnerID: task.OwnerID, - Name: task.Name, - }).Asserts(task, policy.ActionRead).Returns(task) - })) - s.Run("DeleteTask", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - arg := database.DeleteTaskParams{ - ID: task.ID, - DeletedAt: dbtime.Now(), - } - dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes() - dbm.EXPECT().DeleteTask(gomock.Any(), arg).Return(task.ID, nil).AnyTimes() - check.Args(arg).Asserts(task, policy.ActionDelete).Returns(task.ID) - })) - s.Run("InsertTask", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - tpl := testutil.Fake(s.T(), faker, database.Template{}) - tv := testutil.Fake(s.T(), faker, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true}, - OrganizationID: tpl.OrganizationID, - }) - - arg := testutil.Fake(s.T(), faker, database.InsertTaskParams{ - OrganizationID: tpl.OrganizationID, - TemplateVersionID: tv.ID, - }) - - dbm.EXPECT().GetTemplateVersionByID(gomock.Any(), tv.ID).Return(tv, nil).AnyTimes() - dbm.EXPECT().GetTemplateByID(gomock.Any(), tpl.ID).Return(tpl, nil).AnyTimes() - dbm.EXPECT().InsertTask(gomock.Any(), arg).Return(database.TaskTable{}, nil).AnyTimes() - - check.Args(arg).Asserts( - tpl, policy.ActionRead, - rbac.ResourceTask.InOrg(arg.OrganizationID).WithOwner(arg.OwnerID.String()), policy.ActionCreate, - ).Returns(database.TaskTable{}) - })) - s.Run("UpsertTaskWorkspaceApp", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - arg := database.UpsertTaskWorkspaceAppParams{ - TaskID: task.ID, - WorkspaceBuildNumber: 1, - } - - dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes() - dbm.EXPECT().UpsertTaskWorkspaceApp(gomock.Any(), arg).Return(database.TaskWorkspaceApp{}, nil).AnyTimes() - - check.Args(arg).Asserts(task, policy.ActionUpdate).Returns(database.TaskWorkspaceApp{}) - })) - s.Run("UpdateTaskWorkspaceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - ws := testutil.Fake(s.T(), faker, database.Workspace{}) - arg := database.UpdateTaskWorkspaceIDParams{ - ID: task.ID, - WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, - } - - dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes() - dbm.EXPECT().GetWorkspaceByID(gomock.Any(), ws.ID).Return(ws, nil).AnyTimes() - dbm.EXPECT().UpdateTaskWorkspaceID(gomock.Any(), arg).Return(database.TaskTable{}, nil).AnyTimes() - - check.Args(arg).Asserts(task, policy.ActionUpdate, ws, policy.ActionUpdate).Returns(database.TaskTable{}) - })) - s.Run("UpdateTaskPrompt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - arg := database.UpdateTaskPromptParams{ - ID: task.ID, - Prompt: "Updated prompt text", - } - - // Create a copy of the task with the updated prompt - updatedTask := task - updatedTask.Prompt = arg.Prompt - - dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes() - dbm.EXPECT().UpdateTaskPrompt(gomock.Any(), arg).Return(updatedTask.TaskTable(), nil).AnyTimes() - - check.Args(arg).Asserts(task, policy.ActionUpdate).Returns(updatedTask.TaskTable()) - })) - s.Run("GetTaskByWorkspaceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - task.WorkspaceID = uuid.NullUUID{UUID: uuid.New(), Valid: true} - dbm.EXPECT().GetTaskByWorkspaceID(gomock.Any(), task.WorkspaceID.UUID).Return(task, nil).AnyTimes() - check.Args(task.WorkspaceID.UUID).Asserts(task, policy.ActionRead).Returns(task) - })) - s.Run("ListTasks", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - u1 := testutil.Fake(s.T(), faker, database.User{}) - u2 := testutil.Fake(s.T(), faker, database.User{}) - org1 := testutil.Fake(s.T(), faker, database.Organization{}) - org2 := testutil.Fake(s.T(), faker, database.Organization{}) - _ = testutil.Fake(s.T(), faker, database.OrganizationMember{UserID: u1.ID, OrganizationID: org1.ID}) - _ = testutil.Fake(s.T(), faker, database.OrganizationMember{UserID: u2.ID, OrganizationID: org2.ID}) - t1 := testutil.Fake(s.T(), faker, database.Task{OwnerID: u1.ID}) - t2 := testutil.Fake(s.T(), faker, database.Task{OwnerID: u2.ID}) - dbm.EXPECT().ListTasks(gomock.Any(), gomock.Any()).Return([]database.Task{t1, t2}, nil).AnyTimes() - check.Args(database.ListTasksParams{}).Asserts(t1, policy.ActionRead, t2, policy.ActionRead).Returns([]database.Task{t1, t2}) - })) - s.Run("GetTaskSnapshot", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - snapshot := testutil.Fake(s.T(), faker, database.TaskSnapshot{TaskID: task.ID}) - dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes() - dbm.EXPECT().GetTaskSnapshot(gomock.Any(), task.ID).Return(snapshot, nil).AnyTimes() - check.Args(task.ID).Asserts(task, policy.ActionRead, task, policy.ActionRead).Returns(snapshot) - })) - s.Run("UpsertTaskSnapshot", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { - task := testutil.Fake(s.T(), faker, database.Task{}) - arg := database.UpsertTaskSnapshotParams{ - TaskID: task.ID, - LogSnapshot: []byte(`{"format":"agentapi","data":[]}`), - LogSnapshotCreatedAt: dbtime.Now(), - } - dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes() - dbm.EXPECT().UpsertTaskSnapshot(gomock.Any(), arg).Return(nil).AnyTimes() - check.Args(arg).Asserts(task, policy.ActionRead, task, policy.ActionUpdate).Returns() - })) -} - func (s *MethodTestSuite) TestProvisionerKeys() { s.Run("InsertProvisionerKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { org := testutil.Fake(s.T(), faker, database.Organization{}) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 1bcd25408a3..fe465fda4a6 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -40,7 +40,6 @@ type WorkspaceResponse struct { Agents []database.WorkspaceAgent AgentToken string TemplateVersionResponse - Task database.Task } // WorkspaceBuildBuilder generates workspace builds and associated @@ -56,8 +55,6 @@ type WorkspaceBuildBuilder struct { params []database.WorkspaceBuildParameter agentToken string jobStatus database.ProvisionerJobStatus - taskAppID uuid.UUID - taskSeed database.TaskTable // Individual timestamp fields for job customization. jobCreatedAt time.Time @@ -192,31 +189,6 @@ func (b WorkspaceBuildBuilder) WithAgent(mutations ...func([]*sdkproto.Agent) [] return b } -func (b WorkspaceBuildBuilder) WithTask(taskSeed database.TaskTable, appSeed *sdkproto.App) WorkspaceBuildBuilder { - //nolint:revive // returns modified struct - b.taskSeed = taskSeed - - if appSeed == nil { - appSeed = &sdkproto.App{} - } - - var err error - //nolint: revive // returns modified struct - b.taskAppID, err = uuid.Parse(takeFirst(appSeed.Id, uuid.NewString())) - require.NoError(b.t, err) - - return b.WithAgent(func(a []*sdkproto.Agent) []*sdkproto.Agent { - a[0].Apps = []*sdkproto.App{ - { - Id: b.taskAppID.String(), - Slug: takeFirst(appSeed.Slug, "task-app"), - Url: takeFirst(appSeed.Url, ""), - }, - } - return a - }) -} - // Starting sets the job to running status. func (b WorkspaceBuildBuilder) Starting(opts ...BuilderOption) WorkspaceBuildBuilder { //nolint: revive // returns modified struct @@ -336,39 +308,6 @@ func (b WorkspaceBuildBuilder) doInTX() WorkspaceResponse { b.seed.WorkspaceID = b.ws.ID b.seed.InitiatorID = takeFirst(b.seed.InitiatorID, b.ws.OwnerID) - // If a task was requested, ensure it exists and is associated with this - // workspace. - if b.taskAppID != uuid.Nil { - b.logger.Debug(context.Background(), "creating or updating task", slog.F("task_id", b.taskSeed.ID)) - b.taskSeed.OrganizationID = takeFirst(b.taskSeed.OrganizationID, b.ws.OrganizationID) - b.taskSeed.OwnerID = takeFirst(b.taskSeed.OwnerID, b.ws.OwnerID) - b.taskSeed.Name = takeFirst(b.taskSeed.Name, b.ws.Name) - b.taskSeed.WorkspaceID = uuid.NullUUID{UUID: takeFirst(b.taskSeed.WorkspaceID.UUID, b.ws.ID), Valid: true} - b.taskSeed.TemplateVersionID = takeFirst(b.taskSeed.TemplateVersionID, b.seed.TemplateVersionID) - - // Try to fetch existing task and update its workspace ID. - if task, err := b.db.GetTaskByID(ownerCtx, b.taskSeed.ID); err == nil { - if !task.WorkspaceID.Valid { - b.logger.Info(context.Background(), "updating task workspace id", - slog.F("task_id", b.taskSeed.ID), - slog.F("workspace_id", b.ws.ID)) - _, err = b.db.UpdateTaskWorkspaceID(ownerCtx, database.UpdateTaskWorkspaceIDParams{ - ID: b.taskSeed.ID, - WorkspaceID: uuid.NullUUID{UUID: b.ws.ID, Valid: true}, - }) - require.NoError(b.t, err, "update task workspace id") - } else if task.WorkspaceID.UUID != b.ws.ID { - require.Fail(b.t, "task already has a workspace id, mismatch", task.WorkspaceID.UUID, b.ws.ID) - } - } else if errors.Is(err, sql.ErrNoRows) { - task := dbgen.Task(b.t, b.db, b.taskSeed) - b.taskSeed.ID = task.ID - b.logger.Info(context.Background(), "created new task", slog.F("task_id", b.taskSeed.ID)) - } else { - require.NoError(b.t, err, "get task by id") - } - } - // Create a provisioner job for the build! payload, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: b.seed.ID, @@ -492,45 +431,6 @@ func (b WorkspaceBuildBuilder) doInTX() WorkspaceResponse { slog.F("workspace_id", resp.Workspace.ID), slog.F("build_number", resp.Build.BuildNumber)) - // If this is a task workspace, link it to the workspace build. - task, err := b.db.GetTaskByWorkspaceID(ownerCtx, resp.Workspace.ID) - if err != nil { - if b.taskAppID != uuid.Nil { - require.Fail(b.t, "task app configured but failed to get task by workspace id", err) - } - } else { - if b.taskAppID == uuid.Nil { - require.Fail(b.t, "task app not configured but workspace is a task workspace") - } - - workspaceAgentID := uuid.NullUUID{} - workspaceAppID := uuid.NullUUID{} - // Workspace agent and app are only properly set upon job completion, and - // only start builds have agents. - isStart := b.seed.Transition == "" || b.seed.Transition == database.WorkspaceTransitionStart - if isStart && b.jobStatus != database.ProvisionerJobStatusPending && b.jobStatus != database.ProvisionerJobStatusRunning { - app := mustWorkspaceAppByWorkspaceAndBuildAndAppID(ownerCtx, b.t, b.db, resp.Workspace.ID, resp.Build.BuildNumber, b.taskAppID) - workspaceAgentID = uuid.NullUUID{UUID: app.AgentID, Valid: true} - workspaceAppID = uuid.NullUUID{UUID: app.ID, Valid: true} - } - - _, err = b.db.UpsertTaskWorkspaceApp(ownerCtx, database.UpsertTaskWorkspaceAppParams{ - TaskID: task.ID, - WorkspaceBuildNumber: resp.Build.BuildNumber, - WorkspaceAgentID: workspaceAgentID, - WorkspaceAppID: workspaceAppID, - }) - require.NoError(b.t, err, "upsert task workspace app") - b.logger.Debug(context.Background(), "linked task to workspace build", - slog.F("task_id", task.ID), - slog.F("build_number", resp.Build.BuildNumber)) - - // Update task after linking. - task, err = b.db.GetTaskByID(ownerCtx, task.ID) - require.NoError(b.t, err, "get task by id") - resp.Task = task - } - for i := range b.params { b.params[i].WorkspaceBuildID = resp.Build.ID } @@ -904,30 +804,3 @@ func takeFirstTime(values ...time.Time) time.Time { } return time.Time{} } - -// mustWorkspaceAppByWorkspaceAndBuildAndAppID finds a workspace app by -// workspace ID, build number, and app ID. It returns the workspace app -// if found, otherwise fails the test. -func mustWorkspaceAppByWorkspaceAndBuildAndAppID(ctx context.Context, t testing.TB, db database.Store, workspaceID uuid.UUID, buildNumber int32, appID uuid.UUID) database.WorkspaceApp { - t.Helper() - - agents, err := db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx, database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{ - WorkspaceID: workspaceID, - BuildNumber: buildNumber, - }) - require.NoError(t, err, "get workspace agents") - require.NotEmpty(t, agents, "no agents found for workspace") - - for _, agent := range agents { - apps, err := db.GetWorkspaceAppsByAgentID(ctx, agent.ID) - require.NoError(t, err, "get workspace apps") - for _, app := range apps { - if app.ID == appID { - return app - } - } - } - - require.FailNow(t, "could not find workspace app", "workspaceID=%s buildNumber=%d appID=%s", workspaceID, buildNumber, appID) - return database.WorkspaceApp{} // Unreachable. -} diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index b6a8f82867c..975e2ca32d3 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -2167,50 +2167,6 @@ func AIBridgeModelThought(t testing.TB, db database.Store, seed database.InsertA return thought } -func Task(t testing.TB, db database.Store, orig database.TaskTable) database.Task { - t.Helper() - - parameters := orig.TemplateParameters - if parameters == nil { - parameters = json.RawMessage([]byte("{}")) - } - - task, err := db.InsertTask(genCtx, database.InsertTaskParams{ - ID: takeFirst(orig.ID, uuid.New()), - OrganizationID: orig.OrganizationID, - OwnerID: orig.OwnerID, - Name: takeFirst(orig.Name, testutil.GetRandomNameHyphenated(t)), - DisplayName: takeFirst(orig.DisplayName, testutil.GetRandomNameHyphenated(t)), - WorkspaceID: orig.WorkspaceID, - TemplateVersionID: orig.TemplateVersionID, - TemplateParameters: parameters, - Prompt: orig.Prompt, - CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()), - }) - require.NoError(t, err, "failed to insert task") - - // Return the Task from the view instead of the TaskTable - fetched, err := db.GetTaskByID(genCtx, task.ID) - require.NoError(t, err, "failed to fetch task") - require.Equal(t, task.ID, fetched.ID) - - return fetched -} - -func TaskWorkspaceApp(t testing.TB, db database.Store, orig database.TaskWorkspaceApp) database.TaskWorkspaceApp { - t.Helper() - - app, err := db.UpsertTaskWorkspaceApp(genCtx, database.UpsertTaskWorkspaceAppParams{ - TaskID: orig.TaskID, - WorkspaceBuildNumber: orig.WorkspaceBuildNumber, - WorkspaceAgentID: orig.WorkspaceAgentID, - WorkspaceAppID: orig.WorkspaceAppID, - }) - require.NoError(t, err, "failed to upsert task workspace app") - - return app -} - func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming { timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{ JobID: takeFirst(seed.JobID, uuid.New()), diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 07b671294d1..4bebd0a80cf 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -881,14 +881,6 @@ func (m queryMetricsStore) DeleteTailnetTunnel(ctx context.Context, arg database return r0, r1 } -func (m queryMetricsStore) DeleteTask(ctx context.Context, arg database.DeleteTaskParams) (uuid.UUID, error) { - start := time.Now() - r0, r1 := m.s.DeleteTask(ctx, arg) - m.queryLatencies.WithLabelValues("DeleteTask").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteTask").Inc() - return r0, r1 -} - func (m queryMetricsStore) DeleteUnlinkedChatFilesByIDs(ctx context.Context, arg database.DeleteUnlinkedChatFilesByIDsParams) (int64, error) { start := time.Now() r0, r1 := m.s.DeleteUnlinkedChatFilesByIDs(ctx, arg) @@ -2977,38 +2969,6 @@ func (m queryMetricsStore) GetTailnetTunnelPeerIDsBatch(ctx context.Context, ids return r0, r1 } -func (m queryMetricsStore) GetTaskByID(ctx context.Context, id uuid.UUID) (database.Task, error) { - start := time.Now() - r0, r1 := m.s.GetTaskByID(ctx, id) - m.queryLatencies.WithLabelValues("GetTaskByID").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetTaskByID").Inc() - return r0, r1 -} - -func (m queryMetricsStore) GetTaskByOwnerIDAndName(ctx context.Context, arg database.GetTaskByOwnerIDAndNameParams) (database.Task, error) { - start := time.Now() - r0, r1 := m.s.GetTaskByOwnerIDAndName(ctx, arg) - m.queryLatencies.WithLabelValues("GetTaskByOwnerIDAndName").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetTaskByOwnerIDAndName").Inc() - return r0, r1 -} - -func (m queryMetricsStore) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.Task, error) { - start := time.Now() - r0, r1 := m.s.GetTaskByWorkspaceID(ctx, workspaceID) - m.queryLatencies.WithLabelValues("GetTaskByWorkspaceID").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetTaskByWorkspaceID").Inc() - return r0, r1 -} - -func (m queryMetricsStore) GetTaskSnapshot(ctx context.Context, taskID uuid.UUID) (database.TaskSnapshot, error) { - start := time.Now() - r0, r1 := m.s.GetTaskSnapshot(ctx, taskID) - m.queryLatencies.WithLabelValues("GetTaskSnapshot").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetTaskSnapshot").Inc() - return r0, r1 -} - func (m queryMetricsStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { start := time.Now() r0, r1 := m.s.GetTelemetryItem(ctx, key) @@ -3025,14 +2985,6 @@ func (m queryMetricsStore) GetTelemetryItems(ctx context.Context) ([]database.Te return r0, r1 } -func (m queryMetricsStore) GetTelemetryTaskEvents(ctx context.Context, createdAfter database.GetTelemetryTaskEventsParams) ([]database.GetTelemetryTaskEventsRow, error) { - start := time.Now() - r0, r1 := m.s.GetTelemetryTaskEvents(ctx, createdAfter) - m.queryLatencies.WithLabelValues("GetTelemetryTaskEvents").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetTelemetryTaskEvents").Inc() - return r0, r1 -} - func (m queryMetricsStore) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { start := time.Now() r0, r1 := m.s.GetTemplateAppInsights(ctx, arg) @@ -4489,14 +4441,6 @@ func (m queryMetricsStore) InsertReplica(ctx context.Context, arg database.Inser return r0, r1 } -func (m queryMetricsStore) InsertTask(ctx context.Context, arg database.InsertTaskParams) (database.TaskTable, error) { - start := time.Now() - r0, r1 := m.s.InsertTask(ctx, arg) - m.queryLatencies.WithLabelValues("InsertTask").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertTask").Inc() - return r0, r1 -} - func (m queryMetricsStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { start := time.Now() r0 := m.s.InsertTelemetryItemIfNotExists(ctx, arg) @@ -4897,14 +4841,6 @@ func (m queryMetricsStore) ListProvisionerKeysByOrganizationExcludeReserved(ctx return r0, r1 } -func (m queryMetricsStore) ListTasks(ctx context.Context, arg database.ListTasksParams) ([]database.Task, error) { - start := time.Now() - r0, r1 := m.s.ListTasks(ctx, arg) - m.queryLatencies.WithLabelValues("ListTasks").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListTasks").Inc() - return r0, r1 -} - func (m queryMetricsStore) ListUsageEventCreatedAtsByTypeSince(ctx context.Context, arg database.ListUsageEventCreatedAtsByTypeSinceParams) ([]time.Time, error) { start := time.Now() r0, r1 := m.s.ListUsageEventCreatedAtsByTypeSince(ctx, arg) @@ -5721,22 +5657,6 @@ func (m queryMetricsStore) UpdateTailnetPeerStatusByCoordinator(ctx context.Cont return r0, r1 } -func (m queryMetricsStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) { - start := time.Now() - r0, r1 := m.s.UpdateTaskPrompt(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateTaskPrompt").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateTaskPrompt").Inc() - return r0, r1 -} - -func (m queryMetricsStore) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) { - start := time.Now() - r0, r1 := m.s.UpdateTaskWorkspaceID(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateTaskWorkspaceID").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateTaskWorkspaceID").Inc() - return r0, r1 -} - func (m queryMetricsStore) UpdateTemplateACLByID(ctx context.Context, arg database.UpdateTemplateACLByIDParams) error { start := time.Now() r0 := m.s.UpdateTemplateACLByID(ctx, arg) @@ -6609,22 +6529,6 @@ func (m queryMetricsStore) UpsertTailnetTunnel(ctx context.Context, arg database return r0, r1 } -func (m queryMetricsStore) UpsertTaskSnapshot(ctx context.Context, arg database.UpsertTaskSnapshotParams) error { - start := time.Now() - r0 := m.s.UpsertTaskSnapshot(ctx, arg) - m.queryLatencies.WithLabelValues("UpsertTaskSnapshot").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertTaskSnapshot").Inc() - return r0 -} - -func (m queryMetricsStore) UpsertTaskWorkspaceApp(ctx context.Context, arg database.UpsertTaskWorkspaceAppParams) (database.TaskWorkspaceApp, error) { - start := time.Now() - r0, r1 := m.s.UpsertTaskWorkspaceApp(ctx, arg) - m.queryLatencies.WithLabelValues("UpsertTaskWorkspaceApp").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertTaskWorkspaceApp").Inc() - return r0, r1 -} - func (m queryMetricsStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { start := time.Now() r0 := m.s.UpsertTelemetryItem(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 9185d784019..6426f67415a 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1514,21 +1514,6 @@ func (mr *MockStoreMockRecorder) DeleteTailnetTunnel(ctx, arg any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTailnetTunnel", reflect.TypeOf((*MockStore)(nil).DeleteTailnetTunnel), ctx, arg) } -// DeleteTask mocks base method. -func (m *MockStore) DeleteTask(ctx context.Context, arg database.DeleteTaskParams) (uuid.UUID, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteTask", ctx, arg) - ret0, _ := ret[0].(uuid.UUID) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DeleteTask indicates an expected call of DeleteTask. -func (mr *MockStoreMockRecorder) DeleteTask(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTask", reflect.TypeOf((*MockStore)(nil).DeleteTask), ctx, arg) -} - // DeleteUnlinkedChatFilesByIDs mocks base method. func (m *MockStore) DeleteUnlinkedChatFilesByIDs(ctx context.Context, arg database.DeleteUnlinkedChatFilesByIDsParams) (int64, error) { m.ctrl.T.Helper() @@ -5580,66 +5565,6 @@ func (mr *MockStoreMockRecorder) GetTailnetTunnelPeerIDsBatch(ctx, ids any) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetTunnelPeerIDsBatch", reflect.TypeOf((*MockStore)(nil).GetTailnetTunnelPeerIDsBatch), ctx, ids) } -// GetTaskByID mocks base method. -func (m *MockStore) GetTaskByID(ctx context.Context, id uuid.UUID) (database.Task, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTaskByID", ctx, id) - ret0, _ := ret[0].(database.Task) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTaskByID indicates an expected call of GetTaskByID. -func (mr *MockStoreMockRecorder) GetTaskByID(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskByID", reflect.TypeOf((*MockStore)(nil).GetTaskByID), ctx, id) -} - -// GetTaskByOwnerIDAndName mocks base method. -func (m *MockStore) GetTaskByOwnerIDAndName(ctx context.Context, arg database.GetTaskByOwnerIDAndNameParams) (database.Task, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTaskByOwnerIDAndName", ctx, arg) - ret0, _ := ret[0].(database.Task) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTaskByOwnerIDAndName indicates an expected call of GetTaskByOwnerIDAndName. -func (mr *MockStoreMockRecorder) GetTaskByOwnerIDAndName(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskByOwnerIDAndName", reflect.TypeOf((*MockStore)(nil).GetTaskByOwnerIDAndName), ctx, arg) -} - -// GetTaskByWorkspaceID mocks base method. -func (m *MockStore) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.Task, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTaskByWorkspaceID", ctx, workspaceID) - ret0, _ := ret[0].(database.Task) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTaskByWorkspaceID indicates an expected call of GetTaskByWorkspaceID. -func (mr *MockStoreMockRecorder) GetTaskByWorkspaceID(ctx, workspaceID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskByWorkspaceID", reflect.TypeOf((*MockStore)(nil).GetTaskByWorkspaceID), ctx, workspaceID) -} - -// GetTaskSnapshot mocks base method. -func (m *MockStore) GetTaskSnapshot(ctx context.Context, taskID uuid.UUID) (database.TaskSnapshot, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTaskSnapshot", ctx, taskID) - ret0, _ := ret[0].(database.TaskSnapshot) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTaskSnapshot indicates an expected call of GetTaskSnapshot. -func (mr *MockStoreMockRecorder) GetTaskSnapshot(ctx, taskID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTaskSnapshot", reflect.TypeOf((*MockStore)(nil).GetTaskSnapshot), ctx, taskID) -} - // GetTelemetryItem mocks base method. func (m *MockStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { m.ctrl.T.Helper() @@ -5670,21 +5595,6 @@ func (mr *MockStoreMockRecorder) GetTelemetryItems(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryItems", reflect.TypeOf((*MockStore)(nil).GetTelemetryItems), ctx) } -// GetTelemetryTaskEvents mocks base method. -func (m *MockStore) GetTelemetryTaskEvents(ctx context.Context, arg database.GetTelemetryTaskEventsParams) ([]database.GetTelemetryTaskEventsRow, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTelemetryTaskEvents", ctx, arg) - ret0, _ := ret[0].([]database.GetTelemetryTaskEventsRow) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetTelemetryTaskEvents indicates an expected call of GetTelemetryTaskEvents. -func (mr *MockStoreMockRecorder) GetTelemetryTaskEvents(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTelemetryTaskEvents", reflect.TypeOf((*MockStore)(nil).GetTelemetryTaskEvents), ctx, arg) -} - // GetTemplateAppInsights mocks base method. func (m *MockStore) GetTemplateAppInsights(ctx context.Context, arg database.GetTemplateAppInsightsParams) ([]database.GetTemplateAppInsightsRow, error) { m.ctrl.T.Helper() @@ -8454,21 +8364,6 @@ func (mr *MockStoreMockRecorder) InsertReplica(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertReplica", reflect.TypeOf((*MockStore)(nil).InsertReplica), ctx, arg) } -// InsertTask mocks base method. -func (m *MockStore) InsertTask(ctx context.Context, arg database.InsertTaskParams) (database.TaskTable, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InsertTask", ctx, arg) - ret0, _ := ret[0].(database.TaskTable) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// InsertTask indicates an expected call of InsertTask. -func (mr *MockStoreMockRecorder) InsertTask(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTask", reflect.TypeOf((*MockStore)(nil).InsertTask), ctx, arg) -} - // InsertTelemetryItemIfNotExists mocks base method. func (m *MockStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { m.ctrl.T.Helper() @@ -9283,21 +9178,6 @@ func (mr *MockStoreMockRecorder) ListProvisionerKeysByOrganizationExcludeReserve return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProvisionerKeysByOrganizationExcludeReserved", reflect.TypeOf((*MockStore)(nil).ListProvisionerKeysByOrganizationExcludeReserved), ctx, organizationID) } -// ListTasks mocks base method. -func (m *MockStore) ListTasks(ctx context.Context, arg database.ListTasksParams) ([]database.Task, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListTasks", ctx, arg) - ret0, _ := ret[0].([]database.Task) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListTasks indicates an expected call of ListTasks. -func (mr *MockStoreMockRecorder) ListTasks(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTasks", reflect.TypeOf((*MockStore)(nil).ListTasks), ctx, arg) -} - // ListUsageEventCreatedAtsByTypeSince mocks base method. func (m *MockStore) ListUsageEventCreatedAtsByTypeSince(ctx context.Context, arg database.ListUsageEventCreatedAtsByTypeSinceParams) ([]time.Time, error) { m.ctrl.T.Helper() @@ -10825,36 +10705,6 @@ func (mr *MockStoreMockRecorder) UpdateTailnetPeerStatusByCoordinator(ctx, arg a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTailnetPeerStatusByCoordinator", reflect.TypeOf((*MockStore)(nil).UpdateTailnetPeerStatusByCoordinator), ctx, arg) } -// UpdateTaskPrompt mocks base method. -func (m *MockStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTaskPrompt", ctx, arg) - ret0, _ := ret[0].(database.TaskTable) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateTaskPrompt indicates an expected call of UpdateTaskPrompt. -func (mr *MockStoreMockRecorder) UpdateTaskPrompt(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskPrompt", reflect.TypeOf((*MockStore)(nil).UpdateTaskPrompt), ctx, arg) -} - -// UpdateTaskWorkspaceID mocks base method. -func (m *MockStore) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTaskWorkspaceID", ctx, arg) - ret0, _ := ret[0].(database.TaskTable) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateTaskWorkspaceID indicates an expected call of UpdateTaskWorkspaceID. -func (mr *MockStoreMockRecorder) UpdateTaskWorkspaceID(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskWorkspaceID", reflect.TypeOf((*MockStore)(nil).UpdateTaskWorkspaceID), ctx, arg) -} - // UpdateTemplateACLByID mocks base method. func (m *MockStore) UpdateTemplateACLByID(ctx context.Context, arg database.UpdateTemplateACLByIDParams) error { m.ctrl.T.Helper() @@ -12422,35 +12272,6 @@ func (mr *MockStoreMockRecorder) UpsertTailnetTunnel(ctx, arg any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetTunnel", reflect.TypeOf((*MockStore)(nil).UpsertTailnetTunnel), ctx, arg) } -// UpsertTaskSnapshot mocks base method. -func (m *MockStore) UpsertTaskSnapshot(ctx context.Context, arg database.UpsertTaskSnapshotParams) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertTaskSnapshot", ctx, arg) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpsertTaskSnapshot indicates an expected call of UpsertTaskSnapshot. -func (mr *MockStoreMockRecorder) UpsertTaskSnapshot(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTaskSnapshot", reflect.TypeOf((*MockStore)(nil).UpsertTaskSnapshot), ctx, arg) -} - -// UpsertTaskWorkspaceApp mocks base method. -func (m *MockStore) UpsertTaskWorkspaceApp(ctx context.Context, arg database.UpsertTaskWorkspaceAppParams) (database.TaskWorkspaceApp, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertTaskWorkspaceApp", ctx, arg) - ret0, _ := ret[0].(database.TaskWorkspaceApp) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpsertTaskWorkspaceApp indicates an expected call of UpsertTaskWorkspaceApp. -func (mr *MockStoreMockRecorder) UpsertTaskWorkspaceApp(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTaskWorkspaceApp", reflect.TypeOf((*MockStore)(nil).UpsertTaskWorkspaceApp), ctx, arg) -} - // UpsertTelemetryItem mocks base method. func (m *MockStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index d33a18ef14b..edeae3f00b8 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -644,15 +644,6 @@ CREATE TYPE tailnet_status AS ENUM ( 'lost' ); -CREATE TYPE task_status AS ENUM ( - 'pending', - 'initializing', - 'active', - 'paused', - 'unknown', - 'error' -); - CREATE TYPE user_status AS ENUM ( 'active', 'suspended', @@ -3039,265 +3030,6 @@ CREATE UNLOGGED TABLE tailnet_tunnels ( updated_at timestamp with time zone NOT NULL ); -CREATE TABLE task_snapshots ( - task_id uuid NOT NULL, - log_snapshot jsonb NOT NULL, - log_snapshot_created_at timestamp with time zone DEFAULT now() NOT NULL -); - -COMMENT ON TABLE task_snapshots IS 'Stores snapshots of task state when paused, currently limited to conversation history.'; - -COMMENT ON COLUMN task_snapshots.task_id IS 'The task this snapshot belongs to.'; - -COMMENT ON COLUMN task_snapshots.log_snapshot IS 'Task conversation history in JSON format, allowing users to view logs when the workspace is stopped.'; - -COMMENT ON COLUMN task_snapshots.log_snapshot_created_at IS 'When this log snapshot was captured.'; - -CREATE TABLE task_workspace_apps ( - task_id uuid NOT NULL, - workspace_agent_id uuid, - workspace_app_id uuid, - workspace_build_number integer NOT NULL -); - -CREATE TABLE tasks ( - id uuid NOT NULL, - organization_id uuid NOT NULL, - owner_id uuid NOT NULL, - name text NOT NULL, - workspace_id uuid, - template_version_id uuid NOT NULL, - template_parameters jsonb DEFAULT '{}'::jsonb NOT NULL, - prompt text NOT NULL, - created_at timestamp with time zone NOT NULL, - deleted_at timestamp with time zone, - display_name character varying(127) DEFAULT ''::character varying NOT NULL -); - -COMMENT ON COLUMN tasks.display_name IS 'Display name is a custom, human-friendly task name.'; - -CREATE TABLE workspace_agents ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - name character varying(64) NOT NULL, - first_connected_at timestamp with time zone, - last_connected_at timestamp with time zone, - disconnected_at timestamp with time zone, - resource_id uuid NOT NULL, - auth_token uuid NOT NULL, - auth_instance_id character varying, - architecture character varying(64) NOT NULL, - environment_variables jsonb, - operating_system character varying(64) NOT NULL, - instance_metadata jsonb, - resource_metadata jsonb, - directory character varying(4096) DEFAULT ''::character varying NOT NULL, - version text DEFAULT ''::text NOT NULL, - last_connected_replica_id uuid, - connection_timeout_seconds integer DEFAULT 0 NOT NULL, - troubleshooting_url text DEFAULT ''::text NOT NULL, - motd_file text DEFAULT ''::text NOT NULL, - lifecycle_state workspace_agent_lifecycle_state DEFAULT 'created'::workspace_agent_lifecycle_state NOT NULL, - expanded_directory character varying(4096) DEFAULT ''::character varying NOT NULL, - logs_length integer DEFAULT 0 NOT NULL, - logs_overflowed boolean DEFAULT false NOT NULL, - started_at timestamp with time zone, - ready_at timestamp with time zone, - subsystems workspace_agent_subsystem[] DEFAULT '{}'::workspace_agent_subsystem[], - display_apps display_app[] DEFAULT '{vscode,vscode_insiders,web_terminal,ssh_helper,port_forwarding_helper}'::display_app[], - api_version text DEFAULT ''::text NOT NULL, - display_order integer DEFAULT 0 NOT NULL, - parent_id uuid, - api_key_scope agent_key_scope_enum DEFAULT 'all'::agent_key_scope_enum NOT NULL, - deleted boolean DEFAULT false NOT NULL, - CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576)), - CONSTRAINT subsystems_not_none CHECK ((NOT ('none'::workspace_agent_subsystem = ANY (subsystems)))) -); - -COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.'; - -COMMENT ON COLUMN workspace_agents.connection_timeout_seconds IS 'Connection timeout in seconds, 0 means disabled.'; - -COMMENT ON COLUMN workspace_agents.troubleshooting_url IS 'URL for troubleshooting the agent.'; - -COMMENT ON COLUMN workspace_agents.motd_file IS 'Path to file inside workspace containing the message of the day (MOTD) to show to the user when logging in via SSH.'; - -COMMENT ON COLUMN workspace_agents.lifecycle_state IS 'The current lifecycle state reported by the workspace agent.'; - -COMMENT ON COLUMN workspace_agents.expanded_directory IS 'The resolved path of a user-specified directory. e.g. ~/coder -> /home/coder/coder'; - -COMMENT ON COLUMN workspace_agents.logs_length IS 'Total length of startup logs'; - -COMMENT ON COLUMN workspace_agents.logs_overflowed IS 'Whether the startup logs overflowed in length'; - -COMMENT ON COLUMN workspace_agents.started_at IS 'The time the agent entered the starting lifecycle state'; - -COMMENT ON COLUMN workspace_agents.ready_at IS 'The time the agent entered the ready or start_error lifecycle state'; - -COMMENT ON COLUMN workspace_agents.display_order IS 'Specifies the order in which to display agents in user interfaces.'; - -COMMENT ON COLUMN workspace_agents.api_key_scope IS 'Defines the scope of the API key associated with the agent. ''all'' allows access to everything, ''no_user_data'' restricts it to exclude user data.'; - -COMMENT ON COLUMN workspace_agents.deleted IS 'Indicates whether or not the agent has been deleted. This is currently only applicable to sub agents.'; - -CREATE TABLE workspace_apps ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - agent_id uuid NOT NULL, - display_name character varying(64) NOT NULL, - icon character varying(256) NOT NULL, - command character varying(65534), - url character varying(65534), - healthcheck_url text DEFAULT ''::text NOT NULL, - healthcheck_interval integer DEFAULT 0 NOT NULL, - healthcheck_threshold integer DEFAULT 0 NOT NULL, - health workspace_app_health DEFAULT 'disabled'::workspace_app_health NOT NULL, - subdomain boolean DEFAULT false NOT NULL, - sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL, - slug text NOT NULL, - external boolean DEFAULT false NOT NULL, - display_order integer DEFAULT 0 NOT NULL, - hidden boolean DEFAULT false NOT NULL, - open_in workspace_app_open_in DEFAULT 'slim-window'::workspace_app_open_in NOT NULL, - display_group text, - tooltip character varying(2048) DEFAULT ''::character varying NOT NULL -); - -COMMENT ON COLUMN workspace_apps.display_order IS 'Specifies the order in which to display agent app in user interfaces.'; - -COMMENT ON COLUMN workspace_apps.hidden IS 'Determines if the app is not shown in user interfaces.'; - -COMMENT ON COLUMN workspace_apps.tooltip IS 'Markdown text that is displayed when hovering over workspace apps.'; - -CREATE TABLE workspace_builds ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - workspace_id uuid NOT NULL, - template_version_id uuid NOT NULL, - build_number integer NOT NULL, - transition workspace_transition NOT NULL, - initiator_id uuid NOT NULL, - provisioner_state bytea, - job_id uuid NOT NULL, - deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, - reason build_reason DEFAULT 'initiator'::build_reason NOT NULL, - daily_cost integer DEFAULT 0 NOT NULL, - max_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, - template_version_preset_id uuid, - has_external_agent boolean, - notified_autostop_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, - CONSTRAINT workspace_builds_deadline_below_max_deadline CHECK ((((deadline <> '0001-01-01 00:00:00+00'::timestamp with time zone) AND (deadline <= max_deadline)) OR (max_deadline = '0001-01-01 00:00:00+00'::timestamp with time zone))) -); - -COMMENT ON COLUMN workspace_builds.notified_autostop_deadline IS 'The autostop deadline value that an autostop reminder notification was last sent for. Used for idempotence: when it equals the build deadline the reminder has already been sent, and it re-arms automatically when the deadline changes.'; - -CREATE TABLE workspaces ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - owner_id uuid NOT NULL, - organization_id uuid NOT NULL, - template_id uuid NOT NULL, - deleted boolean DEFAULT false NOT NULL, - name character varying(64) NOT NULL, - autostart_schedule text, - ttl bigint, - last_used_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, - dormant_at timestamp with time zone, - deleting_at timestamp with time zone, - automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL, - favorite boolean DEFAULT false NOT NULL, - next_start_at timestamp with time zone, - group_acl jsonb DEFAULT '{}'::jsonb NOT NULL, - user_acl jsonb DEFAULT '{}'::jsonb NOT NULL, - CONSTRAINT group_acl_is_object CHECK ((jsonb_typeof(group_acl) = 'object'::text)), - CONSTRAINT user_acl_is_object CHECK ((jsonb_typeof(user_acl) = 'object'::text)) -); - -COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.'; - -CREATE VIEW tasks_with_status AS - SELECT tasks.id, - tasks.organization_id, - tasks.owner_id, - tasks.name, - tasks.workspace_id, - tasks.template_version_id, - tasks.template_parameters, - tasks.prompt, - tasks.created_at, - tasks.deleted_at, - tasks.display_name, - COALESCE(workspaces.group_acl, '{}'::jsonb) AS workspace_group_acl, - COALESCE(workspaces.user_acl, '{}'::jsonb) AS workspace_user_acl, - CASE - WHEN (tasks.workspace_id IS NULL) THEN 'pending'::task_status - WHEN (build_status.status <> 'active'::task_status) THEN build_status.status - WHEN (agent_status.status <> 'active'::task_status) THEN agent_status.status - ELSE app_status.status - END AS status, - jsonb_build_object('build', jsonb_build_object('transition', latest_build_raw.transition, 'job_status', latest_build_raw.job_status, 'computed', build_status.status), 'agent', jsonb_build_object('lifecycle_state', agent_raw.lifecycle_state, 'computed', agent_status.status), 'app', jsonb_build_object('health', app_raw.health, 'computed', app_status.status)) AS status_debug, - task_app.workspace_build_number, - task_app.workspace_agent_id, - task_app.workspace_app_id, - agent_raw.lifecycle_state AS workspace_agent_lifecycle_state, - app_raw.health AS workspace_app_health, - task_owner.owner_username, - task_owner.owner_name, - task_owner.owner_avatar_url - FROM (((((((((tasks - LEFT JOIN workspaces ON ((workspaces.id = tasks.workspace_id))) - CROSS JOIN LATERAL ( SELECT vu.username AS owner_username, - vu.name AS owner_name, - vu.avatar_url AS owner_avatar_url - FROM visible_users vu - WHERE (vu.id = tasks.owner_id)) task_owner) - LEFT JOIN LATERAL ( SELECT task_app_1.workspace_build_number, - task_app_1.workspace_agent_id, - task_app_1.workspace_app_id - FROM task_workspace_apps task_app_1 - WHERE (task_app_1.task_id = tasks.id) - ORDER BY task_app_1.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_raw ON (true)) - LEFT JOIN LATERAL ( SELECT workspace_agent.lifecycle_state - FROM workspace_agents workspace_agent - WHERE (workspace_agent.id = task_app.workspace_agent_id)) agent_raw ON (true)) - LEFT JOIN LATERAL ( SELECT workspace_app.health - FROM workspace_apps workspace_app - WHERE (workspace_app.id = task_app.workspace_app_id)) app_raw ON (true)) - CROSS JOIN LATERAL ( SELECT - CASE - WHEN (latest_build_raw.job_status IS NULL) THEN 'pending'::task_status - WHEN (latest_build_raw.job_status = ANY (ARRAY['failed'::provisioner_job_status, 'canceling'::provisioner_job_status, 'canceled'::provisioner_job_status])) THEN 'error'::task_status - WHEN ((latest_build_raw.transition = ANY (ARRAY['stop'::workspace_transition, 'delete'::workspace_transition])) AND (latest_build_raw.job_status = 'succeeded'::provisioner_job_status)) THEN 'paused'::task_status - WHEN ((latest_build_raw.transition = 'start'::workspace_transition) AND (latest_build_raw.job_status = 'pending'::provisioner_job_status)) THEN 'pending'::task_status - WHEN ((latest_build_raw.transition = 'start'::workspace_transition) AND (latest_build_raw.job_status = ANY (ARRAY['running'::provisioner_job_status, 'succeeded'::provisioner_job_status]))) THEN 'active'::task_status - ELSE 'unknown'::task_status - END AS status) build_status) - CROSS JOIN LATERAL ( SELECT - CASE - WHEN ((agent_raw.lifecycle_state IS NULL) OR (agent_raw.lifecycle_state = ANY (ARRAY['created'::workspace_agent_lifecycle_state, 'starting'::workspace_agent_lifecycle_state]))) THEN 'initializing'::task_status - WHEN (agent_raw.lifecycle_state = ANY (ARRAY['ready'::workspace_agent_lifecycle_state, 'start_timeout'::workspace_agent_lifecycle_state, 'start_error'::workspace_agent_lifecycle_state])) THEN 'active'::task_status - WHEN (agent_raw.lifecycle_state <> ALL (ARRAY['created'::workspace_agent_lifecycle_state, 'starting'::workspace_agent_lifecycle_state, 'ready'::workspace_agent_lifecycle_state, 'start_timeout'::workspace_agent_lifecycle_state, 'start_error'::workspace_agent_lifecycle_state])) THEN 'unknown'::task_status - ELSE 'unknown'::task_status - END AS status) agent_status) - CROSS JOIN LATERAL ( SELECT - CASE - WHEN (app_raw.health = 'initializing'::workspace_app_health) THEN 'initializing'::task_status - WHEN (app_raw.health = 'unhealthy'::workspace_app_health) THEN 'error'::task_status - WHEN (app_raw.health = ANY (ARRAY['healthy'::workspace_app_health, 'disabled'::workspace_app_health])) THEN 'active'::task_status - ELSE 'unknown'::task_status - END AS status) app_status) - WHERE (tasks.deleted_at IS NULL); - CREATE TABLE telemetry_items ( key text NOT NULL, value text NOT NULL, @@ -3975,6 +3707,71 @@ CREATE TABLE workspace_agent_volume_resource_monitors ( debounced_until timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL ); +CREATE TABLE workspace_agents ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + name character varying(64) NOT NULL, + first_connected_at timestamp with time zone, + last_connected_at timestamp with time zone, + disconnected_at timestamp with time zone, + resource_id uuid NOT NULL, + auth_token uuid NOT NULL, + auth_instance_id character varying, + architecture character varying(64) NOT NULL, + environment_variables jsonb, + operating_system character varying(64) NOT NULL, + instance_metadata jsonb, + resource_metadata jsonb, + directory character varying(4096) DEFAULT ''::character varying NOT NULL, + version text DEFAULT ''::text NOT NULL, + last_connected_replica_id uuid, + connection_timeout_seconds integer DEFAULT 0 NOT NULL, + troubleshooting_url text DEFAULT ''::text NOT NULL, + motd_file text DEFAULT ''::text NOT NULL, + lifecycle_state workspace_agent_lifecycle_state DEFAULT 'created'::workspace_agent_lifecycle_state NOT NULL, + expanded_directory character varying(4096) DEFAULT ''::character varying NOT NULL, + logs_length integer DEFAULT 0 NOT NULL, + logs_overflowed boolean DEFAULT false NOT NULL, + started_at timestamp with time zone, + ready_at timestamp with time zone, + subsystems workspace_agent_subsystem[] DEFAULT '{}'::workspace_agent_subsystem[], + display_apps display_app[] DEFAULT '{vscode,vscode_insiders,web_terminal,ssh_helper,port_forwarding_helper}'::display_app[], + api_version text DEFAULT ''::text NOT NULL, + display_order integer DEFAULT 0 NOT NULL, + parent_id uuid, + api_key_scope agent_key_scope_enum DEFAULT 'all'::agent_key_scope_enum NOT NULL, + deleted boolean DEFAULT false NOT NULL, + CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576)), + CONSTRAINT subsystems_not_none CHECK ((NOT ('none'::workspace_agent_subsystem = ANY (subsystems)))) +); + +COMMENT ON COLUMN workspace_agents.version IS 'Version tracks the version of the currently running workspace agent. Workspace agents register their version upon start.'; + +COMMENT ON COLUMN workspace_agents.connection_timeout_seconds IS 'Connection timeout in seconds, 0 means disabled.'; + +COMMENT ON COLUMN workspace_agents.troubleshooting_url IS 'URL for troubleshooting the agent.'; + +COMMENT ON COLUMN workspace_agents.motd_file IS 'Path to file inside workspace containing the message of the day (MOTD) to show to the user when logging in via SSH.'; + +COMMENT ON COLUMN workspace_agents.lifecycle_state IS 'The current lifecycle state reported by the workspace agent.'; + +COMMENT ON COLUMN workspace_agents.expanded_directory IS 'The resolved path of a user-specified directory. e.g. ~/coder -> /home/coder/coder'; + +COMMENT ON COLUMN workspace_agents.logs_length IS 'Total length of startup logs'; + +COMMENT ON COLUMN workspace_agents.logs_overflowed IS 'Whether the startup logs overflowed in length'; + +COMMENT ON COLUMN workspace_agents.started_at IS 'The time the agent entered the starting lifecycle state'; + +COMMENT ON COLUMN workspace_agents.ready_at IS 'The time the agent entered the ready or start_error lifecycle state'; + +COMMENT ON COLUMN workspace_agents.display_order IS 'Specifies the order in which to display agents in user interfaces.'; + +COMMENT ON COLUMN workspace_agents.api_key_scope IS 'Defines the scope of the API key associated with the agent. ''all'' allows access to everything, ''no_user_data'' restricts it to exclude user data.'; + +COMMENT ON COLUMN workspace_agents.deleted IS 'Indicates whether or not the agent has been deleted. This is currently only applicable to sub agents.'; + CREATE UNLOGGED TABLE workspace_app_audit_sessions ( agent_id uuid NOT NULL, app_id uuid NOT NULL, @@ -4063,6 +3860,35 @@ CREATE TABLE workspace_app_statuses ( uri text ); +CREATE TABLE workspace_apps ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + agent_id uuid NOT NULL, + display_name character varying(64) NOT NULL, + icon character varying(256) NOT NULL, + command character varying(65534), + url character varying(65534), + healthcheck_url text DEFAULT ''::text NOT NULL, + healthcheck_interval integer DEFAULT 0 NOT NULL, + healthcheck_threshold integer DEFAULT 0 NOT NULL, + health workspace_app_health DEFAULT 'disabled'::workspace_app_health NOT NULL, + subdomain boolean DEFAULT false NOT NULL, + sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL, + slug text NOT NULL, + external boolean DEFAULT false NOT NULL, + display_order integer DEFAULT 0 NOT NULL, + hidden boolean DEFAULT false NOT NULL, + open_in workspace_app_open_in DEFAULT 'slim-window'::workspace_app_open_in NOT NULL, + display_group text, + tooltip character varying(2048) DEFAULT ''::character varying NOT NULL +); + +COMMENT ON COLUMN workspace_apps.display_order IS 'Specifies the order in which to display agent app in user interfaces.'; + +COMMENT ON COLUMN workspace_apps.hidden IS 'Determines if the app is not shown in user interfaces.'; + +COMMENT ON COLUMN workspace_apps.tooltip IS 'Markdown text that is displayed when hovering over workspace apps.'; + CREATE TABLE workspace_build_orchestrations ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -4111,6 +3937,29 @@ COMMENT ON COLUMN workspace_build_parameters.name IS 'Parameter name'; COMMENT ON COLUMN workspace_build_parameters.value IS 'Parameter value'; +CREATE TABLE workspace_builds ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + workspace_id uuid NOT NULL, + template_version_id uuid NOT NULL, + build_number integer NOT NULL, + transition workspace_transition NOT NULL, + initiator_id uuid NOT NULL, + provisioner_state bytea, + job_id uuid NOT NULL, + deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, + reason build_reason DEFAULT 'initiator'::build_reason NOT NULL, + daily_cost integer DEFAULT 0 NOT NULL, + max_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, + template_version_preset_id uuid, + has_external_agent boolean, + notified_autostop_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, + CONSTRAINT workspace_builds_deadline_below_max_deadline CHECK ((((deadline <> '0001-01-01 00:00:00+00'::timestamp with time zone) AND (deadline <= max_deadline)) OR (max_deadline = '0001-01-01 00:00:00+00'::timestamp with time zone))) +); + +COMMENT ON COLUMN workspace_builds.notified_autostop_deadline IS 'The autostop deadline value that an autostop reminder notification was last sent for. Used for idempotence: when it equals the build deadline the reminder has already been sent, and it re-arms automatically when the deadline changes.'; + CREATE VIEW workspace_build_with_user AS SELECT workspace_builds.id, workspace_builds.created_at, @@ -4136,6 +3985,31 @@ CREATE VIEW workspace_build_with_user AS COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.'; +CREATE TABLE workspaces ( + id uuid NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + owner_id uuid NOT NULL, + organization_id uuid NOT NULL, + template_id uuid NOT NULL, + deleted boolean DEFAULT false NOT NULL, + name character varying(64) NOT NULL, + autostart_schedule text, + ttl bigint, + last_used_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL, + dormant_at timestamp with time zone, + deleting_at timestamp with time zone, + automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL, + favorite boolean DEFAULT false NOT NULL, + next_start_at timestamp with time zone, + group_acl jsonb DEFAULT '{}'::jsonb NOT NULL, + user_acl jsonb DEFAULT '{}'::jsonb NOT NULL, + CONSTRAINT group_acl_is_object CHECK ((jsonb_typeof(group_acl) = 'object'::text)), + CONSTRAINT user_acl_is_object CHECK ((jsonb_typeof(user_acl) = 'object'::text)) +); + +COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.'; + CREATE VIEW workspace_latest_builds AS SELECT latest_build.id, latest_build.workspace_id, @@ -4323,18 +4197,16 @@ CREATE VIEW workspaces_expanded AS templates.display_name AS template_display_name, templates.icon AS template_icon, templates.description AS template_description, - tasks.id AS task_id, COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(g.name, ''::text), 'avatar_url', COALESCE(g.avatar_url, ''::text))) AS jsonb_object_agg FROM (jsonb_each(workspaces.group_acl) acl(key, value) LEFT JOIN groups g ON ((g.id = (acl.key)::uuid)))), '{}'::jsonb) AS group_acl_display_info, COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(vu.name, ''::text), 'avatar_url', COALESCE(vu.avatar_url, ''::text))) AS jsonb_object_agg FROM (jsonb_each(workspaces.user_acl) acl(key, value) LEFT JOIN visible_users vu ON ((vu.id = (acl.key)::uuid)))), '{}'::jsonb) AS user_acl_display_info - FROM ((((workspaces + FROM (((workspaces JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) JOIN organizations ON ((workspaces.organization_id = organizations.id))) - JOIN templates ON ((workspaces.template_id = templates.id))) - LEFT JOIN tasks ON ((workspaces.id = tasks.workspace_id))); + JOIN templates ON ((workspaces.template_id = templates.id))); COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; @@ -4605,15 +4477,6 @@ ALTER TABLE ONLY tailnet_peers ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); -ALTER TABLE ONLY task_snapshots - ADD CONSTRAINT task_snapshots_pkey PRIMARY KEY (task_id); - -ALTER TABLE ONLY task_workspace_apps - ADD CONSTRAINT task_workspace_apps_pkey PRIMARY KEY (task_id, workspace_build_number); - -ALTER TABLE ONLY tasks - ADD CONSTRAINT tasks_pkey PRIMARY KEY (id); - ALTER TABLE ONLY telemetry_items ADD CONSTRAINT telemetry_items_pkey PRIMARY KEY (key); @@ -5050,16 +4913,6 @@ COMMENT ON INDEX provisioner_jobs_worker_id_organization_id_completed_at_idx IS CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower((name)::text)); -CREATE INDEX tasks_organization_id_idx ON tasks USING btree (organization_id); - -CREATE INDEX tasks_owner_id_idx ON tasks USING btree (owner_id); - -CREATE UNIQUE INDEX tasks_owner_id_name_unique_idx ON tasks USING btree (owner_id, lower(name)) WHERE (deleted_at IS NULL); - -COMMENT ON INDEX tasks_owner_id_name_unique_idx IS 'Index to ensure uniqueness for task owner/name'; - -CREATE INDEX tasks_workspace_id_idx ON tasks USING btree (workspace_id); - CREATE INDEX template_usage_stats_start_time_idx ON template_usage_stats USING btree (start_time DESC); COMMENT ON INDEX template_usage_stats_start_time_idx IS 'Index for querying MAX(start_time).'; @@ -5507,30 +5360,6 @@ ALTER TABLE ONLY tailnet_peers ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; -ALTER TABLE ONLY task_snapshots - ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; - -ALTER TABLE ONLY task_workspace_apps - ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; - -ALTER TABLE ONLY task_workspace_apps - ADD CONSTRAINT task_workspace_apps_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - -ALTER TABLE ONLY task_workspace_apps - ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE; - -ALTER TABLE ONLY tasks - ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - -ALTER TABLE ONLY tasks - ADD CONSTRAINT tasks_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - -ALTER TABLE ONLY tasks - ADD CONSTRAINT tasks_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - -ALTER TABLE ONLY tasks - ADD CONSTRAINT tasks_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; - ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 251ce1aec5c..876a407ae75 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -93,14 +93,6 @@ const ( ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE; - ForeignKeyTaskSnapshotsTaskID ForeignKeyConstraint = "task_snapshots_task_id_fkey" // ALTER TABLE ONLY task_snapshots ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsTaskID ForeignKeyConstraint = "task_workspace_apps_task_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsWorkspaceAgentID ForeignKeyConstraint = "task_workspace_apps_workspace_agent_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; - ForeignKeyTaskWorkspaceAppsWorkspaceAppID ForeignKeyConstraint = "task_workspace_apps_workspace_app_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE; - ForeignKeyTasksOrganizationID ForeignKeyConstraint = "tasks_organization_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; - ForeignKeyTasksOwnerID ForeignKeyConstraint = "tasks_owner_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; - ForeignKeyTasksTemplateVersionID ForeignKeyConstraint = "tasks_template_version_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; - ForeignKeyTasksWorkspaceID ForeignKeyConstraint = "tasks_workspace_id_fkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; ForeignKeyTemplateVersionParametersTemplateVersionID ForeignKeyConstraint = "template_version_parameters_template_version_id_fkey" // ALTER TABLE ONLY template_version_parameters ADD CONSTRAINT template_version_parameters_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; ForeignKeyTemplateVersionPresetParametTemplateVersionPresetID ForeignKeyConstraint = "template_version_preset_paramet_template_version_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_parameters ADD CONSTRAINT template_version_preset_paramet_template_version_preset_id_fkey FOREIGN KEY (template_version_preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; ForeignKeyTemplateVersionPresetPrebuildSchedulesPresetID ForeignKeyConstraint = "template_version_preset_prebuild_schedules_preset_id_fkey" // ALTER TABLE ONLY template_version_preset_prebuild_schedules ADD CONSTRAINT template_version_preset_prebuild_schedules_preset_id_fkey FOREIGN KEY (preset_id) REFERENCES template_version_presets(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000594_drop_task_tables.down.sql b/coderd/database/migrations/000594_drop_task_tables.down.sql new file mode 100644 index 00000000000..36d8d947e1f --- /dev/null +++ b/coderd/database/migrations/000594_drop_task_tables.down.sql @@ -0,0 +1,209 @@ +-- Drop views before modifying underlying tables and columns. +DROP VIEW IF EXISTS workspaces_expanded; + +-- Recreate task status enum type. +CREATE TYPE task_status AS ENUM ( + 'pending', + 'initializing', + 'active', + 'paused', + 'unknown', + 'error' +); + +-- Recreate tasks table. Deleted task rows cannot be recovered. +CREATE TABLE tasks ( + id uuid NOT NULL, + organization_id uuid NOT NULL, + owner_id uuid NOT NULL, + name text NOT NULL, + workspace_id uuid, + template_version_id uuid NOT NULL, + template_parameters jsonb DEFAULT '{}'::jsonb NOT NULL, + prompt text NOT NULL, + created_at timestamp with time zone NOT NULL, + deleted_at timestamp with time zone, + display_name character varying(127) DEFAULT ''::character varying NOT NULL +); + +COMMENT ON COLUMN tasks.display_name IS 'Display name is a custom, human-friendly task name.'; + +ALTER TABLE ONLY tasks + ADD CONSTRAINT tasks_pkey PRIMARY KEY (id); + +CREATE INDEX tasks_organization_id_idx ON tasks USING btree (organization_id); +CREATE INDEX tasks_owner_id_idx ON tasks USING btree (owner_id); +CREATE UNIQUE INDEX tasks_owner_id_name_unique_idx ON tasks USING btree (owner_id, lower(name)) WHERE (deleted_at IS NULL); +COMMENT ON INDEX tasks_owner_id_name_unique_idx IS 'Index to ensure uniqueness for task owner/name'; +CREATE INDEX tasks_workspace_id_idx ON tasks USING btree (workspace_id); + +ALTER TABLE ONLY tasks + ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; +ALTER TABLE ONLY tasks + ADD CONSTRAINT tasks_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; +ALTER TABLE ONLY tasks + ADD CONSTRAINT tasks_template_version_id_fkey FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE CASCADE; +ALTER TABLE ONLY tasks + ADD CONSTRAINT tasks_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + +-- Recreate task_workspace_apps table. +CREATE TABLE task_workspace_apps ( + task_id uuid NOT NULL, + workspace_agent_id uuid, + workspace_app_id uuid, + workspace_build_number integer NOT NULL +); + +ALTER TABLE ONLY task_workspace_apps + ADD CONSTRAINT task_workspace_apps_pkey PRIMARY KEY (task_id, workspace_build_number); + +ALTER TABLE ONLY task_workspace_apps + ADD CONSTRAINT task_workspace_apps_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; +ALTER TABLE ONLY task_workspace_apps + ADD CONSTRAINT task_workspace_apps_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; +ALTER TABLE ONLY task_workspace_apps + ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE; + +-- Recreate task_snapshots table. +CREATE TABLE task_snapshots ( + task_id uuid NOT NULL, + log_snapshot jsonb NOT NULL, + log_snapshot_created_at timestamp with time zone DEFAULT now() NOT NULL +); + +COMMENT ON TABLE task_snapshots IS 'Stores snapshots of task state when paused, currently limited to conversation history.'; +COMMENT ON COLUMN task_snapshots.task_id IS 'The task this snapshot belongs to.'; +COMMENT ON COLUMN task_snapshots.log_snapshot IS 'Task conversation history in JSON format, allowing users to view logs when the workspace is stopped.'; +COMMENT ON COLUMN task_snapshots.log_snapshot_created_at IS 'When this log snapshot was captured.'; + +ALTER TABLE ONLY task_snapshots + ADD CONSTRAINT task_snapshots_pkey PRIMARY KEY (task_id); + +ALTER TABLE ONLY task_snapshots + ADD CONSTRAINT task_snapshots_task_id_fkey FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE; + +-- Recreate views. +CREATE VIEW workspaces_expanded AS + SELECT workspaces.id, + workspaces.created_at, + workspaces.updated_at, + workspaces.owner_id, + workspaces.organization_id, + workspaces.template_id, + workspaces.deleted, + workspaces.name, + workspaces.autostart_schedule, + workspaces.ttl, + workspaces.last_used_at, + workspaces.dormant_at, + workspaces.deleting_at, + workspaces.automatic_updates, + workspaces.favorite, + workspaces.next_start_at, + workspaces.group_acl, + workspaces.user_acl, + visible_users.avatar_url AS owner_avatar_url, + visible_users.username AS owner_username, + visible_users.name AS owner_name, + organizations.name AS organization_name, + organizations.display_name AS organization_display_name, + organizations.icon AS organization_icon, + organizations.description AS organization_description, + templates.name AS template_name, + templates.display_name AS template_display_name, + templates.icon AS template_icon, + templates.description AS template_description, + tasks.id AS task_id, + COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(g.name, ''::text), 'avatar_url', COALESCE(g.avatar_url, ''::text))) AS jsonb_object_agg + FROM (jsonb_each(workspaces.group_acl) acl(key, value) + LEFT JOIN groups g ON ((g.id = (acl.key)::uuid)))), '{}'::jsonb) AS group_acl_display_info, + COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(vu.name, ''::text), 'avatar_url', COALESCE(vu.avatar_url, ''::text))) AS jsonb_object_agg + FROM (jsonb_each(workspaces.user_acl) acl(key, value) + LEFT JOIN visible_users vu ON ((vu.id = (acl.key)::uuid)))), '{}'::jsonb) AS user_acl_display_info + FROM ((((workspaces + JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) + JOIN organizations ON ((workspaces.organization_id = organizations.id))) + JOIN templates ON ((workspaces.template_id = templates.id))) + LEFT JOIN tasks ON ((workspaces.id = tasks.workspace_id))); + +COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; + +CREATE VIEW tasks_with_status AS + SELECT tasks.id, + tasks.organization_id, + tasks.owner_id, + tasks.name, + tasks.workspace_id, + tasks.template_version_id, + tasks.template_parameters, + tasks.prompt, + tasks.created_at, + tasks.deleted_at, + tasks.display_name, + COALESCE(workspaces.group_acl, '{}'::jsonb) AS workspace_group_acl, + COALESCE(workspaces.user_acl, '{}'::jsonb) AS workspace_user_acl, + CASE + WHEN (tasks.workspace_id IS NULL) THEN 'pending'::task_status + WHEN (build_status.status <> 'active'::task_status) THEN build_status.status + WHEN (agent_status.status <> 'active'::task_status) THEN agent_status.status + ELSE app_status.status + END AS status, + jsonb_build_object('build', jsonb_build_object('transition', latest_build_raw.transition, 'job_status', latest_build_raw.job_status, 'computed', build_status.status), 'agent', jsonb_build_object('lifecycle_state', agent_raw.lifecycle_state, 'computed', agent_status.status), 'app', jsonb_build_object('health', app_raw.health, 'computed', app_status.status)) AS status_debug, + task_app.workspace_build_number, + task_app.workspace_agent_id, + task_app.workspace_app_id, + agent_raw.lifecycle_state AS workspace_agent_lifecycle_state, + app_raw.health AS workspace_app_health, + task_owner.owner_username, + task_owner.owner_name, + task_owner.owner_avatar_url + FROM (((((((((tasks + LEFT JOIN workspaces ON ((workspaces.id = tasks.workspace_id))) + CROSS JOIN LATERAL ( SELECT vu.username AS owner_username, + vu.name AS owner_name, + vu.avatar_url AS owner_avatar_url + FROM visible_users vu + WHERE (vu.id = tasks.owner_id)) task_owner) + LEFT JOIN LATERAL ( SELECT task_app_1.workspace_build_number, + task_app_1.workspace_agent_id, + task_app_1.workspace_app_id + FROM task_workspace_apps task_app_1 + WHERE (task_app_1.task_id = tasks.id) + ORDER BY task_app_1.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_raw ON (true)) + LEFT JOIN LATERAL ( SELECT workspace_agent.lifecycle_state + FROM workspace_agents workspace_agent + WHERE (workspace_agent.id = task_app.workspace_agent_id)) agent_raw ON (true)) + LEFT JOIN LATERAL ( SELECT workspace_app.health + FROM workspace_apps workspace_app + WHERE (workspace_app.id = task_app.workspace_app_id)) app_raw ON (true)) + CROSS JOIN LATERAL ( SELECT + CASE + WHEN (latest_build_raw.job_status IS NULL) THEN 'pending'::task_status + WHEN (latest_build_raw.job_status = ANY (ARRAY['failed'::provisioner_job_status, 'canceling'::provisioner_job_status, 'canceled'::provisioner_job_status])) THEN 'error'::task_status + WHEN ((latest_build_raw.transition = ANY (ARRAY['stop'::workspace_transition, 'delete'::workspace_transition])) AND (latest_build_raw.job_status = 'succeeded'::provisioner_job_status)) THEN 'paused'::task_status + WHEN ((latest_build_raw.transition = 'start'::workspace_transition) AND (latest_build_raw.job_status = 'pending'::provisioner_job_status)) THEN 'pending'::task_status + WHEN ((latest_build_raw.transition = 'start'::workspace_transition) AND (latest_build_raw.job_status = ANY (ARRAY['running'::provisioner_job_status, 'succeeded'::provisioner_job_status]))) THEN 'active'::task_status + ELSE 'unknown'::task_status + END AS status) build_status) + CROSS JOIN LATERAL ( SELECT + CASE + WHEN ((agent_raw.lifecycle_state IS NULL) OR (agent_raw.lifecycle_state = ANY (ARRAY['created'::workspace_agent_lifecycle_state, 'starting'::workspace_agent_lifecycle_state]))) THEN 'initializing'::task_status + WHEN (agent_raw.lifecycle_state = ANY (ARRAY['ready'::workspace_agent_lifecycle_state, 'start_timeout'::workspace_agent_lifecycle_state, 'start_error'::workspace_agent_lifecycle_state])) THEN 'active'::task_status + WHEN (agent_raw.lifecycle_state <> ALL (ARRAY['created'::workspace_agent_lifecycle_state, 'starting'::workspace_agent_lifecycle_state, 'ready'::workspace_agent_lifecycle_state, 'start_timeout'::workspace_agent_lifecycle_state, 'start_error'::workspace_agent_lifecycle_state])) THEN 'unknown'::task_status + ELSE 'unknown'::task_status + END AS status) agent_status) + CROSS JOIN LATERAL ( SELECT + CASE + WHEN (app_raw.health = 'initializing'::workspace_app_health) THEN 'initializing'::task_status + WHEN (app_raw.health = 'unhealthy'::workspace_app_health) THEN 'error'::task_status + WHEN (app_raw.health = ANY (ARRAY['healthy'::workspace_app_health, 'disabled'::workspace_app_health])) THEN 'active'::task_status + ELSE 'unknown'::task_status + END AS status) app_status) + WHERE (tasks.deleted_at IS NULL); diff --git a/coderd/database/migrations/000594_drop_task_tables.up.sql b/coderd/database/migrations/000594_drop_task_tables.up.sql new file mode 100644 index 00000000000..8cbc77bcc76 --- /dev/null +++ b/coderd/database/migrations/000594_drop_task_tables.up.sql @@ -0,0 +1,54 @@ +-- Drop task views first to remove dependencies on tables and columns. +DROP VIEW IF EXISTS tasks_with_status; +DROP VIEW IF EXISTS workspaces_expanded; + +-- Drop task tables. +DROP TABLE IF EXISTS task_snapshots; +DROP TABLE IF EXISTS task_workspace_apps; +DROP TABLE IF EXISTS tasks; + +-- Drop task status enum type. +DROP TYPE IF EXISTS task_status; + +CREATE VIEW workspaces_expanded AS + SELECT workspaces.id, + workspaces.created_at, + workspaces.updated_at, + workspaces.owner_id, + workspaces.organization_id, + workspaces.template_id, + workspaces.deleted, + workspaces.name, + workspaces.autostart_schedule, + workspaces.ttl, + workspaces.last_used_at, + workspaces.dormant_at, + workspaces.deleting_at, + workspaces.automatic_updates, + workspaces.favorite, + workspaces.next_start_at, + workspaces.group_acl, + workspaces.user_acl, + visible_users.avatar_url AS owner_avatar_url, + visible_users.username AS owner_username, + visible_users.name AS owner_name, + organizations.name AS organization_name, + organizations.display_name AS organization_display_name, + organizations.icon AS organization_icon, + organizations.description AS organization_description, + templates.name AS template_name, + templates.display_name AS template_display_name, + templates.icon AS template_icon, + templates.description AS template_description, + COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(g.name, ''::text), 'avatar_url', COALESCE(g.avatar_url, ''::text))) AS jsonb_object_agg + FROM (jsonb_each(workspaces.group_acl) acl(key, value) + LEFT JOIN groups g ON ((g.id = (acl.key)::uuid)))), '{}'::jsonb) AS group_acl_display_info, + COALESCE(( SELECT jsonb_object_agg(acl.key, jsonb_build_object('name', COALESCE(vu.name, ''::text), 'avatar_url', COALESCE(vu.avatar_url, ''::text))) AS jsonb_object_agg + FROM (jsonb_each(workspaces.user_acl) acl(key, value) + LEFT JOIN visible_users vu ON ((vu.id = (acl.key)::uuid)))), '{}'::jsonb) AS user_acl_display_info + FROM (((workspaces + JOIN visible_users ON ((workspaces.owner_id = visible_users.id))) + JOIN organizations ON ((workspaces.organization_id = organizations.id))) + JOIN templates ON ((workspaces.template_id = templates.id))); + +COMMENT ON VIEW workspaces_expanded IS 'Joins in the display name information such as username, avatar, and organization name.'; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 3d413e2ef35..81b0e0807ef 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -171,47 +171,6 @@ func (w ConnectionLog) RBACObject() rbac.Object { return obj } -// TaskTable converts a Task to it's reduced version. -// A more generalized solution is to use json marshaling to -// consistently keep these two structs in sync. -// That would be a lot of overhead, and a more costly unit test is -// written to make sure these match up. -func (t Task) TaskTable() TaskTable { - return TaskTable{ - ID: t.ID, - OrganizationID: t.OrganizationID, - OwnerID: t.OwnerID, - Name: t.Name, - DisplayName: t.DisplayName, - WorkspaceID: t.WorkspaceID, - TemplateVersionID: t.TemplateVersionID, - TemplateParameters: t.TemplateParameters, - Prompt: t.Prompt, - CreatedAt: t.CreatedAt, - DeletedAt: t.DeletedAt, - } -} - -func (t Task) RBACObject() rbac.Object { - obj := rbac.ResourceTask. - WithID(t.ID). - WithOwner(t.OwnerID.String()). - InOrg(t.OrganizationID) - - if rbac.WorkspaceACLDisabled() { - return obj - } - - if t.WorkspaceGroupACL != nil { - obj = obj.WithGroupACL(t.WorkspaceGroupACL.RBACACL()) - } - if t.WorkspaceUserACL != nil { - obj = obj.WithACLUserList(t.WorkspaceUserACL.RBACACL()) - } - - return obj -} - func (c Chat) RBACObject() rbac.Object { obj := rbac.ResourceChat. WithID(c.ID). @@ -816,7 +775,6 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) ([]Workspace, error) { TemplateIcon: r.TemplateIcon, TemplateDescription: r.TemplateDescription, NextStartAt: r.NextStartAt, - TaskID: r.TaskID, } var err error diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index b0d06b65d5d..85d68f7ad21 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -385,7 +385,6 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, - &i.TaskID, &i.GroupACLDisplayInfo, &i.UserACLDisplayInfo, &i.TemplateVersionID, diff --git a/coderd/database/modelqueries_internal_test.go b/coderd/database/modelqueries_internal_test.go index ed5dc9a8d22..f0b2e19236f 100644 --- a/coderd/database/modelqueries_internal_test.go +++ b/coderd/database/modelqueries_internal_test.go @@ -61,45 +61,6 @@ func TestWorkspaceTableConvert(t *testing.T) { "To resolve this, go to the 'func (w Workspace) WorkspaceTable()' and ensure all fields are converted.") } -// TestTaskTableConvert verifies all task fields are converted -// when reducing a `Task` to a `TaskTable`. -// This test is a guard rail to prevent developer oversight mistakes. -func TestTaskTableConvert(t *testing.T) { - t.Parallel() - - staticRandoms := &testutil.Random{ - String: func() string { return "foo" }, - Bool: func() bool { return true }, - Int: func() int64 { return 500 }, - Uint: func() uint64 { return 126 }, - Float: func() float64 { return 3.14 }, - Complex: func() complex128 { return 6.24 }, - Time: func() time.Time { - return time.Date(2020, 5, 2, 5, 19, 21, 30, time.UTC) - }, - } - - // Copies the approach taken by TestWorkspaceTableConvert. - // - // If you use 'PopulateStruct' to create 2 tasks, using the same - // "random" values for each type. Then they should be identical. - // - // So if 'task.TaskTable()' was missing any fields in its - // conversion, the comparison would fail. - - var task Task - err := testutil.PopulateStruct(&task, staticRandoms) - require.NoError(t, err) - - var subset TaskTable - err = testutil.PopulateStruct(&subset, staticRandoms) - require.NoError(t, err) - - require.Equal(t, task.TaskTable(), subset, - "'task.TaskTable()' is not missing at least 1 field when converting to 'TaskTable'. "+ - "To resolve this, go to the 'func (t Task) TaskTable()' and ensure all fields are converted.") -} - // TestAuditLogsQueryConsistency ensures that GetAuditLogsOffset and CountAuditLogs // have identical WHERE clauses to prevent filtering inconsistencies. // This test is a guard rail to prevent developer oversight mistakes. diff --git a/coderd/database/models.go b/coderd/database/models.go index d60a493c22c..91b561d91d9 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3988,76 +3988,6 @@ func AllTailnetStatusValues() []TailnetStatus { } } -type TaskStatus string - -const ( - TaskStatusPending TaskStatus = "pending" - TaskStatusInitializing TaskStatus = "initializing" - TaskStatusActive TaskStatus = "active" - TaskStatusPaused TaskStatus = "paused" - TaskStatusUnknown TaskStatus = "unknown" - TaskStatusError TaskStatus = "error" -) - -func (e *TaskStatus) Scan(src interface{}) error { - switch s := src.(type) { - case []byte: - *e = TaskStatus(s) - case string: - *e = TaskStatus(s) - default: - return fmt.Errorf("unsupported scan type for TaskStatus: %T", src) - } - return nil -} - -type NullTaskStatus struct { - TaskStatus TaskStatus `json:"task_status"` - Valid bool `json:"valid"` // Valid is true if TaskStatus is not NULL -} - -// Scan implements the Scanner interface. -func (ns *NullTaskStatus) Scan(value interface{}) error { - if value == nil { - ns.TaskStatus, ns.Valid = "", false - return nil - } - ns.Valid = true - return ns.TaskStatus.Scan(value) -} - -// Value implements the driver Valuer interface. -func (ns NullTaskStatus) Value() (driver.Value, error) { - if !ns.Valid { - return nil, nil - } - return string(ns.TaskStatus), nil -} - -func (e TaskStatus) Valid() bool { - switch e { - case TaskStatusPending, - TaskStatusInitializing, - TaskStatusActive, - TaskStatusPaused, - TaskStatusUnknown, - TaskStatusError: - return true - } - return false -} - -func AllTaskStatusValues() []TaskStatus { - return []TaskStatus{ - TaskStatusPending, - TaskStatusInitializing, - TaskStatusActive, - TaskStatusPaused, - TaskStatusUnknown, - TaskStatusError, - } -} - // Defines the users status: active, dormant, or suspended. type UserStatus string @@ -5995,64 +5925,6 @@ type TailnetTunnel struct { UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } -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"` - DisplayName string `db:"display_name" json:"display_name"` - WorkspaceGroupACL WorkspaceACL `db:"workspace_group_acl" json:"workspace_group_acl"` - WorkspaceUserACL WorkspaceACL `db:"workspace_user_acl" json:"workspace_user_acl"` - Status TaskStatus `db:"status" json:"status"` - StatusDebug json.RawMessage `db:"status_debug" json:"status_debug"` - 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"` - WorkspaceAgentLifecycleState NullWorkspaceAgentLifecycleState `db:"workspace_agent_lifecycle_state" json:"workspace_agent_lifecycle_state"` - WorkspaceAppHealth NullWorkspaceAppHealth `db:"workspace_app_health" json:"workspace_app_health"` - OwnerUsername string `db:"owner_username" json:"owner_username"` - OwnerName string `db:"owner_name" json:"owner_name"` - OwnerAvatarUrl string `db:"owner_avatar_url" json:"owner_avatar_url"` -} - -// Stores snapshots of task state when paused, currently limited to conversation history. -type TaskSnapshot struct { - // The task this snapshot belongs to. - TaskID uuid.UUID `db:"task_id" json:"task_id"` - // Task conversation history in JSON format, allowing users to view logs when the workspace is stopped. - LogSnapshot json.RawMessage `db:"log_snapshot" json:"log_snapshot"` - // When this log snapshot was captured. - LogSnapshotCreatedAt time.Time `db:"log_snapshot_created_at" json:"log_snapshot_created_at"` -} - -type TaskTable 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"` - // Display name is a custom, human-friendly task name. - DisplayName string `db:"display_name" json:"display_name"` -} - -type TaskWorkspaceApp struct { - TaskID uuid.UUID `db:"task_id" json:"task_id"` - WorkspaceAgentID uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"` - WorkspaceAppID uuid.NullUUID `db:"workspace_app_id" json:"workspace_app_id"` - WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"` -} - type TelemetryItem struct { Key string `db:"key" json:"key"` Value string `db:"value" json:"value"` @@ -6518,7 +6390,6 @@ type Workspace struct { TemplateDisplayName string `db:"template_display_name" json:"template_display_name"` TemplateIcon string `db:"template_icon" json:"template_icon"` TemplateDescription string `db:"template_description" json:"template_description"` - TaskID uuid.NullUUID `db:"task_id" json:"task_id"` GroupACLDisplayInfo WorkspaceACLDisplayInfo `db:"group_acl_display_info" json:"group_acl_display_info"` UserACLDisplayInfo WorkspaceACLDisplayInfo `db:"user_acl_display_info" json:"user_acl_display_info"` } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index cd88b60024f..721daf760d8 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -238,7 +238,6 @@ type sqlcQuerier interface { DeleteStaleWorkspaceAgentContextResources(ctx context.Context, arg DeleteStaleWorkspaceAgentContextResourcesParams) error DeleteTailnetPeer(ctx context.Context, arg DeleteTailnetPeerParams) (DeleteTailnetPeerRow, error) DeleteTailnetTunnel(ctx context.Context, arg DeleteTailnetTunnelParams) (DeleteTailnetTunnelRow, error) - DeleteTask(ctx context.Context, arg DeleteTaskParams) (uuid.UUID, error) DeleteUnlinkedChatFilesByIDs(ctx context.Context, arg DeleteUnlinkedChatFilesByIDsParams) (int64, error) DeleteUserAIBudgetOverride(ctx context.Context, userID uuid.UUID) (UserAIBudgetOverride, error) DeleteUserAIProviderKey(ctx context.Context, arg DeleteUserAIProviderKeyParams) error @@ -824,29 +823,8 @@ type sqlcQuerier interface { GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) GetTailnetTunnelPeerBindingsBatch(ctx context.Context, ids []uuid.UUID) ([]GetTailnetTunnelPeerBindingsBatchRow, error) GetTailnetTunnelPeerIDsBatch(ctx context.Context, ids []uuid.UUID) ([]GetTailnetTunnelPeerIDsBatchRow, error) - GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error) - GetTaskByOwnerIDAndName(ctx context.Context, arg GetTaskByOwnerIDAndNameParams) (Task, error) - GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (Task, error) - GetTaskSnapshot(ctx context.Context, taskID uuid.UUID) (TaskSnapshot, error) GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error) GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error) - // Returns all data needed to build task lifecycle events for telemetry - // in a single round-trip. For each task whose workspace is in the - // given set, fetches: - // - the latest workspace app binding (task_workspace_apps) - // - the most recent stop and start builds (workspace_builds) - // - the last "working" app status (workspace_app_statuses) - // - the first app status after resume, for active workspaces - // - // Assumptions: - // - 1:1 relationship between tasks and workspaces. All builds on the - // workspace are considered task-related. - // - Idle duration approximation: If the agent reports "working", does - // work, then reports "done", we miss that working time. - // - lws and active_dur join across all historical app IDs for the task, - // because each resume cycle provisions a new app ID. This ensures - // pre-pause statuses contribute to idle duration and active duration. - GetTelemetryTaskEvents(ctx context.Context, arg GetTelemetryTaskEventsParams) ([]GetTelemetryTaskEventsRow, error) // GetTemplateAppInsights returns the aggregate usage of each app in a given // timeframe. The result can be filtered on template_ids, meaning only user data // from workspaces based on those templates will be included. @@ -1212,7 +1190,6 @@ type sqlcQuerier interface { InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) - InsertTask(ctx context.Context, arg InsertTaskParams) (TaskTable, error) InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error // Inserts a new lock row into the telemetry_locks table. Replicas should call // this function prior to attempting to generate or publish a heartbeat event to @@ -1326,7 +1303,6 @@ type sqlcQuerier interface { ListChatContextResourcesByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatContextResource, error) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error) - ListTasks(ctx context.Context, arg ListTasksParams) ([]Task, error) // Used by the usage generator to find missing heartbeat buckets. ListUsageEventCreatedAtsByTypeSince(ctx context.Context, arg ListUsageEventCreatedAtsByTypeSinceParams) ([]time.Time, error) ListUserChatCompactionThresholds(ctx context.Context, userID uuid.UUID) ([]UserConfig, error) @@ -1591,8 +1567,6 @@ type sqlcQuerier interface { UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) ([]uuid.UUID, error) - UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error) - UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWorkspaceIDParams) (TaskTable, error) UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error @@ -1736,8 +1710,6 @@ type sqlcQuerier interface { UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) - UpsertTaskSnapshot(ctx context.Context, arg UpsertTaskSnapshotParams) error - UpsertTaskWorkspaceApp(ctx context.Context, arg UpsertTaskWorkspaceAppParams) (TaskWorkspaceApp, error) UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error // This query aggregates the workspace_agent_stats and workspace_app_stats data // into a single table for efficient storage and querying. Half-hour buckets are diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index babef6c49ec..f6cfee0e696 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -10313,735 +10313,6 @@ func TestGetLatestWorkspaceBuildsByWorkspaceIDs(t *testing.T) { } } -func TestTasksWithStatusView(t *testing.T) { - t.Parallel() - - createProvisionerJob := func(t *testing.T, db database.Store, org database.Organization, user database.User, buildStatus database.ProvisionerJobStatus) database.ProvisionerJob { - t.Helper() - - var jobParams database.ProvisionerJob - - switch buildStatus { - case database.ProvisionerJobStatusPending: - jobParams = database.ProvisionerJob{ - OrganizationID: org.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - InitiatorID: user.ID, - } - case database.ProvisionerJobStatusRunning: - jobParams = database.ProvisionerJob{ - OrganizationID: org.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - InitiatorID: user.ID, - StartedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - } - case database.ProvisionerJobStatusFailed: - jobParams = database.ProvisionerJob{ - OrganizationID: org.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - InitiatorID: user.ID, - StartedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - CompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - Error: sql.NullString{Valid: true, String: "job failed"}, - } - case database.ProvisionerJobStatusSucceeded: - jobParams = database.ProvisionerJob{ - OrganizationID: org.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - InitiatorID: user.ID, - StartedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - CompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - } - case database.ProvisionerJobStatusCanceling: - jobParams = database.ProvisionerJob{ - OrganizationID: org.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - InitiatorID: user.ID, - StartedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - CanceledAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - } - case database.ProvisionerJobStatusCanceled: - jobParams = database.ProvisionerJob{ - OrganizationID: org.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - InitiatorID: user.ID, - StartedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - CompletedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - CanceledAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, - } - default: - t.Errorf("invalid build status: %v", buildStatus) - } - - return dbgen.ProvisionerJob(t, db, nil, jobParams) - } - - createTask := func( - ctx context.Context, - t *testing.T, - db database.Store, - org database.Organization, - user database.User, - buildStatus database.ProvisionerJobStatus, - buildTransition database.WorkspaceTransition, - agentState database.WorkspaceAgentLifecycleState, - appHealths []database.WorkspaceAppHealth, - ) database.Task { - t.Helper() - - template := dbgen.Template(t, db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - - if buildStatus == "" { - return dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - Name: "test-task", - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - } - - job := createProvisionerJob(t, db, org, user, buildStatus) - - workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: org.ID, - TemplateID: template.ID, - OwnerID: user.ID, - }) - workspaceID := uuid.NullUUID{Valid: true, UUID: workspace.ID} - - task := dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - Name: "test-task", - WorkspaceID: workspaceID, - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - - workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: workspace.ID, - TemplateVersionID: templateVersion.ID, - BuildNumber: 1, - Transition: buildTransition, - InitiatorID: user.ID, - JobID: job.ID, - }) - workspaceBuildNumber := workspaceBuild.BuildNumber - - _, err := db.UpsertTaskWorkspaceApp(ctx, database.UpsertTaskWorkspaceAppParams{ - TaskID: task.ID, - WorkspaceBuildNumber: workspaceBuildNumber, - }) - require.NoError(t, err) - - resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ - JobID: job.ID, - }) - - if agentState != "" { - agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ - ResourceID: resource.ID, - }) - workspaceAgentID := agent.ID - - _, err := db.UpsertTaskWorkspaceApp(ctx, database.UpsertTaskWorkspaceAppParams{ - TaskID: task.ID, - WorkspaceBuildNumber: workspaceBuildNumber, - WorkspaceAgentID: uuid.NullUUID{UUID: workspaceAgentID, Valid: true}, - }) - require.NoError(t, err) - - err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ - ID: agent.ID, - LifecycleState: agentState, - }) - require.NoError(t, err) - - for i, health := range appHealths { - app := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{ - AgentID: workspaceAgentID, - Slug: fmt.Sprintf("test-app-%d", i), - DisplayName: fmt.Sprintf("Test App %d", i+1), - Health: health, - }) - if i == 0 { - // Assume the first app is the tasks app. - _, err := db.UpsertTaskWorkspaceApp(ctx, database.UpsertTaskWorkspaceAppParams{ - TaskID: task.ID, - WorkspaceBuildNumber: workspaceBuildNumber, - WorkspaceAgentID: uuid.NullUUID{UUID: workspaceAgentID, Valid: true}, - WorkspaceAppID: uuid.NullUUID{UUID: app.ID, Valid: true}, - }) - require.NoError(t, err) - } - } - } - - return task - } - - tests := []struct { - 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", - expectBuildNumberValid: false, - expectWorkspaceAgentValid: false, - expectWorkspaceAppValid: false, - }, - { - 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: "CancelingBuild", - buildStatus: database.ProvisionerJobStatusCanceling, - buildTransition: database.WorkspaceTransitionStart, - expectedStatus: database.TaskStatusError, - description: "Latest workspace build is canceling", - expectBuildNumberValid: true, - expectBuildNumber: 1, - expectWorkspaceAgentValid: false, - expectWorkspaceAppValid: false, - }, - { - name: "CanceledBuild", - buildStatus: database.ProvisionerJobStatusCanceled, - buildTransition: database.WorkspaceTransitionStart, - expectedStatus: database.TaskStatusError, - description: "Latest workspace build was canceled", - expectBuildNumberValid: true, - expectBuildNumber: 1, - expectWorkspaceAgentValid: false, - expectWorkspaceAppValid: false, - }, - { - 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", - expectBuildNumberValid: true, - expectBuildNumber: 1, - expectWorkspaceAgentValid: false, - expectWorkspaceAppValid: false, - }, - { - name: "PendingStart", - buildStatus: database.ProvisionerJobStatusPending, - buildTransition: database.WorkspaceTransitionStart, - expectedStatus: database.TaskStatusPending, - description: "Workspace build pending (not yet picked up by provisioner)", - 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)", - 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", - 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", - 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", - 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", - 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", - 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", - expectBuildNumberValid: true, - expectBuildNumber: 1, - expectWorkspaceAgentValid: true, - expectWorkspaceAppValid: true, - }, - { - name: "AgentStartTimeout", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateStartTimeout, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy}, - expectedStatus: database.TaskStatusActive, - description: "Agent start timed out but app is healthy, defer to app", - expectBuildNumberValid: true, - expectBuildNumber: 1, - expectWorkspaceAgentValid: true, - expectWorkspaceAppValid: true, - }, - { - name: "AgentStartError", - buildStatus: database.ProvisionerJobStatusSucceeded, - buildTransition: database.WorkspaceTransitionStart, - agentState: database.WorkspaceAgentLifecycleStateStartError, - appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy}, - expectedStatus: database.TaskStatusActive, - description: "Agent start failed but app is healthy, defer to app", - expectBuildNumberValid: true, - expectBuildNumber: 1, - expectWorkspaceAgentValid: true, - expectWorkspaceAppValid: true, - }, - { - 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", - 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", - 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", - 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", - 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", - 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", - 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", - expectBuildNumberValid: true, - expectBuildNumber: 1, - expectWorkspaceAgentValid: true, - expectWorkspaceAppValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - db, _ := dbtestutil.NewDB(t) - ctx := testutil.Context(t, testutil.WaitLong) - - org := dbgen.Organization(t, db, database.Organization{}) - user := dbgen.User(t, db, database.User{}) - - task := createTask(ctx, t, db, org, user, tt.buildStatus, tt.buildTransition, tt.agentState, tt.appHealths) - - got, err := db.GetTaskByID(ctx, task.ID) - require.NoError(t, err) - - t.Logf("Task status debug: %s", got.StatusDebug) - - 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) - } - }) - } -} - -func TestGetTaskByWorkspaceID(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - setupTask func(t *testing.T, db database.Store, org database.Organization, user database.User, templateVersion database.TemplateVersion, workspace database.WorkspaceTable) - wantErr bool - }{ - { - name: "task doesn't exist", - wantErr: true, - }, - { - name: "task with no workspace id", - setupTask: func(t *testing.T, db database.Store, org database.Organization, user database.User, templateVersion database.TemplateVersion, workspace database.WorkspaceTable) { - dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - Name: "test-task", - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - }, - wantErr: true, - }, - { - name: "task with workspace id", - setupTask: func(t *testing.T, db database.Store, org database.Organization, user database.User, templateVersion database.TemplateVersion, workspace database.WorkspaceTable) { - workspaceID := uuid.NullUUID{Valid: true, UUID: workspace.ID} - dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - Name: "test-task", - WorkspaceID: workspaceID, - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - }, - wantErr: false, - }, - } - - db, _ := dbtestutil.NewDB(t) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - org := dbgen.Organization(t, db, database.Organization{}) - user := dbgen.User(t, db, database.User{}) - template := dbgen.Template(t, db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - OrganizationID: org.ID, - TemplateID: uuid.NullUUID{Valid: true, UUID: template.ID}, - CreatedBy: user.ID, - }) - workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - TemplateID: template.ID, - }) - - if tt.setupTask != nil { - tt.setupTask(t, db, org, user, templateVersion, workspace) - } - - ctx := testutil.Context(t, testutil.WaitLong) - - 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) - } - }) - } -} - -func TestDeleteTaskDeletesTaskSnapshot(t *testing.T) { - t.Parallel() - - db, _ := dbtestutil.NewDB(t) - ctx := testutil.Context(t, testutil.WaitLong) - - org := dbgen.Organization(t, db, database.Organization{}) - user := dbgen.User(t, db, database.User{}) - template := dbgen.Template(t, db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - task := dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - - err := db.UpsertTaskSnapshot(ctx, database.UpsertTaskSnapshotParams{ - TaskID: task.ID, - LogSnapshot: json.RawMessage(`{"messages":[]}`), - LogSnapshotCreatedAt: dbtime.Now(), - }) - require.NoError(t, err) - - _, err = db.DeleteTask(ctx, database.DeleteTaskParams{ - ID: task.ID, - DeletedAt: dbtime.Now(), - }) - require.NoError(t, err) - - _, err = db.GetTaskSnapshot(ctx, task.ID) - require.ErrorIs(t, err, sql.ErrNoRows) -} - -func TestTaskNameUniqueness(t *testing.T) { - t.Parallel() - - db, _ := dbtestutil.NewDB(t) - - org := dbgen.Organization(t, db, database.Organization{}) - user1 := dbgen.User(t, db, database.User{}) - user2 := dbgen.User(t, db, database.User{}) - template := dbgen.Template(t, db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user1.ID, - }) - tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, - OrganizationID: org.ID, - CreatedBy: user1.ID, - }) - - taskName := "my-task" - - // Create initial task for user1. - task1 := dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user1.ID, - Name: taskName, - TemplateVersionID: tv.ID, - Prompt: "Test prompt", - }) - require.NotEqual(t, uuid.Nil, task1.ID) - - tests := []struct { - name string - ownerID uuid.UUID - taskName string - wantErr bool - }{ - { - name: "duplicate task name same user", - ownerID: user1.ID, - taskName: taskName, - wantErr: true, - }, - { - name: "duplicate task name different case same user", - ownerID: user1.ID, - taskName: "MY-TASK", - wantErr: true, - }, - { - name: "same task name different user", - ownerID: user2.ID, - taskName: taskName, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - ctx := testutil.Context(t, testutil.WaitShort) - - taskID := uuid.New() - task, err := db.InsertTask(ctx, database.InsertTaskParams{ - ID: taskID, - OrganizationID: org.ID, - OwnerID: tt.ownerID, - Name: tt.taskName, - TemplateVersionID: tv.ID, - TemplateParameters: json.RawMessage("{}"), - Prompt: "Test prompt", - CreatedAt: dbtime.Now(), - }) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - require.NotEqual(t, uuid.Nil, task.ID) - require.NotEqual(t, task1.ID, task.ID) - require.Equal(t, taskID, task.ID) - } - }) - } -} - func TestUsageEventsTrigger(t *testing.T) { t.Parallel() @@ -11569,334 +10840,6 @@ func TestListUsageEventCreatedAtsByTypeSince(t *testing.T) { require.ElementsMatch(t, []time.Time{since, since.Add(time.Hour)}, normalized) } -func TestListTasks(t *testing.T) { - t.Parallel() - - db, ps := dbtestutil.NewDB(t) - - // Given: two organizations and two users, one of which is a member of both - org1 := dbgen.Organization(t, db, database.Organization{}) - org2 := dbgen.Organization(t, db, database.Organization{}) - user1 := dbgen.User(t, db, database.User{}) - user2 := dbgen.User(t, db, database.User{}) - _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{ - OrganizationID: org1.ID, - UserID: user1.ID, - }) - _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{ - OrganizationID: org2.ID, - UserID: user2.ID, - }) - - // Given: a template with an active version - tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - CreatedBy: user1.ID, - OrganizationID: org1.ID, - }) - tpl := dbgen.Template(t, db, database.Template{ - CreatedBy: user1.ID, - OrganizationID: org1.ID, - ActiveVersionID: tv.ID, - }) - - // Helper function to create a task - createTask := func(orgID, ownerID uuid.UUID) database.Task { - ws := dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: orgID, - OwnerID: ownerID, - TemplateID: tpl.ID, - }) - pj := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{}) - sidebarAppID := uuid.New() - wb := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - JobID: pj.ID, - TemplateVersionID: tv.ID, - WorkspaceID: ws.ID, - }) - wr := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ - JobID: pj.ID, - }) - agt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ - ResourceID: wr.ID, - }) - wa := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{ - ID: sidebarAppID, - AgentID: agt.ID, - }) - tsk := dbgen.Task(t, db, database.TaskTable{ - OrganizationID: orgID, - OwnerID: ownerID, - Prompt: testutil.GetRandomName(t), - TemplateVersionID: tv.ID, - WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true}, - }) - _ = dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{ - 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 - } - - // Given: user1 has one task, user2 has one task, user3 has two tasks (one in each org) - task1 := createTask(org1.ID, user1.ID) - task2 := createTask(org1.ID, user2.ID) - task3 := createTask(org2.ID, user2.ID) - - // Then: run various filters and assert expected results - for _, tc := range []struct { - name string - filter database.ListTasksParams - expectIDs []uuid.UUID - }{ - { - name: "no filter", - filter: database.ListTasksParams{ - OwnerID: uuid.Nil, - OrganizationID: uuid.Nil, - }, - expectIDs: []uuid.UUID{task3.ID, task2.ID, task1.ID}, - }, - { - name: "filter by user ID", - filter: database.ListTasksParams{ - OwnerID: user1.ID, - OrganizationID: uuid.Nil, - }, - expectIDs: []uuid.UUID{task1.ID}, - }, - { - name: "filter by organization ID", - filter: database.ListTasksParams{ - OwnerID: uuid.Nil, - OrganizationID: org1.ID, - }, - expectIDs: []uuid.UUID{task2.ID, task1.ID}, - }, - { - name: "filter by user and organization ID", - filter: database.ListTasksParams{ - OwnerID: user2.ID, - OrganizationID: org2.ID, - }, - expectIDs: []uuid.UUID{task3.ID}, - }, - { - name: "no results", - filter: database.ListTasksParams{ - OwnerID: user1.ID, - OrganizationID: org2.ID, - }, - expectIDs: nil, - }, - } { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) - tasks, err := db.ListTasks(ctx, tc.filter) - 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) - } - }) - } -} - -func TestUpdateTaskWorkspaceID(t *testing.T) { - t.Parallel() - - db, _ := dbtestutil.NewDB(t) - - // Create organization, users, template, and template version. - org := dbgen.Organization(t, db, database.Organization{}) - user := dbgen.User(t, db, database.User{}) - template := dbgen.Template(t, db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - OrganizationID: org.ID, - TemplateID: uuid.NullUUID{Valid: true, UUID: template.ID}, - CreatedBy: user.ID, - }) - - // Create another template for mismatch test. - template2 := dbgen.Template(t, db, database.Template{ - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - - tests := []struct { - name string - setupTask func(t *testing.T) database.Task - setupWS func(t *testing.T) database.WorkspaceTable - wantErr bool - wantNoRow bool - }{ - { - name: "successful update with matching template", - setupTask: func(t *testing.T) database.Task { - return dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - Name: testutil.GetRandomName(t), - WorkspaceID: uuid.NullUUID{}, - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - }, - setupWS: func(t *testing.T) database.WorkspaceTable { - return dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - TemplateID: template.ID, - }) - }, - wantErr: false, - wantNoRow: false, - }, - { - name: "task already has workspace_id", - setupTask: func(t *testing.T) database.Task { - existingWS := dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - TemplateID: template.ID, - }) - return dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - Name: testutil.GetRandomName(t), - WorkspaceID: uuid.NullUUID{Valid: true, UUID: existingWS.ID}, - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - }, - setupWS: func(t *testing.T) database.WorkspaceTable { - return dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - TemplateID: template.ID, - }) - }, - wantErr: false, - wantNoRow: true, // No row should be returned because WHERE condition fails. - }, - { - name: "template mismatch between task and workspace", - setupTask: func(t *testing.T) database.Task { - return dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - Name: testutil.GetRandomName(t), - WorkspaceID: uuid.NullUUID{}, // NULL workspace_id - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - }, - setupWS: func(t *testing.T) database.WorkspaceTable { - return dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - TemplateID: template2.ID, // Different template, JOIN will fail. - }) - }, - wantErr: false, - wantNoRow: true, // No row should be returned because JOIN condition fails. - }, - { - name: "task does not exist", - setupTask: func(t *testing.T) database.Task { - return database.Task{ - ID: uuid.New(), // Non-existent task ID. - } - }, - setupWS: func(t *testing.T) database.WorkspaceTable { - return dbgen.Workspace(t, db, database.WorkspaceTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - TemplateID: template.ID, - }) - }, - wantErr: false, - wantNoRow: true, - }, - { - name: "workspace does not exist", - setupTask: func(t *testing.T) database.Task { - return dbgen.Task(t, db, database.TaskTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - Name: testutil.GetRandomName(t), - WorkspaceID: uuid.NullUUID{}, - TemplateVersionID: templateVersion.ID, - Prompt: "Test prompt", - }) - }, - setupWS: func(t *testing.T) database.WorkspaceTable { - return database.WorkspaceTable{ - ID: uuid.New(), // Non-existent workspace ID. - } - }, - wantErr: false, - wantNoRow: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - ctx := testutil.Context(t, testutil.WaitShort) - - task := tt.setupTask(t) - workspace := tt.setupWS(t) - - updatedTask, err := db.UpdateTaskWorkspaceID(ctx, database.UpdateTaskWorkspaceIDParams{ - ID: task.ID, - WorkspaceID: uuid.NullUUID{Valid: true, UUID: workspace.ID}, - }) - - if tt.wantErr { - require.Error(t, err) - return - } - - if tt.wantNoRow { - require.ErrorIs(t, err, sql.ErrNoRows) - return - } - - require.NoError(t, err) - require.Equal(t, task.ID, updatedTask.ID) - require.True(t, updatedTask.WorkspaceID.Valid) - require.Equal(t, workspace.ID, updatedTask.WorkspaceID.UUID) - require.Equal(t, task.OrganizationID, updatedTask.OrganizationID) - require.Equal(t, task.OwnerID, updatedTask.OwnerID) - require.Equal(t, task.Name, updatedTask.Name) - require.Equal(t, task.TemplateVersionID, updatedTask.TemplateVersionID) - - // Verify the update persisted by fetching the task again. - fetchedTask, err := db.GetTaskByID(ctx, task.ID) - require.NoError(t, err) - require.True(t, fetchedTask.WorkspaceID.Valid) - require.Equal(t, workspace.ID, fetchedTask.WorkspaceID.UUID) - }) - } -} - func TestUpdateAIBridgeInterceptionEnded(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fe06b407691..ee7d2c72bc5 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -27241,612 +27241,6 @@ func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetT return i, err } -const deleteTask = `-- name: DeleteTask :one -WITH deleted_task AS ( - UPDATE tasks - SET - deleted_at = $1::timestamptz - WHERE - id = $2::uuid - AND deleted_at IS NULL - RETURNING id -), deleted_snapshot AS ( - DELETE FROM task_snapshots - WHERE task_id = $2::uuid -) -SELECT id FROM deleted_task -` - -type DeleteTaskParams struct { - DeletedAt time.Time `db:"deleted_at" json:"deleted_at"` - ID uuid.UUID `db:"id" json:"id"` -} - -func (q *sqlQuerier) DeleteTask(ctx context.Context, arg DeleteTaskParams) (uuid.UUID, error) { - row := q.db.QueryRowContext(ctx, deleteTask, arg.DeletedAt, arg.ID) - var id uuid.UUID - err := row.Scan(&id) - return id, err -} - -const getTaskByID = `-- name: GetTaskByID :one -SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE id = $1::uuid -` - -func (q *sqlQuerier) GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error) { - row := q.db.QueryRowContext(ctx, getTaskByID, id) - var i Task - err := row.Scan( - &i.ID, - &i.OrganizationID, - &i.OwnerID, - &i.Name, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.TemplateParameters, - &i.Prompt, - &i.CreatedAt, - &i.DeletedAt, - &i.DisplayName, - &i.WorkspaceGroupACL, - &i.WorkspaceUserACL, - &i.Status, - &i.StatusDebug, - &i.WorkspaceBuildNumber, - &i.WorkspaceAgentID, - &i.WorkspaceAppID, - &i.WorkspaceAgentLifecycleState, - &i.WorkspaceAppHealth, - &i.OwnerUsername, - &i.OwnerName, - &i.OwnerAvatarUrl, - ) - return i, err -} - -const getTaskByOwnerIDAndName = `-- name: GetTaskByOwnerIDAndName :one -SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status -WHERE - owner_id = $1::uuid - AND deleted_at IS NULL - AND LOWER(name) = LOWER($2::text) -` - -type GetTaskByOwnerIDAndNameParams struct { - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - Name string `db:"name" json:"name"` -} - -func (q *sqlQuerier) GetTaskByOwnerIDAndName(ctx context.Context, arg GetTaskByOwnerIDAndNameParams) (Task, error) { - row := q.db.QueryRowContext(ctx, getTaskByOwnerIDAndName, arg.OwnerID, arg.Name) - var i Task - err := row.Scan( - &i.ID, - &i.OrganizationID, - &i.OwnerID, - &i.Name, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.TemplateParameters, - &i.Prompt, - &i.CreatedAt, - &i.DeletedAt, - &i.DisplayName, - &i.WorkspaceGroupACL, - &i.WorkspaceUserACL, - &i.Status, - &i.StatusDebug, - &i.WorkspaceBuildNumber, - &i.WorkspaceAgentID, - &i.WorkspaceAppID, - &i.WorkspaceAgentLifecycleState, - &i.WorkspaceAppHealth, - &i.OwnerUsername, - &i.OwnerName, - &i.OwnerAvatarUrl, - ) - 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, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url FROM tasks_with_status WHERE workspace_id = $1::uuid -` - -func (q *sqlQuerier) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (Task, error) { - row := q.db.QueryRowContext(ctx, getTaskByWorkspaceID, workspaceID) - var i Task - err := row.Scan( - &i.ID, - &i.OrganizationID, - &i.OwnerID, - &i.Name, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.TemplateParameters, - &i.Prompt, - &i.CreatedAt, - &i.DeletedAt, - &i.DisplayName, - &i.WorkspaceGroupACL, - &i.WorkspaceUserACL, - &i.Status, - &i.StatusDebug, - &i.WorkspaceBuildNumber, - &i.WorkspaceAgentID, - &i.WorkspaceAppID, - &i.WorkspaceAgentLifecycleState, - &i.WorkspaceAppHealth, - &i.OwnerUsername, - &i.OwnerName, - &i.OwnerAvatarUrl, - ) - return i, err -} - -const getTaskSnapshot = `-- name: GetTaskSnapshot :one -SELECT - task_id, log_snapshot, log_snapshot_created_at -FROM - task_snapshots -WHERE - task_id = $1 -` - -func (q *sqlQuerier) GetTaskSnapshot(ctx context.Context, taskID uuid.UUID) (TaskSnapshot, error) { - row := q.db.QueryRowContext(ctx, getTaskSnapshot, taskID) - var i TaskSnapshot - err := row.Scan(&i.TaskID, &i.LogSnapshot, &i.LogSnapshotCreatedAt) - return i, err -} - -const getTelemetryTaskEvents = `-- name: GetTelemetryTaskEvents :many -WITH task_app_ids AS ( - SELECT task_id, workspace_app_id - FROM task_workspace_apps -), -task_status_timeline AS ( - -- All app statuses across every historical app for each task, - -- plus synthetic "boundary" rows at each stop/start build transition. - -- This allows us to correctly take gaps due to pause/resume into account. - SELECT tai.task_id, was.created_at, was.state::text AS state - FROM workspace_app_statuses was - JOIN task_app_ids tai ON tai.workspace_app_id = was.app_id - UNION ALL - SELECT t.id AS task_id, wb.created_at, '_boundary' AS state - FROM tasks t - JOIN workspace_builds wb ON wb.workspace_id = t.workspace_id - WHERE t.deleted_at IS NULL - AND t.workspace_id IS NOT NULL - AND wb.build_number > 1 -), -task_event_data AS ( - SELECT - t.id AS task_id, - t.workspace_id, - twa.workspace_app_id, - -- Latest stop build. - stop_build.created_at AS stop_build_created_at, - stop_build.reason AS stop_build_reason, - -- Latest start build (task_resume only). - start_build.created_at AS start_build_created_at, - start_build.reason AS start_build_reason, - start_build.build_number AS start_build_number, - -- Last "working" app status (for idle duration). - lws.created_at AS last_working_status_at, - -- First app status after resume (for resume-to-status duration). - -- Only populated for workspaces in an active phase (started more - -- recently than stopped). - fsar.created_at AS first_status_after_resume_at, - -- Cumulative time spent in "working" state. - active_dur.total_working_ms AS active_duration_ms - FROM tasks t - LEFT JOIN LATERAL ( - SELECT task_app.workspace_app_id - FROM task_workspace_apps task_app - WHERE task_app.task_id = t.id - ORDER BY task_app.workspace_build_number DESC - LIMIT 1 - ) twa ON TRUE - LEFT JOIN LATERAL ( - SELECT wb.created_at, wb.reason, wb.build_number - FROM workspace_builds wb - WHERE wb.workspace_id = t.workspace_id - AND wb.transition = 'stop' - ORDER BY wb.build_number DESC - LIMIT 1 - ) stop_build ON TRUE - LEFT JOIN LATERAL ( - SELECT wb.created_at, wb.reason, wb.build_number - FROM workspace_builds wb - WHERE wb.workspace_id = t.workspace_id - AND wb.transition = 'start' - ORDER BY wb.build_number DESC - LIMIT 1 - ) start_build ON TRUE - LEFT JOIN LATERAL ( - SELECT tst.created_at - FROM task_status_timeline tst - WHERE tst.task_id = t.id - AND tst.state = 'working' - -- Only consider status before the latest pause so that - -- post-resume statuses don't mask pre-pause idle time. - AND (stop_build.created_at IS NULL - OR tst.created_at <= stop_build.created_at) - ORDER BY tst.created_at DESC - LIMIT 1 - ) lws ON TRUE - LEFT JOIN LATERAL ( - SELECT was.created_at - FROM workspace_app_statuses was - WHERE was.app_id = twa.workspace_app_id - AND was.created_at > start_build.created_at - ORDER BY was.created_at ASC - LIMIT 1 - ) fsar ON twa.workspace_app_id IS NOT NULL - AND start_build.created_at IS NOT NULL - AND (stop_build.created_at IS NULL - OR start_build.created_at > stop_build.created_at) - -- Active duration: cumulative time spent in "working" state across all - -- historical app IDs for this task. Uses LEAD() to convert point-in-time - -- statuses into intervals, then sums intervals where state='working'. For - -- the last status, falls back to stop_build time (if paused) or @now (if - -- still running). - LEFT JOIN LATERAL ( - SELECT COALESCE( - SUM(EXTRACT(EPOCH FROM (interval_end - interval_start)) * 1000)::bigint, - 0 - )::bigint AS total_working_ms - FROM ( - SELECT - tst.created_at AS interval_start, - COALESCE( - LEAD(tst.created_at) OVER (ORDER BY tst.created_at ASC, CASE WHEN tst.state = '_boundary' THEN 1 ELSE 0 END ASC), - CASE WHEN stop_build.created_at IS NOT NULL - AND (start_build.created_at IS NULL - OR stop_build.created_at > start_build.created_at) - THEN stop_build.created_at - ELSE $1::timestamptz - END - ) AS interval_end, - tst.state - FROM task_status_timeline tst - WHERE tst.task_id = t.id - ) intervals - WHERE intervals.state = 'working' - ) active_dur ON TRUE - WHERE t.deleted_at IS NULL - AND t.workspace_id IS NOT NULL - AND EXISTS ( - SELECT 1 FROM workspace_builds wb - WHERE wb.workspace_id = t.workspace_id - AND wb.created_at > $2 - ) -) -SELECT task_id, workspace_id, workspace_app_id, stop_build_created_at, stop_build_reason, start_build_created_at, start_build_reason, start_build_number, last_working_status_at, first_status_after_resume_at, active_duration_ms FROM task_event_data -ORDER BY task_id -` - -type GetTelemetryTaskEventsParams struct { - Now time.Time `db:"now" json:"now"` - CreatedAfter time.Time `db:"created_after" json:"created_after"` -} - -type GetTelemetryTaskEventsRow struct { - TaskID uuid.UUID `db:"task_id" json:"task_id"` - WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` - WorkspaceAppID uuid.NullUUID `db:"workspace_app_id" json:"workspace_app_id"` - StopBuildCreatedAt sql.NullTime `db:"stop_build_created_at" json:"stop_build_created_at"` - StopBuildReason NullBuildReason `db:"stop_build_reason" json:"stop_build_reason"` - StartBuildCreatedAt sql.NullTime `db:"start_build_created_at" json:"start_build_created_at"` - StartBuildReason NullBuildReason `db:"start_build_reason" json:"start_build_reason"` - StartBuildNumber sql.NullInt32 `db:"start_build_number" json:"start_build_number"` - LastWorkingStatusAt sql.NullTime `db:"last_working_status_at" json:"last_working_status_at"` - FirstStatusAfterResumeAt sql.NullTime `db:"first_status_after_resume_at" json:"first_status_after_resume_at"` - ActiveDurationMs int64 `db:"active_duration_ms" json:"active_duration_ms"` -} - -// Returns all data needed to build task lifecycle events for telemetry -// in a single round-trip. For each task whose workspace is in the -// given set, fetches: -// - the latest workspace app binding (task_workspace_apps) -// - the most recent stop and start builds (workspace_builds) -// - the last "working" app status (workspace_app_statuses) -// - the first app status after resume, for active workspaces -// -// Assumptions: -// - 1:1 relationship between tasks and workspaces. All builds on the -// workspace are considered task-related. -// - Idle duration approximation: If the agent reports "working", does -// work, then reports "done", we miss that working time. -// - lws and active_dur join across all historical app IDs for the task, -// because each resume cycle provisions a new app ID. This ensures -// pre-pause statuses contribute to idle duration and active duration. -func (q *sqlQuerier) GetTelemetryTaskEvents(ctx context.Context, arg GetTelemetryTaskEventsParams) ([]GetTelemetryTaskEventsRow, error) { - rows, err := q.db.QueryContext(ctx, getTelemetryTaskEvents, arg.Now, arg.CreatedAfter) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetTelemetryTaskEventsRow - for rows.Next() { - var i GetTelemetryTaskEventsRow - if err := rows.Scan( - &i.TaskID, - &i.WorkspaceID, - &i.WorkspaceAppID, - &i.StopBuildCreatedAt, - &i.StopBuildReason, - &i.StartBuildCreatedAt, - &i.StartBuildReason, - &i.StartBuildNumber, - &i.LastWorkingStatusAt, - &i.FirstStatusAfterResumeAt, - &i.ActiveDurationMs, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertTask = `-- name: InsertTask :one -INSERT INTO tasks - (id, organization_id, owner_id, name, display_name, workspace_id, template_version_id, template_parameters, prompt, created_at) -VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name -` - -type InsertTaskParams 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"` - DisplayName string `db:"display_name" json:"display_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"` -} - -func (q *sqlQuerier) InsertTask(ctx context.Context, arg InsertTaskParams) (TaskTable, error) { - row := q.db.QueryRowContext(ctx, insertTask, - arg.ID, - arg.OrganizationID, - arg.OwnerID, - arg.Name, - arg.DisplayName, - arg.WorkspaceID, - arg.TemplateVersionID, - arg.TemplateParameters, - arg.Prompt, - arg.CreatedAt, - ) - var i TaskTable - err := row.Scan( - &i.ID, - &i.OrganizationID, - &i.OwnerID, - &i.Name, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.TemplateParameters, - &i.Prompt, - &i.CreatedAt, - &i.DeletedAt, - &i.DisplayName, - ) - return i, err -} - -const listTasks = `-- name: ListTasks :many -SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name, workspace_group_acl, workspace_user_acl, status, status_debug, workspace_build_number, workspace_agent_id, workspace_app_id, workspace_agent_lifecycle_state, workspace_app_health, owner_username, owner_name, owner_avatar_url 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 -AND CASE WHEN $3::text != '' THEN tws.status = $3::task_status ELSE TRUE END -ORDER BY tws.created_at DESC -` - -type ListTasksParams struct { - OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - Status string `db:"status" json:"status"` -} - -func (q *sqlQuerier) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task, error) { - rows, err := q.db.QueryContext(ctx, listTasks, arg.OwnerID, arg.OrganizationID, arg.Status) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Task - for rows.Next() { - var i Task - if err := rows.Scan( - &i.ID, - &i.OrganizationID, - &i.OwnerID, - &i.Name, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.TemplateParameters, - &i.Prompt, - &i.CreatedAt, - &i.DeletedAt, - &i.DisplayName, - &i.WorkspaceGroupACL, - &i.WorkspaceUserACL, - &i.Status, - &i.StatusDebug, - &i.WorkspaceBuildNumber, - &i.WorkspaceAgentID, - &i.WorkspaceAppID, - &i.WorkspaceAgentLifecycleState, - &i.WorkspaceAppHealth, - &i.OwnerUsername, - &i.OwnerName, - &i.OwnerAvatarUrl, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateTaskPrompt = `-- name: UpdateTaskPrompt :one -UPDATE - tasks -SET - prompt = $1::text -WHERE - id = $2::uuid - AND deleted_at IS NULL -RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, display_name -` - -type UpdateTaskPromptParams struct { - Prompt string `db:"prompt" json:"prompt"` - ID uuid.UUID `db:"id" json:"id"` -} - -func (q *sqlQuerier) UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error) { - row := q.db.QueryRowContext(ctx, updateTaskPrompt, arg.Prompt, arg.ID) - var i TaskTable - err := row.Scan( - &i.ID, - &i.OrganizationID, - &i.OwnerID, - &i.Name, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.TemplateParameters, - &i.Prompt, - &i.CreatedAt, - &i.DeletedAt, - &i.DisplayName, - ) - return i, err -} - -const updateTaskWorkspaceID = `-- name: UpdateTaskWorkspaceID :one -UPDATE - tasks -SET - workspace_id = $2 -FROM - workspaces w -JOIN - template_versions tv -ON - tv.template_id = w.template_id -WHERE - tasks.id = $1 - AND tasks.workspace_id IS NULL - AND w.id = $2 - AND tv.id = tasks.template_version_id -RETURNING - tasks.id, tasks.organization_id, tasks.owner_id, tasks.name, tasks.workspace_id, tasks.template_version_id, tasks.template_parameters, tasks.prompt, tasks.created_at, tasks.deleted_at, tasks.display_name -` - -type UpdateTaskWorkspaceIDParams struct { - ID uuid.UUID `db:"id" json:"id"` - WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"` -} - -func (q *sqlQuerier) UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWorkspaceIDParams) (TaskTable, error) { - row := q.db.QueryRowContext(ctx, updateTaskWorkspaceID, arg.ID, arg.WorkspaceID) - var i TaskTable - err := row.Scan( - &i.ID, - &i.OrganizationID, - &i.OwnerID, - &i.Name, - &i.WorkspaceID, - &i.TemplateVersionID, - &i.TemplateParameters, - &i.Prompt, - &i.CreatedAt, - &i.DeletedAt, - &i.DisplayName, - ) - return i, err -} - -const upsertTaskSnapshot = `-- name: UpsertTaskSnapshot :exec -INSERT INTO - task_snapshots (task_id, log_snapshot, log_snapshot_created_at) -VALUES - ($1, $2, $3) -ON CONFLICT - (task_id) -DO UPDATE SET - log_snapshot = EXCLUDED.log_snapshot, - log_snapshot_created_at = EXCLUDED.log_snapshot_created_at -` - -type UpsertTaskSnapshotParams struct { - TaskID uuid.UUID `db:"task_id" json:"task_id"` - LogSnapshot json.RawMessage `db:"log_snapshot" json:"log_snapshot"` - LogSnapshotCreatedAt time.Time `db:"log_snapshot_created_at" json:"log_snapshot_created_at"` -} - -func (q *sqlQuerier) UpsertTaskSnapshot(ctx context.Context, arg UpsertTaskSnapshotParams) error { - _, err := q.db.ExecContext(ctx, upsertTaskSnapshot, arg.TaskID, arg.LogSnapshot, arg.LogSnapshotCreatedAt) - return err -} - -const upsertTaskWorkspaceApp = `-- name: UpsertTaskWorkspaceApp :one -INSERT INTO task_workspace_apps - (task_id, workspace_build_number, workspace_agent_id, workspace_app_id) -VALUES - ($1, $2, $3, $4) -ON CONFLICT (task_id, workspace_build_number) -DO UPDATE SET - workspace_agent_id = EXCLUDED.workspace_agent_id, - workspace_app_id = EXCLUDED.workspace_app_id -RETURNING task_id, workspace_agent_id, workspace_app_id, workspace_build_number -` - -type UpsertTaskWorkspaceAppParams struct { - TaskID uuid.UUID `db:"task_id" json:"task_id"` - WorkspaceBuildNumber int32 `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"` -} - -func (q *sqlQuerier) UpsertTaskWorkspaceApp(ctx context.Context, arg UpsertTaskWorkspaceAppParams) (TaskWorkspaceApp, error) { - row := q.db.QueryRowContext(ctx, upsertTaskWorkspaceApp, - arg.TaskID, - arg.WorkspaceBuildNumber, - arg.WorkspaceAgentID, - arg.WorkspaceAppID, - ) - var i TaskWorkspaceApp - err := row.Scan( - &i.TaskID, - &i.WorkspaceAgentID, - &i.WorkspaceAppID, - &i.WorkspaceBuildNumber, - ) - return i, err -} - const getTelemetryItem = `-- name: GetTelemetryItem :one SELECT key, value, created_at, updated_at FROM telemetry_items WHERE key = $1 ` @@ -33968,8 +33362,7 @@ const getAuthenticatedWorkspaceAgentAndBuildByAuthToken = `-- name: GetAuthentic SELECT workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted, - workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_external_agent, workspace_build_with_user.notified_autostop_deadline, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name, - tasks.id AS task_id + workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_external_agent, workspace_build_with_user.notified_autostop_deadline, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name FROM workspace_agents JOIN @@ -33984,10 +33377,6 @@ JOIN workspaces ON workspace_build_with_user.workspace_id = workspaces.id -LEFT JOIN - tasks -ON - tasks.workspace_id = workspaces.id WHERE -- This should only match 1 agent, so 1 returned row or 0. workspace_agents.auth_token = $1::uuid @@ -34034,7 +33423,6 @@ type GetAuthenticatedWorkspaceAgentAndBuildByAuthTokenRow struct { WorkspaceTable WorkspaceTable `db:"workspace_table" json:"workspace_table"` WorkspaceAgent WorkspaceAgent `db:"workspace_agent" json:"workspace_agent"` WorkspaceBuild WorkspaceBuild `db:"workspace_build" json:"workspace_build"` - TaskID uuid.NullUUID `db:"task_id" json:"task_id"` } // GetAuthenticatedWorkspaceAgentAndBuildByAuthToken returns an authenticated @@ -34117,7 +33505,6 @@ func (q *sqlQuerier) GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx conte &i.WorkspaceBuild.InitiatorByAvatarUrl, &i.WorkspaceBuild.InitiatorByUsername, &i.WorkspaceBuild.InitiatorByName, - &i.TaskID, ) return i, err } @@ -39385,7 +38772,7 @@ func (q *sqlQuerier) GetWorkspaceACLByID(ctx context.Context, id uuid.UUID) (Get const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, group_acl_display_info, user_acl_display_info FROM workspaces_expanded as workspaces WHERE @@ -39446,7 +38833,6 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, - &i.TaskID, &i.GroupACLDisplayInfo, &i.UserACLDisplayInfo, ) @@ -39455,7 +38841,7 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI const getWorkspaceByID = `-- name: GetWorkspaceByID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, group_acl_display_info, user_acl_display_info FROM workspaces_expanded WHERE @@ -39497,7 +38883,6 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, - &i.TaskID, &i.GroupACLDisplayInfo, &i.UserACLDisplayInfo, ) @@ -39506,7 +38891,7 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, group_acl_display_info, user_acl_display_info FROM workspaces_expanded as workspaces WHERE @@ -39555,7 +38940,6 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, - &i.TaskID, &i.GroupACLDisplayInfo, &i.UserACLDisplayInfo, ) @@ -39564,7 +38948,7 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo const getWorkspaceByResourceID = `-- name: GetWorkspaceByResourceID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, group_acl_display_info, user_acl_display_info FROM workspaces_expanded as workspaces WHERE @@ -39620,7 +39004,6 @@ func (q *sqlQuerier) GetWorkspaceByResourceID(ctx context.Context, resourceID uu &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, - &i.TaskID, &i.GroupACLDisplayInfo, &i.UserACLDisplayInfo, ) @@ -39629,7 +39012,7 @@ func (q *sqlQuerier) GetWorkspaceByResourceID(ctx context.Context, resourceID uu const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, task_id, group_acl_display_info, user_acl_display_info + id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates, favorite, next_start_at, group_acl, user_acl, owner_avatar_url, owner_username, owner_name, organization_name, organization_display_name, organization_icon, organization_description, template_name, template_display_name, template_icon, template_description, group_acl_display_info, user_acl_display_info FROM workspaces_expanded as workspaces WHERE @@ -39697,7 +39080,6 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, - &i.TaskID, &i.GroupACLDisplayInfo, &i.UserACLDisplayInfo, ) @@ -39749,7 +39131,7 @@ SELECT ), filtered_workspaces AS ( SELECT - workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.owner_name, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description, workspaces.task_id, workspaces.group_acl_display_info, workspaces.user_acl_display_info, + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl, workspaces.owner_avatar_url, workspaces.owner_username, workspaces.owner_name, workspaces.organization_name, workspaces.organization_display_name, workspaces.organization_icon, workspaces.organization_description, workspaces.template_name, workspaces.template_display_name, workspaces.template_icon, workspaces.template_description, workspaces.group_acl_display_info, workspaces.user_acl_display_info, latest_build.template_version_id, latest_build.template_version_name, latest_build.completed_at as latest_build_completed_at, @@ -40019,7 +39401,7 @@ WHERE -- @authorize_filter ), filtered_workspaces_order AS ( SELECT - fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.group_acl_display_info, fw.user_acl_display_info, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_external_agent, fw.latest_build_provisioner_job_id + fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.group_acl_display_info, fw.user_acl_display_info, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_external_agent, fw.latest_build_provisioner_job_id FROM filtered_workspaces fw ORDER BY @@ -40040,7 +39422,7 @@ WHERE $25 ), filtered_workspaces_order_with_summary AS ( SELECT - fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.group_acl_display_info, fwo.user_acl_display_info, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent, fwo.latest_build_provisioner_job_id + fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.group_acl_display_info, fwo.user_acl_display_info, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent, fwo.latest_build_provisioner_job_id FROM filtered_workspaces_order fwo -- Return a technical summary row with total count of workspaces. @@ -40076,7 +39458,6 @@ WHERE '', -- template_display_name '', -- template_icon '', -- template_description - '00000000-0000-0000-0000-000000000000'::uuid, -- task_id '{}'::jsonb, -- group_acl_display_info '{}'::jsonb, -- user_acl_display_info -- Extra columns added to ` + "`" + `filtered_workspaces` + "`" + ` @@ -40098,7 +39479,7 @@ WHERE filtered_workspaces ) SELECT - fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.group_acl_display_info, fwos.user_acl_display_info, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent, fwos.latest_build_provisioner_job_id, + fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.group_acl_display_info, fwos.user_acl_display_info, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent, fwos.latest_build_provisioner_job_id, -- agent_metadata expands the response with the requested agent -- metadata keys for the latest build's agents. The CASE keeps the -- subquery unevaluated for every caller that does not opt in, and @@ -40219,7 +39600,6 @@ type GetWorkspacesRow struct { TemplateDisplayName string `db:"template_display_name" json:"template_display_name"` TemplateIcon string `db:"template_icon" json:"template_icon"` TemplateDescription string `db:"template_description" json:"template_description"` - TaskID uuid.NullUUID `db:"task_id" json:"task_id"` GroupACLDisplayInfo interface{} `db:"group_acl_display_info" json:"group_acl_display_info"` UserACLDisplayInfo interface{} `db:"user_acl_display_info" json:"user_acl_display_info"` TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` @@ -40305,7 +39685,6 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) &i.TemplateDisplayName, &i.TemplateIcon, &i.TemplateDescription, - &i.TaskID, &i.GroupACLDisplayInfo, &i.UserACLDisplayInfo, &i.TemplateVersionID, diff --git a/coderd/database/queries/tasks.sql b/coderd/database/queries/tasks.sql deleted file mode 100644 index 0673c78cc35..00000000000 --- a/coderd/database/queries/tasks.sql +++ /dev/null @@ -1,245 +0,0 @@ --- name: InsertTask :one -INSERT INTO tasks - (id, organization_id, owner_id, name, display_name, workspace_id, template_version_id, template_parameters, prompt, created_at) -VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING *; - --- name: UpdateTaskWorkspaceID :one -UPDATE - tasks -SET - workspace_id = $2 -FROM - workspaces w -JOIN - template_versions tv -ON - tv.template_id = w.template_id -WHERE - tasks.id = $1 - AND tasks.workspace_id IS NULL - AND w.id = $2 - AND tv.id = tasks.template_version_id -RETURNING - tasks.*; - --- name: UpsertTaskWorkspaceApp :one -INSERT INTO task_workspace_apps - (task_id, workspace_build_number, workspace_agent_id, workspace_app_id) -VALUES - ($1, $2, $3, $4) -ON CONFLICT (task_id, workspace_build_number) -DO UPDATE SET - workspace_agent_id = EXCLUDED.workspace_agent_id, - workspace_app_id = EXCLUDED.workspace_app_id -RETURNING *; - --- name: GetTaskByID :one -SELECT * FROM tasks_with_status WHERE id = @id::uuid; - --- name: GetTaskByWorkspaceID :one -SELECT * FROM tasks_with_status WHERE workspace_id = @workspace_id::uuid; - --- name: GetTaskByOwnerIDAndName :one -SELECT * FROM tasks_with_status -WHERE - owner_id = @owner_id::uuid - AND deleted_at IS NULL - AND LOWER(name) = LOWER(@name::text); - --- name: ListTasks :many -SELECT * FROM tasks_with_status tws -WHERE tws.deleted_at IS NULL -AND CASE WHEN @owner_id::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.owner_id = @owner_id::UUID ELSE TRUE END -AND CASE WHEN @organization_id::UUID != '00000000-0000-0000-0000-000000000000' THEN tws.organization_id = @organization_id::UUID ELSE TRUE END -AND CASE WHEN @status::text != '' THEN tws.status = @status::task_status ELSE TRUE END -ORDER BY tws.created_at DESC; - --- name: DeleteTask :one -WITH deleted_task AS ( - UPDATE tasks - SET - deleted_at = @deleted_at::timestamptz - WHERE - id = @id::uuid - AND deleted_at IS NULL - RETURNING id -), deleted_snapshot AS ( - DELETE FROM task_snapshots - WHERE task_id = @id::uuid -) -SELECT id FROM deleted_task; - - --- name: UpdateTaskPrompt :one -UPDATE - tasks -SET - prompt = @prompt::text -WHERE - id = @id::uuid - AND deleted_at IS NULL -RETURNING *; - --- name: UpsertTaskSnapshot :exec -INSERT INTO - task_snapshots (task_id, log_snapshot, log_snapshot_created_at) -VALUES - ($1, $2, $3) -ON CONFLICT - (task_id) -DO UPDATE SET - log_snapshot = EXCLUDED.log_snapshot, - log_snapshot_created_at = EXCLUDED.log_snapshot_created_at; - --- name: GetTaskSnapshot :one -SELECT - * -FROM - task_snapshots -WHERE - task_id = $1; - --- name: GetTelemetryTaskEvents :many --- Returns all data needed to build task lifecycle events for telemetry --- in a single round-trip. For each task whose workspace is in the --- given set, fetches: --- - the latest workspace app binding (task_workspace_apps) --- - the most recent stop and start builds (workspace_builds) --- - the last "working" app status (workspace_app_statuses) --- - the first app status after resume, for active workspaces --- --- Assumptions: --- - 1:1 relationship between tasks and workspaces. All builds on the --- workspace are considered task-related. --- - Idle duration approximation: If the agent reports "working", does --- work, then reports "done", we miss that working time. --- - lws and active_dur join across all historical app IDs for the task, --- because each resume cycle provisions a new app ID. This ensures --- pre-pause statuses contribute to idle duration and active duration. -WITH task_app_ids AS ( - SELECT task_id, workspace_app_id - FROM task_workspace_apps -), -task_status_timeline AS ( - -- All app statuses across every historical app for each task, - -- plus synthetic "boundary" rows at each stop/start build transition. - -- This allows us to correctly take gaps due to pause/resume into account. - SELECT tai.task_id, was.created_at, was.state::text AS state - FROM workspace_app_statuses was - JOIN task_app_ids tai ON tai.workspace_app_id = was.app_id - UNION ALL - SELECT t.id AS task_id, wb.created_at, '_boundary' AS state - FROM tasks t - JOIN workspace_builds wb ON wb.workspace_id = t.workspace_id - WHERE t.deleted_at IS NULL - AND t.workspace_id IS NOT NULL - AND wb.build_number > 1 -), -task_event_data AS ( - SELECT - t.id AS task_id, - t.workspace_id, - twa.workspace_app_id, - -- Latest stop build. - stop_build.created_at AS stop_build_created_at, - stop_build.reason AS stop_build_reason, - -- Latest start build (task_resume only). - start_build.created_at AS start_build_created_at, - start_build.reason AS start_build_reason, - start_build.build_number AS start_build_number, - -- Last "working" app status (for idle duration). - lws.created_at AS last_working_status_at, - -- First app status after resume (for resume-to-status duration). - -- Only populated for workspaces in an active phase (started more - -- recently than stopped). - fsar.created_at AS first_status_after_resume_at, - -- Cumulative time spent in "working" state. - active_dur.total_working_ms AS active_duration_ms - FROM tasks t - LEFT JOIN LATERAL ( - SELECT task_app.workspace_app_id - FROM task_workspace_apps task_app - WHERE task_app.task_id = t.id - ORDER BY task_app.workspace_build_number DESC - LIMIT 1 - ) twa ON TRUE - LEFT JOIN LATERAL ( - SELECT wb.created_at, wb.reason, wb.build_number - FROM workspace_builds wb - WHERE wb.workspace_id = t.workspace_id - AND wb.transition = 'stop' - ORDER BY wb.build_number DESC - LIMIT 1 - ) stop_build ON TRUE - LEFT JOIN LATERAL ( - SELECT wb.created_at, wb.reason, wb.build_number - FROM workspace_builds wb - WHERE wb.workspace_id = t.workspace_id - AND wb.transition = 'start' - ORDER BY wb.build_number DESC - LIMIT 1 - ) start_build ON TRUE - LEFT JOIN LATERAL ( - SELECT tst.created_at - FROM task_status_timeline tst - WHERE tst.task_id = t.id - AND tst.state = 'working' - -- Only consider status before the latest pause so that - -- post-resume statuses don't mask pre-pause idle time. - AND (stop_build.created_at IS NULL - OR tst.created_at <= stop_build.created_at) - ORDER BY tst.created_at DESC - LIMIT 1 - ) lws ON TRUE - LEFT JOIN LATERAL ( - SELECT was.created_at - FROM workspace_app_statuses was - WHERE was.app_id = twa.workspace_app_id - AND was.created_at > start_build.created_at - ORDER BY was.created_at ASC - LIMIT 1 - ) fsar ON twa.workspace_app_id IS NOT NULL - AND start_build.created_at IS NOT NULL - AND (stop_build.created_at IS NULL - OR start_build.created_at > stop_build.created_at) - -- Active duration: cumulative time spent in "working" state across all - -- historical app IDs for this task. Uses LEAD() to convert point-in-time - -- statuses into intervals, then sums intervals where state='working'. For - -- the last status, falls back to stop_build time (if paused) or @now (if - -- still running). - LEFT JOIN LATERAL ( - SELECT COALESCE( - SUM(EXTRACT(EPOCH FROM (interval_end - interval_start)) * 1000)::bigint, - 0 - )::bigint AS total_working_ms - FROM ( - SELECT - tst.created_at AS interval_start, - COALESCE( - LEAD(tst.created_at) OVER (ORDER BY tst.created_at ASC, CASE WHEN tst.state = '_boundary' THEN 1 ELSE 0 END ASC), - CASE WHEN stop_build.created_at IS NOT NULL - AND (start_build.created_at IS NULL - OR stop_build.created_at > start_build.created_at) - THEN stop_build.created_at - ELSE @now::timestamptz - END - ) AS interval_end, - tst.state - FROM task_status_timeline tst - WHERE tst.task_id = t.id - ) intervals - WHERE intervals.state = 'working' - ) active_dur ON TRUE - WHERE t.deleted_at IS NULL - AND t.workspace_id IS NOT NULL - AND EXISTS ( - SELECT 1 FROM workspace_builds wb - WHERE wb.workspace_id = t.workspace_id - AND wb.created_at > @created_after - ) -) -SELECT * FROM task_event_data -ORDER BY task_id; - diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index 1d3ff2c4ac8..d6e0f05926f 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -441,8 +441,7 @@ WHERE SELECT sqlc.embed(workspaces), sqlc.embed(workspace_agents), - sqlc.embed(workspace_build_with_user), - tasks.id AS task_id + sqlc.embed(workspace_build_with_user) FROM workspace_agents JOIN @@ -457,10 +456,6 @@ JOIN workspaces ON workspace_build_with_user.workspace_id = workspaces.id -LEFT JOIN - tasks -ON - tasks.workspace_id = workspaces.id WHERE -- This should only match 1 agent, so 1 returned row or 0. workspace_agents.auth_token = @auth_token::uuid diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 81d57c167f0..090e00ad2db 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -436,7 +436,6 @@ WHERE '', -- template_display_name '', -- template_icon '', -- template_description - '00000000-0000-0000-0000-000000000000'::uuid, -- task_id '{}'::jsonb, -- group_acl_display_info '{}'::jsonb, -- user_acl_display_info -- Extra columns added to `filtered_workspaces` diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index fe3f137552e..180ba145508 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -89,9 +89,6 @@ const ( UniqueTailnetCoordinatorsPkey UniqueConstraint = "tailnet_coordinators_pkey" // ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id); UniqueTailnetPeersPkey UniqueConstraint = "tailnet_peers_pkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_pkey PRIMARY KEY (id, coordinator_id); UniqueTailnetTunnelsPkey UniqueConstraint = "tailnet_tunnels_pkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); - UniqueTaskSnapshotsPkey UniqueConstraint = "task_snapshots_pkey" // ALTER TABLE ONLY task_snapshots ADD CONSTRAINT task_snapshots_pkey PRIMARY KEY (task_id); - UniqueTaskWorkspaceAppsPkey UniqueConstraint = "task_workspace_apps_pkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_pkey PRIMARY KEY (task_id, workspace_build_number); - UniqueTasksPkey UniqueConstraint = "tasks_pkey" // ALTER TABLE ONLY tasks ADD CONSTRAINT tasks_pkey PRIMARY KEY (id); UniqueTelemetryItemsPkey UniqueConstraint = "telemetry_items_pkey" // ALTER TABLE ONLY telemetry_items ADD CONSTRAINT telemetry_items_pkey PRIMARY KEY (key); UniqueTelemetryLocksPkey UniqueConstraint = "telemetry_locks_pkey" // ALTER TABLE ONLY telemetry_locks ADD CONSTRAINT telemetry_locks_pkey PRIMARY KEY (event_type, period_ending_at); UniqueTemplateUsageStatsPkey UniqueConstraint = "template_usage_stats_pkey" // ALTER TABLE ONLY template_usage_stats ADD CONSTRAINT template_usage_stats_pkey PRIMARY KEY (start_time, template_id, user_id); @@ -172,7 +169,6 @@ const ( UniqueNotificationMessagesDedupeHashIndex UniqueConstraint = "notification_messages_dedupe_hash_idx" // CREATE UNIQUE INDEX notification_messages_dedupe_hash_idx ON notification_messages USING btree (dedupe_hash); UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true); UniqueProvisionerKeysOrganizationIDNameIndex UniqueConstraint = "provisioner_keys_organization_id_name_idx" // CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower((name)::text)); - UniqueTasksOwnerIDNameUniqueIndex UniqueConstraint = "tasks_owner_id_name_unique_idx" // CREATE UNIQUE INDEX tasks_owner_id_name_unique_idx ON tasks USING btree (owner_id, lower(name)) WHERE (deleted_at IS NULL); UniqueTemplateUsageStatsStartTimeTemplateIDUserIDIndex UniqueConstraint = "template_usage_stats_start_time_template_id_user_id_idx" // CREATE UNIQUE INDEX template_usage_stats_start_time_template_id_user_id_idx ON template_usage_stats USING btree (start_time, template_id, user_id); UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false); UniqueUserLinksLinkedIDLoginTypeIndex UniqueConstraint = "user_links_linked_id_login_type_idx" // CREATE UNIQUE INDEX user_links_linked_id_login_type_idx ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text);