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:
Kyle Carberry
2025-03-31 10:55:44 -04:00
committed by GitHub
parent 489641d0be
commit 8ea956fc11
35 changed files with 1668 additions and 69 deletions

View File

@ -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,