chore: watch workspace endpoint (#4060)

This commit is contained in:
Garrett Delfosse
2022-09-16 14:54:23 -04:00
committed by GitHub
parent b340634aaa
commit 63fd4945a2
14 changed files with 691 additions and 348 deletions

View File

@ -1431,6 +1431,22 @@ func (q *fakeQuerier) GetWorkspaceResourcesByJobID(_ context.Context, jobID uuid
return resources, nil
}
func (q *fakeQuerier) GetWorkspaceResourcesByJobIDs(_ context.Context, jobIDs []uuid.UUID) ([]database.WorkspaceResource, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
resources := make([]database.WorkspaceResource, 0)
for _, resource := range q.provisionerJobResources {
for _, jobID := range jobIDs {
if resource.JobID != jobID {
continue
}
resources = append(resources, resource)
}
}
return resources, nil
}
func (q *fakeQuerier) GetWorkspaceResourcesCreatedAfter(_ context.Context, after time.Time) ([]database.WorkspaceResource, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()

View File

@ -97,6 +97,7 @@ type querier interface {
GetWorkspaceResourceMetadataByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResourceMetadatum, error)
GetWorkspaceResourceMetadataCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResourceMetadatum, error)
GetWorkspaceResourcesByJobID(ctx context.Context, jobID uuid.UUID) ([]WorkspaceResource, error)
GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error)
GetWorkspaceResourcesCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceResource, error)
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]Workspace, error)
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)

View File

@ -4528,6 +4528,47 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui
return items, nil
}
const getWorkspaceResourcesByJobIDs = `-- name: GetWorkspaceResourcesByJobIDs :many
SELECT
id, created_at, job_id, transition, type, name, hide, icon
FROM
workspace_resources
WHERE
job_id = ANY($1 :: uuid [ ])
`
func (q *sqlQuerier) GetWorkspaceResourcesByJobIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceResource, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceResourcesByJobIDs, pq.Array(ids))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceResource
for rows.Next() {
var i WorkspaceResource
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.JobID,
&i.Transition,
&i.Type,
&i.Name,
&i.Hide,
&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 getWorkspaceResourcesCreatedAfter = `-- name: GetWorkspaceResourcesCreatedAfter :many
SELECT id, created_at, job_id, transition, type, name, hide, icon FROM workspace_resources WHERE created_at > $1
`

View File

@ -14,6 +14,14 @@ FROM
WHERE
job_id = $1;
-- name: GetWorkspaceResourcesByJobIDs :many
SELECT
*
FROM
workspace_resources
WHERE
job_id = ANY(@ids :: uuid [ ]);
-- name: GetWorkspaceResourcesCreatedAfter :many
SELECT * FROM workspace_resources WHERE created_at > $1;

View File

@ -2,12 +2,16 @@ package httpapi
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strings"
"sync"
"time"
"github.com/go-playground/validator/v10"
@ -144,3 +148,75 @@ func WebsocketCloseSprintf(format string, vars ...any) string {
return msg
}
func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context, sse codersdk.ServerSentEvent) error, error) {
var mu sync.Mutex
h := rw.Header()
h.Set("Content-Type", "text/event-stream")
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "keep-alive")
h.Set("X-Accel-Buffering", "no")
f, ok := rw.(http.Flusher)
if !ok {
panic("http.ResponseWriter is not http.Flusher")
}
// Send a heartbeat every 15 seconds to avoid the connection being killed.
go func() {
ticker := time.NewTicker(time.Second * 15)
defer ticker.Stop()
for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
mu.Lock()
_, err := io.WriteString(rw, fmt.Sprintf("event: %s\n\n", codersdk.ServerSentEventTypePing))
if err != nil {
mu.Unlock()
return
}
f.Flush()
mu.Unlock()
}
}
}()
sendEvent := func(ctx context.Context, sse codersdk.ServerSentEvent) error {
if ctx.Err() != nil {
return ctx.Err()
}
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
_, err := buf.Write([]byte(fmt.Sprintf("event: %s\ndata: ", sse.Type)))
if err != nil {
return err
}
err = enc.Encode(sse.Data)
if err != nil {
return err
}
err = buf.WriteByte('\n')
if err != nil {
return err
}
mu.Lock()
defer mu.Unlock()
_, err = rw.Write(buf.Bytes())
if err != nil {
return err
}
f.Flush()
return nil
}
return sendEvent, nil
}

