Skip to content

Commit 68b72ed

Browse files
AbhineetJainkylecarbs
authored andcommitted
feat: update build url to @username/workspace/builds/buildnumber (#2234)
* update build url to @username/workspace/builds/buildnumber * update errors thrown from the API * add unit tests for the new API * add t.parallel * get username and workspace name from params
1 parent 118cc4c commit 68b72ed

File tree

16 files changed

+305
-48
lines changed

16 files changed

+305
-48
lines changed

coderd/coderd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,10 @@ func New(options *Options) *API {
270270
r.Get("/", api.organizationsByUser)
271271
r.Get("/{organizationname}", api.organizationByUserAndName)
272272
})
273-
r.Get("/workspace/{workspacename}", api.workspaceByOwnerAndName)
273+
r.Route("/workspace/{workspacename}", func(r chi.Router) {
274+
r.Get("/", api.workspaceByOwnerAndName)
275+
r.Get("/builds/{buildnumber}", api.workspaceBuildByBuildNumber)
276+
})
274277
r.Get("/gitsshkey", api.gitSSHKey)
275278
r.Put("/gitsshkey", api.regenerateGitSSHKey)
276279
})

coderd/coderd_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"io"
66
"net/http"
7+
"strconv"
78
"strings"
89
"testing"
910
"time"
@@ -163,6 +164,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
163164
AssertObject: rbac.ResourceWorkspace,
164165
AssertAction: rbac.ActionRead,
165166
},
167+
"GET:/api/v2/users/me/workspace/{workspacename}/builds/{buildnumber}": {
168+
AssertObject: rbac.ResourceWorkspace,
169+
AssertAction: rbac.ActionRead,
170+
},
166171
"GET:/api/v2/workspaces/{workspace}/builds/{workspacebuildname}": {
167172
AssertAction: rbac.ActionRead,
168173
AssertObject: workspaceRBACObj,
@@ -388,6 +393,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
388393
route = strings.ReplaceAll(route, "{workspacename}", workspace.Name)
389394
route = strings.ReplaceAll(route, "{workspacebuildname}", workspace.LatestBuild.Name)
390395
route = strings.ReplaceAll(route, "{workspaceagent}", workspaceResources[0].Agents[0].ID.String())
396+
route = strings.ReplaceAll(route, "{buildnumber}", strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10))
391397
route = strings.ReplaceAll(route, "{template}", template.ID.String())
392398
route = strings.ReplaceAll(route, "{hash}", file.Hash)
393399
route = strings.ReplaceAll(route, "{workspaceresource}", workspaceResources[0].ID.String())

coderd/database/databasefake/databasefake.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,22 @@ func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndName(_ context.Context, a
625625
return database.WorkspaceBuild{}, sql.ErrNoRows
626626
}
627627

