From d7451d068622eb79793d0bb77fa568976dd67839 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 10 Jun 2022 05:44:26 +0000 Subject: [PATCH 1/5] update build url to @username/workspace/builds/buildnumber --- coderd/coderd.go | 5 +- coderd/coderd_test.go | 6 ++ coderd/database/databasefake/databasefake.go | 16 +++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 35 ++++++++++ coderd/database/queries/workspacebuilds.sql | 9 +++ coderd/workspacebuilds.go | 65 +++++++++++++++++++ site/src/AppRouter.tsx | 18 ++--- site/src/api/api.ts | 10 ++- .../components/BuildsTable/BuildsTable.tsx | 6 +- site/src/components/Workspace/Workspace.tsx | 9 ++- .../WorkspaceBuildPage.test.tsx | 12 +++- .../WorkspaceBuildPage/WorkspaceBuildPage.tsx | 14 +--- .../src/pages/WorkspacePage/WorkspacePage.tsx | 1 + site/src/testHelpers/handlers.ts | 2 +- .../workspaceBuild/workspaceBuildXService.ts | 40 ++++++------ 16 files changed, 200 insertions(+), 49 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 6e88759fa340a..2cd94d1eac646 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -270,7 +270,10 @@ func New(options *Options) *API { r.Get("/", api.organizationsByUser) r.Get("/{organizationname}", api.organizationByUserAndName) }) - r.Get("/workspace/{workspacename}", api.workspaceByOwnerAndName) + r.Route("/workspace/{workspacename}", func(r chi.Router) { + r.Get("/", api.workspaceByOwnerAndName) + r.Get("/builds/{buildnumber}", api.workspaceBuildByBuildNumber) + }) r.Get("/gitsshkey", api.gitSSHKey) r.Put("/gitsshkey", api.regenerateGitSSHKey) }) diff --git a/coderd/coderd_test.go b/coderd/coderd_test.go index 02fba90966992..7053b4ff56cb8 100644 --- a/coderd/coderd_test.go +++ b/coderd/coderd_test.go @@ -4,6 +4,7 @@ import ( "context" "io" "net/http" + "strconv" "strings" "testing" "time" @@ -163,6 +164,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) { AssertObject: rbac.ResourceWorkspace, AssertAction: rbac.ActionRead, }, + "GET:/api/v2/users/me/workspace/{workspacename}/builds/{buildnumber}": { + AssertObject: rbac.ResourceWorkspace, + AssertAction: rbac.ActionRead, + }, "GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": { AssertAction: rbac.ActionRead, AssertObject: workspaceRBACObj, @@ -388,6 +393,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { route = strings.ReplaceAll(route, "{workspacename}", workspace.Name) route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name) route = strings.ReplaceAll(route, "{workspaceagent}", workspaceResources[0].Agents[0].ID.String()) + route = strings.ReplaceAll(route, "{buildnumber}", strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10)) route = strings.ReplaceAll(route, "{template}", template.ID.String()) route = strings.ReplaceAll(route, "{hash}", file.Hash) route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String()) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 78f85e71c597c..3409d3041e739 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -616,6 +616,22 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a return database.WorkspaceBuild{}, sql.ErrNoRows } +func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, workspaceBuild := range q.workspaceBuilds { + if workspaceBuild.WorkspaceID.String() != arg.WorkspaceID.String() { + continue + } + if workspaceBuild.BuildNumber != arg.BuildNumber { + continue + } + return workspaceBuild, nil + } + return database.WorkspaceBuild{}, sql.ErrNoRows +} + func (q *fakeQuerier) GetWorkspacesByOrganizationIDs(_ context.Context, req database.GetWorkspacesByOrganizationIDsParams) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 59e8f4577ef8e..c3f57d3a9f795 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -70,6 +70,7 @@ type querier interface { GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDParams) ([]WorkspaceBuild, error) + GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceIDAndName(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndNameParams) (WorkspaceBuild, error) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Workspace, error) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWorkspaceByOwnerIDAndNameParams) (Workspace, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 06e5f61105269..e83051d65e667 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3212,6 +3212,41 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceID(ctx context.Context, arg Get return items, nil } +const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one +SELECT + id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline +FROM + workspace_builds +WHERE + workspace_id = $1 + AND build_number = $2 +` + +type GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams struct { + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + BuildNumber int32 `db:"build_number" json:"build_number"` +} + +func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceBuildByWorkspaceIDAndBuildNumber, arg.WorkspaceID, arg.BuildNumber) + var i WorkspaceBuild + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.WorkspaceID, + &i.TemplateVersionID, + &i.Name, + &i.BuildNumber, + &i.Transition, + &i.InitiatorID, + &i.ProvisionerState, + &i.JobID, + &i.Deadline, + ) + return i, err +} + const getWorkspaceBuildByWorkspaceIDAndName = `-- name: GetWorkspaceBuildByWorkspaceIDAndName :one SELECT id, created_at, updated_at, workspace_id, template_version_id, name, build_number, transition, initiator_id, provisioner_state, job_id, deadline diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index 5b53a874060e8..4bace271cb125 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -27,6 +27,15 @@ WHERE workspace_id = $1 AND "name" = $2; +-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one +SELECT + * +FROM + workspace_builds +WHERE + workspace_id = $1 + AND build_number = $2; + -- name: GetWorkspaceBuildByWorkspaceID :many SELECT * diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index e6505a5395c83..54467a6236801 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "github.com/go-chi/chi/v5" "github.com/google/uuid" @@ -160,6 +161,70 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, apiBuilds) } +func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Request) { + owner := httpmw.UserParam(r) + workspaceName := chi.URLParam(r, "workspacename") + buildNumber, err := strconv.ParseInt(chi.URLParam(r, "buildnumber"), 10, 32) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error parsing build number as integer.", + Detail: err.Error(), + }) + return + } + + workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{ + OwnerID: owner.ID, + Name: workspaceName, + }) + + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error fetching workspace by name.", + Detail: err.Error(), + }) + return + } + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. + InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { + return + } + + workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{ + WorkspaceID: workspace.ID, + BuildNumber: int32(buildNumber), + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error fetching workspace build.", + Detail: err.Error(), + }) + return + } + + job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: "Internal error fetching provisioner job.", + Detail: err.Error(), + }) + return + } + + 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.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(rw, http.StatusOK, + convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users), + workspace, workspaceBuild, job)) +} + func (api *API) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) { workspace := httpmw.WorkspaceParam(r) if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 8c8880c9de408..253a3c8e8c63f 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -113,15 +113,6 @@ export const AppRouter: FC = () => ( } /> - - - - } - /> - ( } /> + + + + + } + /> diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 39de3d7aff046..205f42fef8c23 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -268,8 +268,14 @@ export const getWorkspaceBuilds = async (workspaceId: string): Promise => { - const response = await axios.get(`/api/v2/workspacebuilds/${workspaceId}`) +export const getWorkspaceBuildByNumber = async ( + username = "me", + workspaceName: string, + buildNumber: string, +): Promise => { + const response = await axios.get( + `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, + ) return response.data } diff --git a/site/src/components/BuildsTable/BuildsTable.tsx b/site/src/components/BuildsTable/BuildsTable.tsx index 2e924c8157f4f..a59c867ca8cd5 100644 --- a/site/src/components/BuildsTable/BuildsTable.tsx +++ b/site/src/components/BuildsTable/BuildsTable.tsx @@ -26,9 +26,11 @@ export const Language = { export interface BuildsTableProps { builds?: TypesGen.WorkspaceBuild[] className?: string + username: string + workspaceName: string } -export const BuildsTable: FC = ({ builds, className }) => { +export const BuildsTable: FC = ({ builds, className, username, workspaceName }) => { const isLoading = !builds const theme: Theme = useTheme() const navigate = useNavigate() @@ -52,7 +54,7 @@ export const BuildsTable: FC = ({ builds, className }) => { const status = getDisplayWorkspaceBuildStatus(theme, build) const navigateToBuildPage = () => { - navigate(`/builds/${build.id}`) + navigate(`/@${username}/${workspaceName}/builds/${build.build_number}`) } return ( diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 2e7338c5f7c71..2e971bd079614 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -29,6 +29,7 @@ export interface WorkspaceProps { getResourcesError?: Error builds?: TypesGen.WorkspaceBuild[] canUpdateWorkspace: boolean + username: string } /** @@ -46,6 +47,7 @@ export const Workspace: FC = ({ getResourcesError, builds, canUpdateWorkspace, + username, }) => { const styles = useStyles() const navigate = useNavigate() @@ -91,7 +93,12 @@ export const Workspace: FC = ({ )} - + diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx index cebff84da793d..45d13caa88496 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx @@ -1,6 +1,11 @@ import { screen } from "@testing-library/react" import * as API from "../../api/api" -import { MockWorkspaceBuild, MockWorkspaceBuildLogs, renderWithAuth } from "../../testHelpers/renderHelpers" +import { + MockWorkspace, + MockWorkspaceBuild, + MockWorkspaceBuildLogs, + renderWithAuth, +} from "../../testHelpers/renderHelpers" import { WorkspaceBuildPage } from "./WorkspaceBuildPage" describe("WorkspaceBuildPage", () => { @@ -16,7 +21,10 @@ describe("WorkspaceBuildPage", () => { closed: Promise.resolve(undefined), cancel: jest.fn(), }) - renderWithAuth(, { route: `/builds/${MockWorkspaceBuild.id}`, path: "/builds/:buildId" }) + renderWithAuth(, { + route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`, + path: "/@:username/:workspace/builds/:buildNumber", + }) await screen.findByText(MockWorkspaceBuild.workspace_name) await screen.findByText(MockWorkspaceBuildLogs[0].stage) diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 46550ac62225a..9d570db458dfe 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -6,19 +6,9 @@ import { pageTitle } from "../../util/page" import { workspaceBuildMachine } from "../../xServices/workspaceBuild/workspaceBuildXService" import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView" -const useBuildId = () => { - const { buildId } = useParams() - - if (!buildId) { - throw new Error("buildId param is required.") - } - - return buildId -} - export const WorkspaceBuildPage: FC = () => { - const buildId = useBuildId() - const [buildState] = useMachine(workspaceBuildMachine, { context: { buildId } }) + const { username, workspace: workspaceName, buildNumber } = useParams() + const [buildState] = useMachine(workspaceBuildMachine, { context: { username, workspaceName, buildNumber } }) const { logs, build } = buildState.context const isWaitingForLogs = !buildState.matches("logs.loaded") diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index cbc4929646395..94facef4018e9 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -68,6 +68,7 @@ export const WorkspacePage: React.FC = () => { getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined} builds={builds} canUpdateWorkspace={canUpdateWorkspace} + username={username || "me"} /> { return res(ctx.status(200), ctx.json(M.MockBuilds)) }), - rest.get("/api/v2/workspacebuilds/:workspaceBuildId", (req, res, ctx) => { + rest.get("/api/v2/users/:username/workspace/:workspaceName/builds/:buildNumber", (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockWorkspaceBuild)) }), rest.get("/api/v2/workspacebuilds/:workspaceBuildId/resources", (req, res, ctx) => { diff --git a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts index 88008892fe251..81883b3eaaa08 100644 --- a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts +++ b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts @@ -4,6 +4,9 @@ import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated" type LogsContext = { // Build + username: string + workspaceName: string + buildNumber: string buildId: string build?: WorkspaceBuild getBuildError?: Error | unknown @@ -36,28 +39,23 @@ export const workspaceBuildMachine = createMachine( }, }, tsTypes: {} as import("./workspaceBuildXService.typegen").Typegen0, - type: "parallel", + initial: "gettingBuild", states: { - build: { - initial: "gettingBuild", - states: { - gettingBuild: { - entry: "clearGetBuildError", - invoke: { - src: "getWorkspaceBuild", - onDone: { - target: "idle", - actions: "assignBuild", - }, - onError: { - target: "idle", - actions: "assignGetBuildError", - }, - }, + gettingBuild: { + entry: "clearGetBuildError", + invoke: { + src: "getWorkspaceBuild", + onDone: { + target: "logs", + actions: ["assignBuild", "assignBuildId"], + }, + onError: { + target: "idle", + actions: "assignGetBuildError", }, - idle: {}, }, }, + idle: {}, logs: { initial: "gettingExistentLogs", states: { @@ -95,6 +93,10 @@ export const workspaceBuildMachine = createMachine( }, { actions: { + // Build ID + assignBuildId: assign({ + buildId: (_, event) => event.data.id, + }), // Build assignBuild: assign({ build: (_, event) => event.data, @@ -117,7 +119,7 @@ export const workspaceBuildMachine = createMachine( }), }, services: { - getWorkspaceBuild: (ctx) => API.getWorkspaceBuild(ctx.buildId), + getWorkspaceBuild: (ctx) => API.getWorkspaceBuildByNumber(ctx.username, ctx.workspaceName, ctx.buildNumber), getLogs: async (ctx) => API.getWorkspaceBuildLogs(ctx.buildId), streamWorkspaceBuildLogs: (ctx) => async (callback) => { const reader = await API.streamWorkspaceBuildLogs(ctx.buildId) From 318224c514d027438d485ded305508e63b62e33a Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 10 Jun 2022 13:35:19 +0000 Subject: [PATCH 2/5] update errors thrown from the API --- coderd/workspacebuilds.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 54467a6236801..1d98ae56aabdc 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -166,8 +166,8 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ workspaceName := chi.URLParam(r, "workspacename") buildNumber, err := strconv.ParseInt(chi.URLParam(r, "buildnumber"), 10, 32) if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: "Internal error parsing build number as integer.", + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "Failed to parse build number as integer.", Detail: err.Error(), }) return @@ -177,7 +177,12 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ OwnerID: owner.ID, Name: workspaceName, }) - + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("Workspace %q does not exist.", workspaceName), + }) + return + } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error fetching workspace by name.", @@ -185,6 +190,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ }) return } + if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace. InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) { return @@ -194,6 +200,12 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ WorkspaceID: workspace.ID, BuildNumber: int32(buildNumber), }) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: fmt.Sprintf("Workspace %q Build %q does not exist.", workspaceName, buildNumber), + }) + return + } if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ Message: "Internal error fetching workspace build.", From e60aa38aea4612146610dbd43855ade993cce4f5 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 10 Jun 2022 15:16:32 +0000 Subject: [PATCH 3/5] add unit tests for the new API --- coderd/workspacebuilds.go | 2 +- coderd/workspacebuilds_test.go | 86 ++++++++++++++++++++++++++++++++++ codersdk/workspacebuilds.go | 13 +++++ 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 1d98ae56aabdc..cefe4ded6cdc6 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -202,7 +202,7 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ }) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ - Message: fmt.Sprintf("Workspace %q Build %q does not exist.", workspaceName, buildNumber), + Message: fmt.Sprintf("Workspace %q Build %d does not exist.", workspaceName, buildNumber), }) return } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 1734f52b836f9..c92403413b2ea 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -2,7 +2,9 @@ package coderd_test import ( "context" + "fmt" "net/http" + "strconv" "testing" "time" @@ -28,6 +30,90 @@ func TestWorkspaceBuild(t *testing.T) { require.NoError(t, err) } +func TestWorkspaceBuildByBuildNumber(t *testing.T) { + t.Parallel() + t.Run("Successful", func(t *testing.T) { + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + 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, first.OrganizationID, template.ID) + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + context.Background(), + user.Username, + workspace.Name, + strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10), + ) + require.NoError(t, err) + }) + + t.Run("BuildNumberNotInt", func(t *testing.T) { + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + 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, first.OrganizationID, template.ID) + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + context.Background(), + user.Username, + workspace.Name, + "buildNumber", + ) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusBadRequest, apiError.StatusCode()) + require.ErrorContains(t, apiError, "Failed to parse build number as integer.") + }) + + t.Run("WorkspaceNotFound", func(t *testing.T) { + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + 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, first.OrganizationID, template.ID) + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + context.Background(), + user.Username, + "workspaceName", + strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10), + ) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusNotFound, apiError.StatusCode()) + require.ErrorContains(t, apiError, "Workspace \"workspaceName\" does not exist.") + }) + + t.Run("WorkspaceBuildNotFound", func(t *testing.T) { + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) + 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, first.OrganizationID, template.ID) + _, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber( + context.Background(), + user.Username, + workspace.Name, + "200", + ) + var apiError *codersdk.Error + require.ErrorAs(t, err, &apiError) + require.Equal(t, http.StatusNotFound, apiError.StatusCode()) + require.ErrorContains(t, apiError, fmt.Sprintf("Workspace %q Build 200 does not exist.", workspace.Name)) + }) +} + func TestWorkspaceBuilds(t *testing.T) { t.Parallel() t.Run("Single", func(t *testing.T) { diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index ec5dfcbc63ccd..79dae16d8f12c 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -103,3 +103,16 @@ func (c *Client) WorkspaceBuildState(ctx context.Context, build uuid.UUID) ([]by } return io.ReadAll(res.Body) } + +func (c *Client) WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx context.Context, username string, workspaceName string, buildNumber string) (WorkspaceBuild, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspace/%s/builds/%s", username, workspaceName, buildNumber), nil) + if err != nil { + return WorkspaceBuild{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return WorkspaceBuild{}, readBodyAsError(res) + } + var workspaceBuild WorkspaceBuild + return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild) +} From 4c22d126431e2f4e5ccd31b2d01f567008f82212 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 10 Jun 2022 15:21:48 +0000 Subject: [PATCH 4/5] add t.parallel --- coderd/workspacebuilds_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index c92403413b2ea..3833cee09171c 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -33,6 +33,7 @@ func TestWorkspaceBuild(t *testing.T) { func TestWorkspaceBuildByBuildNumber(t *testing.T) { t.Parallel() t.Run("Successful", func(t *testing.T) { + t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) @@ -51,6 +52,7 @@ func TestWorkspaceBuildByBuildNumber(t *testing.T) { }) t.Run("BuildNumberNotInt", func(t *testing.T) { + t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) @@ -72,6 +74,7 @@ func TestWorkspaceBuildByBuildNumber(t *testing.T) { }) t.Run("WorkspaceNotFound", func(t *testing.T) { + t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) @@ -93,6 +96,7 @@ func TestWorkspaceBuildByBuildNumber(t *testing.T) { }) t.Run("WorkspaceBuildNotFound", func(t *testing.T) { + t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true}) first := coderdtest.CreateFirstUser(t, client) user, err := client.User(context.Background(), codersdk.Me) From d127aed8b8ad8b2b937a39572eb07e5f074febb1 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 10 Jun 2022 15:47:04 +0000 Subject: [PATCH 5/5] get username and workspace name from params --- site/src/components/BuildsTable/BuildsTable.tsx | 7 +++---- site/src/components/Workspace/Workspace.tsx | 9 +-------- site/src/pages/WorkspacePage/WorkspacePage.tsx | 1 - 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/site/src/components/BuildsTable/BuildsTable.tsx b/site/src/components/BuildsTable/BuildsTable.tsx index a59c867ca8cd5..c9de9bd6d14ea 100644 --- a/site/src/components/BuildsTable/BuildsTable.tsx +++ b/site/src/components/BuildsTable/BuildsTable.tsx @@ -8,7 +8,7 @@ import TableRow from "@material-ui/core/TableRow" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" import useTheme from "@material-ui/styles/useTheme" import { FC } from "react" -import { useNavigate } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { displayWorkspaceBuildDuration, getDisplayWorkspaceBuildStatus } from "../../util/workspace" import { EmptyState } from "../EmptyState/EmptyState" @@ -26,11 +26,10 @@ export const Language = { export interface BuildsTableProps { builds?: TypesGen.WorkspaceBuild[] className?: string - username: string - workspaceName: string } -export const BuildsTable: FC = ({ builds, className, username, workspaceName }) => { +export const BuildsTable: FC = ({ builds, className }) => { + const { username, workspace: workspaceName } = useParams() const isLoading = !builds const theme: Theme = useTheme() const navigate = useNavigate() diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 2e971bd079614..2e7338c5f7c71 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -29,7 +29,6 @@ export interface WorkspaceProps { getResourcesError?: Error builds?: TypesGen.WorkspaceBuild[] canUpdateWorkspace: boolean - username: string } /** @@ -47,7 +46,6 @@ export const Workspace: FC = ({ getResourcesError, builds, canUpdateWorkspace, - username, }) => { const styles = useStyles() const navigate = useNavigate() @@ -93,12 +91,7 @@ export const Workspace: FC = ({ )} - + diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 94facef4018e9..cbc4929646395 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -68,7 +68,6 @@ export const WorkspacePage: React.FC = () => { getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined} builds={builds} canUpdateWorkspace={canUpdateWorkspace} - username={username || "me"} /> pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy