From b5c5aa7565705c5eff688814582ced38eb6087af Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@coder.com> Date: Wed, 8 Jun 2022 10:15:45 -0500 Subject: [PATCH 1/4] feat: Add initiator_username to workspace builds in apis --- coderd/users.go | 9 +++++++ coderd/workspacebuilds.go | 49 +++++++++++++++++++++++++++++-------- coderd/workspaces.go | 10 +++++--- codersdk/workspacebuilds.go | 1 + 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/coderd/users.go b/coderd/users.go index 5ef87e7ead824..f892262e21ee6 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -912,3 +912,12 @@ func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]u member := organizationIDsByMemberIDsRows[0] return member.OrganizationIDs, nil } + +func findUser(id uuid.UUID, users []database.User) *database.User { + for _, u := range users { + if u.ID == id { + return &u + } + } + return nil +} diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 5a2f567572f84..e6505a5395c83 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -37,7 +37,7 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - owner, err := api.Database.GetUserByID(r.Context(), workspace.OwnerID) + users, err := api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{workspace.OwnerID, workspaceBuild.InitiatorID}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error fetching user.", @@ -46,7 +46,9 @@ func (api *API) workspaceBuild(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(owner, workspace, workspaceBuild, job)) + httpapi.Write(rw, http.StatusOK, + convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users), + workspace, workspaceBuild, job)) } func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { @@ -128,7 +130,11 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { jobByID[job.ID.String()] = job } - owner, err := api.Database.GetUserByID(r.Context(), workspace.OwnerID) + userIDs := []uuid.UUID{workspace.OwnerID} + for _, build := range builds { + userIDs = append(userIDs, build.InitiatorID) + } + users, err := api.Database.GetUsersByIDs(r.Context(), userIDs) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error fetching user.", @@ -146,7 +152,9 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { }) return } - apiBuilds = append(apiBuilds, convertWorkspaceBuild(owner, workspace, build, job)) + apiBuilds = append(apiBuilds, + convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users), + workspace, build, job)) } httpapi.Write(rw, http.StatusOK, apiBuilds) @@ -185,7 +193,7 @@ func (api *API) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { }) return } - owner, err := api.Database.GetUserByID(r.Context(), workspace.OwnerID) + users, err := api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{workspace.OwnerID, workspaceBuild.InitiatorID}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error getting user.", @@ -194,7 +202,9 @@ func (api *API) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(owner, workspace, workspaceBuild, job)) + httpapi.Write(rw, http.StatusOK, + convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users), + workspace, workspaceBuild, job)) } func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { @@ -368,7 +378,10 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { return } - owner, err := api.Database.GetUserByID(r.Context(), workspace.OwnerID) + users, err := api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{ + workspace.OwnerID, + workspaceBuild.InitiatorID, + }) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error getting user.", @@ -378,7 +391,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { } httpapi.Write(rw, http.StatusCreated, - convertWorkspaceBuild(owner, workspace, workspaceBuild, provisionerJob)) + convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users), + workspace, workspaceBuild, provisionerJob)) } func (api *API) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) { @@ -508,7 +522,8 @@ func (api *API) workspaceBuildState(rw http.ResponseWriter, r *http.Request) { } func convertWorkspaceBuild( - workspaceOwner database.User, + workspaceOwner *database.User, + buildInitiator *database.User, workspace database.Workspace, workspaceBuild database.WorkspaceBuild, job database.ProvisionerJob) codersdk.WorkspaceBuild { @@ -516,12 +531,25 @@ func convertWorkspaceBuild( 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: workspaceOwner.Username, + WorkspaceOwnerName: ownerName, WorkspaceID: workspaceBuild.WorkspaceID, WorkspaceName: workspace.Name, TemplateVersionID: workspaceBuild.TemplateVersionID, @@ -529,6 +557,7 @@ func convertWorkspaceBuild( Name: workspaceBuild.Name, Transition: codersdk.WorkspaceTransition(workspaceBuild.Transition), InitiatorID: workspaceBuild.InitiatorID, + InitiatorUsername: initiatorName, Job: convertProvisionerJob(job), Deadline: workspaceBuild.Deadline, } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 25fd67d581cba..3374394ad8fe8 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -702,16 +702,20 @@ func (api *API) watchWorkspace(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)) + userIDs := 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) + 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) } @@ -722,7 +726,7 @@ 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) + users, err := db.GetUsersByIDs(ctx, userIDs) if err != nil { return nil, xerrors.Errorf("get users: %w", err) } diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index ccf3e917f5d63..ec5dfcbc63ccd 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -34,6 +34,7 @@ type WorkspaceBuild struct { Name string `json:"name"` Transition WorkspaceTransition `json:"transition"` InitiatorID uuid.UUID `json:"initiator_id"` + InitiatorUsername string `json:"initiator_name"` Job ProvisionerJob `json:"job"` Deadline time.Time `json:"deadline"` } From 187c01f2fcd86689ef60fca5aa708e4f28921253 Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@coder.com> Date: Wed, 8 Jun 2022 10:45:17 -0500 Subject: [PATCH 2/4] feat: Convertworkspace fixed --- coderd/workspacebuilds_test.go | 11 +++++---- coderd/workspaces.go | 44 ++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 5b15f54c29d52..1734f52b836f9 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -33,15 +33,18 @@ func TestWorkspaceBuilds(t *testing.T) { t.Run("Single", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) - user := coderdtest.CreateFirstUser(t, client) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + first := coderdtest.CreateFirstUser(t, client) + user, err := client.User(context.Background(), codersdk.Me) + require.NoError(t, err, "fetch me") + version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID) coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID) builds, err := client.WorkspaceBuilds(context.Background(), codersdk.WorkspaceBuildsRequest{WorkspaceID: workspace.ID}) require.Len(t, builds, 1) require.Equal(t, int32(1), builds[0].BuildNumber) + require.Equal(t, user.Username, builds[0].InitiatorUsername) require.NoError(t, err) }) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 3374394ad8fe8..69a1a7c0dd8e0 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -73,7 +73,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { group errgroup.Group job database.ProvisionerJob template database.Template - owner database.User + users []database.User ) group.Go(func() (err error) { job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID) @@ -84,7 +84,7 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { return err }) group.Go(func() (err error) { - owner, err = api.Database.GetUserByID(r.Context(), workspace.OwnerID) + users, err = api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{workspace.OwnerID, build.InitiatorID}) return err }) err = group.Wait() @@ -96,7 +96,8 @@ func (api *API) workspace(rw http.ResponseWriter, r *http.Request) { return } - httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, build, job, template, owner)) + httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, build, job, template, + findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users))) } // workspaces returns all workspaces a user can read. @@ -210,7 +211,16 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) return } - httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, build, job, template, owner)) + initiator, err := api.Database.GetUserByID(r.Context(), build.InitiatorID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error fetching template.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace, build, job, template, &owner, &initiator)) } // Create a new workspace for the currently authenticated user. @@ -443,7 +453,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req }) return } - user, err := api.Database.GetUserByID(r.Context(), apiKey.UserID) + users, err := api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{apiKey.UserID, workspaceBuild.InitiatorID}) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error fetching user.", @@ -452,7 +462,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return } - httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, workspaceBuild, templateVersionJob, template, user)) + httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace, workspaceBuild, templateVersionJob, template, + findUser(apiKey.UserID, users), findUser(workspaceBuild.InitiatorID, users))) } func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) { @@ -669,7 +680,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { group errgroup.Group job database.ProvisionerJob template database.Template - owner database.User + users []database.User ) group.Go(func() (err error) { job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID) @@ -680,7 +691,7 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return err }) group.Go(func() (err error) { - owner, err = api.Database.GetUserByID(r.Context(), workspace.OwnerID) + users, err = api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{workspace.OwnerID, build.InitiatorID}) return err }) err = group.Wait() @@ -692,7 +703,8 @@ func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) { return } - _ = wsjson.Write(ctx, c, convertWorkspace(workspace, build, job, template, owner)) + _ = wsjson.Write(ctx, c, convertWorkspace(workspace, build, job, template, + findUser(workspace.OwnerID, users), findUser(build.InitiatorID, users))) case <-ctx.Done(): return } @@ -785,11 +797,15 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data if !exists { return nil, xerrors.Errorf("build job not found for workspace: %w", err) } - user, exists := userByID[workspace.OwnerID] + owner, exists := userByID[workspace.OwnerID] if !exists { return nil, xerrors.Errorf("owner not found for workspace: %q", workspace.Name) } - apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace, build, job, template, user)) + initiator, exists := userByID[build.InitiatorID] + if !exists { + return nil, xerrors.Errorf("build initiator not found for workspace: %q", workspace.Name) + } + apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace, build, job, template, &owner, &initiator)) } return apiWorkspaces, nil } @@ -798,7 +814,9 @@ func convertWorkspace( workspaceBuild database.WorkspaceBuild, job database.ProvisionerJob, template database.Template, - owner database.User) codersdk.Workspace { + owner *database.User, + initiator *database.User, +) codersdk.Workspace { var autostartSchedule *string if workspace.AutostartSchedule.Valid { autostartSchedule = &workspace.AutostartSchedule.String @@ -812,7 +830,7 @@ func convertWorkspace( OwnerID: workspace.OwnerID, OwnerName: owner.Username, TemplateID: workspace.TemplateID, - LatestBuild: convertWorkspaceBuild(owner, workspace, workspaceBuild, job), + LatestBuild: convertWorkspaceBuild(owner, initiator, workspace, workspaceBuild, job), TemplateName: template.Name, Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(), Name: workspace.Name, From 6bd8b29637078f8b63dc6eb1ec36b6d1f1fd43ae Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@coder.com> Date: Wed, 8 Jun 2022 11:15:40 -0500 Subject: [PATCH 3/4] Make gen --- site/src/api/typesGenerated.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 05432f7e90b1a..810135f8b7ff6 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -454,6 +454,7 @@ export interface WorkspaceBuild { readonly name: string readonly transition: WorkspaceTransition readonly initiator_id: string + readonly initiator_name: string readonly job: ProvisionerJob readonly deadline: string } From 9f5515fc6d0cac340e2d6ddf740317e36271ffb4 Mon Sep 17 00:00:00 2001 From: Steven Masley <stevenmasley@coder.com> Date: Wed, 8 Jun 2022 11:23:06 -0500 Subject: [PATCH 4/4] Update mock type --- site/src/testHelpers/entities.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index c58097d01d9be..53414283e4cc9 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -131,6 +131,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { created_at: "2022-05-17T17:39:01.382927298Z", id: "1", initiator_id: "", + initiator_name: "", job: MockProvisionerJob, name: "a-workspace-build", template_version_id: "",