628+
func (q *fakeQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(_ context.Context, arg database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (database.WorkspaceBuild, error) {
629+
q.mutex.RLock()
630+
defer q.mutex.RUnlock()
631+
632+
for _, workspaceBuild := range q.workspaceBuilds {
633+
if workspaceBuild.WorkspaceID.String() != arg.WorkspaceID.String() {
634+
continue
635+
}
636+
if workspaceBuild.BuildNumber != arg.BuildNumber {
637+
continue
638+
}
639+
return workspaceBuild, nil
640+
}
641+
return database.WorkspaceBuild{}, sql.ErrNoRows
642+
}
643+
628644
func (q *fakeQuerier) GetWorkspacesByOrganizationIDs(_ context.Context, req database.GetWorkspacesByOrganizationIDsParams) ([]database.Workspace, error) {
629645
q.mutex.RLock()
630646
defer q.mutex.RUnlock()

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspacebuilds.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ WHERE
2727
workspace_id = $1
2828
AND "name" = $2;
2929

30+
-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one
31+
SELECT
32+
*
33+
FROM
34+
workspace_builds
35+
WHERE
36+
workspace_id = $1
37+
AND build_number = $2;
38+
3039
-- name: GetWorkspaceBuildByWorkspaceID :many
3140
SELECT
3241
*

coderd/workspacebuilds.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"net/http"
9+
"strconv"
910

1011
"github.com/go-chi/chi/v5"
1112
"github.com/google/uuid"
@@ -160,6 +161,82 @@ func (api *API) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
160161
httpapi.Write(rw, http.StatusOK, apiBuilds)
161162
}
162163

164+
func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Request) {
165+
owner := httpmw.UserParam(r)
166+
workspaceName := chi.URLParam(r, "workspacename")
167+
buildNumber, err := strconv.ParseInt(chi.URLParam(r, "buildnumber"), 10, 32)
168+
if err != nil {
169+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
170+
Message: "Failed to parse build number as integer.",
171+
Detail: err.Error(),
172+
})
173+
return
174+
}
175+
176+
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
177+
OwnerID: owner.ID,
178+
Name: workspaceName,
179+
})
180+
if errors.Is(err, sql.ErrNoRows) {
181+
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
182+
Message: fmt.Sprintf("Workspace %q does not exist.", workspaceName),
183+
})
184+
return
185+
}
186+
if err != nil {
187+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
188+
Message: "Internal error fetching workspace by name.",
189+
Detail: err.Error(),
190+
})
191+
return
192+
}
193+
194+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.
195+
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
196+
return
197+
}
198+
199+
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{
200+
WorkspaceID: workspace.ID,
201+
BuildNumber: int32(buildNumber),
202+
})
203+
if errors.Is(err, sql.ErrNoRows) {
204+
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
205+
Message: fmt.Sprintf("Workspace %q Build %d does not exist.", workspaceName, buildNumber),
206+
})
207+
return
208+
}
209+
if err != nil {
210+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
211+
Message: "Internal error fetching workspace build.",
212+
Detail: err.Error(),
213+
})
214+
return
215+
}
216+
217+
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
218+
if err != nil {
219+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
220+
Message: "Internal error fetching provisioner job.",
221+
Detail: err.Error(),
222+
})
223+
return
224+
}
225+
226+
users, err := api.Database.GetUsersByIDs(r.Context(), []uuid.UUID{workspace.OwnerID, workspaceBuild.InitiatorID})
227+
if err != nil {
228+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
229+
Message: "Internal error fetching user.",
230+
Detail: err.Error(),
231+
})
232+
return
233+
}
234+
235+
httpapi.Write(rw, http.StatusOK,
236+
convertWorkspaceBuild(findUser(workspace.OwnerID, users), findUser(workspaceBuild.InitiatorID, users),
237+
workspace, workspaceBuild, job))
238+
}
239+
163240
func (api *API) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
164241
workspace := httpmw.WorkspaceParam(r)
165242
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceWorkspace.

coderd/workspacebuilds_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package coderd_test
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
7+
"strconv"
68
"testing"
79
"time"
810

@@ -28,6 +30,94 @@ func TestWorkspaceBuild(t *testing.T) {
2830
require.NoError(t, err)
2931
}
3032

