mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00: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:
@ -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,
|
||||
|
Reference in New Issue
Block a user