mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
committed by
GitHub
parent
299a54a99b
commit
952c69f412
@@ -9,10 +9,10 @@ const (
|
||||
CheckOneTimePasscodeSet CheckConstraint = "one_time_passcode_set" // users
|
||||
CheckUsersUsernameMinLength CheckConstraint = "users_username_min_length" // users
|
||||
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
|
||||
CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents
|
||||
CheckWorkspaceBuildsAiTaskSidebarAppIDRequired CheckConstraint = "workspace_builds_ai_task_sidebar_app_id_required" // 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
|
||||
)
|
||||
|
||||
@@ -2885,6 +2885,14 @@ func (q *querier) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uuid.UUID)
|
||||
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) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return database.TelemetryItem{}, err
|
||||
@@ -4110,6 +4118,17 @@ 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
|
||||
@@ -5668,6 +5687,18 @@ func (q *querier) UpsertTailnetTunnel(ctx context.Context, arg database.UpsertTa
|
||||
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 {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
|
||||
return err
|
||||
|
||||
@@ -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() {
|
||||
s.Run("InsertProvisionerKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
org := testutil.Fake(s.T(), faker, database.Organization{})
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"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/cryptorand"
|
||||
"github.com/coder/coder/v2/provisionerd/proto"
|
||||
@@ -1560,6 +1561,43 @@ func AIBridgeToolUsage(t testing.TB, db database.Store, seed database.InsertAIBr
|
||||
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 {
|
||||
timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{
|
||||
JobID: takeFirst(seed.JobID, uuid.New()),
|
||||
|
||||
@@ -1482,6 +1482,20 @@ func (m queryMetricsStore) GetTailnetTunnelPeerIDs(ctx context.Context, srcID uu
|
||||
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) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTelemetryItem(ctx, key)
|
||||
@@ -2455,6 +2469,13 @@ func (m queryMetricsStore) InsertReplica(ctx context.Context, arg database.Inser
|
||||
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 {
|
||||
start := time.Now()
|
||||
r0 := m.s.InsertTelemetryItemIfNotExists(ctx, arg)
|
||||
@@ -3533,6 +3554,13 @@ func (m queryMetricsStore) UpsertTailnetTunnel(ctx context.Context, arg database
|
||||
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 {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpsertTelemetryItem(ctx, arg)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStore) GetTelemetryItem(ctx context.Context, key string) (database.TelemetryItem, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
Generated
+195
-123
@@ -477,6 +477,15 @@ 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',
|
||||
@@ -1796,9 +1805,9 @@ CREATE TABLE tailnet_tunnels (
|
||||
|
||||
CREATE TABLE task_workspace_apps (
|
||||
task_id uuid NOT NULL,
|
||||
workspace_build_id uuid NOT NULL,
|
||||
workspace_agent_id uuid NOT NULL,
|
||||
workspace_app_id uuid NOT NULL
|
||||
workspace_agent_id uuid,
|
||||
workspace_app_id uuid,
|
||||
workspace_build_number integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE tasks (
|
||||
@@ -1814,6 +1823,180 @@ CREATE TABLE tasks (
|
||||
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 (
|
||||
key 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
|
||||
);
|
||||
|
||||
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,
|
||||
@@ -2530,35 +2648,6 @@ 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_parameters (
|
||||
workspace_build_id uuid 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';
|
||||
|
||||
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
|
||||
SELECT workspace_builds.id,
|
||||
workspace_builds.created_at,
|
||||
@@ -3007,6 +3073,9 @@ 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_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);
|
||||
|
||||
@@ -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 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);
|
||||
|
||||
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
|
||||
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
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
+6
@@ -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;
|
||||
@@ -139,6 +139,13 @@ func (t Task) RBACObject() rbac.Object {
|
||||
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 {
|
||||
switch s {
|
||||
case ApiKeyScopeCoderAll:
|
||||
|
||||
@@ -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.
|
||||
type UserStatus string
|
||||
|
||||
@@ -4137,13 +4207,27 @@ type Task struct {
|
||||
Prompt string `db:"prompt" json:"prompt"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"`
|
||||
Status TaskStatus `db:"status" json:"status"`
|
||||
}
|
||||
|
||||
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 {
|
||||
TaskID uuid.UUID `db:"task_id" json:"task_id"`
|
||||
WorkspaceBuildID uuid.UUID `db:"workspace_build_id" json:"workspace_build_id"`
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||
WorkspaceAppID uuid.UUID `db:"workspace_app_id" json:"workspace_app_id"`
|
||||
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 {
|
||||
|
||||
@@ -331,6 +331,8 @@ type sqlcQuerier interface {
|
||||
GetTailnetPeers(ctx context.Context, id uuid.UUID) ([]TailnetPeer, error)
|
||||
GetTailnetTunnelPeerBindings(ctx context.Context, srcID uuid.UUID) ([]GetTailnetTunnelPeerBindingsRow, 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)
|
||||
GetTelemetryItems(ctx context.Context) ([]TelemetryItem, error)
|
||||
// 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)
|
||||
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
|
||||
InsertTemplate(ctx context.Context, arg InsertTemplateParams) error
|
||||
InsertTemplateVersion(ctx context.Context, arg InsertTemplateVersionParams) error
|
||||
@@ -729,6 +732,7 @@ 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)
|
||||
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
|
||||
|
||||
@@ -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) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -12507,6 +12507,134 @@ func (q *sqlQuerier) UpsertTailnetTunnel(ctx context.Context, arg UpsertTailnetT
|
||||
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
|
||||
SELECT key, value, created_at, updated_at FROM telemetry_items WHERE key = $1
|
||||
`
|
||||
|
||||
@@ -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;
|
||||
@@ -112,6 +112,8 @@ sql:
|
||||
workspace_build_with_user: WorkspaceBuild
|
||||
workspace: WorkspaceTable
|
||||
workspaces_expanded: Workspace
|
||||
task: TaskTable
|
||||
tasks_with_status: Task
|
||||
template_version: TemplateVersionTable
|
||||
template_version_with_user: TemplateVersion
|
||||
api_key: APIKey
|
||||
|
||||
@@ -59,6 +59,7 @@ 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);
|
||||
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);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user