33+
func TestWorkspaceBuildByBuildNumber(t *testing.T) {
34+
t.Parallel()
35+
t.Run("Successful", func(t *testing.T) {
36+
t.Parallel()
37+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
38+
first := coderdtest.CreateFirstUser(t, client)
39+
user, err := client.User(context.Background(), codersdk.Me)
40+
require.NoError(t, err, "fetch me")
41+
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
42+
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
43+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
44+
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
45+
_, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
46+
context.Background(),
47+
user.Username,
48+
workspace.Name,
49+
strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
50+
)
51+
require.NoError(t, err)
52+
})
53+
54+
t.Run("BuildNumberNotInt", func(t *testing.T) {
55+
t.Parallel()
56+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
57+
first := coderdtest.CreateFirstUser(t, client)
58+
user, err := client.User(context.Background(), codersdk.Me)
59+
require.NoError(t, err, "fetch me")
60+
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
61+
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
62+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
63+
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
64+
_, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
65+
context.Background(),
66+
user.Username,
67+
workspace.Name,
68+
"buildNumber",
69+
)
70+
var apiError *codersdk.Error
71+
require.ErrorAs(t, err, &apiError)
72+
require.Equal(t, http.StatusBadRequest, apiError.StatusCode())
73+
require.ErrorContains(t, apiError, "Failed to parse build number as integer.")
74+
})
75+
76+
t.Run("WorkspaceNotFound", func(t *testing.T) {
77+
t.Parallel()
78+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
79+
first := coderdtest.CreateFirstUser(t, client)
80+
user, err := client.User(context.Background(), codersdk.Me)
81+
require.NoError(t, err, "fetch me")
82+
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
83+
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
84+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
85+
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
86+
_, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
87+
context.Background(),
88+
user.Username,
89+
"workspaceName",
90+
strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
91+
)
92+
var apiError *codersdk.Error
93+
require.ErrorAs(t, err, &apiError)
94+
require.Equal(t, http.StatusNotFound, apiError.StatusCode())
95+
require.ErrorContains(t, apiError, "Workspace \"workspaceName\" does not exist.")
96+
})
97+
98+
t.Run("WorkspaceBuildNotFound", func(t *testing.T) {
99+
t.Parallel()
100+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
101+
first := coderdtest.CreateFirstUser(t, client)
102+
user, err := client.User(context.Background(), codersdk.Me)
103+
require.NoError(t, err, "fetch me")
104+
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
105+
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
106+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
107+
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
108+
_, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
109+
context.Background(),
110+
user.Username,
111+
workspace.Name,
112+
"200",
113+
)
114+
var apiError *codersdk.Error
115+
require.ErrorAs(t, err, &apiError)
116+
require.Equal(t, http.StatusNotFound, apiError.StatusCode())
117+
require.ErrorContains(t, apiError, fmt.Sprintf("Workspace %q Build 200 does not exist.", workspace.Name))
118+
})
119+
}
120+
31121
func TestWorkspaceBuilds(t *testing.T) {
32122
t.Parallel()
33123
t.Run("Single", func(t *testing.T) {

codersdk/workspacebuilds.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,16 @@ func (c *Client) WorkspaceBuildState(ctx context.Context, build uuid.UUID) ([]by
103103
}
104104
return io.ReadAll(res.Body)
105105
}
106+
107+
func (c *Client) WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx context.Context, username string, workspaceName string, buildNumber string) (WorkspaceBuild, error) {
108+
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/workspace/%s/builds/%s", username, workspaceName, buildNumber), nil)
109+
if err != nil {
110+
return WorkspaceBuild{}, err
111+
}
112+
defer res.Body.Close()
113+
if res.StatusCode != http.StatusOK {
114+
return WorkspaceBuild{}, readBodyAsError(res)
115+
}
116+
var workspaceBuild WorkspaceBuild
117+
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
118+
}

site/src/AppRouter.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,6 @@ export const AppRouter: FC = () => (
113113
<Route path="ssh-keys" element={<SSHKeysPage />} />
114114
</Route>
115115

116-
<Route
117-
path="builds/:buildId"
118-
element={
119-
<AuthAndFrame>
120-
<WorkspaceBuildPage />
121-
</AuthAndFrame>
122-
}
123-
/>
124-
125116
<Route path="/@:username">
126117
<Route path=":workspace">
127118
<Route
@@ -160,6 +151,15 @@ export const AppRouter: FC = () => (
160151
}
161152
/>
162153
</Route>
154+
155+
<Route
156+
path="builds/:buildNumber"
157+
element={
158+
<AuthAndFrame>
159+
<WorkspaceBuildPage />
160+
</AuthAndFrame>
161+
}
162+
/>
163163
</Route>
164164
</Route>
165165

0 commit comments

Comments
 (0)
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