diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index 4b308a9468b6f..ac9bcc2153668 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -69,6 +69,7 @@ "most_recently_seen": null } }, + "latest_app_status": null, "outdated": false, "name": "test-workspace", "autostart_schedule": "CRON_TZ=US/Central 30 9 * * 1-5", diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a543e5b716e8f..79c597be5afe9 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8057,6 +8057,45 @@ const docTemplate = `{ } } }, + "/workspaceagents/me/app-status": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Agents" + ], + "summary": "Patch workspace agent app status", + "operationId": "patch-workspace-agent-app-status", + "parameters": [ + { + "description": "app status", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentsdk.PatchAppStatus" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + } + } + }, "/workspaceagents/me/external-auth": { "get": { "security": [ @@ -10170,6 +10209,29 @@ const docTemplate = `{ } } }, + "agentsdk.PatchAppStatus": { + "type": "object", + "properties": { + "app_slug": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "message": { + "type": "string" + }, + "needs_user_attention": { + "type": "boolean" + }, + "state": { + "$ref": "#/definitions/codersdk.WorkspaceAppStatusState" + }, + "uri": { + "type": "string" + } + } + }, "agentsdk.PatchLogs": { "type": "object", "properties": { @@ -16176,6 +16238,9 @@ const docTemplate = `{ "type": "string", "format": "date-time" }, + "latest_app_status": { + "$ref": "#/definitions/codersdk.WorkspaceAppStatus" + }, "latest_build": { "$ref": "#/definitions/codersdk.WorkspaceBuild" }, @@ -16775,6 +16840,13 @@ const docTemplate = `{ "description": "Slug is a unique identifier within the agent.", "type": "string" }, + "statuses": { + "description": "Statuses is a list of statuses for the app.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAppStatus" + } + }, "subdomain": { "description": "Subdomain denotes whether the app should be accessed via a path on the\n` + "`" + `coder server` + "`" + ` or via a hostname-based dev URL. If this is set to true\nand there is no app wildcard configured on the server, the app will not\nbe accessible in the UI.", "type": "boolean" @@ -16828,6 +16900,61 @@ const docTemplate = `{ "WorkspaceAppSharingLevelPublic" ] }, + "codersdk.WorkspaceAppStatus": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "format": "uuid" + }, + "app_id": { + "type": "string", + "format": "uuid" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "icon": { + "description": "Icon is an external URL to an icon that will be rendered in the UI.", + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "message": { + "type": "string" + }, + "needs_user_attention": { + "type": "boolean" + }, + "state": { + "$ref": "#/definitions/codersdk.WorkspaceAppStatusState" + }, + "uri": { + "description": "URI is the URI of the resource that the status is for.\ne.g. https://github.com/org/repo/pull/123\ne.g. file:///path/to/file", + "type": "string" + }, + "workspace_id": { + "type": "string", + "format": "uuid" + } + } + }, + "codersdk.WorkspaceAppStatusState": { + "type": "string", + "enum": [ + "working", + "complete", + "failure" + ], + "x-enum-varnames": [ + "WorkspaceAppStatusStateWorking", + "WorkspaceAppStatusStateComplete", + "WorkspaceAppStatusStateFailure" + ] + }, "codersdk.WorkspaceBuild": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 586f63e5c6d6f..b4e182cc5e181 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7122,6 +7122,39 @@ } } }, + "/workspaceagents/me/app-status": { + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Agents"], + "summary": "Patch workspace agent app status", + "operationId": "patch-workspace-agent-app-status", + "parameters": [ + { + "description": "app status", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/agentsdk.PatchAppStatus" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } + } + } + } + }, "/workspaceagents/me/external-auth": { "get": { "security": [ @@ -9013,6 +9046,29 @@ } } }, + "agentsdk.PatchAppStatus": { + "type": "object", + "properties": { + "app_slug": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "message": { + "type": "string" + }, + "needs_user_attention": { + "type": "boolean" + }, + "state": { + "$ref": "#/definitions/codersdk.WorkspaceAppStatusState" + }, + "uri": { + "type": "string" + } + } + }, "agentsdk.PatchLogs": { "type": "object", "properties": { @@ -14734,6 +14790,9 @@ "type": "string", "format": "date-time" }, + "latest_app_status": { + "$ref": "#/definitions/codersdk.WorkspaceAppStatus" + }, "latest_build": { "$ref": "#/definitions/codersdk.WorkspaceBuild" }, @@ -15307,6 +15366,13 @@ "description": "Slug is a unique identifier within the agent.", "type": "string" }, + "statuses": { + "description": "Statuses is a list of statuses for the app.", + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.WorkspaceAppStatus" + } + }, "subdomain": { "description": "Subdomain denotes whether the app should be accessed via a path on the\n`coder server` or via a hostname-based dev URL. If this is set to true\nand there is no app wildcard configured on the server, the app will not\nbe accessible in the UI.", "type": "boolean" @@ -15348,6 +15414,57 @@ "WorkspaceAppSharingLevelPublic" ] }, + "codersdk.WorkspaceAppStatus": { + "type": "object", + "properties": { + "agent_id": { + "type": "string", + "format": "uuid" + }, + "app_id": { + "type": "string", + "format": "uuid" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "icon": { + "description": "Icon is an external URL to an icon that will be rendered in the UI.", + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "message": { + "type": "string" + }, + "needs_user_attention": { + "type": "boolean" + }, + "state": { + "$ref": "#/definitions/codersdk.WorkspaceAppStatusState" + }, + "uri": { + "description": "URI is the URI of the resource that the status is for.\ne.g. https://github.com/org/repo/pull/123\ne.g. file:///path/to/file", + "type": "string" + }, + "workspace_id": { + "type": "string", + "format": "uuid" + } + } + }, + "codersdk.WorkspaceAppStatusState": { + "type": "string", + "enum": ["working", "complete", "failure"], + "x-enum-varnames": [ + "WorkspaceAppStatusStateWorking", + "WorkspaceAppStatusStateComplete", + "WorkspaceAppStatusStateFailure" + ] + }, "codersdk.WorkspaceBuild": { "type": "object", "properties": { diff --git a/coderd/coderd.go b/coderd/coderd.go index f68ddeadb6e6b..c9a0f741afd1f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1228,6 +1228,7 @@ func New(options *Options) *API { })) r.Get("/rpc", api.workspaceAgentRPC) r.Patch("/logs", api.patchWorkspaceAgentLogs) + r.Patch("/app-status", api.patchWorkspaceAgentAppStatus) // Deprecated: Required to support legacy agents r.Get("/gitauth", api.workspaceAgentsGitAuth) r.Get("/external-auth", api.workspaceAgentsExternalAuth) diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 41691c5a1d3f1..89676b1d94d46 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -487,7 +487,7 @@ func AppSubdomain(dbApp database.WorkspaceApp, agentName, workspaceName, ownerNa }.String() } -func Apps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace) []codersdk.WorkspaceApp { +func Apps(dbApps []database.WorkspaceApp, statuses []database.WorkspaceAppStatus, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace) []codersdk.WorkspaceApp { sort.Slice(dbApps, func(i, j int) bool { if dbApps[i].DisplayOrder != dbApps[j].DisplayOrder { return dbApps[i].DisplayOrder < dbApps[j].DisplayOrder @@ -498,8 +498,14 @@ func Apps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerNa return dbApps[i].Slug < dbApps[j].Slug }) + statusesByAppID := map[uuid.UUID][]database.WorkspaceAppStatus{} + for _, status := range statuses { + statusesByAppID[status.AppID] = append(statusesByAppID[status.AppID], status) + } + apps := make([]codersdk.WorkspaceApp, 0) for _, dbApp := range dbApps { + statuses := statusesByAppID[dbApp.ID] apps = append(apps, codersdk.WorkspaceApp{ ID: dbApp.ID, URL: dbApp.Url.String, @@ -516,14 +522,33 @@ func Apps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerNa Interval: dbApp.HealthcheckInterval, Threshold: dbApp.HealthcheckThreshold, }, - Health: codersdk.WorkspaceAppHealth(dbApp.Health), - Hidden: dbApp.Hidden, - OpenIn: codersdk.WorkspaceAppOpenIn(dbApp.OpenIn), + Health: codersdk.WorkspaceAppHealth(dbApp.Health), + Hidden: dbApp.Hidden, + OpenIn: codersdk.WorkspaceAppOpenIn(dbApp.OpenIn), + Statuses: WorkspaceAppStatuses(statuses), }) } return apps } +func WorkspaceAppStatuses(statuses []database.WorkspaceAppStatus) []codersdk.WorkspaceAppStatus { + return List(statuses, WorkspaceAppStatus) +} + +func WorkspaceAppStatus(status database.WorkspaceAppStatus) codersdk.WorkspaceAppStatus { + return codersdk.WorkspaceAppStatus{ + ID: status.ID, + CreatedAt: status.CreatedAt, + AgentID: status.AgentID, + AppID: status.AppID, + NeedsUserAttention: status.NeedsUserAttention, + URI: status.Uri.String, + Icon: status.Icon.String, + Message: status.Message, + State: codersdk.WorkspaceAppStatusState(status.State), + } +} + func ProvisionerDaemon(dbDaemon database.ProvisionerDaemon) codersdk.ProvisionerDaemon { result := codersdk.ProvisionerDaemon{ ID: dbDaemon.ID, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 87778c66d3dab..7ab078d32ad4f 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1840,6 +1840,13 @@ func (q *querier) GetLatestCryptoKeyByFeature(ctx context.Context, feature datab return q.db.GetLatestCryptoKeyByFeature(ctx, feature) } +func (q *querier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx, ids) +} + func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { if _, err := q.GetWorkspaceByID(ctx, workspaceID); err != nil { return database.WorkspaceBuild{}, err @@ -2854,6 +2861,13 @@ func (q *querier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg datab return q.db.GetWorkspaceAppByAgentIDAndSlug(ctx, arg) } +func (q *querier) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { + return nil, err + } + return q.db.GetWorkspaceAppStatusesByAppIDs(ctx, ids) +} + func (q *querier) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]database.WorkspaceApp, error) { if _, err := q.GetWorkspaceByAgentID(ctx, agentID); err != nil { return nil, err @@ -3547,6 +3561,13 @@ func (q *querier) InsertWorkspaceAppStats(ctx context.Context, arg database.Inse return q.db.InsertWorkspaceAppStats(ctx, arg) } +func (q *querier) InsertWorkspaceAppStatus(ctx context.Context, arg database.InsertWorkspaceAppStatusParams) (database.WorkspaceAppStatus, error) { + if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil { + return database.WorkspaceAppStatus{}, err + } + return q.db.InsertWorkspaceAppStatus(ctx, arg) +} + func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error { w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) if err != nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 70c2a33443a16..cdc1c8e9ca197 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -3706,6 +3706,12 @@ func (s *MethodTestSuite) TestSystemFunctions() { LoginType: database.LoginTypeGithub, }).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns(l) })) + s.Run("GetLatestWorkspaceAppStatusesByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) { + check.Args([]uuid.UUID{}).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) + s.Run("GetWorkspaceAppStatusesByAppIDs", s.Subtest(func(db database.Store, check *expects) { + check.Args([]uuid.UUID{}).Asserts(rbac.ResourceSystem, policy.ActionRead) + })) s.Run("GetLatestWorkspaceBuildsByWorkspaceIDs", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) @@ -4135,6 +4141,13 @@ func (s *MethodTestSuite) TestSystemFunctions() { Options: json.RawMessage("{}"), }).Asserts(rbac.ResourceSystem, policy.ActionCreate) })) + s.Run("InsertWorkspaceAppStatus", s.Subtest(func(db database.Store, check *expects) { + dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) + check.Args(database.InsertWorkspaceAppStatusParams{ + ID: uuid.New(), + State: "working", + }).Asserts(rbac.ResourceSystem, policy.ActionCreate) + })) s.Run("InsertWorkspaceResource", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) check.Args(database.InsertWorkspaceResourceParams{ diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 46f2de5a5820e..87275b1051efe 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -259,6 +259,7 @@ type data struct { workspaceAgentVolumeResourceMonitors []database.WorkspaceAgentVolumeResourceMonitor workspaceAgentDevcontainers []database.WorkspaceAgentDevcontainer workspaceApps []database.WorkspaceApp + workspaceAppStatuses []database.WorkspaceAppStatus workspaceAppAuditSessions []database.WorkspaceAppAuditSession workspaceAppStatsLastInsertID int64 workspaceAppStats []database.WorkspaceAppStat @@ -3697,6 +3698,34 @@ func (q *FakeQuerier) GetLatestCryptoKeyByFeature(_ context.Context, feature dat return latestKey, nil } +func (q *FakeQuerier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + // Map to track latest status per workspace ID + latestByWorkspace := make(map[uuid.UUID]database.WorkspaceAppStatus) + + // Find latest status for each workspace ID + for _, appStatus := range q.workspaceAppStatuses { + if !slices.Contains(ids, appStatus.WorkspaceID) { + continue + } + + current, exists := latestByWorkspace[appStatus.WorkspaceID] + if !exists || appStatus.CreatedAt.After(current.CreatedAt) { + latestByWorkspace[appStatus.WorkspaceID] = appStatus + } + } + + // Convert map to slice + appStatuses := make([]database.WorkspaceAppStatus, 0, len(latestByWorkspace)) + for _, status := range latestByWorkspace { + appStatuses = append(appStatuses, status) + } + + return appStatuses, nil +} + func (q *FakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -7488,6 +7517,21 @@ func (q *FakeQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg d return q.getWorkspaceAppByAgentIDAndSlugNoLock(ctx, arg) } +func (q *FakeQuerier) GetWorkspaceAppStatusesByAppIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + statuses := make([]database.WorkspaceAppStatus, 0) + for _, status := range q.workspaceAppStatuses { + for _, id := range ids { + if status.AppID == id { + statuses = append(statuses, status) + } + } + } + return statuses, nil +} + func (q *FakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID) ([]database.WorkspaceApp, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -9584,6 +9628,31 @@ InsertWorkspaceAppStatsLoop: return nil } +func (q *FakeQuerier) InsertWorkspaceAppStatus(_ context.Context, arg database.InsertWorkspaceAppStatusParams) (database.WorkspaceAppStatus, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.WorkspaceAppStatus{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + status := database.WorkspaceAppStatus{ + ID: arg.ID, + CreatedAt: arg.CreatedAt, + WorkspaceID: arg.WorkspaceID, + AgentID: arg.AgentID, + AppID: arg.AppID, + NeedsUserAttention: arg.NeedsUserAttention, + State: arg.State, + Message: arg.Message, + Uri: arg.Uri, + Icon: arg.Icon, + } + q.workspaceAppStatuses = append(q.workspaceAppStatuses, status) + return status, nil +} + func (q *FakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) error { if err := validateDatabaseType(arg); err != nil { return err diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 05c0418c77acd..91cdf641c3446 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -858,6 +858,13 @@ func (m queryMetricsStore) GetLatestCryptoKeyByFeature(ctx context.Context, feat return r0, r1 } +func (m queryMetricsStore) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { + start := time.Now() + r0, r1 := m.s.GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx, ids) + m.queryLatencies.WithLabelValues("GetLatestWorkspaceAppStatusesByWorkspaceIDs").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { start := time.Now() build, err := m.s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID) @@ -1670,6 +1677,13 @@ func (m queryMetricsStore) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, return app, err } +func (m queryMetricsStore) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { + start := time.Now() + r0, r1 := m.s.GetWorkspaceAppStatusesByAppIDs(ctx, ids) + m.queryLatencies.WithLabelValues("GetWorkspaceAppStatusesByAppIDs").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]database.WorkspaceApp, error) { start := time.Now() apps, err := m.s.GetWorkspaceAppsByAgentID(ctx, agentID) @@ -2265,6 +2279,13 @@ func (m queryMetricsStore) InsertWorkspaceAppStats(ctx context.Context, arg data return r0 } +func (m queryMetricsStore) InsertWorkspaceAppStatus(ctx context.Context, arg database.InsertWorkspaceAppStatusParams) (database.WorkspaceAppStatus, error) { + start := time.Now() + r0, r1 := m.s.InsertWorkspaceAppStatus(ctx, arg) + m.queryLatencies.WithLabelValues("InsertWorkspaceAppStatus").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error { start := time.Now() err := m.s.InsertWorkspaceBuild(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index c5a5db20a9e90..109462e5f1996 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1729,6 +1729,21 @@ func (mr *MockStoreMockRecorder) GetLatestCryptoKeyByFeature(ctx, feature any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestCryptoKeyByFeature", reflect.TypeOf((*MockStore)(nil).GetLatestCryptoKeyByFeature), ctx, feature) } +// GetLatestWorkspaceAppStatusesByWorkspaceIDs mocks base method. +func (m *MockStore) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLatestWorkspaceAppStatusesByWorkspaceIDs", ctx, ids) + ret0, _ := ret[0].([]database.WorkspaceAppStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLatestWorkspaceAppStatusesByWorkspaceIDs indicates an expected call of GetLatestWorkspaceAppStatusesByWorkspaceIDs. +func (mr *MockStoreMockRecorder) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx, ids any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestWorkspaceAppStatusesByWorkspaceIDs", reflect.TypeOf((*MockStore)(nil).GetLatestWorkspaceAppStatusesByWorkspaceIDs), ctx, ids) +} + // GetLatestWorkspaceBuildByWorkspaceID mocks base method. func (m *MockStore) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) { m.ctrl.T.Helper() @@ -3499,6 +3514,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceAppByAgentIDAndSlug(ctx, arg any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAppByAgentIDAndSlug", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAppByAgentIDAndSlug), ctx, arg) } +// GetWorkspaceAppStatusesByAppIDs mocks base method. +func (m *MockStore) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetWorkspaceAppStatusesByAppIDs", ctx, ids) + ret0, _ := ret[0].([]database.WorkspaceAppStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetWorkspaceAppStatusesByAppIDs indicates an expected call of GetWorkspaceAppStatusesByAppIDs. +func (mr *MockStoreMockRecorder) GetWorkspaceAppStatusesByAppIDs(ctx, ids any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceAppStatusesByAppIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceAppStatusesByAppIDs), ctx, ids) +} + // GetWorkspaceAppsByAgentID mocks base method. func (m *MockStore) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]database.WorkspaceApp, error) { m.ctrl.T.Helper() @@ -4776,6 +4806,21 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceAppStats(ctx, arg any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAppStats", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAppStats), ctx, arg) } +// InsertWorkspaceAppStatus mocks base method. +func (m *MockStore) InsertWorkspaceAppStatus(ctx context.Context, arg database.InsertWorkspaceAppStatusParams) (database.WorkspaceAppStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertWorkspaceAppStatus", ctx, arg) + ret0, _ := ret[0].(database.WorkspaceAppStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertWorkspaceAppStatus indicates an expected call of InsertWorkspaceAppStatus. +func (mr *MockStoreMockRecorder) InsertWorkspaceAppStatus(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceAppStatus", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceAppStatus), ctx, arg) +} + // InsertWorkspaceBuild mocks base method. func (m *MockStore) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index b7908a8880107..b4207c41deff2 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -293,6 +293,12 @@ CREATE TYPE workspace_app_open_in AS ENUM ( 'slim-window' ); +CREATE TYPE workspace_app_status_state AS ENUM ( + 'working', + 'complete', + 'failure' +); + CREATE TYPE workspace_transition AS ENUM ( 'start', 'stop', @@ -1896,6 +1902,19 @@ CREATE SEQUENCE workspace_app_stats_id_seq ALTER SEQUENCE workspace_app_stats_id_seq OWNED BY workspace_app_stats.id; +CREATE TABLE workspace_app_statuses ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + agent_id uuid NOT NULL, + app_id uuid NOT NULL, + workspace_id uuid NOT NULL, + state workspace_app_status_state NOT NULL, + needs_user_attention boolean NOT NULL, + message text NOT NULL, + uri text, + icon text +); + CREATE TABLE workspace_apps ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, @@ -2359,6 +2378,9 @@ ALTER TABLE ONLY workspace_app_stats ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_agent_id_session_id_key UNIQUE (user_id, agent_id, session_id); +ALTER TABLE ONLY workspace_app_statuses + ADD CONSTRAINT workspace_app_statuses_pkey PRIMARY KEY (id); + ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug); @@ -2451,6 +2473,8 @@ CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); +CREATE INDEX idx_workspace_app_statuses_workspace_id_created_at ON workspace_app_statuses USING btree (workspace_id, created_at DESC); + CREATE UNIQUE INDEX notification_messages_dedupe_hash_idx ON notification_messages USING btree (dedupe_hash); CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true); @@ -2802,6 +2826,15 @@ ALTER TABLE ONLY workspace_app_stats ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); +ALTER TABLE ONLY workspace_app_statuses + ADD CONSTRAINT workspace_app_statuses_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); + +ALTER TABLE ONLY workspace_app_statuses + ADD CONSTRAINT workspace_app_statuses_app_id_fkey FOREIGN KEY (app_id) REFERENCES workspace_apps(id); + +ALTER TABLE ONLY workspace_app_statuses + ADD CONSTRAINT workspace_app_statuses_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); + ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; diff --git a/coderd/database/foreign_key_constraint.go b/coderd/database/foreign_key_constraint.go index 7dab8519a897c..3f5ce963e6fdb 100644 --- a/coderd/database/foreign_key_constraint.go +++ b/coderd/database/foreign_key_constraint.go @@ -73,6 +73,9 @@ const ( ForeignKeyWorkspaceAppStatsAgentID ForeignKeyConstraint = "workspace_app_stats_agent_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); ForeignKeyWorkspaceAppStatsUserID ForeignKeyConstraint = "workspace_app_stats_user_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); ForeignKeyWorkspaceAppStatsWorkspaceID ForeignKeyConstraint = "workspace_app_stats_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); + ForeignKeyWorkspaceAppStatusesAgentID ForeignKeyConstraint = "workspace_app_statuses_agent_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id); + ForeignKeyWorkspaceAppStatusesAppID ForeignKeyConstraint = "workspace_app_statuses_app_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_app_id_fkey FOREIGN KEY (app_id) REFERENCES workspace_apps(id); + ForeignKeyWorkspaceAppStatusesWorkspaceID ForeignKeyConstraint = "workspace_app_statuses_workspace_id_fkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id); ForeignKeyWorkspaceAppsAgentID ForeignKeyConstraint = "workspace_apps_agent_id_fkey" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE; ForeignKeyWorkspaceBuildParametersWorkspaceBuildID ForeignKeyConstraint = "workspace_build_parameters_workspace_build_id_fkey" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_fkey FOREIGN KEY (workspace_build_id) REFERENCES workspace_builds(id) ON DELETE CASCADE; ForeignKeyWorkspaceBuildsJobID ForeignKeyConstraint = "workspace_builds_job_id_fkey" // ALTER TABLE ONLY workspace_builds ADD CONSTRAINT workspace_builds_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000313_workspace_app_statuses.down.sql b/coderd/database/migrations/000313_workspace_app_statuses.down.sql new file mode 100644 index 0000000000000..59d38cc8bc21c --- /dev/null +++ b/coderd/database/migrations/000313_workspace_app_statuses.down.sql @@ -0,0 +1,3 @@ +DROP TABLE workspace_app_statuses; + +DROP TYPE workspace_app_status_state; diff --git a/coderd/database/migrations/000313_workspace_app_statuses.up.sql b/coderd/database/migrations/000313_workspace_app_statuses.up.sql new file mode 100644 index 0000000000000..4bbeb64efc231 --- /dev/null +++ b/coderd/database/migrations/000313_workspace_app_statuses.up.sql @@ -0,0 +1,28 @@ +CREATE TYPE workspace_app_status_state AS ENUM ('working', 'complete', 'failure'); + +-- Workspace app statuses allow agents to report statuses per-app in the UI. +CREATE TABLE workspace_app_statuses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + -- The agent that the status is for. + agent_id UUID NOT NULL REFERENCES workspace_agents(id), + -- The slug of the app that the status is for. This will be used + -- to reference the app in the UI - with an icon. + app_id UUID NOT NULL REFERENCES workspace_apps(id), + -- workspace_id is the workspace that the status is for. + workspace_id UUID NOT NULL REFERENCES workspaces(id), + -- The status determines how the status is displayed in the UI. + state workspace_app_status_state NOT NULL, + -- Whether the status needs user attention. + needs_user_attention BOOLEAN NOT NULL, + -- The message is the main text that will be displayed in the UI. + message TEXT NOT NULL, + -- The URI of the resource that the status is for. + -- e.g. https://github.com/org/repo/pull/123 + -- e.g. file:///path/to/file + uri TEXT, + -- Icon is an external URL to an icon that will be rendered in the UI. + icon TEXT +); + +CREATE INDEX idx_workspace_app_statuses_workspace_id_created_at ON workspace_app_statuses(workspace_id, created_at DESC); diff --git a/coderd/database/migrations/testdata/fixtures/000313_workspace_app_statuses.up.sql b/coderd/database/migrations/testdata/fixtures/000313_workspace_app_statuses.up.sql new file mode 100644 index 0000000000000..c36f5c66c3dd0 --- /dev/null +++ b/coderd/database/migrations/testdata/fixtures/000313_workspace_app_statuses.up.sql @@ -0,0 +1,19 @@ +INSERT INTO workspace_app_statuses ( + id, + created_at, + agent_id, + app_id, + workspace_id, + state, + needs_user_attention, + message +) VALUES ( + gen_random_uuid(), + NOW(), + '7a1ce5f8-8d00-431c-ad1b-97a846512804', + '36b65d0c-042b-4653-863a-655ee739861c', + '3a9a1feb-e89d-457c-9d53-ac751b198ebe', + 'working', + false, + 'Creating SQL queries for test data!' +); diff --git a/coderd/database/models.go b/coderd/database/models.go index 634cb6b59a41a..4339191f7afa2 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -2414,6 +2414,67 @@ func AllWorkspaceAppOpenInValues() []WorkspaceAppOpenIn { } } +type WorkspaceAppStatusState string + +const ( + WorkspaceAppStatusStateWorking WorkspaceAppStatusState = "working" + WorkspaceAppStatusStateComplete WorkspaceAppStatusState = "complete" + WorkspaceAppStatusStateFailure WorkspaceAppStatusState = "failure" +) + +func (e *WorkspaceAppStatusState) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = WorkspaceAppStatusState(s) + case string: + *e = WorkspaceAppStatusState(s) + default: + return fmt.Errorf("unsupported scan type for WorkspaceAppStatusState: %T", src) + } + return nil +} + +type NullWorkspaceAppStatusState struct { + WorkspaceAppStatusState WorkspaceAppStatusState `json:"workspace_app_status_state"` + Valid bool `json:"valid"` // Valid is true if WorkspaceAppStatusState is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullWorkspaceAppStatusState) Scan(value interface{}) error { + if value == nil { + ns.WorkspaceAppStatusState, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.WorkspaceAppStatusState.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullWorkspaceAppStatusState) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.WorkspaceAppStatusState), nil +} + +func (e WorkspaceAppStatusState) Valid() bool { + switch e { + case WorkspaceAppStatusStateWorking, + WorkspaceAppStatusStateComplete, + WorkspaceAppStatusStateFailure: + return true + } + return false +} + +func AllWorkspaceAppStatusStateValues() []WorkspaceAppStatusState { + return []WorkspaceAppStatusState{ + WorkspaceAppStatusStateWorking, + WorkspaceAppStatusStateComplete, + WorkspaceAppStatusStateFailure, + } +} + type WorkspaceTransition string const ( @@ -3515,6 +3576,19 @@ type WorkspaceAppStat struct { Requests int32 `db:"requests" json:"requests"` } +type WorkspaceAppStatus struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + AppID uuid.UUID `db:"app_id" json:"app_id"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + State WorkspaceAppStatusState `db:"state" json:"state"` + NeedsUserAttention bool `db:"needs_user_attention" json:"needs_user_attention"` + Message string `db:"message" json:"message"` + Uri sql.NullString `db:"uri" json:"uri"` + Icon sql.NullString `db:"icon" json:"icon"` +} + // Joins in the username + avatar url of the initiated by user. type WorkspaceBuild struct { ID uuid.UUID `db:"id" json:"id"` diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 892582c1201e5..59b53ac5950d8 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -198,6 +198,7 @@ type sqlcQuerier interface { GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) GetLastUpdateCheck(ctx context.Context) (string, error) GetLatestCryptoKeyByFeature(ctx context.Context, feature CryptoKeyFeature) (CryptoKey, error) + GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error) GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error) @@ -369,6 +370,7 @@ type sqlcQuerier interface { GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg GetWorkspaceAppByAgentIDAndSlugParams) (WorkspaceApp, error) + GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) GetWorkspaceAppsByAgentID(ctx context.Context, agentID uuid.UUID) ([]WorkspaceApp, error) GetWorkspaceAppsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceApp, error) GetWorkspaceAppsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceApp, error) @@ -474,6 +476,7 @@ type sqlcQuerier interface { InsertWorkspaceAgentStats(ctx context.Context, arg InsertWorkspaceAgentStatsParams) error InsertWorkspaceApp(ctx context.Context, arg InsertWorkspaceAppParams) (WorkspaceApp, error) InsertWorkspaceAppStats(ctx context.Context, arg InsertWorkspaceAppStatsParams) error + InsertWorkspaceAppStatus(ctx context.Context, arg InsertWorkspaceAppStatusParams) (WorkspaceAppStatus, error) InsertWorkspaceBuild(ctx context.Context, arg InsertWorkspaceBuildParams) error InsertWorkspaceBuildParameters(ctx context.Context, arg InsertWorkspaceBuildParametersParams) error InsertWorkspaceModule(ctx context.Context, arg InsertWorkspaceModuleParams) (WorkspaceModule, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 221c9f2c51df6..59d717531324a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15108,6 +15108,48 @@ func (q *sqlQuerier) UpsertWorkspaceAppAuditSession(ctx context.Context, arg Ups return new_or_stale, err } +const getLatestWorkspaceAppStatusesByWorkspaceIDs = `-- name: GetLatestWorkspaceAppStatusesByWorkspaceIDs :many +SELECT DISTINCT ON (workspace_id) + id, created_at, agent_id, app_id, workspace_id, state, needs_user_attention, message, uri, icon +FROM workspace_app_statuses +WHERE workspace_id = ANY($1 :: uuid[]) +ORDER BY workspace_id, created_at DESC +` + +func (q *sqlQuerier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) { + rows, err := q.db.QueryContext(ctx, getLatestWorkspaceAppStatusesByWorkspaceIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAppStatus + for rows.Next() { + var i WorkspaceAppStatus + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.AppID, + &i.WorkspaceID, + &i.State, + &i.NeedsUserAttention, + &i.Message, + &i.Uri, + &i.Icon, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWorkspaceAppByAgentIDAndSlug = `-- name: GetWorkspaceAppByAgentIDAndSlug :one SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in FROM workspace_apps WHERE agent_id = $1 AND slug = $2 ` @@ -15143,6 +15185,44 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg Ge return i, err } +const getWorkspaceAppStatusesByAppIDs = `-- name: GetWorkspaceAppStatusesByAppIDs :many +SELECT id, created_at, agent_id, app_id, workspace_id, state, needs_user_attention, message, uri, icon FROM workspace_app_statuses WHERE app_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAppStatusesByAppIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAppStatus + for rows.Next() { + var i WorkspaceAppStatus + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.AppID, + &i.WorkspaceID, + &i.State, + &i.NeedsUserAttention, + &i.Message, + &i.Uri, + &i.Icon, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getWorkspaceAppsByAgentID = `-- name: GetWorkspaceAppsByAgentID :many SELECT id, created_at, agent_id, display_name, icon, command, url, healthcheck_url, healthcheck_interval, healthcheck_threshold, health, subdomain, sharing_level, slug, external, display_order, hidden, open_in FROM workspace_apps WHERE agent_id = $1 ORDER BY slug ASC ` @@ -15373,6 +15453,54 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace return i, err } +const insertWorkspaceAppStatus = `-- name: InsertWorkspaceAppStatus :one +INSERT INTO workspace_app_statuses (id, created_at, workspace_id, agent_id, app_id, state, message, needs_user_attention, uri, icon) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +RETURNING id, created_at, agent_id, app_id, workspace_id, state, needs_user_attention, message, uri, icon +` + +type InsertWorkspaceAppStatusParams struct { + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + AppID uuid.UUID `db:"app_id" json:"app_id"` + State WorkspaceAppStatusState `db:"state" json:"state"` + Message string `db:"message" json:"message"` + NeedsUserAttention bool `db:"needs_user_attention" json:"needs_user_attention"` + Uri sql.NullString `db:"uri" json:"uri"` + Icon sql.NullString `db:"icon" json:"icon"` +} + +func (q *sqlQuerier) InsertWorkspaceAppStatus(ctx context.Context, arg InsertWorkspaceAppStatusParams) (WorkspaceAppStatus, error) { + row := q.db.QueryRowContext(ctx, insertWorkspaceAppStatus, + arg.ID, + arg.CreatedAt, + arg.WorkspaceID, + arg.AgentID, + arg.AppID, + arg.State, + arg.Message, + arg.NeedsUserAttention, + arg.Uri, + arg.Icon, + ) + var i WorkspaceAppStatus + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.AgentID, + &i.AppID, + &i.WorkspaceID, + &i.State, + &i.NeedsUserAttention, + &i.Message, + &i.Uri, + &i.Icon, + ) + return i, err +} + const updateWorkspaceAppHealthByID = `-- name: UpdateWorkspaceAppHealthByID :exec UPDATE workspace_apps diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index 2f431268a4c41..e402ee1402922 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -42,3 +42,18 @@ SET health = $2 WHERE id = $1; + +-- name: InsertWorkspaceAppStatus :one +INSERT INTO workspace_app_statuses (id, created_at, workspace_id, agent_id, app_id, state, message, needs_user_attention, uri, icon) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +RETURNING *; + +-- name: GetWorkspaceAppStatusesByAppIDs :many +SELECT * FROM workspace_app_statuses WHERE app_id = ANY(@ids :: uuid [ ]); + +-- name: GetLatestWorkspaceAppStatusesByWorkspaceIDs :many +SELECT DISTINCT ON (workspace_id) + * +FROM workspace_app_statuses +WHERE workspace_id = ANY(@ids :: uuid[]) +ORDER BY workspace_id, created_at DESC; diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 9318e1af1678b..d9f8ce275bfdf 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -86,6 +86,7 @@ const ( UniqueWorkspaceAppAuditSessionsPkey UniqueConstraint = "workspace_app_audit_sessions_pkey" // ALTER TABLE ONLY workspace_app_audit_sessions ADD CONSTRAINT workspace_app_audit_sessions_pkey PRIMARY KEY (id); UniqueWorkspaceAppStatsPkey UniqueConstraint = "workspace_app_stats_pkey" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_pkey PRIMARY KEY (id); UniqueWorkspaceAppStatsUserIDAgentIDSessionIDKey UniqueConstraint = "workspace_app_stats_user_id_agent_id_session_id_key" // ALTER TABLE ONLY workspace_app_stats ADD CONSTRAINT workspace_app_stats_user_id_agent_id_session_id_key UNIQUE (user_id, agent_id, session_id); + UniqueWorkspaceAppStatusesPkey UniqueConstraint = "workspace_app_statuses_pkey" // ALTER TABLE ONLY workspace_app_statuses ADD CONSTRAINT workspace_app_statuses_pkey PRIMARY KEY (id); UniqueWorkspaceAppsAgentIDSlugIndex UniqueConstraint = "workspace_apps_agent_id_slug_idx" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_agent_id_slug_idx UNIQUE (agent_id, slug); UniqueWorkspaceAppsPkey UniqueConstraint = "workspace_apps_pkey" // ALTER TABLE ONLY workspace_apps ADD CONSTRAINT workspace_apps_pkey PRIMARY KEY (id); UniqueWorkspaceBuildParametersWorkspaceBuildIDNameKey UniqueConstraint = "workspace_build_parameters_workspace_build_id_name_key" // ALTER TABLE ONLY workspace_build_parameters ADD CONSTRAINT workspace_build_parameters_workspace_build_id_name_key UNIQUE (workspace_build_id, name); diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 975803cb5e1d1..3ed880d40970f 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -93,6 +93,20 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { return } + appIDs := []uuid.UUID{} + for _, app := range dbApps { + appIDs = append(appIDs, app.ID) + } + // nolint:gocritic // This is a system restricted operation. + statuses, err := api.Database.GetWorkspaceAppStatusesByAppIDs(dbauthz.AsSystemRestricted(ctx), appIDs) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace app statuses.", + Detail: err.Error(), + }) + return + } + resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -127,7 +141,7 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) { } apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, db2sdk.Apps(dbApps, workspaceAgent, owner.Username, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, + api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, db2sdk.Apps(dbApps, statuses, workspaceAgent, owner.Username, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), ) if err != nil { @@ -300,6 +314,81 @@ func (api *API) patchWorkspaceAgentLogs(rw http.ResponseWriter, r *http.Request) httpapi.Write(ctx, rw, http.StatusOK, nil) } +// @Summary Patch workspace agent app status +// @ID patch-workspace-agent-app-status +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Agents +// @Param request body agentsdk.PatchAppStatus true "app status" +// @Success 200 {object} codersdk.Response +// @Router /workspaceagents/me/app-status [patch] +func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + workspaceAgent := httpmw.WorkspaceAgent(r) + + var req agentsdk.PatchAppStatus + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + app, err := api.Database.GetWorkspaceAppByAgentIDAndSlug(ctx, database.GetWorkspaceAppByAgentIDAndSlugParams{ + AgentID: workspaceAgent.ID, + Slug: req.AppSlug, + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to get workspace app.", + Detail: err.Error(), + }) + return + } + + workspace, err := api.Database.GetWorkspaceByAgentID(ctx, workspaceAgent.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Failed to get workspace.", + Detail: err.Error(), + }) + return + } + + // nolint:gocritic // This is a system restricted operation. + _, err = api.Database.InsertWorkspaceAppStatus(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceAppStatusParams{ + ID: uuid.New(), + CreatedAt: dbtime.Now(), + WorkspaceID: workspace.ID, + AgentID: workspaceAgent.ID, + AppID: app.ID, + State: database.WorkspaceAppStatusState(req.State), + Message: req.Message, + Uri: sql.NullString{ + String: req.URI, + Valid: req.URI != "", + }, + Icon: sql.NullString{ + String: req.Icon, + Valid: req.Icon != "", + }, + NeedsUserAttention: req.NeedsUserAttention, + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to insert workspace app status.", + Detail: err.Error(), + }) + return + } + + api.publishWorkspaceUpdate(ctx, workspace.OwnerID, wspubsub.WorkspaceEvent{ + Kind: wspubsub.WorkspaceEventKindAgentAppStatusUpdate, + WorkspaceID: workspace.ID, + AgentID: &workspaceAgent.ID, + }) + + httpapi.Write(ctx, rw, http.StatusOK, nil) +} + // workspaceAgentLogs returns the logs associated with a workspace agent // // @Summary Get logs by workspace agent @@ -1054,7 +1143,7 @@ func (api *API) workspaceAgentPostLogSource(rw http.ResponseWriter, r *http.Requ // convertProvisionedApps converts applications that are in the middle of provisioning process. // It means that they may not have an agent or workspace assigned (dry-run job). func convertProvisionedApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp { - return db2sdk.Apps(dbApps, database.WorkspaceAgent{}, "", database.Workspace{}) + return db2sdk.Apps(dbApps, []database.WorkspaceAppStatus{}, database.WorkspaceAgent{}, "", database.Workspace{}) } func convertLogSources(dbLogSources []database.WorkspaceAgentLogSource) []codersdk.WorkspaceAgentLogSource { diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index c45cc8c2a6c2f..186c66bfd6f8e 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -339,6 +339,46 @@ func TestWorkspaceAgentLogs(t *testing.T) { }) } +func TestWorkspaceAgentAppStatus(t *testing.T) { + t.Parallel() + t.Run("Success", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + client, db := coderdtest.NewWithDatabase(t, nil) + user := coderdtest.CreateFirstUser(t, client) + client, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) + + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user2.ID, + }).WithAgent(func(a []*proto.Agent) []*proto.Agent { + a[0].Apps = []*proto.App{ + { + Slug: "vscode", + }, + } + return a + }).Do() + + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(r.AgentToken) + err := agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{ + AppSlug: "vscode", + Message: "testing", + URI: "https://example.com", + Icon: "https://example.com/icon.png", + State: codersdk.WorkspaceAppStatusStateComplete, + }) + require.NoError(t, err) + + workspace, err := client.Workspace(ctx, r.Workspace.ID) + require.NoError(t, err) + agent, err := client.WorkspaceAgent(ctx, workspace.LatestBuild.Resources[0].Agents[0].ID) + require.NoError(t, err) + require.Len(t, agent.Apps[0].Statuses, 1) + }) +} + func TestWorkspaceAgentConnectRPC(t *testing.T) { t.Parallel() diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index f159d4a4e8bf1..7bd32e00cd830 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -84,6 +84,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { data.metadata, data.agents, data.apps, + data.appStatuses, data.scripts, data.logSources, data.templateVersions[0], @@ -202,6 +203,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { data.metadata, data.agents, data.apps, + data.appStatuses, data.scripts, data.logSources, data.templateVersions, @@ -292,6 +294,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ data.metadata, data.agents, data.apps, + data.appStatuses, data.scripts, data.logSources, data.templateVersions[0], @@ -432,6 +435,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, []database.WorkspaceApp{}, + []database.WorkspaceAppStatus{}, []database.WorkspaceAgentScript{}, []database.WorkspaceAgentLogSource{}, database.TemplateVersion{}, @@ -764,6 +768,7 @@ type workspaceBuildsData struct { metadata []database.WorkspaceResourceMetadatum agents []database.WorkspaceAgent apps []database.WorkspaceApp + appStatuses []database.WorkspaceAppStatus scripts []database.WorkspaceAgentScript logSources []database.WorkspaceAgentLogSource provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow @@ -874,6 +879,17 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab return workspaceBuildsData{}, err } + appIDs := make([]uuid.UUID, 0) + for _, app := range apps { + appIDs = append(appIDs, app.ID) + } + + // nolint:gocritic // Getting workspace app statuses by app IDs is a system function. + statuses, err := api.Database.GetWorkspaceAppStatusesByAppIDs(dbauthz.AsSystemRestricted(ctx), appIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return workspaceBuildsData{}, xerrors.Errorf("get workspace app statuses: %w", err) + } + return workspaceBuildsData{ jobs: jobs, templateVersions: templateVersions, @@ -881,6 +897,7 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []datab metadata: metadata, agents: agents, apps: apps, + appStatuses: statuses, scripts: scripts, logSources: logSources, provisionerDaemons: pendingJobProvisioners, @@ -895,6 +912,7 @@ func (api *API) convertWorkspaceBuilds( resourceMetadata []database.WorkspaceResourceMetadatum, resourceAgents []database.WorkspaceAgent, agentApps []database.WorkspaceApp, + agentAppStatuses []database.WorkspaceAppStatus, agentScripts []database.WorkspaceAgentScript, agentLogSources []database.WorkspaceAgentLogSource, templateVersions []database.TemplateVersion, @@ -937,6 +955,7 @@ func (api *API) convertWorkspaceBuilds( resourceMetadata, resourceAgents, agentApps, + agentAppStatuses, agentScripts, agentLogSources, templateVersion, @@ -960,6 +979,7 @@ func (api *API) convertWorkspaceBuild( resourceMetadata []database.WorkspaceResourceMetadatum, resourceAgents []database.WorkspaceAgent, agentApps []database.WorkspaceApp, + agentAppStatuses []database.WorkspaceAppStatus, agentScripts []database.WorkspaceAgentScript, agentLogSources []database.WorkspaceAgentLogSource, templateVersion database.TemplateVersion, @@ -997,6 +1017,10 @@ func (api *API) convertWorkspaceBuild( provisionerDaemonsForThisWorkspaceBuild = append(provisionerDaemonsForThisWorkspaceBuild, provisionerDaemon.ProvisionerDaemon) } matchedProvisioners := db2sdk.MatchedProvisioners(provisionerDaemonsForThisWorkspaceBuild, job.ProvisionerJob.CreatedAt, provisionerdserver.StaleInterval) + statusesByAgentID := map[uuid.UUID][]database.WorkspaceAppStatus{} + for _, status := range agentAppStatuses { + statusesByAgentID[status.AgentID] = append(statusesByAgentID[status.AgentID], status) + } resources := resourcesByJobID[job.ProvisionerJob.ID] apiResources := make([]codersdk.WorkspaceResource, 0) @@ -1018,9 +1042,10 @@ func (api *API) convertWorkspaceBuild( apps := appsByAgentID[agent.ID] scripts := scriptsByAgentID[agent.ID] + statuses := statusesByAgentID[agent.ID] logSources := logSourcesByAgentID[agent.ID] apiAgent, err := db2sdk.WorkspaceAgent( - api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, agent, workspace.OwnerUsername, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, + api.DERPMap(), *api.TailnetCoordinator.Load(), agent, db2sdk.Apps(apps, statuses, agent, workspace.OwnerUsername, workspace), convertScripts(scripts), convertLogSources(logSources), api.AgentInactiveDisconnectTimeout, api.DeploymentValues.AgentFallbackTroubleshootingURL.String(), ) if err != nil { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 7022938062c64..84862d9c400c9 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -14,6 +14,7 @@ import ( "github.com/dustin/go-humanize" "github.com/go-chi/chi/v5" "github.com/google/uuid" + "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "cdr.dev/slog" @@ -102,12 +103,18 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { return } + appStatus := codersdk.WorkspaceAppStatus{} + if len(data.appStatuses) > 0 { + appStatus = data.appStatuses[0] + } + w, err := convertWorkspace( apiKey.UserID, workspace, data.builds[0], data.templates[0], api.Options.AllowWorkspaceRenames, + appStatus, ) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -300,12 +307,18 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) return } + appStatus := codersdk.WorkspaceAppStatus{} + if len(data.appStatuses) > 0 { + appStatus = data.appStatuses[0] + } + w, err := convertWorkspace( apiKey.UserID, workspace, data.builds[0], data.templates[0], api.Options.AllowWorkspaceRenames, + appStatus, ) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -731,6 +744,7 @@ func createWorkspace( []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, []database.WorkspaceApp{}, + []database.WorkspaceAppStatus{}, []database.WorkspaceAgentScript{}, []database.WorkspaceAgentLogSource{}, database.TemplateVersion{}, @@ -750,6 +764,7 @@ func createWorkspace( apiBuild, template, api.Options.AllowWorkspaceRenames, + codersdk.WorkspaceAppStatus{}, ) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -1234,12 +1249,18 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) { aReq.New = newWorkspace + appStatus := codersdk.WorkspaceAppStatus{} + if len(data.appStatuses) > 0 { + appStatus = data.appStatuses[0] + } + w, err := convertWorkspace( apiKey.UserID, workspace, data.builds[0], data.templates[0], api.Options.AllowWorkspaceRenames, + appStatus, ) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -1771,12 +1792,17 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } + appStatus := codersdk.WorkspaceAppStatus{} + if len(data.appStatuses) > 0 { + appStatus = data.appStatuses[0] + } w, err := convertWorkspace( apiKey.UserID, workspace, data.builds[0], data.templates[0], api.Options.AllowWorkspaceRenames, + appStatus, ) if err != nil { _ = sendEvent(ctx, codersdk.ServerSentEvent{ @@ -1887,6 +1913,7 @@ func (api *API) workspaceTimings(rw http.ResponseWriter, r *http.Request) { type workspaceData struct { templates []database.Template builds []codersdk.WorkspaceBuild + appStatuses []codersdk.WorkspaceAppStatus allowRenames bool } @@ -1902,18 +1929,42 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa templateIDs = append(templateIDs, workspace.TemplateID) } - templates, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{ - IDs: templateIDs, + var ( + templates []database.Template + builds []database.WorkspaceBuild + appStatuses []database.WorkspaceAppStatus + eg errgroup.Group + ) + eg.Go(func() (err error) { + templates, err = api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{ + IDs: templateIDs, + }) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("get templates: %w", err) + } + return nil }) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return workspaceData{}, xerrors.Errorf("get templates: %w", err) - } - - // This query must be run as system restricted to be efficient. - // nolint:gocritic - builds, err := api.Database.GetLatestWorkspaceBuildsByWorkspaceIDs(dbauthz.AsSystemRestricted(ctx), workspaceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return workspaceData{}, xerrors.Errorf("get workspace builds: %w", err) + eg.Go(func() (err error) { + // This query must be run as system restricted to be efficient. + // nolint:gocritic + builds, err = api.Database.GetLatestWorkspaceBuildsByWorkspaceIDs(dbauthz.AsSystemRestricted(ctx), workspaceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("get workspace builds: %w", err) + } + return nil + }) + eg.Go(func() (err error) { + // This query must be run as system restricted to be efficient. + // nolint:gocritic + appStatuses, err = api.Database.GetLatestWorkspaceAppStatusesByWorkspaceIDs(dbauthz.AsSystemRestricted(ctx), workspaceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return xerrors.Errorf("get workspace app statuses: %w", err) + } + return nil + }) + err := eg.Wait() + if err != nil { + return workspaceData{}, err } data, err := api.workspaceBuildsData(ctx, builds) @@ -1929,6 +1980,7 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa data.metadata, data.agents, data.apps, + data.appStatuses, data.scripts, data.logSources, data.templateVersions, @@ -1940,6 +1992,7 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa return workspaceData{ templates: templates, + appStatuses: db2sdk.WorkspaceAppStatuses(appStatuses), builds: apiBuilds, allowRenames: api.Options.AllowWorkspaceRenames, }, nil @@ -1954,6 +2007,10 @@ func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, d for _, template := range data.templates { templateByID[template.ID] = template } + appStatusesByWorkspaceID := map[uuid.UUID]codersdk.WorkspaceAppStatus{} + for _, appStatus := range data.appStatuses { + appStatusesByWorkspaceID[appStatus.WorkspaceID] = appStatus + } apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces)) for _, workspace := range workspaces { @@ -1970,6 +2027,7 @@ func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, d if !exists { continue } + appStatus := appStatusesByWorkspaceID[workspace.ID] w, err := convertWorkspace( requesterID, @@ -1977,6 +2035,7 @@ func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, d build, template, data.allowRenames, + appStatus, ) if err != nil { return nil, xerrors.Errorf("convert workspace: %w", err) @@ -1993,6 +2052,7 @@ func convertWorkspace( workspaceBuild codersdk.WorkspaceBuild, template database.Template, allowRenames bool, + latestAppStatus codersdk.WorkspaceAppStatus, ) (codersdk.Workspace, error) { if requesterID == uuid.Nil { return codersdk.Workspace{}, xerrors.Errorf("developer error: requesterID cannot be uuid.Nil!") @@ -2036,6 +2096,10 @@ func convertWorkspace( // Only show favorite status if you own the workspace. requesterFavorite := workspace.OwnerID == requesterID && workspace.Favorite + appStatus := &latestAppStatus + if latestAppStatus.ID == uuid.Nil { + appStatus = nil + } return codersdk.Workspace{ ID: workspace.ID, CreatedAt: workspace.CreatedAt, @@ -2047,6 +2111,7 @@ func convertWorkspace( OrganizationName: workspace.OrganizationName, TemplateID: workspace.TemplateID, LatestBuild: workspaceBuild, + LatestAppStatus: appStatus, TemplateName: workspace.TemplateName, TemplateIcon: workspace.TemplateIcon, TemplateDisplayName: workspace.TemplateDisplayName, diff --git a/coderd/wspubsub/wspubsub.go b/coderd/wspubsub/wspubsub.go index 0326efa695304..1175ce5830292 100644 --- a/coderd/wspubsub/wspubsub.go +++ b/coderd/wspubsub/wspubsub.go @@ -55,6 +55,7 @@ const ( WorkspaceEventKindAgentFirstLogs WorkspaceEventKind = "agt_first_logs" WorkspaceEventKindAgentLogsOverflow WorkspaceEventKind = "agt_logs_overflow" WorkspaceEventKindAgentTimeout WorkspaceEventKind = "agt_timeout" + WorkspaceEventKindAgentAppStatusUpdate WorkspaceEventKind = "agt_app_status_update" ) func (w *WorkspaceEvent) Validate() error { diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index a6207f238fcac..4f7d0a8baef31 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -581,6 +581,28 @@ func (c *Client) PatchLogs(ctx context.Context, req PatchLogs) error { return nil } +// PatchAppStatus updates the status of a workspace app. +type PatchAppStatus struct { + AppSlug string `json:"app_slug"` + NeedsUserAttention bool `json:"needs_user_attention"` + State codersdk.WorkspaceAppStatusState `json:"state"` + Message string `json:"message"` + URI string `json:"uri"` + Icon string `json:"icon"` +} + +func (c *Client) PatchAppStatus(ctx context.Context, req PatchAppStatus) error { + res, err := c.SDK.Request(ctx, http.MethodPatch, "/api/v2/workspaceagents/me/app-status", req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return codersdk.ReadBodyAsError(res) + } + return nil +} + type PostLogSourceRequest struct { // ID is a unique identifier for the log source. // It is scoped to a workspace agent, and can be statically diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index 25e45ac5eb305..ec5a7c4414f76 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -1,6 +1,8 @@ package codersdk import ( + "time" + "github.com/google/uuid" ) @@ -13,6 +15,14 @@ const ( WorkspaceAppHealthUnhealthy WorkspaceAppHealth = "unhealthy" ) +type WorkspaceAppStatusState string + +const ( + WorkspaceAppStatusStateWorking WorkspaceAppStatusState = "working" + WorkspaceAppStatusStateComplete WorkspaceAppStatusState = "complete" + WorkspaceAppStatusStateFailure WorkspaceAppStatusState = "failure" +) + var MapWorkspaceAppHealths = map[WorkspaceAppHealth]struct{}{ WorkspaceAppHealthDisabled: {}, WorkspaceAppHealthInitializing: {}, @@ -75,6 +85,9 @@ type WorkspaceApp struct { Health WorkspaceAppHealth `json:"health"` Hidden bool `json:"hidden"` OpenIn WorkspaceAppOpenIn `json:"open_in"` + + // Statuses is a list of statuses for the app. + Statuses []WorkspaceAppStatus `json:"statuses"` } type Healthcheck struct { @@ -85,3 +98,20 @@ type Healthcheck struct { // Threshold specifies the number of consecutive failed health checks before returning "unhealthy". Threshold int32 `json:"threshold"` } + +type WorkspaceAppStatus struct { + ID uuid.UUID `json:"id" format:"uuid"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` + AgentID uuid.UUID `json:"agent_id" format:"uuid"` + AppID uuid.UUID `json:"app_id" format:"uuid"` + State WorkspaceAppStatusState `json:"state"` + NeedsUserAttention bool `json:"needs_user_attention"` + Message string `json:"message"` + // URI is the URI of the resource that the status is for. + // e.g. https://github.com/org/repo/pull/123 + // e.g. file:///path/to/file + URI string `json:"uri"` + // Icon is an external URL to an icon that will be rendered in the UI. + Icon string `json:"icon"` +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index da3df12eb9364..f9377c1767451 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -26,27 +26,28 @@ const ( // Workspace is a deployment of a template. It references a specific // version and can be updated. type Workspace struct { - ID uuid.UUID `json:"id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - UpdatedAt time.Time `json:"updated_at" format:"date-time"` - OwnerID uuid.UUID `json:"owner_id" format:"uuid"` - OwnerName string `json:"owner_name"` - OwnerAvatarURL string `json:"owner_avatar_url"` - OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` - OrganizationName string `json:"organization_name"` - TemplateID uuid.UUID `json:"template_id" format:"uuid"` - TemplateName string `json:"template_name"` - TemplateDisplayName string `json:"template_display_name"` - TemplateIcon string `json:"template_icon"` - TemplateAllowUserCancelWorkspaceJobs bool `json:"template_allow_user_cancel_workspace_jobs"` - TemplateActiveVersionID uuid.UUID `json:"template_active_version_id" format:"uuid"` - TemplateRequireActiveVersion bool `json:"template_require_active_version"` - LatestBuild WorkspaceBuild `json:"latest_build"` - Outdated bool `json:"outdated"` - Name string `json:"name"` - AutostartSchedule *string `json:"autostart_schedule,omitempty"` - TTLMillis *int64 `json:"ttl_ms,omitempty"` - LastUsedAt time.Time `json:"last_used_at" format:"date-time"` + ID uuid.UUID `json:"id" format:"uuid"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + UpdatedAt time.Time `json:"updated_at" format:"date-time"` + OwnerID uuid.UUID `json:"owner_id" format:"uuid"` + OwnerName string `json:"owner_name"` + OwnerAvatarURL string `json:"owner_avatar_url"` + OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` + OrganizationName string `json:"organization_name"` + TemplateID uuid.UUID `json:"template_id" format:"uuid"` + TemplateName string `json:"template_name"` + TemplateDisplayName string `json:"template_display_name"` + TemplateIcon string `json:"template_icon"` + TemplateAllowUserCancelWorkspaceJobs bool `json:"template_allow_user_cancel_workspace_jobs"` + TemplateActiveVersionID uuid.UUID `json:"template_active_version_id" format:"uuid"` + TemplateRequireActiveVersion bool `json:"template_require_active_version"` + LatestBuild WorkspaceBuild `json:"latest_build"` + LatestAppStatus *WorkspaceAppStatus `json:"latest_app_status"` + Outdated bool `json:"outdated"` + Name string `json:"name"` + AutostartSchedule *string `json:"autostart_schedule,omitempty"` + TTLMillis *int64 `json:"ttl_ms,omitempty"` + LastUsedAt time.Time `json:"last_used_at" format:"date-time"` // DeletingAt indicates the time at which the workspace will be permanently deleted. // A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index ec996e9f57d7d..8faba29cf7ba5 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -180,6 +180,64 @@ curl -X POST http://coder-server:8080/api/v2/workspaceagents/google-instance-ide To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Patch workspace agent app status + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/workspaceagents/me/app-status \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /workspaceagents/me/app-status` + +> Body parameter + +```json +{ + "app_slug": "string", + "icon": "string", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +|--------|------|--------------------------------------------------------------|----------|-------------| +| `body` | body | [agentsdk.PatchAppStatus](schemas.md#agentsdkpatchappstatus) | true | app status | + +### Example responses + +> 200 Response + +```json +{ + "detail": "string", + "message": "string", + "validations": [ + { + "detail": "string", + "field": "string" + } + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get workspace agent external auth ### Code samples @@ -455,6 +513,20 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \ "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 26f6df4a55b73..0bb4b2e5b0ef3 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -100,6 +100,20 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -314,6 +328,20 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -643,6 +671,20 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -770,6 +812,17 @@ Status Code **200** | `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | | `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | | `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | +| `»»»» agent_id` | string(uuid) | false | | | +| `»»»» app_id` | string(uuid) | false | | | +| `»»»» created_at` | string(date-time) | false | | | +| `»»»» icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» id` | string(uuid) | false | | | +| `»»»» message` | string | false | | | +| `»»»» needs_user_attention` | boolean | false | | | +| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | +| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `»»»» workspace_id` | string(uuid) | false | | | | `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | | `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | | `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | @@ -851,6 +904,9 @@ Status Code **200** | `sharing_level` | `owner` | | `sharing_level` | `authenticated` | | `sharing_level` | `public` | +| `state` | `working` | +| `state` | `complete` | +| `state` | `failure` | | `lifecycle_state` | `created` | | `lifecycle_state` | `starting` | | `lifecycle_state` | `start_timeout` | @@ -970,6 +1026,20 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -1257,6 +1327,20 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -1440,6 +1524,17 @@ Status Code **200** | `»»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | | `»»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | | `»»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»»» statuses` | array | false | | Statuses is a list of statuses for the app. | +| `»»»»» agent_id` | string(uuid) | false | | | +| `»»»»» app_id` | string(uuid) | false | | | +| `»»»»» created_at` | string(date-time) | false | | | +| `»»»»» icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»»» id` | string(uuid) | false | | | +| `»»»»» message` | string | false | | | +| `»»»»» needs_user_attention` | boolean | false | | | +| `»»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | +| `»»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `»»»»» workspace_id` | string(uuid) | false | | | | `»»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | | `»»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | | `»»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | @@ -1544,6 +1639,9 @@ Status Code **200** | `sharing_level` | `owner` | | `sharing_level` | `authenticated` | | `sharing_level` | `public` | +| `state` | `working` | +| `state` | `complete` | +| `state` | `failure` | | `lifecycle_state` | `created` | | `lifecycle_state` | `starting` | | `lifecycle_state` | `start_timeout` | @@ -1699,6 +1797,20 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 4fee5c57d5100..3e86146647030 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -118,6 +118,30 @@ | `level` | [codersdk.LogLevel](#codersdkloglevel) | false | | | | `output` | string | false | | | +## agentsdk.PatchAppStatus + +```json +{ + "app_slug": "string", + "icon": "string", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------------|----------------------------------------------------------------------|----------|--------------|-------------| +| `app_slug` | string | false | | | +| `icon` | string | false | | | +| `message` | string | false | | | +| `needs_user_attention` | boolean | false | | | +| `state` | [codersdk.WorkspaceAppStatusState](#codersdkworkspaceappstatusstate) | false | | | +| `uri` | string | false | | | + ## agentsdk.PatchLogs ```json @@ -7525,6 +7549,18 @@ If the schedule is empty, the user will be updated to use the default schedule.| }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", + "latest_app_status": { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + }, "latest_build": { "build_number": 0, "created_at": "2019-08-24T14:15:22Z", @@ -7599,6 +7635,20 @@ If the schedule is empty, the user will be updated to use the default schedule.| "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -7727,36 +7777,37 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -|---------------------------------------------|--------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `allow_renames` | boolean | false | | | -| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | -| `autostart_schedule` | string | false | | | -| `created_at` | string | false | | | -| `deleting_at` | string | false | | Deleting at indicates the time at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. | -| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time_til_ field on its template. | -| `favorite` | boolean | false | | | -| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. | -| `id` | string | false | | | -| `last_used_at` | string | false | | | -| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | -| `name` | string | false | | | -| `next_start_at` | string | false | | | -| `organization_id` | string | false | | | -| `organization_name` | string | false | | | -| `outdated` | boolean | false | | | -| `owner_avatar_url` | string | false | | | -| `owner_id` | string | false | | | -| `owner_name` | string | false | | | -| `template_active_version_id` | string | false | | | -| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | -| `template_display_name` | string | false | | | -| `template_icon` | string | false | | | -| `template_id` | string | false | | | -| `template_name` | string | false | | | -| `template_require_active_version` | boolean | false | | | -| `ttl_ms` | integer | false | | | -| `updated_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------------------------|------------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `allow_renames` | boolean | false | | | +| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | +| `autostart_schedule` | string | false | | | +| `created_at` | string | false | | | +| `deleting_at` | string | false | | Deleting at indicates the time at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. | +| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time_til_ field on its template. | +| `favorite` | boolean | false | | | +| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. | +| `id` | string | false | | | +| `last_used_at` | string | false | | | +| `latest_app_status` | [codersdk.WorkspaceAppStatus](#codersdkworkspaceappstatus) | false | | | +| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | +| `name` | string | false | | | +| `next_start_at` | string | false | | | +| `organization_id` | string | false | | | +| `organization_name` | string | false | | | +| `outdated` | boolean | false | | | +| `owner_avatar_url` | string | false | | | +| `owner_id` | string | false | | | +| `owner_name` | string | false | | | +| `template_active_version_id` | string | false | | | +| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | +| `template_display_name` | string | false | | | +| `template_icon` | string | false | | | +| `template_id` | string | false | | | +| `template_name` | string | false | | | +| `template_require_active_version` | boolean | false | | | +| `ttl_ms` | integer | false | | | +| `updated_at` | string | false | | | #### Enumerated Values @@ -7787,6 +7838,20 @@ If the schedule is empty, the user will be updated to use the default schedule.| "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -8300,6 +8365,20 @@ If the schedule is empty, the user will be updated to use the default schedule.| "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -8321,6 +8400,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `open_in` | [codersdk.WorkspaceAppOpenIn](#codersdkworkspaceappopenin) | false | | | | `sharing_level` | [codersdk.WorkspaceAppSharingLevel](#codersdkworkspaceappsharinglevel) | false | | | | `slug` | string | false | | Slug is a unique identifier within the agent. | +| `statuses` | array of [codersdk.WorkspaceAppStatus](#codersdkworkspaceappstatus) | false | | Statuses is a list of statuses for the app. | | `subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | | `subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | | `url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | @@ -8381,6 +8461,54 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `authenticated` | | `public` | +## codersdk.WorkspaceAppStatus + +```json +{ + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------------------|----------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------| +| `agent_id` | string | false | | | +| `app_id` | string | false | | | +| `created_at` | string | false | | | +| `icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `id` | string | false | | | +| `message` | string | false | | | +| `needs_user_attention` | boolean | false | | | +| `state` | [codersdk.WorkspaceAppStatusState](#codersdkworkspaceappstatusstate) | false | | | +| `uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `workspace_id` | string | false | | | + +## codersdk.WorkspaceAppStatusState + +```json +"working" +``` + +### Properties + +#### Enumerated Values + +| Value | +|------------| +| `working` | +| `complete` | +| `failure` | + ## codersdk.WorkspaceBuild ```json @@ -8458,6 +8586,20 @@ If the schedule is empty, the user will be updated to use the default schedule.| "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -8858,6 +9000,20 @@ If the schedule is empty, the user will be updated to use the default schedule.| "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -9057,6 +9213,18 @@ If the schedule is empty, the user will be updated to use the default schedule.| }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", + "latest_app_status": { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + }, "latest_build": { "build_number": 0, "created_at": "2019-08-24T14:15:22Z", @@ -9127,6 +9295,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [], "subdomain": true, "subdomain_name": "string", "url": "string" diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index ab8b4f1b7c131..b644affbbfc88 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2284,6 +2284,20 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -2411,6 +2425,17 @@ Status Code **200** | `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | | `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | | `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | +| `»»»» agent_id` | string(uuid) | false | | | +| `»»»» app_id` | string(uuid) | false | | | +| `»»»» created_at` | string(date-time) | false | | | +| `»»»» icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» id` | string(uuid) | false | | | +| `»»»» message` | string | false | | | +| `»»»» needs_user_attention` | boolean | false | | | +| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | +| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `»»»» workspace_id` | string(uuid) | false | | | | `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | | `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | | `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | @@ -2492,6 +2517,9 @@ Status Code **200** | `sharing_level` | `owner` | | `sharing_level` | `authenticated` | | `sharing_level` | `public` | +| `state` | `working` | +| `state` | `complete` | +| `state` | `failure` | | `lifecycle_state` | `created` | | `lifecycle_state` | `starting` | | `lifecycle_state` | `start_timeout` | @@ -2777,6 +2805,20 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -2904,6 +2946,17 @@ Status Code **200** | `»»» open_in` | [codersdk.WorkspaceAppOpenIn](schemas.md#codersdkworkspaceappopenin) | false | | | | `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | | | `»»» slug` | string | false | | Slug is a unique identifier within the agent. | +| `»»» statuses` | array | false | | Statuses is a list of statuses for the app. | +| `»»»» agent_id` | string(uuid) | false | | | +| `»»»» app_id` | string(uuid) | false | | | +| `»»»» created_at` | string(date-time) | false | | | +| `»»»» icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» id` | string(uuid) | false | | | +| `»»»» message` | string | false | | | +| `»»»» needs_user_attention` | boolean | false | | | +| `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | +| `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `»»»» workspace_id` | string(uuid) | false | | | | `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. | | `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. | | `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. | @@ -2985,6 +3038,9 @@ Status Code **200** | `sharing_level` | `owner` | | `sharing_level` | `authenticated` | | `sharing_level` | `public` | +| `state` | `working` | +| `state` | `complete` | +| `state` | `failure` | | `lifecycle_state` | `created` | | `lifecycle_state` | `starting` | | `lifecycle_state` | `start_timeout` | diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 7264b6dbb3939..df5c7856de8c2 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -67,6 +67,18 @@ of the template will be used. }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", + "latest_app_status": { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + }, "latest_build": { "build_number": 0, "created_at": "2019-08-24T14:15:22Z", @@ -141,6 +153,20 @@ of the template will be used. "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -317,6 +343,18 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", + "latest_app_status": { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + }, "latest_build": { "build_number": 0, "created_at": "2019-08-24T14:15:22Z", @@ -391,6 +429,20 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -591,6 +643,18 @@ of the template will be used. }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", + "latest_app_status": { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + }, "latest_build": { "build_number": 0, "created_at": "2019-08-24T14:15:22Z", @@ -665,6 +729,20 @@ of the template will be used. "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -844,6 +922,18 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", + "latest_app_status": { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + }, "latest_build": { "build_number": 0, "created_at": "2019-08-24T14:15:22Z", @@ -914,6 +1004,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -1091,6 +1182,18 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", + "latest_app_status": { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + }, "latest_build": { "build_number": 0, "created_at": "2019-08-24T14:15:22Z", @@ -1165,6 +1268,20 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" @@ -1457,6 +1574,18 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "last_used_at": "2019-08-24T14:15:22Z", + "latest_app_status": { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + }, "latest_build": { "build_number": 0, "created_at": "2019-08-24T14:15:22Z", @@ -1531,6 +1660,20 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "open_in": "slim-window", "sharing_level": "owner", "slug": "string", + "statuses": [ + { + "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", + "app_id": "affd1d10-9538-4fc8-9e0b-4594a28c1335", + "created_at": "2019-08-24T14:15:22Z", + "icon": "string", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "message": "string", + "needs_user_attention": true, + "state": "working", + "uri": "string", + "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" + } + ], "subdomain": true, "subdomain_name": "string", "url": "string" diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 602fb582f07c9..fb534dafd3973 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3060,6 +3060,7 @@ export interface Workspace { readonly template_active_version_id: string; readonly template_require_active_version: boolean; readonly latest_build: WorkspaceBuild; + readonly latest_app_status: WorkspaceAppStatus | null; readonly outdated: boolean; readonly name: string; readonly autostart_schedule?: string; @@ -3307,6 +3308,7 @@ export interface WorkspaceApp { readonly health: WorkspaceAppHealth; readonly hidden: boolean; readonly open_in: WorkspaceAppOpenIn; + readonly statuses: readonly WorkspaceAppStatus[]; } // From codersdk/workspaceapps.go @@ -3337,6 +3339,29 @@ export const WorkspaceAppSharingLevels: WorkspaceAppSharingLevel[] = [ "public", ]; +// From codersdk/workspaceapps.go +export interface WorkspaceAppStatus { + readonly id: string; + readonly created_at: string; + readonly workspace_id: string; + readonly agent_id: string; + readonly app_id: string; + readonly state: WorkspaceAppStatusState; + readonly needs_user_attention: boolean; + readonly message: string; + readonly uri: string; + readonly icon: string; +} + +// From codersdk/workspaceapps.go +export type WorkspaceAppStatusState = "complete" | "failure" | "working"; + +export const WorkspaceAppStatusStates: WorkspaceAppStatusState[] = [ + "complete", + "failure", + "working", +]; + // From codersdk/workspacebuilds.go export interface WorkspaceBuild { readonly id: string; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index f80171122826c..1a233b74a084d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -913,6 +913,7 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = { }, hidden: false, open_in: "slim-window", + statuses: [], }; export const MockWorkspaceAgentLogSource: TypesGen.WorkspaceAgentLogSource = { @@ -1371,6 +1372,7 @@ export const MockWorkspace: TypesGen.Workspace = { healthy: true, failing_agents: [], }, + latest_app_status: null, automatic_updates: "never", allow_renames: true, favorite: false, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy