feat(coderd/database): add task status and status view (#20235)

This change updates the `task_workspace_apps` table structure for
improved linking to workspace builds and adds queries to manage tasks
and a view to expose task status.

Updates coder/internal#948
Supersedes coder/coder#20212
Supersedes coder/coder#19773
This commit is contained in:
Mathias Fredriksson
2025-10-13 12:25:58 +03:00
committed by GitHub
parent 299a54a99b
commit 952c69f412
19 changed files with 1249 additions and 130 deletions
+2 -2
View File
@@ -9,10 +9,10 @@ const (
CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users
CheckUsersUsernameMinLength CheckConstraint = "users_username_min_length" // users CheckUsersUsernameMinLength CheckConstraint = "users_username_min_length" // users
CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events
CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents
CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents
CheckWorkspaceBuildsAiTaskSidebarAppIDRequired CheckConstraint = "workspace_builds_ai_task_sidebar_app_id_required" // workspace_builds CheckWorkspaceBuildsAiTaskSidebarAppIDRequired CheckConstraint = "workspace_builds_ai_task_sidebar_app_id_required" // workspace_builds
CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds CheckWorkspaceBuildsDeadlineBelowMaxDeadline CheckConstraint = "workspace_builds_deadline_below_max_deadline" // workspace_builds
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events
) )
+31
View File
@@ -2885,6 +2885,14 @@ func (q *querier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID)
return q.db.GetTailnetTunnelPeerIDs(ctx, srcID) return q.db.GetTailnetTunnelPeerIDs(ctx, srcID)
} }
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) GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.Task, error) {
return fetch(q.log, q.auth, q.db.GetTaskByWorkspaceID)(ctx, workspaceID)
}
func (q *querier) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { func (q *querier) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return database.TelemetryItem{}, err return database.TelemetryItem{}, err
@@ -4110,6 +4118,17 @@ func (q *querier) InsertReplica(ctx context.Context, arg database.InsertReplicaP
return q.db.InsertReplica(ctx, arg) 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 { func (q *querier) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
return err return err
@@ -5668,6 +5687,18 @@ func (q *querier) UpsertTailnetTunnel(ctx context.Context, arg database.UpsertTa
return q.db.UpsertTailnetTunnel(ctx, arg) return q.db.UpsertTailnetTunnel(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 { func (q *querier) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return err return err
+47
View File
@@ -2347,6 +2347,53 @@ 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("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("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)
}))
}
func (s *MethodTestSuite) TestProvisionerKeys() { func (s *MethodTestSuite) TestProvisionerKeys() {
s.Run("InsertProvisionerKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) { s.Run("InsertProvisionerKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
org := testutil.Fake(s.T(), faker, database.Organization{}) org := testutil.Fake(s.T(), faker, database.Organization{})
+38
View File
@@ -28,6 +28,7 @@ import (
"github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/taskname"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionerd/proto"
@@ -1560,6 +1561,43 @@ func AIBridgeToolUsage(t testing.TB, db database.Store, seed database.InsertAIBr
return toolUsage return toolUsage
} }
func Task(t testing.TB, db database.Store, orig database.TaskTable) database.TaskTable {
t.Helper()
parameters := orig.TemplateParameters
if parameters == nil {
parameters = json.RawMessage([]byte("{}"))
}
task, err := db.InsertTask(genCtx, database.InsertTaskParams{
OrganizationID: orig.OrganizationID,
OwnerID: orig.OwnerID,
Name: takeFirst(orig.Name, taskname.GenerateFallback()),
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 task
}
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 { func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming {
timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{ timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{
JobID: takeFirst(seed.JobID, uuid.New()), JobID: takeFirst(seed.JobID, uuid.New()),
+28
View File
@@ -1482,6 +1482,20 @@ func (m queryMetricsStore) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uu
return r0, r1 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())
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())
return r0, r1
}
func (m queryMetricsStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { func (m queryMetricsStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) {
start := time.Now() start := time.Now()
r0, r1 := m.s.GetTelemetryItem(ctx, key) r0, r1 := m.s.GetTelemetryItem(ctx, key)
@@ -2455,6 +2469,13 @@ func (m queryMetricsStore) InsertReplica(ctx context.Context, arg database.Inser
return replica, err return replica, err
} }
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())
return r0, r1
}
func (m queryMetricsStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { func (m queryMetricsStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error {
start := time.Now() start := time.Now()
r0 := m.s.InsertTelemetryItemIfNotExists(ctx, arg) r0 := m.s.InsertTelemetryItemIfNotExists(ctx, arg)
@@ -3533,6 +3554,13 @@ func (m queryMetricsStore) UpsertTailnetTunnel(ctx context.Context, arg database
return r0, r1 return r0, r1
} }
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())
return r0, r1
}
func (m queryMetricsStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { func (m queryMetricsStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error {
start := time.Now() start := time.Now()
r0 := m.s.UpsertTelemetryItem(ctx, arg) r0 := m.s.UpsertTelemetryItem(ctx, arg)
+60
View File
@@ -3119,6 +3119,36 @@ func (mr *MockStoreMockRecorder) GetTailnetTunnelPeerIDs(ctx, srcID any) *gomock
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetTunnelPeerIDs", reflect.TypeOf((*MockStore)(nil).GetTailnetTunnelPeerIDs), ctx, srcID) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTailnetTunnelPeerIDs", reflect.TypeOf((*MockStore)(nil).GetTailnetTunnelPeerIDs), ctx, srcID)
} }
// 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)
}
// 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)
}
// GetTelemetryItem mocks base method. // GetTelemetryItem mocks base method.
func (m *MockStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) { func (m *MockStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@@ -5244,6 +5274,21 @@ func (mr *MockStoreMockRecorder) InsertReplica(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertReplica", reflect.TypeOf((*MockStore)(nil).InsertReplica), ctx, arg) 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. // InsertTelemetryItemIfNotExists mocks base method.
func (m *MockStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error { func (m *MockStore) InsertTelemetryItemIfNotExists(ctx context.Context, arg database.InsertTelemetryItemIfNotExistsParams) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@@ -7517,6 +7562,21 @@ func (mr *MockStoreMockRecorder) UpsertTailnetTunnel(ctx, arg any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetTunnel", reflect.TypeOf((*MockStore)(nil).UpsertTailnetTunnel), ctx, arg) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertTailnetTunnel", reflect.TypeOf((*MockStore)(nil).UpsertTailnetTunnel), 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. // UpsertTelemetryItem mocks base method.
func (m *MockStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error { func (m *MockStore) UpsertTelemetryItem(ctx context.Context, arg database.UpsertTelemetryItemParams) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
+195 -123
View File
@@ -477,6 +477,15 @@ CREATE TYPE tailnet_status AS ENUM (
'lost' 'lost'
); );
CREATE TYPE task_status AS ENUM (
'pending',
'initializing',
'active',
'paused',
'unknown',
'error'
);
CREATE TYPE user_status AS ENUM ( CREATE TYPE user_status AS ENUM (
'active', 'active',
'suspended', 'suspended',
@@ -1796,9 +1805,9 @@ CREATE TABLE tailnet_tunnels (
CREATE TABLE task_workspace_apps ( CREATE TABLE task_workspace_apps (
task_id uuid NOT NULL, task_id uuid NOT NULL,
workspace_build_id uuid NOT NULL, workspace_agent_id uuid,
workspace_agent_id uuid NOT NULL, workspace_app_id uuid,
workspace_app_id uuid NOT NULL workspace_build_number integer NOT NULL
); );
CREATE TABLE tasks ( CREATE TABLE tasks (
@@ -1814,6 +1823,180 @@ CREATE TABLE tasks (
deleted_at timestamp with time zone deleted_at timestamp with time zone
); );
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_ai_task boolean,
ai_task_sidebar_app_id uuid,
has_external_agent boolean,
CONSTRAINT workspace_builds_ai_task_sidebar_app_id_required CHECK (((((has_ai_task IS NULL) OR (has_ai_task = false)) AND (ai_task_sidebar_app_id IS NULL)) OR ((has_ai_task = true) AND (ai_task_sidebar_app_id IS 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)))
);
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,
CASE
WHEN ((tasks.workspace_id IS NULL) OR (latest_build.job_status IS NULL)) THEN 'pending'::task_status
WHEN (latest_build.job_status = 'failed'::provisioner_job_status) THEN 'error'::task_status
WHEN ((latest_build.transition = ANY (ARRAY['stop'::workspace_transition, 'delete'::workspace_transition])) AND (latest_build.job_status = 'succeeded'::provisioner_job_status)) THEN 'paused'::task_status
WHEN ((latest_build.transition = 'start'::workspace_transition) AND (latest_build.job_status = 'pending'::provisioner_job_status)) THEN 'initializing'::task_status
WHEN ((latest_build.transition = 'start'::workspace_transition) AND (latest_build.job_status = ANY (ARRAY['running'::provisioner_job_status, 'succeeded'::provisioner_job_status]))) THEN
CASE
WHEN agent_status."none" THEN 'initializing'::task_status
WHEN agent_status.connecting THEN 'initializing'::task_status
WHEN agent_status.connected THEN
CASE
WHEN app_status.any_unhealthy THEN 'error'::task_status
WHEN app_status.any_initializing THEN 'initializing'::task_status
WHEN app_status.all_healthy_or_disabled THEN 'active'::task_status
ELSE 'unknown'::task_status
END
ELSE 'unknown'::task_status
END
ELSE 'unknown'::task_status
END AS status
FROM ((((tasks
LEFT JOIN LATERAL ( SELECT 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 ON (true))
CROSS JOIN LATERAL ( SELECT (count(*) = 0) AS "none",
bool_or((workspace_agent.lifecycle_state = ANY (ARRAY['created'::workspace_agent_lifecycle_state, 'starting'::workspace_agent_lifecycle_state]))) AS connecting,
bool_and((workspace_agent.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)) AS connected
FROM workspace_agents workspace_agent
WHERE (workspace_agent.id = task_app.workspace_agent_id)) agent_status)
CROSS JOIN LATERAL ( SELECT bool_or((workspace_app.health = 'unhealthy'::workspace_app_health)) AS any_unhealthy,
bool_or((workspace_app.health = 'initializing'::workspace_app_health)) AS any_initializing,
bool_and((workspace_app.health = ANY (ARRAY['healthy'::workspace_app_health, 'disabled'::workspace_app_health]))) AS all_healthy_or_disabled
FROM workspace_apps workspace_app
WHERE (workspace_app.id = task_app.workspace_app_id)) app_status)
WHERE (tasks.deleted_at IS NULL);
CREATE TABLE telemetry_items ( CREATE TABLE telemetry_items (
key text NOT NULL, key text NOT NULL,
value text NOT NULL, value text NOT NULL,
@@ -2377,71 +2560,6 @@ 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 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 ( CREATE UNLOGGED TABLE workspace_app_audit_sessions (
agent_id uuid NOT NULL, agent_id uuid NOT NULL,
app_id uuid NOT NULL, app_id uuid NOT NULL,
@@ -2530,35 +2648,6 @@ CREATE TABLE workspace_app_statuses (
uri text 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_parameters ( CREATE TABLE workspace_build_parameters (
workspace_build_id uuid NOT NULL, workspace_build_id uuid NOT NULL,
name text NOT NULL, name text NOT NULL,
@@ -2569,29 +2658,6 @@ COMMENT ON COLUMN workspace_build_parameters.name IS 'Parameter name';
COMMENT ON COLUMN workspace_build_parameters.value IS 'Parameter value'; 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_ai_task boolean,
ai_task_sidebar_app_id uuid,
has_external_agent boolean,
CONSTRAINT workspace_builds_ai_task_sidebar_app_id_required CHECK (((((has_ai_task IS NULL) OR (has_ai_task = false)) AND (ai_task_sidebar_app_id IS NULL)) OR ((has_ai_task = true) AND (ai_task_sidebar_app_id IS 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)))
);
CREATE VIEW workspace_build_with_user AS CREATE VIEW workspace_build_with_user AS
SELECT workspace_builds.id, SELECT workspace_builds.id,
workspace_builds.created_at, workspace_builds.created_at,
@@ -3007,6 +3073,9 @@ ALTER TABLE ONLY tailnet_peers
ALTER TABLE ONLY tailnet_tunnels ALTER TABLE ONLY tailnet_tunnels
ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id); ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id);
ALTER TABLE ONLY task_workspace_apps
ADD CONSTRAINT task_workspace_apps_pkey PRIMARY KEY (task_id, workspace_build_number);
ALTER TABLE ONLY tasks ALTER TABLE ONLY tasks
ADD CONSTRAINT tasks_pkey PRIMARY KEY (id); ADD CONSTRAINT tasks_pkey PRIMARY KEY (id);
@@ -3272,6 +3341,12 @@ 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 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 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); 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).'; COMMENT ON INDEX template_usage_stats_start_time_idx IS 'Index for querying MAX(start_time).';
@@ -3552,9 +3627,6 @@ ALTER TABLE ONLY task_workspace_apps
ALTER TABLE ONLY task_workspace_apps 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; ADD CONSTRAINT task_workspace_apps_workspace_app_id_fkey FOREIGN KEY (workspace_app_id) REFERENCES workspace_apps(id) ON DELETE CASCADE;
ALTER TABLE ONLY task_workspace_apps
ADD CONSTRAINT task_workspace_apps_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE;
ALTER TABLE ONLY tasks ALTER TABLE ONLY tasks
ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; ADD CONSTRAINT tasks_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
@@ -48,7 +48,6 @@ const (
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; 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; 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; 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;
ForeignKeyTaskWorkspaceAppsWorkspaceBuildID ForeignKeyConstraint = "task_workspace_apps_workspace_build_id_fkey" // ALTER TABLE ONLY task_workspace_apps ADD CONSTRAINT task_workspace_apps_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(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; 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; 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; 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;
@@ -0,0 +1,33 @@
DROP VIEW IF EXISTS tasks_with_status;
DROP TYPE IF EXISTS task_status;
DROP INDEX IF EXISTS tasks_organization_id_idx;
DROP INDEX IF EXISTS tasks_owner_id_idx;
DROP INDEX IF EXISTS tasks_workspace_id_idx;
ALTER TABLE task_workspace_apps
DROP CONSTRAINT IF EXISTS task_workspace_apps_pkey;
-- Add back workspace_build_id column.
ALTER TABLE task_workspace_apps
ADD COLUMN workspace_build_id UUID;
-- Try to populate workspace_build_id from workspace_builds.
UPDATE task_workspace_apps
SET workspace_build_id = workspace_builds.id
FROM workspace_builds
WHERE workspace_builds.build_number = task_workspace_apps.workspace_build_number
AND workspace_builds.workspace_id IN (
SELECT workspace_id FROM tasks WHERE tasks.id = task_workspace_apps.task_id
);
-- Remove rows that couldn't be restored.
DELETE FROM task_workspace_apps
WHERE workspace_build_id IS NULL;
-- Restore original schema.
ALTER TABLE task_workspace_apps
DROP COLUMN workspace_build_number,
ALTER COLUMN workspace_build_id SET NOT NULL,
ALTER COLUMN workspace_agent_id SET NOT NULL,
ALTER COLUMN workspace_app_id SET NOT NULL;
@@ -0,0 +1,104 @@
-- Replace workspace_build_id with workspace_build_number.
ALTER TABLE task_workspace_apps
ADD COLUMN workspace_build_number INTEGER;
-- Try to populate workspace_build_number from workspace_builds.
UPDATE task_workspace_apps
SET workspace_build_number = workspace_builds.build_number
FROM workspace_builds
WHERE workspace_builds.id = task_workspace_apps.workspace_build_id;
-- Remove rows that couldn't be migrated.
DELETE FROM task_workspace_apps
WHERE workspace_build_number IS NULL;
ALTER TABLE task_workspace_apps
DROP COLUMN workspace_build_id,
ALTER COLUMN workspace_build_number SET NOT NULL,
ALTER COLUMN workspace_agent_id DROP NOT NULL,
ALTER COLUMN workspace_app_id DROP NOT NULL,
ADD CONSTRAINT task_workspace_apps_pkey PRIMARY KEY (task_id, workspace_build_number);
-- Add indexes for common joins or filters.
CREATE INDEX IF NOT EXISTS tasks_workspace_id_idx ON tasks (workspace_id);
CREATE INDEX IF NOT EXISTS tasks_owner_id_idx ON tasks (owner_id);
CREATE INDEX IF NOT EXISTS tasks_organization_id_idx ON tasks (organization_id);
CREATE TYPE task_status AS ENUM (
'pending',
'initializing',
'active',
'paused',
'unknown',
'error'
);
CREATE VIEW
tasks_with_status
AS
SELECT
tasks.*,
CASE
WHEN tasks.workspace_id IS NULL OR latest_build.job_status IS NULL THEN 'pending'::task_status
WHEN latest_build.job_status = 'failed' THEN 'error'::task_status
WHEN latest_build.transition IN ('stop', 'delete')
AND latest_build.job_status = 'succeeded' THEN 'paused'::task_status
WHEN latest_build.transition = 'start'
AND latest_build.job_status = 'pending' THEN 'initializing'::task_status
WHEN latest_build.transition = 'start' AND latest_build.job_status IN ('running', 'succeeded') THEN
CASE
WHEN agent_status.none THEN 'initializing'::task_status
WHEN agent_status.connecting THEN 'initializing'::task_status
WHEN agent_status.connected THEN
CASE
WHEN app_status.any_unhealthy THEN 'error'::task_status
WHEN app_status.any_initializing THEN 'initializing'::task_status
WHEN app_status.all_healthy_or_disabled THEN 'active'::task_status
ELSE 'unknown'::task_status
END
ELSE 'unknown'::task_status
END
ELSE 'unknown'::task_status
END AS status
FROM
tasks
LEFT JOIN LATERAL (
SELECT workspace_build_number, workspace_agent_id, workspace_app_id
FROM task_workspace_apps task_app
WHERE task_id = tasks.id
ORDER BY workspace_build_number DESC
LIMIT 1
) task_app ON TRUE
LEFT JOIN LATERAL (
SELECT
workspace_build.transition,
provisioner_job.job_status,
workspace_build.job_id
FROM workspace_builds workspace_build
JOIN provisioner_jobs provisioner_job ON provisioner_job.id = workspace_build.job_id
WHERE workspace_build.workspace_id = tasks.workspace_id
AND workspace_build.build_number = task_app.workspace_build_number
) latest_build ON TRUE
CROSS JOIN LATERAL (
SELECT
COUNT(*) = 0 AS none,
bool_or(workspace_agent.lifecycle_state IN ('created', 'starting')) AS connecting,
bool_and(workspace_agent.lifecycle_state = 'ready') AS connected
FROM workspace_agents workspace_agent
WHERE workspace_agent.id = task_app.workspace_agent_id
) agent_status
CROSS JOIN LATERAL (
SELECT
bool_or(workspace_app.health = 'unhealthy') AS any_unhealthy,
bool_or(workspace_app.health = 'initializing') AS any_initializing,
bool_and(workspace_app.health IN ('healthy', 'disabled')) AS all_healthy_or_disabled
FROM workspace_apps workspace_app
WHERE workspace_app.id = task_app.workspace_app_id
) app_status
WHERE
tasks.deleted_at IS NULL;
@@ -0,0 +1,6 @@
INSERT INTO public.task_workspace_apps VALUES (
'f5a1c3e4-8b2d-4f6a-9d7e-2a8b5c9e1f3d', -- task_id
NULL, -- workspace_agent_id
NULL, -- workspace_app_id
99 -- workspace_build_number
) ON CONFLICT DO NOTHING;
+7
View File
@@ -139,6 +139,13 @@ func (t Task) RBACObject() rbac.Object {
InOrg(t.OrganizationID) InOrg(t.OrganizationID)
} }
func (t TaskTable) RBACObject() rbac.Object {
return rbac.ResourceTask.
WithID(t.ID).
WithOwner(t.OwnerID.String()).
InOrg(t.OrganizationID)
}
func (s APIKeyScope) ToRBAC() rbac.ScopeName { func (s APIKeyScope) ToRBAC() rbac.ScopeName {
switch s { switch s {
case ApiKeyScopeCoderAll: case ApiKeyScopeCoderAll:
+88 -4
View File
@@ -2885,6 +2885,76 @@ 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. // Defines the users status: active, dormant, or suspended.
type UserStatus string type UserStatus string
@@ -4137,13 +4207,27 @@ type Task struct {
Prompt string `db:"prompt" json:"prompt"` Prompt string `db:"prompt" json:"prompt"`
CreatedAt time.Time `db:"created_at" json:"created_at"` CreatedAt time.Time `db:"created_at" json:"created_at"`
DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"` DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"`
Status TaskStatus `db:"status" json:"status"`
}
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"`
} }
type TaskWorkspaceApp struct { type TaskWorkspaceApp struct {
TaskID uuid.UUID `db:"task_id" json:"task_id"` TaskID uuid.UUID `db:"task_id" json:"task_id"`
WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"` WorkspaceAgentID uuid.NullUUID `db:"workspace_agent_id" json:"workspace_agent_id"`
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` WorkspaceAppID uuid.NullUUID `db:"workspace_app_id" json:"workspace_app_id"`
WorkspaceAppID uuid.UUID `db:"workspace_app_id" json:"workspace_app_id"` WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"`
} }
type TelemetryItem struct { type TelemetryItem struct {
+4
View File
@@ -331,6 +331,8 @@ type sqlcQuerier interface {
GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error) GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error)
GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, error) GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, error)
GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerIDsRow, error) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerIDsRow, error)
GetTaskByID(ctx context.Context, id uuid.UUID) (Task, error)
GetTaskByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (Task, error)
GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error) GetTelemetryItem(ctx context.Context, key string) (TelemetryItem, error)
GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error) GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error)
// GetTemplateAppInsights returns the aggregate usage of each app in a given // GetTemplateAppInsights returns the aggregate usage of each app in a given
@@ -550,6 +552,7 @@ type sqlcQuerier interface {
InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error) InsertProvisionerJobTimings(ctx context.Context, arg InsertProvisionerJobTimingsParams) ([]ProvisionerJobTiming, error)
InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error) InsertProvisionerKey(ctx context.Context, arg InsertProvisionerKeyParams) (ProvisionerKey, error)
InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error) InsertReplica(ctx context.Context, arg InsertReplicaParams) (Replica, error)
InsertTask(ctx context.Context, arg InsertTaskParams) (TaskTable, error)
InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error InsertTelemetryItemIfNotExists(ctx context.Context, arg InsertTelemetryItemIfNotExistsParams) error
InsertTemplate(ctx context.Context, arg InsertTemplateParams) error InsertTemplate(ctx context.Context, arg InsertTemplateParams) error
InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error
@@ -729,6 +732,7 @@ type sqlcQuerier interface {
UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error) UpsertTailnetCoordinator(ctx context.Context, id uuid.UUID) (TailnetCoordinator, error)
UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error) UpsertTailnetPeer(ctx context.Context, arg UpsertTailnetPeerParams) (TailnetPeer, error)
UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetTunnelParams) (TailnetTunnel, error)
UpsertTaskWorkspaceApp(ctx context.Context, arg UpsertTaskWorkspaceAppParams) (TaskWorkspaceApp, error)
UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error UpsertTelemetryItem(ctx context.Context, arg UpsertTelemetryItemParams) error
// This query aggregates the workspace_agent_stats and workspace_app_stats data // 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 // into a single table for efficient storage and querying. Half-hour buckets are
+452
View File
@@ -6653,6 +6653,458 @@ 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()},
}
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.TaskTable {
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
}{
{
name: "NoWorkspace",
expectedStatus: "pending",
description: "Task with no workspace assigned",
},
{
name: "FailedBuild",
buildStatus: database.ProvisionerJobStatusFailed,
buildTransition: database.WorkspaceTransitionStart,
expectedStatus: database.TaskStatusError,
description: "Latest workspace build failed",
},
{
name: "StoppedWorkspace",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStop,
expectedStatus: database.TaskStatusPaused,
description: "Workspace is stopped",
},
{
name: "DeletedWorkspace",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionDelete,
expectedStatus: database.TaskStatusPaused,
description: "Workspace is deleted",
},
{
name: "PendingStart",
buildStatus: database.ProvisionerJobStatusPending,
buildTransition: database.WorkspaceTransitionStart,
expectedStatus: database.TaskStatusInitializing,
description: "Workspace build is starting (pending)",
},
{
name: "RunningStart",
buildStatus: database.ProvisionerJobStatusRunning,
buildTransition: database.WorkspaceTransitionStart,
expectedStatus: database.TaskStatusInitializing,
description: "Workspace build is starting (running)",
},
{
name: "StartingAgent",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateStarting,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing},
expectedStatus: database.TaskStatusInitializing,
description: "Workspace is running but agent is starting",
},
{
name: "CreatedAgent",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateCreated,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing},
expectedStatus: database.TaskStatusInitializing,
description: "Workspace is running but agent is created",
},
{
name: "ReadyAgentInitializingApp",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateReady,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing},
expectedStatus: database.TaskStatusInitializing,
description: "Agent is ready but app is initializing",
},
{
name: "ReadyAgentHealthyApp",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateReady,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy},
expectedStatus: database.TaskStatusActive,
description: "Agent is ready and app is healthy",
},
{
name: "ReadyAgentDisabledApp",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateReady,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthDisabled},
expectedStatus: database.TaskStatusActive,
description: "Agent is ready and app health checking is disabled",
},
{
name: "ReadyAgentUnhealthyApp",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateReady,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthUnhealthy},
expectedStatus: database.TaskStatusError,
description: "Agent is ready but app is unhealthy",
},
{
name: "AgentStartTimeout",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateStartTimeout,
expectedStatus: database.TaskStatusUnknown,
description: "Agent start timed out",
},
{
name: "AgentStartError",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateStartError,
expectedStatus: database.TaskStatusUnknown,
description: "Agent failed to start",
},
{
name: "AgentShuttingDown",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateShuttingDown,
expectedStatus: database.TaskStatusUnknown,
description: "Agent is shutting down",
},
{
name: "AgentOff",
buildStatus: database.ProvisionerJobStatusSucceeded,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateOff,
expectedStatus: database.TaskStatusUnknown,
description: "Agent is off",
},
{
name: "RunningJobReadyAgentHealthyApp",
buildStatus: database.ProvisionerJobStatusRunning,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateReady,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthHealthy},
expectedStatus: database.TaskStatusActive,
description: "Running job with ready agent and healthy app should be active",
},
{
name: "RunningJobReadyAgentInitializingApp",
buildStatus: database.ProvisionerJobStatusRunning,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateReady,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing},
expectedStatus: database.TaskStatusInitializing,
description: "Running job with ready agent but initializing app should be initializing",
},
{
name: "RunningJobReadyAgentUnhealthyApp",
buildStatus: database.ProvisionerJobStatusRunning,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateReady,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthUnhealthy},
expectedStatus: database.TaskStatusError,
description: "Running job with ready agent but unhealthy app should be error",
},
{
name: "RunningJobConnectingAgent",
buildStatus: database.ProvisionerJobStatusRunning,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateStarting,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthInitializing},
expectedStatus: database.TaskStatusInitializing,
description: "Running job with connecting agent should be initializing",
},
{
name: "RunningJobReadyAgentDisabledApp",
buildStatus: database.ProvisionerJobStatusRunning,
buildTransition: database.WorkspaceTransitionStart,
agentState: database.WorkspaceAgentLifecycleStateReady,
appHealths: []database.WorkspaceAppHealth{database.WorkspaceAppHealthDisabled},
expectedStatus: database.TaskStatusActive,
description: "Running job with ready agent and disabled app health checking should be active",
},
{
name: "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",
},
}
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)
require.Equal(t, tt.expectedStatus, got.Status, "unexpected status for test case: %s", tt.description)
})
}
}
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 {
tt := tt
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)
_, err := db.GetTaskByWorkspaceID(ctx, workspace.ID)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestUsageEventsTrigger(t *testing.T) { func TestUsageEventsTrigger(t *testing.T) {
t.Parallel() t.Parallel()
+128
View File
@@ -12507,6 +12507,134 @@ func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetT
return i, err return i, 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, status 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.Status,
)
return i, err
}
const getTaskByWorkspaceID = `-- name: GetTaskByWorkspaceID :one
SELECT id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at, status FROM tasks_with_status WHERE workspace_id = $1::uuid
`
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.Status,
)
return i, err
}
const insertTask = `-- name: InsertTask :one
INSERT INTO tasks
(id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at)
VALUES
(gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at
`
type InsertTaskParams struct {
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"`
}
func (q *sqlQuerier) InsertTask(ctx context.Context, arg InsertTaskParams) (TaskTable, error) {
row := q.db.QueryRowContext(ctx, insertTask,
arg.OrganizationID,
arg.OwnerID,
arg.Name,
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,
)
return i, 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 const getTelemetryItem = `-- name: GetTelemetryItem :one
SELECT key, value, created_at, updated_at FROM telemetry_items WHERE key = $1 SELECT key, value, created_at, updated_at FROM telemetry_items WHERE key = $1
` `
+23
View File
@@ -0,0 +1,23 @@
-- name: InsertTask :one
INSERT INTO tasks
(id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at)
VALUES
(gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- 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;
+2
View File
@@ -112,6 +112,8 @@ sql:
workspace_build_with_user: WorkspaceBuild workspace_build_with_user: WorkspaceBuild
workspace: WorkspaceTable workspace: WorkspaceTable
workspaces_expanded: Workspace workspaces_expanded: Workspace
task: TaskTable
tasks_with_status: Task
template_version: TemplateVersionTable template_version: TemplateVersionTable
template_version_with_user: TemplateVersion template_version_with_user: TemplateVersion
api_key: APIKey api_key: APIKey
+1
View File
@@ -59,6 +59,7 @@ const (
UniqueTailnetCoordinatorsPkey UniqueConstraint = "tailnet_coordinators_pkey" // ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id); 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); 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); UniqueTailnetTunnelsPkey UniqueConstraint = "tailnet_tunnels_pkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_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); 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); UniqueTelemetryItemsPkey UniqueConstraint = "telemetry_items_pkey" // ALTER TABLE ONLY telemetry_items ADD CONSTRAINT telemetry_items_pkey PRIMARY KEY (key);
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); 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);