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: "",