From dbd5b4a47b134badd74b497ef7b68af6ab9c08e5 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 13 May 2022 20:41:21 -0500 Subject: [PATCH] feat: Add workspace owner name to response (#1448) This will be rendered in the workspace page! --- cli/templatedelete_test.go | 3 +- coderd/database/databasefake/databasefake.go | 16 ++++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 36 ++++++++++++ .../database/queries/organizationmembers.sql | 2 +- coderd/database/queries/users.sql | 3 + coderd/organizations.go | 11 +++- coderd/workspaces.go | 55 ++++++++++++++----- codersdk/workspaces.go | 1 + site/src/api/typesGenerated.ts | 7 ++- site/src/testHelpers/entities.ts | 1 + 11 files changed, 116 insertions(+), 20 deletions(-) diff --git a/cli/templatedelete_test.go b/cli/templatedelete_test.go index d374585650..95328f44f9 100644 --- a/cli/templatedelete_test.go +++ b/cli/templatedelete_test.go @@ -4,11 +4,12 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" + "github.com/coder/coder/cli/clitest" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" "github.com/coder/coder/pty/ptytest" - "github.com/stretchr/testify/require" ) func TestTemplateDelete(t *testing.T) { diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 68bf74bee4..ae24c55cb8 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -242,6 +242,22 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams return users, nil } +func (q *fakeQuerier) GetUsersByIDs(_ context.Context, ids []uuid.UUID) ([]database.User, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + users := make([]database.User, 0) + for _, user := range q.users { + for _, id := range ids { + if user.ID.String() != id.String() { + continue + } + users = append(users, user) + } + } + return users, nil +} + func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (database.GetAllUserRolesRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 088ed4f0c3..9787aca37e 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -54,6 +54,7 @@ type querier interface { GetUserByID(ctx context.Context, id uuid.UUID) (User, error) GetUserCount(ctx context.Context) (int64, error) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, error) + GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e7527c2218..c6d8538a42 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2251,6 +2251,42 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, return items, nil } +const getUsersByIDs = `-- name: GetUsersByIDs :many +SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles FROM users WHERE id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getUsersByIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.HashedPassword, + &i.CreatedAt, + &i.UpdatedAt, + &i.Status, + pq.Array(&i.RBACRoles), + ); 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 insertUser = `-- name: InsertUser :one INSERT INTO users ( diff --git a/coderd/database/queries/organizationmembers.sql b/coderd/database/queries/organizationmembers.sql index 34d1d8635c..8877b77b18 100644 --- a/coderd/database/queries/organizationmembers.sql +++ b/coderd/database/queries/organizationmembers.sql @@ -49,4 +49,4 @@ SET WHERE user_id = @user_id AND organization_id = @org_id -RETURNING *; \ No newline at end of file +RETURNING *; diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index c3d72b3637..d9f68ec440 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -8,6 +8,9 @@ WHERE LIMIT 1; +-- name: GetUsersByIDs :many +SELECT * FROM users WHERE id = ANY(@ids :: uuid [ ]); + -- name: GetUserByEmailOrUsername :one SELECT * diff --git a/coderd/organizations.go b/coderd/organizations.go index 1796c8f985..43bdf5432d 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -426,7 +426,7 @@ func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) } httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, - convertWorkspaceBuild(build, convertProvisionerJob(job)), template)) + convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner)) } // Create a new workspace for the currently authenticated user. @@ -617,9 +617,16 @@ func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req }) return } + user, err := api.Database.GetUserByID(r.Context(), apiKey.UserID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get user: %s", err), + }) + return + } httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, - convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template)) + convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template, user)) } // convertOrganization consumes the database representation and outputs an API friendly representation. diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 428a9cf774..704ee42177 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -11,6 +11,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/moby/moby/pkg/namesgenerator" + "golang.org/x/sync/errgroup" "golang.org/x/xerrors" "github.com/coder/coder/coderd/autobuild/schedule" @@ -30,21 +31,34 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) { }) return } - job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID) + var ( + group errgroup.Group + job database.ProvisionerJob + template database.Template + owner 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) { + owner, err = api.Database.GetUserByID(r.Context(), workspace.OwnerID) + return err + }) + err = group.Wait() if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get workspace build job: %s", err), + Message: fmt.Sprintf("fetch resource: %s", err), }) return } - template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get template: %s", err), - }) - return - } - httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template)) + + httpapi.Write(rw, http.StatusOK, + convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner)) } func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { @@ -359,9 +373,11 @@ func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) { func convertWorkspaces(ctx context.Context, db database.Store, workspaces []database.Workspace) ([]codersdk.Workspace, error) { workspaceIDs := make([]uuid.UUID, 0, len(workspaces)) templateIDs := make([]uuid.UUID, 0, len(workspaces)) + ownerIDs := make([]uuid.UUID, 0, len(workspaces)) for _, workspace := range workspaces { workspaceIDs = append(workspaceIDs, workspace.ID) templateIDs = append(templateIDs, workspace.TemplateID) + ownerIDs = append(ownerIDs, workspace.OwnerID) } workspaceBuilds, err := db.GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(ctx, workspaceIDs) if errors.Is(err, sql.ErrNoRows) { @@ -377,6 +393,10 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data if err != nil { return nil, xerrors.Errorf("get templates: %w", err) } + users, err := db.GetUsersByIDs(ctx, ownerIDs) + 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) @@ -397,6 +417,10 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data for _, template := range templates { templateByID[template.ID] = template } + 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 @@ -413,20 +437,25 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data } job, exists := jobByID[build.JobID] if !exists { - return nil, xerrors.Errorf("build job not found for workspace: %q", err) + return nil, xerrors.Errorf("build job not found for workspace: %w", err) + } + user, exists := userByID[workspace.OwnerID] + if !exists { + return nil, xerrors.Errorf("owner not found for workspace: %q", workspace.Name) } apiWorkspaces = append(apiWorkspaces, - convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template)) + convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, user)) } return apiWorkspaces, nil } -func convertWorkspace(workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template) codersdk.Workspace { +func convertWorkspace(workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template, owner database.User) codersdk.Workspace { return codersdk.Workspace{ ID: workspace.ID, CreatedAt: workspace.CreatedAt, UpdatedAt: workspace.UpdatedAt, OwnerID: workspace.OwnerID, + OwnerName: owner.Username, TemplateID: workspace.TemplateID, LatestBuild: workspaceBuild, TemplateName: template.Name, diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 2fa5dd4b76..644f90422f 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -20,6 +20,7 @@ type Workspace struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` OwnerID uuid.UUID `json:"owner_id"` + OwnerName string `json:"owner_name"` TemplateID uuid.UUID `json:"template_id"` TemplateName string `json:"template_name"` LatestBuild WorkspaceBuild `json:"latest_build"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6e11d55276..7f75b89687 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -85,7 +85,7 @@ export interface CreateUserRequest { readonly organization_id: string } -// From codersdk/workspaces.go:33:6 +// From codersdk/workspaces.go:34:6 export interface CreateWorkspaceBuildRequest { readonly template_version_id?: string // This is likely an enum in an external package ("github.com/coder/coder/coderd/database.WorkspaceTransition") @@ -289,12 +289,12 @@ export interface UpdateUserProfileRequest { readonly username: string } -// From codersdk/workspaces.go:95:6 +// From codersdk/workspaces.go:96:6 export interface UpdateWorkspaceAutostartRequest { readonly schedule: string } -// From codersdk/workspaces.go:115:6 +// From codersdk/workspaces.go:116:6 export interface UpdateWorkspaceAutostopRequest { readonly schedule: string } @@ -355,6 +355,7 @@ export interface Workspace { readonly created_at: string readonly updated_at: string readonly owner_id: string + readonly owner_name: string readonly template_id: string readonly template_name: string readonly latest_build: WorkspaceBuild diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 6d6139c9e1..32174ec7c7 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -124,6 +124,7 @@ export const MockWorkspace: TypesGen.Workspace = { template_name: MockTemplate.name, outdated: false, owner_id: MockUser.id, + owner_name: MockUser.username, autostart_schedule: MockWorkspaceAutostartEnabled.schedule, autostop_schedule: MockWorkspaceAutostopEnabled.schedule, latest_build: MockWorkspaceBuild,