mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add app status tracking to the backend (#17163)
This does ~95% of the backend work required to integrate the AI work. Most left to integrate from the tasks branch is just frontend, which will be a lot smaller I believe. The real difference between this branch and that one is the abstraction -- this now attaches statuses to apps, and returns the latest status reported as part of a workspace. This change enables us to have a similar UX to in the tasks branch, but for agents other than Claude Code as well. Any app can report status now.
This commit is contained in:
Generated
+127
@@ -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": [
|
||||
@@ -10245,6 +10284,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": {
|
||||
@@ -16273,6 +16335,9 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"latest_app_status": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAppStatus"
|
||||
},
|
||||
"latest_build": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceBuild"
|
||||
},
|
||||
@@ -16872,6 +16937,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"
|
||||
@@ -16925,6 +16997,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": {
|
||||
|
||||
Generated
+117
@@ -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": [
|
||||
@@ -9080,6 +9113,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": {
|
||||
@@ -14819,6 +14875,9 @@
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"latest_app_status": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceAppStatus"
|
||||
},
|
||||
"latest_build": {
|
||||
"$ref": "#/definitions/codersdk.WorkspaceBuild"
|
||||
},
|
||||
@@ -15392,6 +15451,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"
|
||||
@@ -15433,6 +15499,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": {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Generated
+33
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
DROP TABLE workspace_app_statuses;
|
||||
|
||||
DROP TYPE workspace_app_status_state;
|
||||
@@ -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);
|
||||
+19
@@ -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!'
|
||||
);
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+76
-11
@@ -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{
|
||||
@@ -1792,12 +1813,17 @@ func (api *API) watchWorkspace(
|
||||
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(codersdk.ServerSentEvent{
|
||||
@@ -1908,6 +1934,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
|
||||
}
|
||||
|
||||
@@ -1923,18 +1950,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)
|
||||
@@ -1950,6 +2001,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,
|
||||
@@ -1961,6 +2013,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
|
||||
@@ -1975,6 +2028,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 {
|
||||
@@ -1991,6 +2048,7 @@ func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, d
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
appStatus := appStatusesByWorkspaceID[workspace.ID]
|
||||
|
||||
w, err := convertWorkspace(
|
||||
requesterID,
|
||||
@@ -1998,6 +2056,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)
|
||||
@@ -2014,6 +2073,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!")
|
||||
@@ -2057,6 +2117,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,
|
||||
@@ -2068,6 +2132,7 @@ func convertWorkspace(
|
||||
OrganizationName: workspace.OrganizationName,
|
||||
TemplateID: workspace.TemplateID,
|
||||
LatestBuild: workspaceBuild,
|
||||
LatestAppStatus: appStatus,
|
||||
TemplateName: workspace.TemplateName,
|
||||
TemplateIcon: workspace.TemplateIcon,
|
||||
TemplateDisplayName: workspace.TemplateDisplayName,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user