View File

@ -72,3 +72,11 @@ func (w *StatusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
func (w *StatusWriter) ResponseBody() []byte {
return w.responseBody
}
func (w *StatusWriter) Flush() {
f, ok := w.ResponseWriter.(http.Flusher)
if !ok {
panic("http.ResponseWriter is not http.Flusher")
}
f.Flush()
}

View File

@ -1,6 +1,7 @@
package coderd
import (
"context"
"database/sql"
"encoding/json"
"errors"
@ -29,29 +30,34 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
return
}
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
data, err := api.workspaceBuildsData(r.Context(), []database.Workspace{workspace}, []database.WorkspaceBuild{workspaceBuild})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job.",
Message: "Internal error getting workspace build data.",
Detail: err.Error(),
})
return
}
users, err := api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{
IDs: []uuid.UUID{workspace.OwnerID, workspaceBuild.InitiatorID},
})
apiBuild, err := api.convertWorkspaceBuild(
workspaceBuild,
workspace,
data.jobs[0],
data.users,
data.resources,
data.metadata,
data.agents,
data.apps,
)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user.",
Message: "Internal error converting workspace build.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK,
convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users),
workspace, workspaceBuild, job))
httpapi.Write(rw, http.StatusOK, apiBuild)
}
func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
@ -67,7 +73,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
return
}
var builds []database.WorkspaceBuild
var workspaceBuilds []database.WorkspaceBuild
// Ensure all db calls happen in the same tx
err := api.Database.InTx(func(store database.Store) error {
var err error
@ -95,7 +101,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
OffsetOpt: int32(paginationParams.Offset),
LimitOpt: int32(paginationParams.Limit),
}
builds, err = store.GetWorkspaceBuildByWorkspaceID(r.Context(), req)
workspaceBuilds, err = store.GetWorkspaceBuildByWorkspaceID(r.Context(), req)
if xerrors.Is(err, sql.ErrNoRows) {
err = nil
}
@ -113,53 +119,31 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
return
}
jobIDs := make([]uuid.UUID, 0, len(builds))
for _, build := range builds {
jobIDs = append(jobIDs, build.JobID)
}
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
data, err := api.workspaceBuildsData(r.Context(), []database.Workspace{workspace}, workspaceBuilds)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner jobs.",
Detail: err.Error(),
})
return
}
jobByID := map[string]database.ProvisionerJob{}
for _, job := range jobs {
jobByID[job.ID.String()] = job
}
userIDs := []uuid.UUID{workspace.OwnerID}
for _, build := range builds {
userIDs = append(userIDs, build.InitiatorID)
}
users, err := api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{
IDs: userIDs,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user.",
Message: "Internal error getting workspace build data.",
Detail: err.Error(),
})
return
}
apiBuilds := make([]codersdk.WorkspaceBuild, 0)
for _, build := range builds {
job, exists := jobByID[build.JobID.String()]
if !exists {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: fmt.Sprintf("Job %q doesn't exist for build %q.", build.JobID, build.ID),
})
return
}
apiBuilds = append(apiBuilds,
convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users),
workspace, build, job))
apiBuilds, err := api.convertWorkspaceBuilds(
workspaceBuilds,
[]database.Workspace{workspace},
data.jobs,
data.users,
data.resources,
data.metadata,
data.agents,
data.apps,
)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspace build.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, apiBuilds)
@ -216,29 +200,34 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
return
}
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
data, err := api.workspaceBuildsData(r.Context(), []database.Workspace{workspace}, []database.WorkspaceBuild{workspaceBuild})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job.",
Message: "Internal error getting workspace build data.",
Detail: err.Error(),
})
return
}
users, err := api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{
IDs: []uuid.UUID{workspace.OwnerID, workspaceBuild.InitiatorID},
})
apiBuild, err := api.convertWorkspaceBuild(
workspaceBuild,
workspace,
data.jobs[0],
data.users,
data.resources,
data.metadata,
data.agents,
data.apps,
)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user.",
Message: "Internal error converting workspace build.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK,
convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users),
workspace, workspaceBuild, job))
httpapi.Write(rw, http.StatusOK, apiBuild)
}
func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
@ -496,9 +485,25 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
return
}
httpapi.Write(rw, http.StatusCreated,
convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users),
workspace, workspaceBuild, provisionerJob))
apiBuild, err := api.convertWorkspaceBuild(
workspaceBuild,
workspace,
provisionerJob,
users,
[]database.WorkspaceResource{},
[]database.WorkspaceResourceMetadatum{},
[]database.WorkspaceAgent{},
[]database.WorkspaceApp{},
)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspace build.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusCreated, apiBuild)
}
func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
@ -627,47 +632,220 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) {
_, _ = rw.Write(workspaceBuild.ProvisionerState)
}
func convertWorkspaceBuild(
workspaceOwner *database.User,
buildInitiator *database.User,
type workspaceBuildsData struct {
users []database.User
jobs []database.ProvisionerJob
resources []database.WorkspaceResource
metadata []database.WorkspaceResourceMetadatum
agents []database.WorkspaceAgent
apps []database.WorkspaceApp
}
func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.Workspace, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) {
userIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
for _, build := range workspaceBuilds {
userIDs = append(userIDs, build.InitiatorID)
}
for _, workspace := range workspaces {
userIDs = append(userIDs, workspace.OwnerID)
}
users, err := api.Database.GetUsersByIDs(ctx, database.GetUsersByIDsParams{
IDs: userIDs,
})
if err != nil {
return workspaceBuildsData{}, xerrors.Errorf("get users: %w", err)
}
jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
for _, build := range workspaceBuilds {
jobIDs = append(jobIDs, build.JobID)
}
jobs, err := api.Database.GetProvisionerJobsByIDs(ctx, jobIDs)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceBuildsData{}, xerrors.Errorf("get provisioner jobs: %w", err)
}
resources, err := api.Database.GetWorkspaceResourcesByJobIDs(ctx, jobIDs)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceBuildsData{}, xerrors.Errorf("get workspace resources by job: %w", err)
}
if len(resources) == 0 {
return workspaceBuildsData{
users: users,
jobs: jobs,
}, nil
}
resourceIDs := make([]uuid.UUID, 0)
for _, resource := range resources {
resourceIDs = append(resourceIDs, resource.ID)
}
metadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(ctx, resourceIDs)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceBuildsData{}, xerrors.Errorf("fetching resource metadata: %w", err)
}
agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceBuildsData{}, xerrors.Errorf("get workspace agents: %w", err)
}
if len(resources) == 0 {
return workspaceBuildsData{
users: users,
jobs: jobs,
resources: resources,
metadata: metadata,
}, nil
}
agentIDs := make([]uuid.UUID, 0)
for _, agent := range agents {
agentIDs = append(agentIDs, agent.ID)
}
apps, err := api.Database.GetWorkspaceAppsByAgentIDs(ctx, agentIDs)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceBuildsData{}, xerrors.Errorf("fetching workspace apps: %w", err)
}
return workspaceBuildsData{
users: users,
jobs: jobs,
resources: resources,
metadata: metadata,
agents: agents,
apps: apps,
}, nil
}
func (api *API) convertWorkspaceBuilds(
workspaceBuilds []database.WorkspaceBuild,
workspaces []database.Workspace,
jobs []database.ProvisionerJob,
users []database.User,
workspaceResources []database.WorkspaceResource,
resourceMetadata []database.WorkspaceResourceMetadatum,
resourceAgents []database.WorkspaceAgent,
agentApps []database.WorkspaceApp,
) ([]codersdk.WorkspaceBuild, error) {
workspaceByID := map[uuid.UUID]database.Workspace{}
for _, workspace := range workspaces {
workspaceByID[workspace.ID] = workspace
}
jobByID := map[uuid.UUID]database.ProvisionerJob{}
for _, job := range jobs {
jobByID[job.ID] = job
}
var apiBuilds []codersdk.WorkspaceBuild
for _, build := range workspaceBuilds {
job, exists := jobByID[build.JobID]
if !exists {
return nil, xerrors.New("build job not found")
}
workspace, exists := workspaceByID[build.WorkspaceID]
if !exists {
return nil, xerrors.New("workspace not found")
}
apiBuild, err := api.convertWorkspaceBuild(
build,
workspace,
job,
users,
workspaceResources,
resourceMetadata,
resourceAgents,
agentApps,
)
if err != nil {
return nil, xerrors.Errorf("converting workspace build: %w", err)
}
apiBuilds = append(apiBuilds, apiBuild)
}
return apiBuilds, nil
}
func (api *API) convertWorkspaceBuild(
build database.WorkspaceBuild,
workspace database.Workspace,
workspaceBuild database.WorkspaceBuild,
job database.ProvisionerJob,
) codersdk.WorkspaceBuild {
//nolint:unconvert
if workspace.ID != workspaceBuild.WorkspaceID {
panic("workspace and build do not match")
users []database.User,
workspaceResources []database.WorkspaceResource,
resourceMetadata []database.WorkspaceResourceMetadatum,
resourceAgents []database.WorkspaceAgent,
agentApps []database.WorkspaceApp,
) (codersdk.WorkspaceBuild, error) {
userByID := map[uuid.UUID]database.User{}
for _, user := range users {
userByID[user.ID] = user
}
resourcesByJobID := map[uuid.UUID][]database.WorkspaceResource{}
for _, resource := range workspaceResources {
resourcesByJobID[resource.JobID] = append(resourcesByJobID[resource.JobID], resource)
}
metadataByResourceID := map[uuid.UUID][]database.WorkspaceResourceMetadatum{}
for _, metadata := range resourceMetadata {
metadataByResourceID[metadata.WorkspaceResourceID] = append(metadataByResourceID[metadata.WorkspaceResourceID], metadata)
}
agentsByResourceID := map[uuid.UUID][]database.WorkspaceAgent{}
for _, agent := range resourceAgents {
agentsByResourceID[agent.ResourceID] = append(agentsByResourceID[agent.ResourceID], agent)
}
appsByAgentID := map[uuid.UUID][]database.WorkspaceApp{}
for _, app := range agentApps {
appsByAgentID[app.AgentID] = append(appsByAgentID[app.AgentID], app)
}
// Both owner and initiator should always be present. But from a static
// code analysis POV, these could be nil.
ownerName := "unknown"
if workspaceOwner != nil {
ownerName = workspaceOwner.Username
owner, exists := userByID[workspace.OwnerID]
if !exists {
return codersdk.WorkspaceBuild{}, xerrors.Errorf("owner not found for workspace: %q", workspace.Name)
}
initiator, exists := userByID[build.InitiatorID]
if !exists {
return codersdk.WorkspaceBuild{}, xerrors.Errorf("build initiator not found for workspace: %q", workspace.Name)
}
initiatorName := "unknown"
if workspaceOwner != nil {
initiatorName = buildInitiator.Username
resources := resourcesByJobID[job.ID]
apiResources := make([]codersdk.WorkspaceResource, 0)
for _, resource := range resources {
agents := agentsByResourceID[resource.ID]
apiAgents := make([]codersdk.WorkspaceAgent, 0)
for _, agent := range agents {
apps := appsByAgentID[agent.ID]
apiAgent, err := convertWorkspaceAgent(api.DERPMap, api.TailnetCoordinator, agent, convertApps(apps), api.AgentInactiveDisconnectTimeout)
if err != nil {
return codersdk.WorkspaceBuild{}, xerrors.Errorf("converting workspace agent: %w", err)
}
apiAgents = append(apiAgents, apiAgent)
}
metadata := append(make([]database.WorkspaceResourceMetadatum, 0), metadataByResourceID[resource.ID]...)
apiResources = append(apiResources, convertWorkspaceResource(resource, apiAgents, metadata))
}
return codersdk.WorkspaceBuild{
ID: workspaceBuild.ID,
CreatedAt: workspaceBuild.CreatedAt,
UpdatedAt: workspaceBuild.UpdatedAt,
ID: build.ID,
CreatedAt: build.CreatedAt,
UpdatedAt: build.UpdatedAt,
WorkspaceOwnerID: workspace.OwnerID,
WorkspaceOwnerName: ownerName,
WorkspaceID: workspaceBuild.WorkspaceID,
WorkspaceOwnerName: owner.Username,
WorkspaceID: build.WorkspaceID,
WorkspaceName: workspace.Name,
TemplateVersionID: workspaceBuild.TemplateVersionID,
BuildNumber: workspaceBuild.BuildNumber,
Transition: codersdk.WorkspaceTransition(workspaceBuild.Transition),
InitiatorID: workspaceBuild.InitiatorID,
InitiatorUsername: initiatorName,
TemplateVersionID: build.TemplateVersionID,
BuildNumber: build.BuildNumber,
Transition: codersdk.WorkspaceTransition(build.Transition),
InitiatorID: build.InitiatorID,
InitiatorUsername: initiator.Username,
Job: convertProvisionerJob(job),
Deadline: codersdk.NewNullTime(workspaceBuild.Deadline, !workspaceBuild.Deadline.IsZero()),
Reason: codersdk.BuildReason(workspaceBuild.Reason),
}
Deadline: codersdk.NewNullTime(build.Deadline, !build.Deadline.IsZero()),
Reason: codersdk.BuildReason(build.Reason),
Resources: apiResources,
}, nil
}
func convertWorkspaceResource(resource database.WorkspaceResource, agents []codersdk.WorkspaceAgent, metadata []database.WorkspaceResourceMetadatum) codersdk.WorkspaceResource {

View File

@ -15,10 +15,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"cdr.dev/slog"
@ -75,45 +72,21 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
return
}
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
data, err := api.workspaceData(r.Context(), []database.Workspace{workspace})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace build.",
Detail: err.Error(),
})
return
}
var (
group errgroup.Group
job database.ProvisionerJob
template database.Template
users []database.User
)
group.Go(func() (err error) {
job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
return err
})
group.Go(func() (err error) {
template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
return err
})
group.Go(func() (err error) {
users, err = api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{
IDs: []uuid.UUID{workspace.OwnerID, build.InitiatorID},
})
return err
})
err = group.Wait()
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching resource.",
Message: "Internal error fetching workspace resources.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, build, job, template,
findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)))
httpapi.Write(rw, http.StatusOK, convertWorkspace(
workspace,
data.builds[0],
data.templates[0],
findUser(workspace.OwnerID, data.users),
))
}
// workspaces returns all workspaces a user can read.
@ -155,15 +128,25 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
return
}
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces)
data, err := api.workspaceData(r.Context(), workspaces)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error reading workspace.",
Message: "Internal error fetching workspace resources.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
wss, err := convertWorkspaces(workspaces, data)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspaces.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, wss)
}
func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) {
@ -212,41 +195,21 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
return
}
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
data, err := api.workspaceData(r.Context(), []database.Workspace{workspace})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace build.",
Detail: err.Error(),
})
return
}
job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner job.",
Detail: err.Error(),
})
return
}
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template.",
Message: "Internal error fetching workspace resources.",
Detail: err.Error(),
})
return
}
initiator, err := api.Database.GetUserByID(r.Context(), build.InitiatorID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching template.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, build, job, template, &owner, &initiator))
httpapi.Write(rw, http.StatusOK, convertWorkspace(
workspace,
data.builds[0],
data.templates[0],
findUser(workspace.OwnerID, data.users),
))
}
// Create a new workspace for the currently authenticated user.
@ -488,8 +451,30 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
WorkspaceBuilds: []telemetry.WorkspaceBuild{telemetry.ConvertWorkspaceBuild(workspaceBuild)},
})
httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, workspaceBuild, templateVersionJob, template,
findUser(apiKey.UserID, users), findUser(workspaceBuild.InitiatorID, users)))
apiBuild, err := api.convertWorkspaceBuild(
workspaceBuild,
workspace,
provisionerJob,
users,
[]database.WorkspaceResource{},
[]database.WorkspaceResourceMetadatum{},
[]database.WorkspaceAgent{},
[]database.WorkspaceApp{},
)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error converting workspace build.",
Detail: err.Error(),
})
return
}
httpapi.Write(rw, http.StatusCreated, convertWorkspace(
workspace,
apiBuild,
template,
findUser(apiKey.UserID, users),
))
}
func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) {
@ -790,174 +775,125 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
return
}
c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
// Fix for Safari 15.1:
// There is a bug in latest Safari in which compressed web socket traffic
// isn't handled correctly. Turning off compression is a workaround:
// https://github.com/nhooyr/websocket/issues/218
CompressionMode: websocket.CompressionDisabled,
})
sendEvent, err := httpapi.ServerSentEventSender(rw, r)
if err != nil {
api.Logger.Warn(r.Context(), "accept websocket connection", slog.Error(err))
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error setting up server-sent events.",
Detail: err.Error(),
})
return
}
defer c.Close(websocket.StatusInternalError, "internal error")
// Makes the websocket connection write-only
ctx := c.CloseRead(r.Context())
// Send a heartbeat every 15 seconds to avoid the websocket being killed.
go func() {
ticker := time.NewTicker(time.Second * 15)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
err := c.Ping(ctx)
if err != nil {
return
}
}
}
}()
t := time.NewTicker(time.Second * 1)
defer t.Stop()
for {
select {
case <-r.Context().Done():
return
case <-t.C:
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID)
if err != nil {
_ = wsjson.Write(ctx, c, codersdk.Response{
Message: "Internal error fetching workspace.",
Detail: err.Error(),
})
return
}
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
if err != nil {
_ = wsjson.Write(ctx, c, codersdk.Response{
Message: "Internal error fetching workspace build.",
Detail: err.Error(),
})
return
}
var (
group errgroup.Group
job database.ProvisionerJob
template database.Template
users []database.User
)
group.Go(func() (err error) {
job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
return err
})
group.Go(func() (err error) {
template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
return err
})
group.Go(func() (err error) {
users, err = api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{
IDs: []uuid.UUID{workspace.OwnerID, build.InitiatorID},
})
return err
})
err = group.Wait()
if err != nil {
_ = wsjson.Write(ctx, c, codersdk.Response{
Message: "Internal error fetching resource.",
Detail: err.Error(),
_ = sendEvent(r.Context(), codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Internal error fetching workspace.",
Detail: err.Error(),
},
})
return
}
_ = wsjson.Write(ctx, c, convertWorkspace(workspace, build, job, template,
findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)))
case <-ctx.Done():
return
data, err := api.workspaceData(r.Context(), []database.Workspace{workspace})
if err != nil {
_ = sendEvent(r.Context(), codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeError,
Data: codersdk.Response{
Message: "Internal error fetching workspace data.",
Detail: err.Error(),
},
})
return
}
_ = sendEvent(r.Context(), codersdk.ServerSentEvent{
Type: codersdk.ServerSentEventTypeData,
Data: convertWorkspace(
workspace,
data.builds[0],
data.templates[0],
findUser(workspace.OwnerID, data.users),
),
})
}
}
}
func convertWorkspaces(ctx context.Context, db database.Store, workspaces []database.Workspace) ([]codersdk.Workspace, error) {
type workspaceData struct {
templates []database.Template
builds []codersdk.WorkspaceBuild
users []database.User
}
func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspace) (workspaceData, error) {
workspaceIDs := make([]uuid.UUID, 0, len(workspaces))
templateIDs := make([]uuid.UUID, 0, len(workspaces))
userIDs := make([]uuid.UUID, 0, len(workspaces))
for _, workspace := range workspaces {
workspaceIDs = append(workspaceIDs, workspace.ID)
templateIDs = append(templateIDs, workspace.TemplateID)
userIDs = append(userIDs, workspace.OwnerID)
}
workspaceBuilds, err := db.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, workspaceIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
for _, build := range workspaceBuilds {
userIDs = append(userIDs, build.InitiatorID)
}
if err != nil {
return nil, xerrors.Errorf("get workspace builds: %w", err)
}
templates, err := db.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
templates, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
IDs: templateIDs,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
return nil, xerrors.Errorf("get templates: %w", err)
}
users, err := db.GetUsersByIDs(ctx, database.GetUsersByIDsParams{
IDs: userIDs,
})
if err != nil {
return nil, xerrors.Errorf("get users: %w", err)
}
jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
for _, build := range workspaceBuilds {
jobIDs = append(jobIDs, build.JobID)
}
jobs, err := db.GetProvisionerJobsByIDs(ctx, jobIDs)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
return nil, xerrors.Errorf("get provisioner jobs: %w", err)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceData{}, xerrors.Errorf("get templates: %w", err)
}
buildByWorkspaceID := map[uuid.UUID]database.WorkspaceBuild{}
for _, workspaceBuild := range workspaceBuilds {
buildByWorkspaceID[workspaceBuild.WorkspaceID] = database.WorkspaceBuild{
ID: workspaceBuild.ID,
CreatedAt: workspaceBuild.CreatedAt,
UpdatedAt: workspaceBuild.UpdatedAt,
WorkspaceID: workspaceBuild.WorkspaceID,
TemplateVersionID: workspaceBuild.TemplateVersionID,
BuildNumber: workspaceBuild.BuildNumber,
Transition: workspaceBuild.Transition,
InitiatorID: workspaceBuild.InitiatorID,
ProvisionerState: workspaceBuild.ProvisionerState,
JobID: workspaceBuild.JobID,
Deadline: workspaceBuild.Deadline,
Reason: workspaceBuild.Reason,
}
builds, err := api.Database.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, workspaceIDs)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return workspaceData{}, xerrors.Errorf("get workspace builds: %w", err)
}
data, err := api.workspaceBuildsData(ctx, workspaces, builds)
if err != nil {
return workspaceData{}, xerrors.Errorf("get workspace builds data: %w", err)
}
apiBuilds, err := api.convertWorkspaceBuilds(
builds,
workspaces,
data.jobs,
data.users,
data.resources,
data.metadata,
data.agents,
data.apps,
)
if err != nil {
return workspaceData{}, xerrors.Errorf("convert workspace builds: %w", err)
}
return workspaceData{
templates: templates,
builds: apiBuilds,
users: data.users,
}, nil
}
func convertWorkspaces(workspaces []database.Workspace, data workspaceData) ([]codersdk.Workspace, error) {
buildByWorkspaceID := map[uuid.UUID]codersdk.WorkspaceBuild{}
for _, workspaceBuild := range data.builds {
buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild
}
templateByID := map[uuid.UUID]database.Template{}
for _, template := range templates {
for _, template := range data.templates {
templateByID[template.ID] = template
}
userByID := map[uuid.UUID]database.User{}
for _, user := range users {
for _, user := range data.users {
userByID[user.ID] = user
}
jobByID := map[uuid.UUID]database.ProvisionerJob{}
for _, job := range jobs {
jobByID[job.ID] = job
}
apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
build, exists := buildByWorkspaceID[workspace.ID]
@ -968,19 +904,17 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data
if !exists {
return nil, xerrors.Errorf("template not found for workspace %q", workspace.Name)
}
job, exists := jobByID[build.JobID]
if !exists {
return nil, xerrors.Errorf("build job not found for workspace: %w", err)
}
owner, exists := userByID[workspace.OwnerID]
if !exists {
return nil, xerrors.Errorf("owner not found for workspace: %q", workspace.Name)
}
initiator, exists := userByID[build.InitiatorID]
if !exists {
return nil, xerrors.Errorf("build initiator not found for workspace: %q", workspace.Name)
}
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace, build, job, template, &owner, &initiator))
apiWorkspaces = append(apiWorkspaces, convertWorkspace(
workspace,
build,
template,
&owner,
))
}
sort.Slice(apiWorkspaces, func(i, j int) bool {
iw := apiWorkspaces[i]
@ -996,11 +930,9 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data
func convertWorkspace(
workspace database.Workspace,
workspaceBuild database.WorkspaceBuild,
job database.ProvisionerJob,
workspaceBuild codersdk.WorkspaceBuild,
template database.Template,
owner *database.User,
initiator *database.User,
) codersdk.Workspace {
var autostartSchedule *string
if workspace.AutostartSchedule.Valid {
@ -1015,7 +947,7 @@ func convertWorkspace(
OwnerID: workspace.OwnerID,
OwnerName: owner.Username,
TemplateID: workspace.TemplateID,
LatestBuild: convertWorkspaceBuild(owner, initiator, workspace, workspaceBuild, job),
LatestBuild: workspaceBuild,
TemplateName: template.Name,
TemplateIcon: template.Icon,
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),