From 9c5c0473b2f7578c4a1e49f7ab7da37f86e87876 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 25 Aug 2022 16:22:33 +0000 Subject: [PATCH 01/34] fix: match term color --- site/src/pages/TerminalPage/TerminalPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index abd9a153675b2..90a47a7672525 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -12,6 +12,7 @@ import "xterm/css/xterm.css" import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" import { pageTitle } from "../../util/page" import { terminalMachine } from "../../xServices/terminal/terminalXService" +import { colors } from "theme/colors" export const Language = { workspaceErrorMessagePrefix: "Unable to fetch workspace: ", From b3a171bb34503ee2b8fd8afdedda8a84e063863c Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 25 Aug 2022 16:25:49 +0000 Subject: [PATCH 02/34] fmt: --- site/src/pages/TerminalPage/TerminalPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index 90a47a7672525..abd9a153675b2 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -12,7 +12,6 @@ import "xterm/css/xterm.css" import { MONOSPACE_FONT_FAMILY } from "../../theme/constants" import { pageTitle } from "../../util/page" import { terminalMachine } from "../../xServices/terminal/terminalXService" -import { colors } from "theme/colors" export const Language = { workspaceErrorMessagePrefix: "Unable to fetch workspace: ", From 4e4a3651b44fb598d780474a067ea45ab4d0d34d Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 25 Aug 2022 21:21:09 +0000 Subject: [PATCH 03/34] Add resources to workspace watch endpoint --- coderd/workspaces.go | 83 ++++++++++++++++++++++++++++++---- codersdk/workspacebuilds.go | 3 +- site/src/api/typesGenerated.ts | 3 +- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 81b3038a742c5..bfc9e2eefca8d 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -846,14 +846,81 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } var ( - group errgroup.Group - job database.ProvisionerJob - template database.Template - users []database.User + group errgroup.Group + job database.ProvisionerJob + template database.Template + users []database.User + apiResources []codersdk.WorkspaceResource ) group.Go(func() (err error) { job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID) - return err + if err != nil { + return err + } + + if !job.CompletedAt.Valid { + return nil + } + + resources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), job.ID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + + resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + resourceAgentIDs := make([]uuid.UUID, 0) + for _, agent := range resourceAgents { + resourceAgentIDs = append(resourceAgentIDs, agent.ID) + } + + apps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) + if err != nil { + return err + } + + for _, resource := range resources { + agents := make([]codersdk.WorkspaceAgent, 0) + for _, agent := range resourceAgents { + if agent.ResourceID != resource.ID { + continue + } + dbApps := make([]database.WorkspaceApp, 0) + for _, app := range apps { + if app.AgentID == agent.ID { + dbApps = append(dbApps, app) + } + } + + apiAgent, err := convertWorkspaceAgent(agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout) + if err != nil { + return err + } + agents = append(agents, apiAgent) + } + metadata := make([]database.WorkspaceResourceMetadatum, 0) + for _, field := range resourceMetadata { + if field.WorkspaceResourceID == resource.ID { + metadata = append(metadata, field) + } + } + apiResources = append(apiResources, convertWorkspaceResource(resource, agents, metadata)) + } + + return nil }) group.Go(func() (err error) { template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) @@ -873,9 +940,9 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) return } - - _ = wsjson.Write(ctx, c, convertWorkspace(workspace, build, job, template, - findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users))) + apiWorkspace := convertWorkspace(workspace, build, job, template, findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)) + apiWorkspace.LatestBuild.Resources = apiResources + _ = wsjson.Write(ctx, c, apiWorkspace) case <-ctx.Done(): return } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 8020926834c9d..6ffa3fc117817 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -49,8 +49,9 @@ type WorkspaceBuild struct { InitiatorID uuid.UUID `json:"initiator_id"` InitiatorUsername string `json:"initiator_name"` Job ProvisionerJob `json:"job"` - Deadline NullTime `json:"deadline,omitempty"` Reason BuildReason `db:"reason" json:"reason"` + Deadline NullTime `json:"deadline,omitempty"` + Resources []WorkspaceResource `json:"resources,omitempty"` } // WorkspaceBuild returns a single workspace build for a workspace. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c5a90816563a4..ece922c6753b6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -609,8 +609,9 @@ export interface WorkspaceBuild { readonly initiator_id: string readonly initiator_name: string readonly job: ProvisionerJob - readonly deadline?: string readonly reason: BuildReason + readonly deadline?: string + readonly resources?: WorkspaceResource[] } // From codersdk/workspaces.go From fae7dd2de316bf6bf6529febb14d6bf4cda2e974 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 25 Aug 2022 21:28:12 +0000 Subject: [PATCH 04/34] better wrapped errors --- coderd/workspaces.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index bfc9e2eefca8d..494abc6df0d9f 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -855,7 +855,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { group.Go(func() (err error) { job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID) if err != nil { - return err + return xerrors.Errorf("fetching workspace build job: %w", err) } if !job.CompletedAt.Valid { @@ -864,7 +864,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { resources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), job.ID) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err + return xerrors.Errorf("fetching workspace resources by job: %w", err) } resourceIDs := make([]uuid.UUID, 0) @@ -874,7 +874,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err + return xerrors.Errorf("fetching workspace agents: %w", err) } resourceAgentIDs := make([]uuid.UUID, 0) @@ -884,7 +884,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { apps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return err + return xerrors.Errorf("fetching workspace apps: %w", err) } resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) @@ -907,7 +907,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { apiAgent, err := convertWorkspaceAgent(agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout) if err != nil { - return err + return xerrors.Errorf("converting workspace agent: %w", err) } agents = append(agents, apiAgent) } @@ -924,13 +924,21 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) group.Go(func() (err error) { template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) - return err + if err != nil { + return xerrors.Errorf("fetching template: %w", err) + } + + return nil }) group.Go(func() (err error) { users, err = api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{ IDs: []uuid.UUID{workspace.OwnerID, build.InitiatorID}, }) - return err + if err != nil { + return xerrors.Errorf("fetching users: %w", err) + } + + return nil }) err = group.Wait() if err != nil { From 8e5cda7a12a3a22039844918564eee7acb93c9bc Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 25 Aug 2022 22:10:26 +0000 Subject: [PATCH 05/34] support sse --- coderd/httpapi/httpapi.go | 34 ++++++++++++++++++++++++++++ coderd/workspaces.go | 47 ++++++++++++++++++++------------------- 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 5393a79bfc06c..818de62ac8c9f 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/go-playground/validator/v10" + "golang.org/x/xerrors" "github.com/coder/coder/codersdk" ) @@ -144,3 +145,36 @@ func WebsocketCloseSprintf(format string, vars ...any) string { return msg } + +func Event(rw http.ResponseWriter, event interface{}) error { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(true) + err := enc.Encode(struct { + Data interface{} `json:"data"` + }{Data: event}) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return err + } + + _, err = buf.Write([]byte{'\n'}) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return err + } + + _, err = rw.Write(buf.Bytes()) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return err + } + + f, ok := rw.(http.Flusher) + if !ok { + return xerrors.New("http.ResponseWriter is not http.Flusher") + } + f.Flush() + + return nil +} diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 494abc6df0d9f..75651fe8b52ae 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "net/url" "sort" @@ -17,8 +18,6 @@ import ( "github.com/google/uuid" "golang.org/x/sync/errgroup" "golang.org/x/xerrors" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" "cdr.dev/slog" @@ -790,36 +789,38 @@ 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, - }) - if err != nil { - api.Logger.Warn(r.Context(), "accept websocket connection", slog.Error(err)) + 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 { return } - defer c.Close(websocket.StatusInternalError, "internal error") - // Makes the websocket connection write-only - ctx := c.CloseRead(r.Context()) + _, err := io.WriteString(rw, ": ping\n\n") + if err != nil { + return + } + f.Flush() - // Send a heartbeat every 15 seconds to avoid the websocket being killed. + // 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 <-ctx.Done(): + case <-r.Context().Done(): return case <-ticker.C: - err := c.Ping(ctx) + _, err := io.WriteString(rw, ": ping\n\n") if err != nil { return } + f.Flush() } } }() @@ -831,7 +832,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { case <-t.C: workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID) if err != nil { - _ = wsjson.Write(ctx, c, codersdk.Response{ + _ = httpapi.Event(rw, codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), }) @@ -839,8 +840,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { - _ = wsjson.Write(ctx, c, codersdk.Response{ - Message: "Internal error fetching workspace build.", + _ = httpapi.Event(rw, codersdk.Response{ + Message: "Internal error fetching workspace.", Detail: err.Error(), }) return @@ -942,7 +943,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) err = group.Wait() if err != nil { - _ = wsjson.Write(ctx, c, codersdk.Response{ + _ = httpapi.Event(rw, codersdk.Response{ Message: "Internal error fetching resource.", Detail: err.Error(), }) @@ -950,8 +951,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } apiWorkspace := convertWorkspace(workspace, build, job, template, findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)) apiWorkspace.LatestBuild.Resources = apiResources - _ = wsjson.Write(ctx, c, apiWorkspace) - case <-ctx.Done(): + _ = httpapi.Event(rw, apiWorkspace) + case <-r.Context().Done(): return } } From e82e20190260c0fce88e3d7af8528997a8e18ec9 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 25 Aug 2022 22:16:20 +0000 Subject: [PATCH 06/34] correct schema --- coderd/httpapi/httpapi.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 818de62ac8c9f..74e867b7d9288 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -150,9 +150,14 @@ func Event(rw http.ResponseWriter, event interface{}) error { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(true) - err := enc.Encode(struct { - Data interface{} `json:"data"` - }{Data: event}) + + _, err := buf.Write([]byte("data: ")) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return err + } + + err = enc.Encode(event) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return err From 8635e4a83d0442a49b356f42f5fd59e1fe525d33 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 25 Aug 2022 22:21:53 +0000 Subject: [PATCH 07/34] share SetupSSE func --- coderd/httpapi/httpapi.go | 42 +++++++++++++++++++++++++++++++++++++++ coderd/workspaces.go | 38 +++++------------------------------ 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 74e867b7d9288..b1a8d631aef29 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -5,9 +5,11 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "reflect" "strings" + "time" "github.com/go-playground/validator/v10" "golang.org/x/xerrors" @@ -146,6 +148,46 @@ func WebsocketCloseSprintf(format string, vars ...any) string { return msg } +func SetupSSE(rw http.ResponseWriter, r *http.Request) error { + 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 { + return xerrors.New("http.ResponseWriter is not http.Flusher") + } + + _, err := io.WriteString(rw, ": ping\n\n") + if err != nil { + return err + } + f.Flush() + + // 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: + _, err := io.WriteString(rw, ": ping\n\n") + if err != nil { + return + } + f.Flush() + } + } + }() + + return nil +} + func Event(rw http.ResponseWriter, event interface{}) error { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 75651fe8b52ae..fbcad27e5709c 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "net/http" "net/url" "sort" @@ -789,41 +788,14 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } - 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 { - return - } - - _, err := io.WriteString(rw, ": ping\n\n") + err := httpapi.SetupSSE(rw, r) if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error setting up server-side events.", + Detail: err.Error(), + }) return } - f.Flush() - - // 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: - _, err := io.WriteString(rw, ": ping\n\n") - if err != nil { - return - } - f.Flush() - } - } - }() t := time.NewTicker(time.Second * 1) defer t.Stop() From 784b361a932194b9444900cd4862fb59e1cae486 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 17:37:33 +0000 Subject: [PATCH 08/34] fix events --- coderd/httpapi/httpapi.go | 84 ++++++++++++++++++++++----------------- coderd/workspaces.go | 16 ++++---- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index b1a8d631aef29..09d9568f08784 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -2,6 +2,7 @@ package httpapi import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -9,6 +10,7 @@ import ( "net/http" "reflect" "strings" + "sync" "time" "github.com/go-playground/validator/v10" @@ -148,7 +150,16 @@ func WebsocketCloseSprintf(format string, vars ...any) string { return msg } -func SetupSSE(rw http.ResponseWriter, r *http.Request) error { +type EventType string + +const ( + EventTypePing EventType = "ping" + EventTypeData EventType = "data" + EventTypeError EventType = "error" +) + +func SetupSSE(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context, t EventType, event interface{}) error, error) { + var mu sync.Mutex h := rw.Header() h.Set("Content-Type", "text/event-stream") h.Set("Cache-Control", "no-cache") @@ -157,12 +168,13 @@ func SetupSSE(rw http.ResponseWriter, r *http.Request) error { f, ok := rw.(http.Flusher) if !ok { - return xerrors.New("http.ResponseWriter is not http.Flusher") + return nil, xerrors.New("http.ResponseWriter is not http.Flusher") } - _, err := io.WriteString(rw, ": ping\n\n") + pingMsg := fmt.Sprintf("event: %s\n\n", EventTypePing) + _, err := io.WriteString(rw, pingMsg) if err != nil { - return err + return nil, err } f.Flush() @@ -176,52 +188,52 @@ func SetupSSE(rw http.ResponseWriter, r *http.Request) error { case <-r.Context().Done(): return case <-ticker.C: - _, err := io.WriteString(rw, ": ping\n\n") + mu.Lock() + _, err := io.WriteString(rw, pingMsg) if err != nil { + mu.Unlock() return } f.Flush() + mu.Unlock() } } }() - return nil -} + sendEvent := func(ctx context.Context, t EventType, event interface{}) error { + if r.Context().Err() != nil { + return err + } -func Event(rw http.ResponseWriter, event interface{}) error { - buf := &bytes.Buffer{} - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(true) + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(true) - _, err := buf.Write([]byte("data: ")) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return err - } + _, err := buf.Write([]byte(fmt.Sprintf("event: %s\ndata: ", t))) + if err != nil { + return err + } - err = enc.Encode(event) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return err - } + err = enc.Encode(event) + if err != nil { + return err + } - _, err = buf.Write([]byte{'\n'}) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return err - } + err = buf.WriteByte('\n') + if err != nil { + return err + } - _, err = rw.Write(buf.Bytes()) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return err - } + mu.Lock() + defer mu.Unlock() + _, err = rw.Write(buf.Bytes()) + if err != nil { + return err + } + f.Flush() - f, ok := rw.(http.Flusher) - if !ok { - return xerrors.New("http.ResponseWriter is not http.Flusher") + return nil } - f.Flush() - return nil + return sendEvent, nil } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index fbcad27e5709c..87c0901e8c8b1 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -788,7 +788,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } - err := httpapi.SetupSSE(rw, r) + sendEvent, err := httpapi.SetupSSE(rw, r) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error setting up server-side events.", @@ -801,10 +801,12 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { 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 { - _ = httpapi.Event(rw, codersdk.Response{ + _ = sendEvent(r.Context(), httpapi.EventTypeError, codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), }) @@ -812,7 +814,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { - _ = httpapi.Event(rw, codersdk.Response{ + _ = sendEvent(r.Context(), httpapi.EventTypeError, codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), }) @@ -878,7 +880,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } } - apiAgent, err := convertWorkspaceAgent(agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout) + apiAgent, err := convertWorkspaceAgent(api.DERPMap, api.TailnetCoordinator, agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout) if err != nil { return xerrors.Errorf("converting workspace agent: %w", err) } @@ -915,7 +917,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) err = group.Wait() if err != nil { - _ = httpapi.Event(rw, codersdk.Response{ + _ = sendEvent(r.Context(), httpapi.EventTypeError, codersdk.Response{ Message: "Internal error fetching resource.", Detail: err.Error(), }) @@ -923,9 +925,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } apiWorkspace := convertWorkspace(workspace, build, job, template, findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)) apiWorkspace.LatestBuild.Resources = apiResources - _ = httpapi.Event(rw, apiWorkspace) - case <-r.Context().Done(): - return + _ = sendEvent(r.Context(), httpapi.EventTypeData, apiWorkspace) } } } From f32cdddb5479b3a65ff4e6c25f8333ccafe528c4 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 17:51:05 +0000 Subject: [PATCH 09/34] better err --- coderd/workspaces.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 87c0901e8c8b1..52d8e4e50971c 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -864,7 +864,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) if err != nil { - return err + return xerrors.Errorf("fetching resource metadata: %w", err) } for _, resource := range resources { From 800e0a8a5be44c00357f491e6760e96c35ef50c5 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 18:52:29 +0000 Subject: [PATCH 10/34] Fix casting --- coderd/httpapi/httpapi.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 09d9568f08784..fdd9741338172 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -166,7 +166,12 @@ func SetupSSE(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context h.Set("Connection", "keep-alive") h.Set("X-Accel-Buffering", "no") - f, ok := rw.(http.Flusher) + sw, ok := rw.(*StatusWriter) + if !ok { + return nil, xerrors.New("http.ResponseWriter is not StatusWriter") + } + + f, ok := sw.ResponseWriter.(http.Flusher) if !ok { return nil, xerrors.New("http.ResponseWriter is not http.Flusher") } From efcbbab361b348f7ab193a51cbf010bd197ae073 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 21:19:01 +0000 Subject: [PATCH 11/34] fix test --- coderd/httpapi/httpapi.go | 18 +++------ coderd/workspaces.go | 34 +++++++++++----- codersdk/client.go | 1 + codersdk/sse.go | 84 +++++++++++++++++++++++++++++++++++++++ codersdk/workspaces.go | 30 +++++++++----- 5 files changed, 134 insertions(+), 33 deletions(-) create mode 100644 codersdk/sse.go diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index fdd9741338172..a16fe026e6fad 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -150,15 +150,7 @@ func WebsocketCloseSprintf(format string, vars ...any) string { return msg } -type EventType string - -const ( - EventTypePing EventType = "ping" - EventTypeData EventType = "data" - EventTypeError EventType = "error" -) - -func SetupSSE(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context, t EventType, event interface{}) error, error) { +func ServerSideEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context, sse codersdk.ServerSideEvent) error, error) { var mu sync.Mutex h := rw.Header() h.Set("Content-Type", "text/event-stream") @@ -176,7 +168,7 @@ func SetupSSE(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context return nil, xerrors.New("http.ResponseWriter is not http.Flusher") } - pingMsg := fmt.Sprintf("event: %s\n\n", EventTypePing) + pingMsg := fmt.Sprintf("event: %s\n\n", codersdk.EventTypePing) _, err := io.WriteString(rw, pingMsg) if err != nil { return nil, err @@ -205,7 +197,7 @@ func SetupSSE(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context } }() - sendEvent := func(ctx context.Context, t EventType, event interface{}) error { + sendEvent := func(ctx context.Context, sse codersdk.ServerSideEvent) error { if r.Context().Err() != nil { return err } @@ -214,12 +206,12 @@ func SetupSSE(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context enc := json.NewEncoder(buf) enc.SetEscapeHTML(true) - _, err := buf.Write([]byte(fmt.Sprintf("event: %s\ndata: ", t))) + _, err := buf.Write([]byte(fmt.Sprintf("event: %s\ndata: ", sse.Type))) if err != nil { return err } - err = enc.Encode(event) + err = enc.Encode(sse.Data) if err != nil { return err } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 52d8e4e50971c..a765a2a447255 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -788,7 +788,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } - sendEvent, err := httpapi.SetupSSE(rw, r) + sendEvent, err := httpapi.ServerSideEventSender(rw, r) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error setting up server-side events.", @@ -806,17 +806,23 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { case <-t.C: workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID) if err != nil { - _ = sendEvent(r.Context(), httpapi.EventTypeError, codersdk.Response{ - Message: "Internal error fetching workspace.", - Detail: err.Error(), + _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ + Type: codersdk.EventTypeError, + Data: codersdk.Response{ + Message: "Internal error fetching workspace.", + Detail: err.Error(), + }, }) return } build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { - _ = sendEvent(r.Context(), httpapi.EventTypeError, codersdk.Response{ - Message: "Internal error fetching workspace.", - Detail: err.Error(), + _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ + Type: codersdk.EventTypeError, + Data: codersdk.Response{ + Message: "Internal error fetching workspace.", + Detail: err.Error(), + }, }) return } @@ -917,15 +923,21 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) err = group.Wait() if err != nil { - _ = sendEvent(r.Context(), httpapi.EventTypeError, codersdk.Response{ - Message: "Internal error fetching resource.", - Detail: err.Error(), + _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ + Type: codersdk.EventTypeError, + Data: codersdk.Response{ + Message: "Internal error fetching workspace.", + Detail: err.Error(), + }, }) return } apiWorkspace := convertWorkspace(workspace, build, job, template, findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)) apiWorkspace.LatestBuild.Resources = apiResources - _ = sendEvent(r.Context(), httpapi.EventTypeData, apiWorkspace) + _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ + Type: codersdk.EventTypeData, + Data: apiWorkspace, + }) } } } diff --git a/codersdk/client.go b/codersdk/client.go index 6fa34e91b3e06..c012f86deb90b 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -130,6 +130,7 @@ func (c *Client) dialWebsocket(ctx context.Context, path string) (*websocket.Con // readBodyAsError reads the response as an .Message, and // wraps it in a codersdk.Error type for easy marshaling. func readBodyAsError(res *http.Response) error { + defer res.Body.Close() contentType := res.Header.Get("Content-Type") var method, u string diff --git a/codersdk/sse.go b/codersdk/sse.go new file mode 100644 index 0000000000000..a1d5e3f4fa85c --- /dev/null +++ b/codersdk/sse.go @@ -0,0 +1,84 @@ +package codersdk + +import ( + "bufio" + "fmt" + "io" + "strings" + + "golang.org/x/xerrors" +) + +type ServerSideEvent struct { + Type EventType + Data interface{} +} + +type EventType string + +const ( + EventTypePing EventType = "ping" + EventTypeData EventType = "data" + EventTypeError EventType = "error" +) + +func ServerSideEventReader(rc io.ReadCloser) func() (*ServerSideEvent, error) { + reader := bufio.NewReader(rc) + nextLineValue := func(prefix string) ([]byte, error) { + var line string + var err error + for { + line, err = reader.ReadString('\n') + if err != nil { + return nil, xerrors.Errorf("reading next string: %w", err) + } + if strings.TrimSpace(line) != "" { + break + } + } + + if !strings.HasPrefix(line, fmt.Sprintf("%s: ", prefix)) { + return nil, xerrors.Errorf("expecting %s prefix, got: %s", prefix, line) + } + s := strings.TrimPrefix(line, fmt.Sprintf("%s: ", prefix)) + s = strings.TrimSpace(s) + return []byte(s), nil + } + return func() (*ServerSideEvent, error) { + for { + t, err := nextLineValue("event") + if err != nil { + return nil, xerrors.Errorf("reading next line value: %w", err) + } + + switch EventType(t) { + case EventTypePing: + return &ServerSideEvent{ + Type: EventTypePing, + }, nil + case EventTypeData: + d, err := nextLineValue("data") + if err != nil { + return nil, xerrors.Errorf("reading next line value: %w", err) + } + + return &ServerSideEvent{ + Type: EventTypeData, + Data: d, + }, nil + case EventTypeError: + d, err := nextLineValue("data") + if err != nil { + return nil, xerrors.Errorf("reading next line value: %w", err) + } + + return &ServerSideEvent{ + Type: EventTypeError, + Data: d, + }, nil + default: + return nil, xerrors.Errorf("unknown event type: %s", t) + } + } + } +} diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 53247b5b86ed1..d1527f35a5036 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -10,8 +10,6 @@ import ( "github.com/google/uuid" "golang.org/x/xerrors" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" ) // Workspace is a deployment of a template. It references a specific @@ -123,28 +121,42 @@ func (c *Client) CreateWorkspaceBuild(ctx context.Context, workspace uuid.UUID, } func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Workspace, error) { - conn, err := c.dialWebsocket(ctx, fmt.Sprintf("/api/v2/workspaces/%s/watch", id)) + //nolint:bodyclose + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/watch", id), nil) if err != nil { return nil, err } - wc := make(chan Workspace, 256) + if res.StatusCode != http.StatusOK { + return nil, readBodyAsError(res) + } + nextEvent := ServerSideEventReader(res.Body) + wc := make(chan Workspace, 256) go func() { defer close(wc) - defer conn.Close(websocket.StatusNormalClosure, "") + defer res.Body.Close() for { select { case <-ctx.Done(): return default: - var ws Workspace - err := wsjson.Read(ctx, conn, &ws) + sse, err := nextEvent() if err != nil { - conn.Close(websocket.StatusInternalError, "failed to read workspace") return } - wc <- ws + if sse.Type == EventTypeData { + var ws Workspace + b, ok := sse.Data.([]byte) + if !ok { + return + } + err = json.Unmarshal(b, &ws) + if err != nil { + return + } + wc <- ws + } } } }() From cf59581deae33fa9cccb5ffcd2b313916b43d0a0 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 21:23:02 +0000 Subject: [PATCH 12/34] remove dead code --- codersdk/client.go | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/codersdk/client.go b/codersdk/client.go index c012f86deb90b..fd788d1d9f089 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -12,7 +12,6 @@ import ( "strings" "golang.org/x/xerrors" - "nhooyr.io/websocket" ) // These cookies are Coder-specific. If a new one is added or changed, the name @@ -95,38 +94,6 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return resp, err } -// dialWebsocket opens a dialWebsocket connection on that path provided. -// The caller is responsible for closing the dialWebsocket.Conn. -func (c *Client) dialWebsocket(ctx context.Context, path string) (*websocket.Conn, error) { - serverURL, err := c.URL.Parse(path) - if err != nil { - return nil, xerrors.Errorf("parse path: %w", err) - } - - apiURL, err := url.Parse(serverURL.String()) - if err != nil { - return nil, xerrors.Errorf("parse server url: %w", err) - } - apiURL.Scheme = "ws" - if serverURL.Scheme == "https" { - apiURL.Scheme = "wss" - } - apiURL.Path = path - q := apiURL.Query() - q.Add(SessionTokenKey, c.SessionToken) - apiURL.RawQuery = q.Encode() - - //nolint:bodyclose - conn, _, err := websocket.Dial(ctx, apiURL.String(), &websocket.DialOptions{ - HTTPClient: c.HTTPClient, - }) - if err != nil { - return nil, xerrors.Errorf("dial websocket: %w", err) - } - - return conn, nil -} - // readBodyAsError reads the response as an .Message, and // wraps it in a codersdk.Error type for easy marshaling. func readBodyAsError(res *http.Response) error { From 35f58942e885d1b6a7829714431c6831d0cfbf66 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 21:28:15 +0000 Subject: [PATCH 13/34] panic on bad cast --- coderd/httpapi/httpapi.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index a16fe026e6fad..fa2b049d29d64 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -14,7 +14,6 @@ import ( "time" "github.com/go-playground/validator/v10" - "golang.org/x/xerrors" "github.com/coder/coder/codersdk" ) @@ -160,12 +159,12 @@ func ServerSideEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co sw, ok := rw.(*StatusWriter) if !ok { - return nil, xerrors.New("http.ResponseWriter is not StatusWriter") + panic("http.ResponseWriter is not StatusWriter") } f, ok := sw.ResponseWriter.(http.Flusher) if !ok { - return nil, xerrors.New("http.ResponseWriter is not http.Flusher") + panic("http.ResponseWriter is not http.Flusher") } pingMsg := fmt.Sprintf("event: %s\n\n", codersdk.EventTypePing) From 316f0ba15865d478e842b7c416374a38dd8d7141 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 21:32:34 +0000 Subject: [PATCH 14/34] fix flusher --- coderd/httpapi/httpapi.go | 7 +------ coderd/tracing/status_writer.go | 8 ++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index fa2b049d29d64..72d4325e1c3da 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -157,12 +157,7 @@ func ServerSideEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co h.Set("Connection", "keep-alive") h.Set("X-Accel-Buffering", "no") - sw, ok := rw.(*StatusWriter) - if !ok { - panic("http.ResponseWriter is not StatusWriter") - } - - f, ok := sw.ResponseWriter.(http.Flusher) + f, ok := rw.(http.Flusher) if !ok { panic("http.ResponseWriter is not http.Flusher") } diff --git a/coderd/tracing/status_writer.go b/coderd/tracing/status_writer.go index 3caa6b9cb6263..18a362176c69e 100644 --- a/coderd/tracing/status_writer.go +++ b/coderd/tracing/status_writer.go @@ -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() +} From 953bb620ce3c0ef4c39395612c05b63dfb4ce544 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 21:36:26 +0000 Subject: [PATCH 15/34] rename serversideeventtype --- coderd/httpapi/httpapi.go | 2 +- coderd/workspaces.go | 8 ++++---- codersdk/sse.go | 24 ++++++++++++------------ codersdk/workspaces.go | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 72d4325e1c3da..bc4d309f86807 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -162,7 +162,7 @@ func ServerSideEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co panic("http.ResponseWriter is not http.Flusher") } - pingMsg := fmt.Sprintf("event: %s\n\n", codersdk.EventTypePing) + pingMsg := fmt.Sprintf("event: %s\n\n", codersdk.ServerSideEventTypePing) _, err := io.WriteString(rw, pingMsg) if err != nil { return nil, err diff --git a/coderd/workspaces.go b/coderd/workspaces.go index a765a2a447255..990dc5b5f3b67 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -807,7 +807,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID) if err != nil { _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ - Type: codersdk.EventTypeError, + Type: codersdk.ServerSideEventTypeError, Data: codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), @@ -818,7 +818,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ - Type: codersdk.EventTypeError, + Type: codersdk.ServerSideEventTypeError, Data: codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), @@ -924,7 +924,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { err = group.Wait() if err != nil { _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ - Type: codersdk.EventTypeError, + Type: codersdk.ServerSideEventTypeError, Data: codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), @@ -935,7 +935,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { apiWorkspace := convertWorkspace(workspace, build, job, template, findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)) apiWorkspace.LatestBuild.Resources = apiResources _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ - Type: codersdk.EventTypeData, + Type: codersdk.ServerSideEventTypeData, Data: apiWorkspace, }) } diff --git a/codersdk/sse.go b/codersdk/sse.go index a1d5e3f4fa85c..9a4684a30f1a7 100644 --- a/codersdk/sse.go +++ b/codersdk/sse.go @@ -10,16 +10,16 @@ import ( ) type ServerSideEvent struct { - Type EventType + Type ServerSideEventType Data interface{} } -type EventType string +type ServerSideEventType string const ( - EventTypePing EventType = "ping" - EventTypeData EventType = "data" - EventTypeError EventType = "error" + ServerSideEventTypePing ServerSideEventType = "ping" + ServerSideEventTypeData ServerSideEventType = "data" + ServerSideEventTypeError ServerSideEventType = "error" ) func ServerSideEventReader(rc io.ReadCloser) func() (*ServerSideEvent, error) { @@ -51,29 +51,29 @@ func ServerSideEventReader(rc io.ReadCloser) func() (*ServerSideEvent, error) { return nil, xerrors.Errorf("reading next line value: %w", err) } - switch EventType(t) { - case EventTypePing: + switch ServerSideEventType(t) { + case ServerSideEventTypePing: return &ServerSideEvent{ - Type: EventTypePing, + Type: ServerSideEventTypePing, }, nil - case EventTypeData: + case ServerSideEventTypeData: d, err := nextLineValue("data") if err != nil { return nil, xerrors.Errorf("reading next line value: %w", err) } return &ServerSideEvent{ - Type: EventTypeData, + Type: ServerSideEventTypeData, Data: d, }, nil - case EventTypeError: + case ServerSideEventTypeError: d, err := nextLineValue("data") if err != nil { return nil, xerrors.Errorf("reading next line value: %w", err) } return &ServerSideEvent{ - Type: EventTypeError, + Type: ServerSideEventTypeError, Data: d, }, nil default: diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index d1527f35a5036..81a6e4b929404 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -145,7 +145,7 @@ func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Works if err != nil { return } - if sse.Type == EventTypeData { + if sse.Type == ServerSideEventTypeData { var ws Workspace b, ok := sse.Data.([]byte) if !ok { From e1f238c1f69e107bb2022609788cac4e09fc6d03 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 21:40:55 +0000 Subject: [PATCH 16/34] formatting --- codersdk/sse.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/codersdk/sse.go b/codersdk/sse.go index 9a4684a30f1a7..9b9a88e2a88cd 100644 --- a/codersdk/sse.go +++ b/codersdk/sse.go @@ -44,7 +44,8 @@ func ServerSideEventReader(rc io.ReadCloser) func() (*ServerSideEvent, error) { s = strings.TrimSpace(s) return []byte(s), nil } - return func() (*ServerSideEvent, error) { + + nextEvent := func() (*ServerSideEvent, error) { for { t, err := nextLineValue("event") if err != nil { @@ -81,4 +82,6 @@ func ServerSideEventReader(rc io.ReadCloser) func() (*ServerSideEvent, error) { } } } + + return nextEvent } From 0ebb9aa1846c7aad53238d3b2ef6ae1ade123e43 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 21:42:45 +0000 Subject: [PATCH 17/34] make gen --- site/src/api/typesGenerated.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ece922c6753b6..90c1214d6c03f 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -368,6 +368,13 @@ export interface Role { readonly display_name: string } +// From codersdk/sse.go +export interface ServerSideEvent { + readonly Type: ServerSideEventType + // eslint-disable-next-line + readonly Data: any +} + // From codersdk/templates.go export interface Template { readonly id: string @@ -705,6 +712,9 @@ export type ResourceType = | "user" | "workspace" +// From codersdk/sse.go +export type ServerSideEventType = "data" | "error" | "ping" + // From codersdk/users.go export type UserStatus = "active" | "suspended" From ec97a71c5ee3583e0a14f4e30fa81f63af3555ee Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 14 Sep 2022 21:45:13 +0000 Subject: [PATCH 18/34] fix var --- codersdk/sse.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/codersdk/sse.go b/codersdk/sse.go index 9b9a88e2a88cd..04081be947c0c 100644 --- a/codersdk/sse.go +++ b/codersdk/sse.go @@ -25,8 +25,10 @@ const ( func ServerSideEventReader(rc io.ReadCloser) func() (*ServerSideEvent, error) { reader := bufio.NewReader(rc) nextLineValue := func(prefix string) ([]byte, error) { - var line string - var err error + var ( + line string + err error + ) for { line, err = reader.ReadString('\n') if err != nil { From d7172089f89a5731c59d17eeca1ccee7d28a979b Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 15 Sep 2022 18:09:03 +0000 Subject: [PATCH 19/34] server-sent --- coderd/httpapi/httpapi.go | 16 ++++------------ coderd/workspaces.go | 20 ++++++++++---------- codersdk/sse.go | 36 ++++++++++++++++++------------------ codersdk/workspaces.go | 4 ++-- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index bc4d309f86807..9a30494330bc6 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -149,7 +149,7 @@ func WebsocketCloseSprintf(format string, vars ...any) string { return msg } -func ServerSideEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx context.Context, sse codersdk.ServerSideEvent) error, error) { +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") @@ -162,13 +162,6 @@ func ServerSideEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co panic("http.ResponseWriter is not http.Flusher") } - pingMsg := fmt.Sprintf("event: %s\n\n", codersdk.ServerSideEventTypePing) - _, err := io.WriteString(rw, pingMsg) - if err != nil { - return nil, err - } - f.Flush() - // Send a heartbeat every 15 seconds to avoid the connection being killed. go func() { ticker := time.NewTicker(time.Second * 15) @@ -180,7 +173,7 @@ func ServerSideEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co return case <-ticker.C: mu.Lock() - _, err := io.WriteString(rw, pingMsg) + _, err := io.WriteString(rw, fmt.Sprintf("event: %s\n\n", codersdk.ServerSentEventTypePing)) if err != nil { mu.Unlock() return @@ -191,14 +184,13 @@ func ServerSideEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co } }() - sendEvent := func(ctx context.Context, sse codersdk.ServerSideEvent) error { + sendEvent := func(ctx context.Context, sse codersdk.ServerSentEvent) error { if r.Context().Err() != nil { - return err + return r.Context().Err() } buf := &bytes.Buffer{} enc := json.NewEncoder(buf) - enc.SetEscapeHTML(true) _, err := buf.Write([]byte(fmt.Sprintf("event: %s\ndata: ", sse.Type))) if err != nil { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 990dc5b5f3b67..6015b5bc581a8 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -788,10 +788,10 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } - sendEvent, err := httpapi.ServerSideEventSender(rw, r) + sendEvent, err := httpapi.ServerSentEventSender(rw, r) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error setting up server-side events.", + Message: "Internal error setting up server-sent events.", Detail: err.Error(), }) return @@ -806,8 +806,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { case <-t.C: workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID) if err != nil { - _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ - Type: codersdk.ServerSideEventTypeError, + _ = sendEvent(r.Context(), codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeError, Data: codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), @@ -817,8 +817,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) if err != nil { - _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ - Type: codersdk.ServerSideEventTypeError, + _ = sendEvent(r.Context(), codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeError, Data: codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), @@ -923,8 +923,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) err = group.Wait() if err != nil { - _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ - Type: codersdk.ServerSideEventTypeError, + _ = sendEvent(r.Context(), codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeError, Data: codersdk.Response{ Message: "Internal error fetching workspace.", Detail: err.Error(), @@ -934,8 +934,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { } apiWorkspace := convertWorkspace(workspace, build, job, template, findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)) apiWorkspace.LatestBuild.Resources = apiResources - _ = sendEvent(r.Context(), codersdk.ServerSideEvent{ - Type: codersdk.ServerSideEventTypeData, + _ = sendEvent(r.Context(), codersdk.ServerSentEvent{ + Type: codersdk.ServerSentEventTypeData, Data: apiWorkspace, }) } diff --git a/codersdk/sse.go b/codersdk/sse.go index 04081be947c0c..45852f57ff81d 100644 --- a/codersdk/sse.go +++ b/codersdk/sse.go @@ -9,20 +9,20 @@ import ( "golang.org/x/xerrors" ) -type ServerSideEvent struct { - Type ServerSideEventType +type ServerSentEvent struct { + Type ServerSentEventType Data interface{} } -type ServerSideEventType string +type ServerSentEventType string const ( - ServerSideEventTypePing ServerSideEventType = "ping" - ServerSideEventTypeData ServerSideEventType = "data" - ServerSideEventTypeError ServerSideEventType = "error" + ServerSentEventTypePing ServerSentEventType = "ping" + ServerSentEventTypeData ServerSentEventType = "data" + ServerSentEventTypeError ServerSentEventType = "error" ) -func ServerSideEventReader(rc io.ReadCloser) func() (*ServerSideEvent, error) { +func ServerSentEventReader(rc io.ReadCloser) func() (*ServerSentEvent, error) { reader := bufio.NewReader(rc) nextLineValue := func(prefix string) ([]byte, error) { var ( @@ -47,36 +47,36 @@ func ServerSideEventReader(rc io.ReadCloser) func() (*ServerSideEvent, error) { return []byte(s), nil } - nextEvent := func() (*ServerSideEvent, error) { + nextEvent := func() (*ServerSentEvent, error) { for { t, err := nextLineValue("event") if err != nil { return nil, xerrors.Errorf("reading next line value: %w", err) } - switch ServerSideEventType(t) { - case ServerSideEventTypePing: - return &ServerSideEvent{ - Type: ServerSideEventTypePing, + switch ServerSentEventType(t) { + case ServerSentEventTypePing: + return &ServerSentEvent{ + Type: ServerSentEventTypePing, }, nil - case ServerSideEventTypeData: + case ServerSentEventTypeData: d, err := nextLineValue("data") if err != nil { return nil, xerrors.Errorf("reading next line value: %w", err) } - return &ServerSideEvent{ - Type: ServerSideEventTypeData, + return &ServerSentEvent{ + Type: ServerSentEventTypeData, Data: d, }, nil - case ServerSideEventTypeError: + case ServerSentEventTypeError: d, err := nextLineValue("data") if err != nil { return nil, xerrors.Errorf("reading next line value: %w", err) } - return &ServerSideEvent{ - Type: ServerSideEventTypeError, + return &ServerSentEvent{ + Type: ServerSentEventTypeError, Data: d, }, nil default: diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 81a6e4b929404..8933908104bbb 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -129,7 +129,7 @@ func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Works if res.StatusCode != http.StatusOK { return nil, readBodyAsError(res) } - nextEvent := ServerSideEventReader(res.Body) + nextEvent := ServerSentEventReader(res.Body) wc := make(chan Workspace, 256) go func() { @@ -145,7 +145,7 @@ func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Works if err != nil { return } - if sse.Type == ServerSideEventTypeData { + if sse.Type == ServerSentEventTypeData { var ws Workspace b, ok := sse.Data.([]byte) if !ok { From a1646b7ace71ba61160f0bbc5c2e3bdeee4ec41a Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 15 Sep 2022 18:12:42 +0000 Subject: [PATCH 20/34] ts types --- site/src/api/typesGenerated.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 90c1214d6c03f..021e9bca6eab6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -369,8 +369,8 @@ export interface Role { } // From codersdk/sse.go -export interface ServerSideEvent { - readonly Type: ServerSideEventType +export interface ServerSentEvent { + readonly Type: ServerSentEventType // eslint-disable-next-line readonly Data: any } @@ -713,7 +713,7 @@ export type ResourceType = | "workspace" // From codersdk/sse.go -export type ServerSideEventType = "data" | "error" | "ping" +export type ServerSentEventType = "data" | "error" | "ping" // From codersdk/users.go export type UserStatus = "active" | "suspended" From 5ca27e5bcf070ab7c6af574cc82f3d5cb4df985b Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 03:51:50 +0000 Subject: [PATCH 21/34] new convert workspace build --- coderd/database/databasefake/databasefake.go | 16 + coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 41 ++ .../database/queries/workspaceresources.sql | 8 + coderd/workspacebuilds.go | 371 +++++++++++++++--- coderd/workspaces.go | 309 +++++---------- codersdk/workspacebuilds.go | 2 +- site/src/api/typesGenerated.ts | 2 +- site/webpack.dev.ts | 2 + 9 files changed, 492 insertions(+), 260 deletions(-) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 4512e72f5b735..5ba5460edff93 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -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() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index c254a4ea62947..0129fca8f7a53 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -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) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 88dd2091a7718..1e258e483e65a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -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 ` diff --git a/coderd/database/queries/workspaceresources.sql b/coderd/database/queries/workspaceresources.sql index 8cb2219a7c42f..373090fbdeb9c 100644 --- a/coderd/database/queries/workspaceresources.sql +++ b/coderd/database/queries/workspaceresources.sql @@ -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; diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 7133e5f779779..1dffdbd5211c4 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -49,9 +49,71 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(rw, http.StatusOK, - convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users), - workspace, workspaceBuild, job)) + workspaceResources, err := api.Database.GetWorkspaceResourcesByJobIDs(r.Context(), []uuid.UUID{job.ID}) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resources.", + Detail: err.Error(), + }) + return + } + + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range workspaceResources { + resourceIDs = append(resourceIDs, resource.ID) + } + + resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resource metadata.", + Detail: err.Error(), + }) + return + } + + resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resource agents.", + Detail: err.Error(), + }) + return + } + + resourceAgentIDs := make([]uuid.UUID, 0) + for _, agent := range resourceAgents { + resourceAgentIDs = append(resourceAgentIDs, agent.ID) + } + + agentApps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace apps.", + Detail: err.Error(), + }) + return + } + + wsb, err := api.convertWorkspaceBuilds( + []database.WorkspaceBuild{workspaceBuild}, + []database.Workspace{workspace}, + users, + []database.ProvisionerJob{job}, + workspaceResources, + resourceMetadata, + resourceAgents, + agentApps, + ) + 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, wsb) } func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { @@ -67,7 +129,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 +157,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 +175,94 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - jobIDs := make([]uuid.UUID, 0, len(builds)) - for _, build := range builds { + userIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) + for _, build := range workspaceBuilds { + userIDs = append(userIDs, build.InitiatorID) + } + users, err := api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{ + IDs: userIDs, + }) + + jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) + for _, build := range workspaceBuilds { jobIDs = append(jobIDs, build.JobID) } jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs) - if errors.Is(err, sql.ErrNoRows) { - err = nil - } - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching provisioner jobs.", + Message: "Internal error fetching workspace jobs.", Detail: err.Error(), }) return } - jobByID := map[string]database.ProvisionerJob{} + + jobByID := map[uuid.UUID]database.ProvisionerJob{} for _, job := range jobs { - jobByID[job.ID.String()] = job + jobByID[job.ID] = job } - userIDs := []uuid.UUID{workspace.OwnerID} - for _, build := range builds { - userIDs = append(userIDs, build.InitiatorID) + workspaceResources, err := api.Database.GetWorkspaceResourcesByJobIDs(r.Context(), jobIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resources.", + Detail: err.Error(), + }) + return } - users, err := api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{ - IDs: userIDs, - }) - if err != nil { + + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range workspaceResources { + resourceIDs = append(resourceIDs, resource.ID) + } + + resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching user.", + Message: "Internal error fetching workspace resource metadata.", 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)) + resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace agents.", + Detail: err.Error(), + }) + return + } + + resourceAgentIDs := make([]uuid.UUID, 0) + for _, agent := range resourceAgents { + resourceAgentIDs = append(resourceAgentIDs, agent.ID) + } + + agentApps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace apps.", + Detail: err.Error(), + }) + return + } + + apiBuilds, err := api.convertWorkspaceBuilds( + workspaceBuilds, + []database.Workspace{workspace}, + users, + jobs, + workspaceResources, + resourceMetadata, + resourceAgents, + agentApps, + ) + 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 +319,89 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ return } - job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + initiator, err := api.Database.GetUserByID(r.Context(), workspaceBuild.InitiatorID) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching provisioner job.", + Message: "Internal error fetching workspace build initiator.", Detail: err.Error(), }) return } - users, err := api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{ - IDs: []uuid.UUID{workspace.OwnerID, workspaceBuild.InitiatorID}, - }) + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace jobs.", + Detail: err.Error(), + }) + return + } + + workspaceResources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), job.ID) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resources.", + Detail: err.Error(), + }) + return + } + + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range workspaceResources { + resourceIDs = append(resourceIDs, resource.ID) + } + + resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resource metadata.", + Detail: err.Error(), + }) + return + } + + resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace agents.", + Detail: err.Error(), + }) + return + } + + resourceAgentIDs := make([]uuid.UUID, 0) + for _, agent := range resourceAgents { + resourceAgentIDs = append(resourceAgentIDs, agent.ID) + } + + agentApps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace apps.", + Detail: err.Error(), + }) + return + } + + wsb, err := api.convertWorkspaceBuilds( + []database.WorkspaceBuild{workspaceBuild}, + []database.Workspace{workspace}, + []database.User{owner, initiator}, + []database.ProvisionerJob{job}, + workspaceResources, + resourceMetadata, + resourceAgents, + agentApps, + ) 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, wsb) } func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { @@ -496,9 +659,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)) + wsb, err := api.convertWorkspaceBuilds( + []database.WorkspaceBuild{workspaceBuild}, + []database.Workspace{workspace}, + users, + []database.ProvisionerJob{provisionerJob}, + []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, wsb) } func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) { @@ -667,7 +846,107 @@ func convertWorkspaceBuild( Job: convertProvisionerJob(job), Deadline: codersdk.NewNullTime(workspaceBuild.Deadline, !workspaceBuild.Deadline.IsZero()), Reason: codersdk.BuildReason(workspaceBuild.Reason), + Resources: []codersdk.WorkspaceResource{}, + } +} + +func (api *API) convertWorkspaceBuilds( + workspaceBuilds []database.WorkspaceBuild, + workspaces []database.Workspace, + users []database.User, + jobs []database.ProvisionerJob, + 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 + } + userByID := map[uuid.UUID]database.User{} + for _, user := range users { + userByID[user.ID] = user + } + jobByID := map[uuid.UUID]database.ProvisionerJob{} + for _, job := range jobs { + jobByID[job.ID] = job + } + 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) } + + 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") + } + 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) + } + + resources := resourcesByJobID[job.ID] + var apiResources []codersdk.WorkspaceResource + for _, resource := range resources { + apiAgents := make([]codersdk.WorkspaceAgent, 0) + agents := agentsByResourceID[resource.ID] + for _, agent := range agents { + apps := appsByAgentID[agent.ID] + apiAgent, err := convertWorkspaceAgent(api.DERPMap, api.TailnetCoordinator, agent, convertApps(apps), api.AgentInactiveDisconnectTimeout) + if err != nil { + return nil, xerrors.Errorf("converting workspace agent: %w", err) + } + apiAgents = append(apiAgents, apiAgent) + } + metadata := metadataByResourceID[resource.ID] + apiResources = append(apiResources, convertWorkspaceResource(resource, apiAgents, metadata)) + } + + apiBuilds = append(apiBuilds, codersdk.WorkspaceBuild{ + ID: build.ID, + CreatedAt: build.CreatedAt, + UpdatedAt: build.UpdatedAt, + WorkspaceOwnerID: workspace.OwnerID, + WorkspaceOwnerName: owner.Username, + WorkspaceID: build.WorkspaceID, + WorkspaceName: workspace.Name, + TemplateVersionID: build.TemplateVersionID, + BuildNumber: build.BuildNumber, + Transition: codersdk.WorkspaceTransition(build.Transition), + InitiatorID: build.InitiatorID, + InitiatorUsername: initiator.Username, + Job: convertProvisionerJob(job), + Deadline: codersdk.NewNullTime(build.Deadline, !build.Deadline.IsZero()), + Reason: codersdk.BuildReason(build.Reason), + Resources: apiResources, + }) + } + + return apiBuilds, nil } func convertWorkspaceResource(resource database.WorkspaceResource, agents []codersdk.WorkspaceAgent, metadata []database.WorkspaceResourceMetadatum) codersdk.WorkspaceResource { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 6015b5bc581a8..8a422d94c2ffb 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -15,7 +15,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" - "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "cdr.dev/slog" @@ -73,45 +72,16 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { return } - build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) + wss, err := api.convertWorkspaces(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, wss[0]) } // workspaces returns all workspaces a user can read. @@ -153,7 +123,7 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { return } - apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces) + wss, err := api.convertWorkspaces(r.Context(), workspaces) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error reading workspace.", @@ -161,7 +131,7 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { }) return } - httpapi.Write(rw, http.StatusOK, apiWorkspaces) + httpapi.Write(rw, http.StatusOK, wss) } func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) { @@ -210,41 +180,16 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) return } - build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) - 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.", - Detail: err.Error(), - }) - return - } - - initiator, err := api.Database.GetUserByID(r.Context(), build.InitiatorID) + wss, err := api.convertWorkspaces(r.Context(), []database.Workspace{workspace}) 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 } - httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, build, job, template, &owner, &initiator)) + httpapi.Write(rw, http.StatusOK, wss[0]) } // Create a new workspace for the currently authenticated user. @@ -815,134 +760,28 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { }) return } - build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID) - if err != nil { - _ = sendEvent(r.Context(), codersdk.ServerSentEvent{ - Type: codersdk.ServerSentEventTypeError, - Data: codersdk.Response{ - Message: "Internal error fetching workspace.", - Detail: err.Error(), - }, - }) - return - } - var ( - group errgroup.Group - job database.ProvisionerJob - template database.Template - users []database.User - apiResources []codersdk.WorkspaceResource - ) - group.Go(func() (err error) { - job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID) - if err != nil { - return xerrors.Errorf("fetching workspace build job: %w", err) - } - - if !job.CompletedAt.Valid { - return nil - } - - resources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), job.ID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return xerrors.Errorf("fetching workspace resources by job: %w", err) - } - - resourceIDs := make([]uuid.UUID, 0) - for _, resource := range resources { - resourceIDs = append(resourceIDs, resource.ID) - } - - resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return xerrors.Errorf("fetching workspace agents: %w", err) - } - - resourceAgentIDs := make([]uuid.UUID, 0) - for _, agent := range resourceAgents { - resourceAgentIDs = append(resourceAgentIDs, agent.ID) - } - - apps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return xerrors.Errorf("fetching workspace apps: %w", err) - } - - resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) - if err != nil { - return xerrors.Errorf("fetching resource metadata: %w", err) - } - - for _, resource := range resources { - agents := make([]codersdk.WorkspaceAgent, 0) - for _, agent := range resourceAgents { - if agent.ResourceID != resource.ID { - continue - } - dbApps := make([]database.WorkspaceApp, 0) - for _, app := range apps { - if app.AgentID == agent.ID { - dbApps = append(dbApps, app) - } - } - - apiAgent, err := convertWorkspaceAgent(api.DERPMap, api.TailnetCoordinator, agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout) - if err != nil { - return xerrors.Errorf("converting workspace agent: %w", err) - } - agents = append(agents, apiAgent) - } - metadata := make([]database.WorkspaceResourceMetadatum, 0) - for _, field := range resourceMetadata { - if field.WorkspaceResourceID == resource.ID { - metadata = append(metadata, field) - } - } - apiResources = append(apiResources, convertWorkspaceResource(resource, agents, metadata)) - } - - return nil - }) - group.Go(func() (err error) { - template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) - if err != nil { - return xerrors.Errorf("fetching template: %w", err) - } - - return nil - }) - group.Go(func() (err error) { - users, err = api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{ - IDs: []uuid.UUID{workspace.OwnerID, build.InitiatorID}, - }) - if err != nil { - return xerrors.Errorf("fetching users: %w", err) - } - return nil - }) - err = group.Wait() + wss, err := api.convertWorkspaces(r.Context(), []database.Workspace{workspace}) if err != nil { _ = sendEvent(r.Context(), codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeError, Data: codersdk.Response{ - Message: "Internal error fetching workspace.", + Message: "Internal error fetching workspace resources.", Detail: err.Error(), }, }) return } - apiWorkspace := convertWorkspace(workspace, build, job, template, findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users)) - apiWorkspace.LatestBuild.Resources = apiResources + _ = sendEvent(r.Context(), codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeData, - Data: apiWorkspace, + Data: wss[0], }) } } } -func convertWorkspaces(ctx context.Context, db database.Store, workspaces []database.Workspace) ([]codersdk.Workspace, error) { +func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Workspace) ([]codersdk.Workspace, error) { workspaceIDs := make([]uuid.UUID, 0, len(workspaces)) templateIDs := make([]uuid.UUID, 0, len(workspaces)) userIDs := make([]uuid.UUID, 0, len(workspaces)) @@ -951,60 +790,69 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data 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 { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, xerrors.Errorf("get templates: %w", err) } - users, err := db.GetUsersByIDs(ctx, database.GetUsersByIDsParams{ + + // + workspaceBuilds, err := api.Database.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, workspaceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get workspace builds: %w", err) + } + + for _, build := range workspaceBuilds { + userIDs = append(userIDs, build.InitiatorID) + } + users, err := api.Database.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 + jobs, err := api.Database.GetProvisionerJobsByIDs(ctx, jobIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get provisioner jobs: %w", err) + } + + workspaceResources, err := api.Database.GetWorkspaceResourcesByJobIDs(ctx, jobIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get workspace resources by job: %w", err) } + + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range workspaceResources { + resourceIDs = append(resourceIDs, resource.ID) + } + + resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(ctx, resourceIDs) if err != nil { - return nil, xerrors.Errorf("get provisioner jobs: %w", err) + return nil, xerrors.Errorf("fetching resource metadata: %w", err) + } + + resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get workspace agents: %w", err) + } + + resourceAgentIDs := make([]uuid.UUID, 0) + for _, agent := range resourceAgents { + resourceAgentIDs = append(resourceAgentIDs, agent.ID) + } + + agentApps, err := api.Database.GetWorkspaceAppsByAgentIDs(ctx, resourceAgentIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("fetching workspace apps: %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, - } + buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild } templateByID := map[uuid.UUID]database.Template{} for _, template := range templates { @@ -1018,6 +866,23 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data for _, job := range jobs { jobByID[job.ID] = job } + 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) + } + apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces)) for _, workspace := range workspaces { build, exists := buildByWorkspaceID[workspace.ID] @@ -1040,7 +905,27 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data 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)) + + resources := resourcesByJobID[job.ID] + var apiResources []codersdk.WorkspaceResource + for _, resource := range resources { + apiAgents := make([]codersdk.WorkspaceAgent, 0) + agents := agentsByResourceID[resource.ID] + for _, agent := range agents { + apps := appsByAgentID[agent.ID] + apiAgent, err := convertWorkspaceAgent(api.DERPMap, api.TailnetCoordinator, agent, convertApps(apps), api.AgentInactiveDisconnectTimeout) + if err != nil { + return nil, xerrors.Errorf("converting workspace agent: %w", err) + } + apiAgents = append(apiAgents, apiAgent) + } + metadata := metadataByResourceID[resource.ID] + apiResources = append(apiResources, convertWorkspaceResource(resource, apiAgents, metadata)) + } + + apiWorkspace := convertWorkspace(workspace, build, job, template, &owner, &initiator) + apiWorkspace.LatestBuild.Resources = apiResources + apiWorkspaces = append(apiWorkspaces, apiWorkspace) } sort.Slice(apiWorkspaces, func(i, j int) bool { iw := apiWorkspaces[i] diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 6ffa3fc117817..1dc75e9239849 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -50,8 +50,8 @@ type WorkspaceBuild struct { InitiatorUsername string `json:"initiator_name"` Job ProvisionerJob `json:"job"` Reason BuildReason `db:"reason" json:"reason"` + Resources []WorkspaceResource `json:"resources"` Deadline NullTime `json:"deadline,omitempty"` - Resources []WorkspaceResource `json:"resources,omitempty"` } // WorkspaceBuild returns a single workspace build for a workspace. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 021e9bca6eab6..ff875a6ccf755 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -617,8 +617,8 @@ export interface WorkspaceBuild { readonly initiator_name: string readonly job: ProvisionerJob readonly reason: BuildReason + readonly resources: WorkspaceResource[] readonly deadline?: string - readonly resources?: WorkspaceResource[] } // From codersdk/workspaces.go diff --git a/site/webpack.dev.ts b/site/webpack.dev.ts index 9e59efa6aa3a9..f35e0b3f02a56 100644 --- a/site/webpack.dev.ts +++ b/site/webpack.dev.ts @@ -74,6 +74,8 @@ const config: Configuration = { secure: false, }, }, + // To disable compression : + compress: false, static: ["./static"], }, From 19ff07e77e8f88cd75f0a598857c92622a23fbba Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 14:23:46 +0000 Subject: [PATCH 22/34] consolidate --- coderd/workspacebuilds.go | 45 ---------------- coderd/workspaces.go | 107 ++++++++++++++++++-------------------- 2 files changed, 51 insertions(+), 101 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 1dffdbd5211c4..f4bd80ca8c8a5 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -806,50 +806,6 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { _, _ = rw.Write(workspaceBuild.ProvisionerState) } -func convertWorkspaceBuild( - workspaceOwner *database.User, - buildInitiator *database.User, - 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") - } - - // 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 - } - - initiatorName := "unknown" - if workspaceOwner != nil { - initiatorName = buildInitiator.Username - } - - return codersdk.WorkspaceBuild{ - ID: workspaceBuild.ID, - CreatedAt: workspaceBuild.CreatedAt, - UpdatedAt: workspaceBuild.UpdatedAt, - WorkspaceOwnerID: workspace.OwnerID, - WorkspaceOwnerName: ownerName, - WorkspaceID: workspaceBuild.WorkspaceID, - WorkspaceName: workspace.Name, - TemplateVersionID: workspaceBuild.TemplateVersionID, - BuildNumber: workspaceBuild.BuildNumber, - Transition: codersdk.WorkspaceTransition(workspaceBuild.Transition), - InitiatorID: workspaceBuild.InitiatorID, - InitiatorUsername: initiatorName, - Job: convertProvisionerJob(job), - Deadline: codersdk.NewNullTime(workspaceBuild.Deadline, !workspaceBuild.Deadline.IsZero()), - Reason: codersdk.BuildReason(workspaceBuild.Reason), - Resources: []codersdk.WorkspaceResource{}, - } -} - func (api *API) convertWorkspaceBuilds( workspaceBuilds []database.WorkspaceBuild, workspaces []database.Workspace, @@ -860,7 +816,6 @@ func (api *API) convertWorkspaceBuilds( resourceAgents []database.WorkspaceAgent, agentApps []database.WorkspaceApp, ) ([]codersdk.WorkspaceBuild, error) { - workspaceByID := map[uuid.UUID]database.Workspace{} for _, workspace := range workspaces { workspaceByID[workspace.ID] = workspace diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 8a422d94c2ffb..73f2934e29d26 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -431,8 +431,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))) + wsb, err := api.convertWorkspaceBuilds( + []database.WorkspaceBuild{workspaceBuild}, + []database.Workspace{workspace}, + users, + []database.ProvisionerJob{provisionerJob}, + []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, + wsb[0], + template, + findUser(apiKey.UserID, users), + )) } func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) { @@ -810,6 +832,9 @@ func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Wor users, err := api.Database.GetUsersByIDs(ctx, database.GetUsersByIDsParams{ IDs: userIDs, }) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, xerrors.Errorf("get users: %w", err) + } jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) for _, build := range workspaceBuilds { @@ -850,8 +875,22 @@ func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Wor return nil, xerrors.Errorf("fetching workspace apps: %w", err) } - buildByWorkspaceID := map[uuid.UUID]database.WorkspaceBuild{} - for _, workspaceBuild := range workspaceBuilds { + apiBuilds, err := api.convertWorkspaceBuilds( + workspaceBuilds, + workspaces, + users, + jobs, + workspaceResources, + resourceMetadata, + resourceAgents, + agentApps, + ) + if err != nil { + return nil, xerrors.Errorf("converting workspace build: %w", err) + } + + buildByWorkspaceID := map[uuid.UUID]codersdk.WorkspaceBuild{} + for _, workspaceBuild := range apiBuilds { buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild } templateByID := map[uuid.UUID]database.Template{} @@ -862,26 +901,6 @@ func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Wor for _, user := range users { userByID[user.ID] = user } - jobByID := map[uuid.UUID]database.ProvisionerJob{} - for _, job := range jobs { - jobByID[job.ID] = job - } - 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) - } apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces)) for _, workspace := range workspaces { @@ -893,39 +912,17 @@ func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Wor 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) - } - - resources := resourcesByJobID[job.ID] - var apiResources []codersdk.WorkspaceResource - for _, resource := range resources { - apiAgents := make([]codersdk.WorkspaceAgent, 0) - agents := agentsByResourceID[resource.ID] - for _, agent := range agents { - apps := appsByAgentID[agent.ID] - apiAgent, err := convertWorkspaceAgent(api.DERPMap, api.TailnetCoordinator, agent, convertApps(apps), api.AgentInactiveDisconnectTimeout) - if err != nil { - return nil, xerrors.Errorf("converting workspace agent: %w", err) - } - apiAgents = append(apiAgents, apiAgent) - } - metadata := metadataByResourceID[resource.ID] - apiResources = append(apiResources, convertWorkspaceResource(resource, apiAgents, metadata)) - } - apiWorkspace := convertWorkspace(workspace, build, job, template, &owner, &initiator) - apiWorkspace.LatestBuild.Resources = apiResources - apiWorkspaces = append(apiWorkspaces, apiWorkspace) + apiWorkspaces = append(apiWorkspaces, convertWorkspace( + workspace, + build, + template, + &owner, + )) } sort.Slice(apiWorkspaces, func(i, j int) bool { iw := apiWorkspaces[i] @@ -941,11 +938,9 @@ func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Wor 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 { @@ -960,7 +955,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(), From 13a6e3f60ba8ea24226454c4604a765bbdfe2922 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 14:49:47 +0000 Subject: [PATCH 23/34] add workspace build converter --- coderd/workspacebuilds.go | 182 +++++++++++++++++++++++--------------- coderd/workspaces.go | 10 +-- 2 files changed, 114 insertions(+), 78 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index f4bd80ca8c8a5..df09a0635b6db 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -95,11 +95,11 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - wsb, err := api.convertWorkspaceBuilds( - []database.WorkspaceBuild{workspaceBuild}, - []database.Workspace{workspace}, + apiBuild, err := api.convertWorkspaceBuild( + workspaceBuild, + workspace, + job, users, - []database.ProvisionerJob{job}, workspaceResources, resourceMetadata, resourceAgents, @@ -113,7 +113,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(rw, http.StatusOK, wsb) + httpapi.Write(rw, http.StatusOK, apiBuild) } func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { @@ -182,6 +182,13 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { 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 users.", + Detail: err.Error(), + }) + return + } jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) for _, build := range workspaceBuilds { @@ -383,11 +390,11 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ return } - wsb, err := api.convertWorkspaceBuilds( - []database.WorkspaceBuild{workspaceBuild}, - []database.Workspace{workspace}, + apiBuild, err := api.convertWorkspaceBuild( + workspaceBuild, + workspace, + job, []database.User{owner, initiator}, - []database.ProvisionerJob{job}, workspaceResources, resourceMetadata, resourceAgents, @@ -401,7 +408,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ return } - httpapi.Write(rw, http.StatusOK, wsb) + httpapi.Write(rw, http.StatusOK, apiBuild) } func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { @@ -659,11 +666,11 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - wsb, err := api.convertWorkspaceBuilds( - []database.WorkspaceBuild{workspaceBuild}, - []database.Workspace{workspace}, + apiBuild, err := api.convertWorkspaceBuild( + workspaceBuild, + workspace, + provisionerJob, users, - []database.ProvisionerJob{provisionerJob}, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -677,7 +684,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(rw, http.StatusCreated, wsb) + httpapi.Write(rw, http.StatusCreated, apiBuild) } func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) { @@ -820,14 +827,56 @@ func (api *API) convertWorkspaceBuilds( for _, workspace := range workspaces { workspaceByID[workspace.ID] = workspace } - userByID := map[uuid.UUID]database.User{} - for _, user := range users { - userByID[user.ID] = user - } 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, + job database.ProvisionerJob, + 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) @@ -845,63 +894,50 @@ func (api *API) convertWorkspaceBuilds( appsByAgentID[app.AgentID] = append(appsByAgentID[app.AgentID], app) } - 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") - } - 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) - } + 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) + } - resources := resourcesByJobID[job.ID] - var apiResources []codersdk.WorkspaceResource - for _, resource := range resources { - apiAgents := make([]codersdk.WorkspaceAgent, 0) - agents := agentsByResourceID[resource.ID] - for _, agent := range agents { - apps := appsByAgentID[agent.ID] - apiAgent, err := convertWorkspaceAgent(api.DERPMap, api.TailnetCoordinator, agent, convertApps(apps), api.AgentInactiveDisconnectTimeout) - if err != nil { - return nil, xerrors.Errorf("converting workspace agent: %w", err) - } - apiAgents = append(apiAgents, apiAgent) + resources := resourcesByJobID[job.ID] + apiResources := []codersdk.WorkspaceResource{} + for _, resource := range resources { + agents := agentsByResourceID[resource.ID] + apiAgents := []codersdk.WorkspaceAgent{} + 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) } - metadata := metadataByResourceID[resource.ID] - apiResources = append(apiResources, convertWorkspaceResource(resource, apiAgents, metadata)) + apiAgents = append(apiAgents, apiAgent) } - - apiBuilds = append(apiBuilds, codersdk.WorkspaceBuild{ - ID: build.ID, - CreatedAt: build.CreatedAt, - UpdatedAt: build.UpdatedAt, - WorkspaceOwnerID: workspace.OwnerID, - WorkspaceOwnerName: owner.Username, - WorkspaceID: build.WorkspaceID, - WorkspaceName: workspace.Name, - TemplateVersionID: build.TemplateVersionID, - BuildNumber: build.BuildNumber, - Transition: codersdk.WorkspaceTransition(build.Transition), - InitiatorID: build.InitiatorID, - InitiatorUsername: initiator.Username, - Job: convertProvisionerJob(job), - Deadline: codersdk.NewNullTime(build.Deadline, !build.Deadline.IsZero()), - Reason: codersdk.BuildReason(build.Reason), - Resources: apiResources, - }) - } - - return apiBuilds, nil + metadata := metadataByResourceID[resource.ID] + apiResources = append(apiResources, convertWorkspaceResource(resource, apiAgents, metadata)) + } + + return codersdk.WorkspaceBuild{ + ID: build.ID, + CreatedAt: build.CreatedAt, + UpdatedAt: build.UpdatedAt, + WorkspaceOwnerID: workspace.OwnerID, + WorkspaceOwnerName: owner.Username, + WorkspaceID: build.WorkspaceID, + WorkspaceName: workspace.Name, + TemplateVersionID: build.TemplateVersionID, + BuildNumber: build.BuildNumber, + Transition: codersdk.WorkspaceTransition(build.Transition), + InitiatorID: build.InitiatorID, + InitiatorUsername: initiator.Username, + Job: convertProvisionerJob(job), + 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 { diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 73f2934e29d26..8437cfc72d5e3 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -431,11 +431,11 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req WorkspaceBuilds: []telemetry.WorkspaceBuild{telemetry.ConvertWorkspaceBuild(workspaceBuild)}, }) - wsb, err := api.convertWorkspaceBuilds( - []database.WorkspaceBuild{workspaceBuild}, - []database.Workspace{workspace}, + apiBuild, err := api.convertWorkspaceBuild( + workspaceBuild, + workspace, + provisionerJob, users, - []database.ProvisionerJob{provisionerJob}, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -451,7 +451,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req httpapi.Write(rw, http.StatusCreated, convertWorkspace( workspace, - wsb[0], + apiBuild, template, findUser(apiKey.UserID, users), )) From 03cc931ad638271a3ab9b7d5a71ff6749a4c48ea Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 15:11:27 +0000 Subject: [PATCH 24/34] consolidate data collection --- coderd/workspacebuilds.go | 144 +++++--------------------------------- coderd/workspaces.go | 70 ++++++++++++++++++ 2 files changed, 86 insertions(+), 128 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index df09a0635b6db..a316dbc1a2f22 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -29,67 +29,10 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + data, err := api.getWorkspaceBuildData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching provisioner job.", - Detail: err.Error(), - }) - return - } - - users, err := api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{ - IDs: []uuid.UUID{workspace.OwnerID, workspaceBuild.InitiatorID}, - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching user.", - Detail: err.Error(), - }) - return - } - - workspaceResources, err := api.Database.GetWorkspaceResourcesByJobIDs(r.Context(), []uuid.UUID{job.ID}) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resources.", - Detail: err.Error(), - }) - return - } - - resourceIDs := make([]uuid.UUID, 0) - for _, resource := range workspaceResources { - resourceIDs = append(resourceIDs, resource.ID) - } - - resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resource metadata.", - Detail: err.Error(), - }) - return - } - - resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resource agents.", - Detail: err.Error(), - }) - return - } - - resourceAgentIDs := make([]uuid.UUID, 0) - for _, agent := range resourceAgents { - resourceAgentIDs = append(resourceAgentIDs, agent.ID) - } - - agentApps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace apps.", + Message: "Internal error getting workspace build data.", Detail: err.Error(), }) return @@ -98,12 +41,12 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { apiBuild, err := api.convertWorkspaceBuild( workspaceBuild, workspace, - job, - users, - workspaceResources, - resourceMetadata, - resourceAgents, - agentApps, + data.jobs[0], + data.users, + data.resources, + data.metadata, + data.agents, + data.apps, ) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ @@ -326,65 +269,10 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ return } - initiator, err := api.Database.GetUserByID(r.Context(), workspaceBuild.InitiatorID) + data, err := api.getWorkspaceBuildData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace build initiator.", - Detail: err.Error(), - }) - return - } - - job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace jobs.", - Detail: err.Error(), - }) - return - } - - workspaceResources, err := api.Database.GetWorkspaceResourcesByJobID(r.Context(), job.ID) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resources.", - Detail: err.Error(), - }) - return - } - - resourceIDs := make([]uuid.UUID, 0) - for _, resource := range workspaceResources { - resourceIDs = append(resourceIDs, resource.ID) - } - - resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resource metadata.", - Detail: err.Error(), - }) - return - } - - resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace agents.", - Detail: err.Error(), - }) - return - } - - resourceAgentIDs := make([]uuid.UUID, 0) - for _, agent := range resourceAgents { - resourceAgentIDs = append(resourceAgentIDs, agent.ID) - } - - agentApps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace apps.", + Message: "Internal error getting workspace build data.", Detail: err.Error(), }) return @@ -393,12 +281,12 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ apiBuild, err := api.convertWorkspaceBuild( workspaceBuild, workspace, - job, - []database.User{owner, initiator}, - workspaceResources, - resourceMetadata, - resourceAgents, - agentApps, + data.jobs[0], + data.users, + data.resources, + data.metadata, + data.agents, + data.apps, ) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 8437cfc72d5e3..5514f5e1ab966 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -936,6 +936,76 @@ func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Wor return apiWorkspaces, nil } +type workspaceBuildData struct { + users []database.User + jobs []database.ProvisionerJob + resources []database.WorkspaceResource + metadata []database.WorkspaceResourceMetadatum + agents []database.WorkspaceAgent + apps []database.WorkspaceApp +} + +func (api *API) getWorkspaceBuildData(ctx context.Context, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildData, error) { + userIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) + for _, build := range workspaceBuilds { + userIDs = append(userIDs, build.InitiatorID) + } + users, err := api.Database.GetUsersByIDs(ctx, database.GetUsersByIDsParams{ + IDs: userIDs, + }) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return workspaceBuildData{}, 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 workspaceBuildData{}, xerrors.Errorf("get provisioner jobs: %w", err) + } + + resources, err := api.Database.GetWorkspaceResourcesByJobIDs(ctx, jobIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return workspaceBuildData{}, xerrors.Errorf("get workspace resources by job: %w", err) + } + + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + + metadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(ctx, resourceIDs) + if err != nil { + return workspaceBuildData{}, xerrors.Errorf("fetching resource metadata: %w", err) + } + + agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return workspaceBuildData{}, xerrors.Errorf("get workspace agents: %w", err) + } + + 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 workspaceBuildData{}, xerrors.Errorf("fetching workspace apps: %w", err) + } + + return workspaceBuildData{ + users: users, + jobs: jobs, + resources: resources, + metadata: metadata, + agents: agents, + apps: apps, + }, nil +} + func convertWorkspace( workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, From 7efae96dedc8bd49187df4d4c0ec32fdca329b95 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 15:16:04 +0000 Subject: [PATCH 25/34] more consolidation --- coderd/workspacebuilds.go | 92 +++++---------------------------------- coderd/workspaces.go | 4 +- 2 files changed, 13 insertions(+), 83 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index a316dbc1a2f22..05c9889e6c59d 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -29,7 +29,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - data, err := api.getWorkspaceBuildData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) + data, err := api.getWorkspaceBuildsData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error getting workspace build data.", @@ -118,80 +118,10 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - userIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) - for _, build := range workspaceBuilds { - userIDs = append(userIDs, build.InitiatorID) - } - users, err := api.Database.GetUsersByIDs(r.Context(), database.GetUsersByIDsParams{ - IDs: userIDs, - }) + data, err := api.getWorkspaceBuildsData(r.Context(), workspaceBuilds) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching users.", - Detail: err.Error(), - }) - return - } - - jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) - for _, build := range workspaceBuilds { - jobIDs = append(jobIDs, build.JobID) - } - jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace jobs.", - Detail: err.Error(), - }) - return - } - - jobByID := map[uuid.UUID]database.ProvisionerJob{} - for _, job := range jobs { - jobByID[job.ID] = job - } - - workspaceResources, err := api.Database.GetWorkspaceResourcesByJobIDs(r.Context(), jobIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resources.", - Detail: err.Error(), - }) - return - } - - resourceIDs := make([]uuid.UUID, 0) - for _, resource := range workspaceResources { - resourceIDs = append(resourceIDs, resource.ID) - } - - resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(r.Context(), resourceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace resource metadata.", - Detail: err.Error(), - }) - return - } - - resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace agents.", - Detail: err.Error(), - }) - return - } - - resourceAgentIDs := make([]uuid.UUID, 0) - for _, agent := range resourceAgents { - resourceAgentIDs = append(resourceAgentIDs, agent.ID) - } - - agentApps, err := api.Database.GetWorkspaceAppsByAgentIDs(r.Context(), resourceAgentIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace apps.", + Message: "Internal error getting workspace build data.", Detail: err.Error(), }) return @@ -200,12 +130,12 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { apiBuilds, err := api.convertWorkspaceBuilds( workspaceBuilds, []database.Workspace{workspace}, - users, - jobs, - workspaceResources, - resourceMetadata, - resourceAgents, - agentApps, + data.jobs, + data.users, + data.resources, + data.metadata, + data.agents, + data.apps, ) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ @@ -269,7 +199,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ return } - data, err := api.getWorkspaceBuildData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) + data, err := api.getWorkspaceBuildsData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error getting workspace build data.", @@ -704,8 +634,8 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { func (api *API) convertWorkspaceBuilds( workspaceBuilds []database.WorkspaceBuild, workspaces []database.Workspace, - users []database.User, jobs []database.ProvisionerJob, + users []database.User, workspaceResources []database.WorkspaceResource, resourceMetadata []database.WorkspaceResourceMetadatum, resourceAgents []database.WorkspaceAgent, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 5514f5e1ab966..330adef533697 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -878,8 +878,8 @@ func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Wor apiBuilds, err := api.convertWorkspaceBuilds( workspaceBuilds, workspaces, - users, jobs, + users, workspaceResources, resourceMetadata, resourceAgents, @@ -945,7 +945,7 @@ type workspaceBuildData struct { apps []database.WorkspaceApp } -func (api *API) getWorkspaceBuildData(ctx context.Context, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildData, error) { +func (api *API) getWorkspaceBuildsData(ctx context.Context, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildData, error) { userIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) for _, build := range workspaceBuilds { userIDs = append(userIDs, build.InitiatorID) From 4178f5a5541aeb577666999c43562e7d82e58b44 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 15:56:53 +0000 Subject: [PATCH 26/34] split conversion and data fetching --- coderd/workspacebuilds.go | 77 +++++++++++++- coderd/workspaces.go | 208 ++++++++++++-------------------------- 2 files changed, 139 insertions(+), 146 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 05c9889e6c59d..a67badcdf83c8 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -1,6 +1,7 @@ package coderd import ( + "context" "database/sql" "encoding/json" "errors" @@ -29,7 +30,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - data, err := api.getWorkspaceBuildsData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) + data, err := api.workspaceBuildsData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error getting workspace build data.", @@ -118,7 +119,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - data, err := api.getWorkspaceBuildsData(r.Context(), workspaceBuilds) + data, err := api.workspaceBuildsData(r.Context(), workspaceBuilds) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error getting workspace build data.", @@ -199,7 +200,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ return } - data, err := api.getWorkspaceBuildsData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) + data, err := api.workspaceBuildsData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error getting workspace build data.", @@ -631,6 +632,76 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { _, _ = rw.Write(workspaceBuild.ProvisionerState) } +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, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) { + userIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) + for _, build := range workspaceBuilds { + userIDs = append(userIDs, build.InitiatorID) + } + users, err := api.Database.GetUsersByIDs(ctx, database.GetUsersByIDsParams{ + IDs: userIDs, + }) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + 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) + } + + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + + metadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(ctx, resourceIDs) + if err != nil { + 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) + } + + 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, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 330adef533697..9bf87773d6600 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -72,7 +72,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { return } - wss, err := api.convertWorkspaces(r.Context(), []database.Workspace{workspace}) + data, err := api.workspaceData(r.Context(), []database.Workspace{workspace}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching workspace resources.", @@ -81,7 +81,12 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(rw, http.StatusOK, wss[0]) + 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. @@ -123,14 +128,24 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { return } - wss, err := api.convertWorkspaces(r.Context(), workspaces) + data, err := api.workspaceData(r.Context(), workspaces) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace resources.", + Detail: err.Error(), + }) + return + } + + wss, err := convertWorkspaces(workspaces, data) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error reading workspace.", + Message: "Internal error converting workspaces.", Detail: err.Error(), }) return } + httpapi.Write(rw, http.StatusOK, wss) } @@ -180,7 +195,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) return } - wss, err := api.convertWorkspaces(r.Context(), []database.Workspace{workspace}) + data, err := api.workspaceData(r.Context(), []database.Workspace{workspace}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching workspace resources.", @@ -189,7 +204,12 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) return } - httpapi.Write(rw, http.StatusOK, wss[0]) + 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. @@ -783,12 +803,12 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } - wss, err := api.convertWorkspaces(r.Context(), []database.Workspace{workspace}) + 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 resources.", + Message: "Internal error fetching workspace data.", Detail: err.Error(), }, }) @@ -797,108 +817,80 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { _ = sendEvent(r.Context(), codersdk.ServerSentEvent{ Type: codersdk.ServerSentEventTypeData, - Data: wss[0], + Data: convertWorkspace( + workspace, + data.builds[0], + data.templates[0], + findUser(workspace.OwnerID, data.users), + ), }) } } } -func (api *API) convertWorkspaces(ctx context.Context, 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) } templates, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{ IDs: templateIDs, }) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get templates: %w", err) + return workspaceData{}, xerrors.Errorf("get templates: %w", err) } - // - workspaceBuilds, err := api.Database.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, workspaceIDs) + builds, err := api.Database.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, workspaceIDs) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get workspace builds: %w", err) + return workspaceData{}, xerrors.Errorf("get workspace builds: %w", err) } - for _, build := range workspaceBuilds { - userIDs = append(userIDs, build.InitiatorID) - } - users, err := api.Database.GetUsersByIDs(ctx, database.GetUsersByIDsParams{ - IDs: userIDs, - }) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - 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 := api.Database.GetProvisionerJobsByIDs(ctx, jobIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get provisioner jobs: %w", err) - } - - workspaceResources, err := api.Database.GetWorkspaceResourcesByJobIDs(ctx, jobIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get workspace resources by job: %w", err) - } - - resourceIDs := make([]uuid.UUID, 0) - for _, resource := range workspaceResources { - resourceIDs = append(resourceIDs, resource.ID) - } - - resourceMetadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(ctx, resourceIDs) + data, err := api.workspaceBuildsData(ctx, builds) if err != nil { - return nil, xerrors.Errorf("fetching resource metadata: %w", err) - } - - resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("get workspace agents: %w", err) - } - - resourceAgentIDs := make([]uuid.UUID, 0) - for _, agent := range resourceAgents { - resourceAgentIDs = append(resourceAgentIDs, agent.ID) - } - - agentApps, err := api.Database.GetWorkspaceAppsByAgentIDs(ctx, resourceAgentIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, xerrors.Errorf("fetching workspace apps: %w", err) + return workspaceData{}, xerrors.Errorf("get workspace builds data: %w", err) } apiBuilds, err := api.convertWorkspaceBuilds( - workspaceBuilds, + builds, workspaces, - jobs, - users, - workspaceResources, - resourceMetadata, - resourceAgents, - agentApps, + data.jobs, + data.users, + data.resources, + data.metadata, + data.agents, + data.apps, ) if err != nil { - return nil, xerrors.Errorf("converting workspace build: %w", err) + 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 apiBuilds { + 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 } @@ -936,76 +928,6 @@ func (api *API) convertWorkspaces(ctx context.Context, workspaces []database.Wor return apiWorkspaces, nil } -type workspaceBuildData struct { - users []database.User - jobs []database.ProvisionerJob - resources []database.WorkspaceResource - metadata []database.WorkspaceResourceMetadatum - agents []database.WorkspaceAgent - apps []database.WorkspaceApp -} - -func (api *API) getWorkspaceBuildsData(ctx context.Context, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildData, error) { - userIDs := make([]uuid.UUID, 0, len(workspaceBuilds)) - for _, build := range workspaceBuilds { - userIDs = append(userIDs, build.InitiatorID) - } - users, err := api.Database.GetUsersByIDs(ctx, database.GetUsersByIDsParams{ - IDs: userIDs, - }) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return workspaceBuildData{}, 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 workspaceBuildData{}, xerrors.Errorf("get provisioner jobs: %w", err) - } - - resources, err := api.Database.GetWorkspaceResourcesByJobIDs(ctx, jobIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return workspaceBuildData{}, xerrors.Errorf("get workspace resources by job: %w", err) - } - - resourceIDs := make([]uuid.UUID, 0) - for _, resource := range resources { - resourceIDs = append(resourceIDs, resource.ID) - } - - metadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(ctx, resourceIDs) - if err != nil { - return workspaceBuildData{}, xerrors.Errorf("fetching resource metadata: %w", err) - } - - agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs) - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return workspaceBuildData{}, xerrors.Errorf("get workspace agents: %w", err) - } - - 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 workspaceBuildData{}, xerrors.Errorf("fetching workspace apps: %w", err) - } - - return workspaceBuildData{ - users: users, - jobs: jobs, - resources: resources, - metadata: metadata, - agents: agents, - apps: apps, - }, nil -} - func convertWorkspace( workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, From e7614dda98ac5ea8349607f71c14e0f3d1be0b5f Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 16:04:39 +0000 Subject: [PATCH 27/34] fix missing owner --- coderd/workspacebuilds.go | 11 +++++++---- coderd/workspaces.go | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index a67badcdf83c8..296e9bb5b8ac5 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -30,7 +30,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - data, err := api.workspaceBuildsData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) + 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 getting workspace build data.", @@ -119,7 +119,7 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - data, err := api.workspaceBuildsData(r.Context(), workspaceBuilds) + data, err := api.workspaceBuildsData(r.Context(), []database.Workspace{workspace}, workspaceBuilds) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error getting workspace build data.", @@ -200,7 +200,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ return } - data, err := api.workspaceBuildsData(r.Context(), []database.WorkspaceBuild{workspaceBuild}) + 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 getting workspace build data.", @@ -641,11 +641,14 @@ type workspaceBuildsData struct { apps []database.WorkspaceApp } -func (api *API) workspaceBuildsData(ctx context.Context, workspaceBuilds []database.WorkspaceBuild) (workspaceBuildsData, error) { +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, }) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 9bf87773d6600..9631499c29089 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -854,7 +854,7 @@ func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspa return workspaceData{}, xerrors.Errorf("get workspace builds: %w", err) } - data, err := api.workspaceBuildsData(ctx, builds) + data, err := api.workspaceBuildsData(ctx, workspaces, builds) if err != nil { return workspaceData{}, xerrors.Errorf("get workspace builds data: %w", err) } From 1f7440bc9ce470d4f0e07689cd628c50345b8467 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 16:52:00 +0000 Subject: [PATCH 28/34] fix errors --- coderd/workspacebuilds.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 296e9bb5b8ac5..30024c824b3b6 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -652,7 +652,7 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.W users, err := api.Database.GetUsersByIDs(ctx, database.GetUsersByIDsParams{ IDs: userIDs, }) - if err != nil && !errors.Is(err, sql.ErrNoRows) { + if err != nil { return workspaceBuildsData{}, xerrors.Errorf("get users: %w", err) } @@ -676,7 +676,7 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.W } metadata, err := api.Database.GetWorkspaceResourceMetadataByResourceIDs(ctx, resourceIDs) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { return workspaceBuildsData{}, xerrors.Errorf("fetching resource metadata: %w", err) } From c38f55b27bbc63510c1e2dc7bf27722775e4a2e7 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 16:53:54 +0000 Subject: [PATCH 29/34] fix js tests --- site/src/testHelpers/entities.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 0192196a96e2e..598d37753a71f 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -211,6 +211,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", deadline: "2022-05-17T23:39:00.00Z", reason: "initiator", + resources: [], } export const MockFailedWorkspaceBuild = ( @@ -231,6 +232,7 @@ export const MockFailedWorkspaceBuild = ( workspace_id: "759f1d46-3174-453d-aa60-980a9c1442f3", deadline: "2022-05-17T23:39:00.00Z", reason: "initiator", + resources: [], }) export const MockWorkspaceBuildStop: TypesGen.WorkspaceBuild = { From eebd889db26f117c5b231f8623f126623051e650 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 16:55:19 +0000 Subject: [PATCH 30/34] revert webpack changes --- site/webpack.dev.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/site/webpack.dev.ts b/site/webpack.dev.ts index f35e0b3f02a56..9e59efa6aa3a9 100644 --- a/site/webpack.dev.ts +++ b/site/webpack.dev.ts @@ -74,8 +74,6 @@ const config: Configuration = { secure: false, }, }, - // To disable compression : - compress: false, static: ["./static"], }, From 86da07de8707de9e4ac42836809d55164df800e6 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 17:19:18 +0000 Subject: [PATCH 31/34] pr comments --- coderd/httpapi/httpapi.go | 4 ++-- codersdk/sse.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index 9a30494330bc6..79303adb5ea02 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -185,8 +185,8 @@ func ServerSentEventSender(rw http.ResponseWriter, r *http.Request) (func(ctx co }() sendEvent := func(ctx context.Context, sse codersdk.ServerSentEvent) error { - if r.Context().Err() != nil { - return r.Context().Err() + if ctx.Err() != nil { + return ctx.Err() } buf := &bytes.Buffer{} diff --git a/codersdk/sse.go b/codersdk/sse.go index 45852f57ff81d..39aaf71decb58 100644 --- a/codersdk/sse.go +++ b/codersdk/sse.go @@ -10,8 +10,8 @@ import ( ) type ServerSentEvent struct { - Type ServerSentEventType - Data interface{} + Type ServerSentEventType `json:"type"` + Data interface{} `json:"data"` } type ServerSentEventType string From ef3737234a5470740be2339a6ebd4cf64de88d0f Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 18:22:01 +0000 Subject: [PATCH 32/34] make gen --- site/src/api/typesGenerated.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ff875a6ccf755..8448099ea8678 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -370,9 +370,9 @@ export interface Role { // From codersdk/sse.go export interface ServerSentEvent { - readonly Type: ServerSentEventType + readonly type: ServerSentEventType // eslint-disable-next-line - readonly Data: any + readonly data: any } // From codersdk/templates.go From 026dd2f24137fc03c8c3be47ce6c082e76de2760 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 18:28:06 +0000 Subject: [PATCH 33/34] save some queries --- coderd/workspacebuilds.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 30024c824b3b6..8ab3222d37ac8 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -670,6 +670,13 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.W 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) @@ -685,6 +692,15 @@ func (api *API) workspaceBuildsData(ctx context.Context, workspaces []database.W 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) From 4e95a9964974e8e993372040d6f37cc34c6849f4 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 16 Sep 2022 18:36:32 +0000 Subject: [PATCH 34/34] always return slive --- coderd/workspacebuilds.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 8ab3222d37ac8..aa73a64e0e63a 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -812,10 +812,10 @@ func (api *API) convertWorkspaceBuild( } resources := resourcesByJobID[job.ID] - apiResources := []codersdk.WorkspaceResource{} + apiResources := make([]codersdk.WorkspaceResource, 0) for _, resource := range resources { agents := agentsByResourceID[resource.ID] - apiAgents := []codersdk.WorkspaceAgent{} + 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) @@ -824,7 +824,7 @@ func (api *API) convertWorkspaceBuild( } apiAgents = append(apiAgents, apiAgent) } - metadata := metadataByResourceID[resource.ID] + metadata := append(make([]database.WorkspaceResourceMetadatum, 0), metadataByResourceID[resource.ID]...) apiResources = append(apiResources, convertWorkspaceResource(resource, apiAgents, metadata)) }