Skip to content

Commit debc29e

Browse files
committed
Move workspace routes
1 parent 4c70d20 commit debc29e

File tree

8 files changed

+143
-105
lines changed

8 files changed

+143
-105
lines changed

coderd/coderd.go

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -66,36 +66,31 @@ func New(options *Options) http.Handler {
6666
r.Get("/", projects.allProjectHistory)
6767
r.Post("/", projects.createProjectHistory)
6868
})
69+
r.Get("/workspaces", workspaces.allWorkspacesForProject)
6970
})
7071
})
7172
})
7273

74+
// Routes specific to the workspace namespace.
75+
// Listing operations specific to resources should go under
76+
// their respective routes. eg. /orgs/<name>/workspaces
7377
r.Route("/workspaces", func(r chi.Router) {
7478
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
75-
r.Get("/", workspaces.allWorkspaces)
76-
r.Route("/{organization}", func(r chi.Router) {
77-
r.Use(httpmw.ExtractOrganizationParam(options.Database))
78-
r.Route("/{project}", func(r chi.Router) {
79-
r.Use(httpmw.ExtractProjectParam(options.Database))
80-
r.Get("/", workspaces.allWorkspacesForProject)
81-
r.Post("/", workspaces.createWorkspace)
79+
r.Get("/", workspaces.listAllWorkspaces)
80+
r.Route("/{user}", func(r chi.Router) {
81+
r.Use(httpmw.ExtractUserParam(options.Database))
82+
r.Post("/", workspaces.createWorkspaceForUser)
83+
r.Route("/{workspace}", func(r chi.Router) {
84+
r.Use(httpmw.ExtractWorkspaceParam(options.Database))
85+
r.Get("/", workspaces.singleWorkspace)
86+
r.Route("/history", func(r chi.Router) {
87+
r.Post("/", workspaces.createWorkspaceHistory)
88+
r.Get("/", workspaces.listAllWorkspaceHistory)
89+
r.Get("/latest", workspaces.latestWorkspaceHistory)
90+
})
8291
})
8392
})
8493
})
85-
86-
r.Route("/workspace/{user}/{workspace}", func(r chi.Router) {
87-
r.Use(
88-
httpmw.ExtractAPIKey(options.Database, nil),
89-
httpmw.ExtractUserParam(options.Database),
90-
httpmw.ExtractWorkspaceParam(options.Database),
91-
)
92-
r.Get("/", workspaces.workspace)
93-
r.Route("/history", func(r chi.Router) {
94-
r.Post("/", workspaces.createWorkspaceBuild)
95-
r.Get("/", workspaces.allWorkspaceHistory)
96-
r.Get("/latest", workspaces.latestWorkspaceHistory)
97-
})
98-
})
9994
})
10095
r.NotFound(site.Handler().ServeHTTP)
10196
return r

coderd/workspaces.go

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ import (
1616
"github.com/coder/coder/httpmw"
1717
)
1818

19-
// Workspace is the JSON representation of a Coder workspace.
20-
// This type matches the database object for now, but is
21-
// abstract for ease of change later on.
19+
// Workspace is a per-user deployment of a project. It tracks
20+
// project versions, and can be updated.
2221
type Workspace database.Workspace
2322

24-
// WorkspaceHistory is the JSON representation of a workspace transitioning
25-
// from state-to-state.
23+
// WorkspaceHistory is an at-point representation of a workspace state.
24+
// Iterate on before/after to determine a chronological history.
2625
type WorkspaceHistory struct {
2726
ID uuid.UUID `json:"id"`
2827
CreatedAt time.Time `json:"created_at"`
@@ -36,13 +35,14 @@ type WorkspaceHistory struct {
3635
Initiator string `json:"initiator"`
3736
}
3837

39-
// CreateWorkspaceRequest enables callers to create a new Workspace.
38+
// CreateWorkspaceRequest provides options for creating a new workspace.
4039
type CreateWorkspaceRequest struct {
41-
Name string `json:"name" validate:"username,required"`
40+
ProjectID uuid.UUID `json:"project_id" validate:"required"`
41+
Name string `json:"name" validate:"username,required"`
4242
}
4343

44-
// CreateWorkspaceBuildRequest enables callers to create a new workspace build.
45-
type CreateWorkspaceBuildRequest struct {
44+
// CreateWorkspaceHistoryRequest provides options to update the latest workspace history.
45+
type CreateWorkspaceHistoryRequest struct {
4646
ProjectHistoryID uuid.UUID `json:"project_history_id" validate:"required"`
4747
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
4848
}
@@ -51,8 +51,8 @@ type workspaces struct {
5151
Database database.Store
5252
}
5353

54-
// allWorkspaces lists all workspaces for the currently authenticated user.
55-
func (w *workspaces) allWorkspaces(rw http.ResponseWriter, r *http.Request) {
54+
// Returns all workspaces across all projects and organizations.
55+
func (w *workspaces) listAllWorkspaces(rw http.ResponseWriter, r *http.Request) {
5656
apiKey := httpmw.APIKey(r)
5757
workspaces, err := w.Database.GetWorkspacesByUserID(r.Context(), apiKey.UserID)
5858
if errors.Is(err, sql.ErrNoRows) {
@@ -73,7 +73,7 @@ func (w *workspaces) allWorkspaces(rw http.ResponseWriter, r *http.Request) {
7373
render.JSON(rw, r, apiWorkspaces)
7474
}
7575

76-
// allWorkspacesForProject lists all projects for the parameterized project.
76+
// Returns all workspaces for a specific project.
7777
func (w *workspaces) allWorkspacesForProject(rw http.ResponseWriter, r *http.Request) {
7878
apiKey := httpmw.APIKey(r)
7979
project := httpmw.ProjectParam(r)
@@ -99,27 +99,61 @@ func (w *workspaces) allWorkspacesForProject(rw http.ResponseWriter, r *http.Req
9999
render.JSON(rw, r, apiWorkspaces)
100100
}
101101

102-
// createWorkspace creates a new workspace for the currently authenticated user.
103-
func (w *workspaces) createWorkspace(rw http.ResponseWriter, r *http.Request) {
102+
// Create a new workspace for the currently authenticated user.
103+
func (w *workspaces) createWorkspaceForUser(rw http.ResponseWriter, r *http.Request) {
104104
var createWorkspace CreateWorkspaceRequest
105105
if !httpapi.Read(rw, r, &createWorkspace) {
106106
return
107107
}
108108
apiKey := httpmw.APIKey(r)
109-
project := httpmw.ProjectParam(r)
109+
project, err := w.Database.GetProjectByID(r.Context(), createWorkspace.ProjectID)
110+
if errors.Is(err, sql.ErrNoRows) {
111+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
112+
Message: fmt.Sprintf("project %q doesn't exist", createWorkspace.ProjectID.String()),
113+
Errors: []httpapi.Error{{
114+
Field: "project_id",
115+
Code: "not_found",
116+
}},
117+
})
118+
return
119+
}
120+
if err != nil {
121+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
122+
Message: fmt.Sprintf("get project: %s", err),
123+
})
124+
return
125+
}
126+
_, err = w.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
127+
OrganizationID: project.OrganizationID,
128+
UserID: apiKey.UserID,
129+
})
130+
if errors.Is(err, sql.ErrNoRows) {
131+
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
132+
Message: "you aren't allowed to access projects in that organization",
133+
})
134+
return
135+
}
136+
if err != nil {
137+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
138+
Message: fmt.Sprintf("get organization member: %s", err),
139+
})
140+
return
141+
}
110142

111143
workspace, err := w.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
112144
OwnerID: apiKey.UserID,
113145
Name: createWorkspace.Name,
114146
})
115147
if err == nil {
148+
// If the workspace already exists, don't allow creation.
116149
project, err := w.Database.GetProjectByID(r.Context(), workspace.ProjectID)
117150
if err != nil {
118151
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
119152
Message: fmt.Sprintf("find project for conflicting workspace name %q: %s", createWorkspace.Name, err),
120153
})
121154
return
122155
}
156+
// The project is fetched for clarity to the user on where the conflicting name may be.
123157
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
124158
Message: fmt.Sprintf("workspace %q already exists in the %q project", createWorkspace.Name, project.Name),
125159
Errors: []httpapi.Error{{
@@ -136,6 +170,7 @@ func (w *workspaces) createWorkspace(rw http.ResponseWriter, r *http.Request) {
136170
return
137171
}
138172

173+
// Workspaces are created without any versions.
139174
workspace, err = w.Database.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
140175
ID: uuid.New(),
141176
CreatedAt: database.Now(),
@@ -155,14 +190,16 @@ func (w *workspaces) createWorkspace(rw http.ResponseWriter, r *http.Request) {
155190
render.JSON(rw, r, convertWorkspace(workspace))
156191
}
157192

158-
func (*workspaces) workspace(rw http.ResponseWriter, r *http.Request) {
193+
// Returns a single singleWorkspace.
194+
func (*workspaces) singleWorkspace(rw http.ResponseWriter, r *http.Request) {
159195
workspace := httpmw.WorkspaceParam(r)
160196

161197
render.Status(r, http.StatusOK)
162198
render.JSON(rw, r, convertWorkspace(workspace))
163199
}
164200

165-
func (w *workspaces) allWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
201+
// Returns all workspace history. This is not sorted. Use before/after to chronologically sort.
202+
func (w *workspaces) listAllWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
166203
workspace := httpmw.WorkspaceParam(r)
167204

168205
histories, err := w.Database.GetWorkspaceHistoryByWorkspaceID(r.Context(), workspace.ID)
@@ -185,6 +222,7 @@ func (w *workspaces) allWorkspaceHistory(rw http.ResponseWriter, r *http.Request
185222
render.JSON(rw, r, apiHistory)
186223
}
187224

225+
// Returns the latest workspace history. This works by querying for history without "after" set.
188226
func (w *workspaces) latestWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
189227
workspace := httpmw.WorkspaceParam(r)
190228

@@ -206,8 +244,10 @@ func (w *workspaces) latestWorkspaceHistory(rw http.ResponseWriter, r *http.Requ
206244
render.JSON(rw, r, convertWorkspaceHistory(history))
207245
}
208246

209-
func (w *workspaces) createWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
210-
var createBuild CreateWorkspaceBuildRequest
247+
// Begins transitioning a workspace to new state. This queues a provision job to asyncronously
248+
// update the underlying infrastructure. Only one historical transition can occur at a time.
249+
func (w *workspaces) createWorkspaceHistory(rw http.ResponseWriter, r *http.Request) {
250+
var createBuild CreateWorkspaceHistoryRequest
211251
if !httpapi.Read(rw, r, &createBuild) {
212252
return
213253
}
@@ -255,6 +295,8 @@ func (w *workspaces) createWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
255295
}
256296

257297
var workspaceHistory database.WorkspaceHistory
298+
// This must happen in a transaction to ensure history can be inserted, and
299+
// the prior history can update it's "after" column to point at the new.
258300
err = w.Database.InTx(func(db database.Store) error {
259301
workspaceHistory, err = db.InsertWorkspaceHistory(r.Context(), database.InsertWorkspaceHistoryParams{
260302
ID: uuid.New(),
@@ -273,11 +315,10 @@ func (w *workspaces) createWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
273315
}
274316

275317
if priorHistoryID.Valid {
318+
// Update the prior history entries "after" column.
276319
err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{
277-
ID: priorHistory.ID,
278-
UpdatedAt: database.Now(),
279-
ProvisionerState: priorHistory.ProvisionerState,
280-
CompletedAt: priorHistory.CompletedAt,
320+
ID: priorHistory.ID,
321+
UpdatedAt: database.Now(),
281322
AfterID: uuid.NullUUID{
282323
UUID: workspaceHistory.ID,
283324
Valid: true,
@@ -301,11 +342,12 @@ func (w *workspaces) createWorkspaceBuild(rw http.ResponseWriter, r *http.Reques
301342
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory))
302343
}
303344

304-
// convertWorkspace consumes the database representation and outputs an API friendly representation.
345+
// Converts the internal workspace representation to a public external-facing model.
305346
func convertWorkspace(workspace database.Workspace) Workspace {
306347
return Workspace(workspace)
307348
}
308349

350+
// Converts the internal history representation to a public external-facing model.
309351
func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory) WorkspaceHistory {
310352
//nolint:unconvert
311353
return WorkspaceHistory(WorkspaceHistory{

coderd/workspaces_test.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func TestWorkspaces(t *testing.T) {
2222
t.Parallel()
2323
server := coderdtest.New(t)
2424
_ = server.RandomInitialUser(t)
25-
workspaces, err := server.Client.Workspaces(context.Background(), "", "")
25+
workspaces, err := server.Client.WorkspacesByUser(context.Background(), "")
2626
require.NoError(t, err)
2727
require.Len(t, workspaces, 0)
2828
})
@@ -33,8 +33,9 @@ func TestWorkspaces(t *testing.T) {
3333
Provisioner: database.ProvisionerTypeTerraform,
3434
})
3535
require.NoError(t, err)
36-
workspace, err := client.CreateWorkspace(context.Background(), user.Organization, project.Name, coderd.CreateWorkspaceRequest{
37-
Name: "hiii",
36+
workspace, err := client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
37+
Name: "hiii",
38+
ProjectID: project.ID,
3839
})
3940
require.NoError(t, err)
4041
return project, workspace
@@ -63,7 +64,7 @@ func TestWorkspaces(t *testing.T) {
6364
server := coderdtest.New(t)
6465
user := server.RandomInitialUser(t)
6566
_, _ = setupProjectAndWorkspace(t, server.Client, user)
66-
workspaces, err := server.Client.Workspaces(context.Background(), "", "")
67+
workspaces, err := server.Client.WorkspacesByUser(context.Background(), "")
6768
require.NoError(t, err)
6869
require.Len(t, workspaces, 1)
6970
})
@@ -77,7 +78,7 @@ func TestWorkspaces(t *testing.T) {
7778
Provisioner: database.ProvisionerTypeTerraform,
7879
})
7980
require.NoError(t, err)
80-
workspaces, err := server.Client.Workspaces(context.Background(), user.Organization, project.Name)
81+
workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
8182
require.NoError(t, err)
8283
require.Len(t, workspaces, 0)
8384
})
@@ -87,7 +88,7 @@ func TestWorkspaces(t *testing.T) {
8788
server := coderdtest.New(t)
8889
user := server.RandomInitialUser(t)
8990
project, _ := setupProjectAndWorkspace(t, server.Client, user)
90-
workspaces, err := server.Client.Workspaces(context.Background(), user.Organization, project.Name)
91+
workspaces, err := server.Client.WorkspacesByProject(context.Background(), user.Organization, project.Name)
9192
require.NoError(t, err)
9293
require.Len(t, workspaces, 1)
9394
})
@@ -101,8 +102,9 @@ func TestWorkspaces(t *testing.T) {
101102
Provisioner: database.ProvisionerTypeTerraform,
102103
})
103104
require.NoError(t, err)
104-
_, err = server.Client.CreateWorkspace(context.Background(), user.Organization, project.Name, coderd.CreateWorkspaceRequest{
105-
Name: "$$$",
105+
_, err = server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
106+
ProjectID: project.ID,
107+
Name: "$$$",
106108
})
107109
require.Error(t, err)
108110
})
@@ -112,8 +114,9 @@ func TestWorkspaces(t *testing.T) {
112114
server := coderdtest.New(t)
113115
user := server.RandomInitialUser(t)
114116
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
115-
_, err := server.Client.CreateWorkspace(context.Background(), user.Organization, project.Name, coderd.CreateWorkspaceRequest{
116-
Name: workspace.Name,
117+
_, err := server.Client.CreateWorkspace(context.Background(), "", coderd.CreateWorkspaceRequest{
118+
Name: workspace.Name,
119+
ProjectID: project.ID,
117120
})
118121
require.Error(t, err)
119122
})
@@ -136,7 +139,7 @@ func TestWorkspaces(t *testing.T) {
136139
require.NoError(t, err)
137140
require.Len(t, history, 0)
138141
projectVersion := setupProjectVersion(t, server.Client, user, project)
139-
_, err = server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{
142+
_, err = server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
140143
ProjectHistoryID: projectVersion.ID,
141144
Transition: database.WorkspaceTransitionCreate,
142145
})
@@ -154,7 +157,7 @@ func TestWorkspaces(t *testing.T) {
154157
_, err := server.Client.LatestWorkspaceHistory(context.Background(), "", workspace.Name)
155158
require.Error(t, err)
156159
projectVersion := setupProjectVersion(t, server.Client, user, project)
157-
_, err = server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{
160+
_, err = server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
158161
ProjectHistoryID: projectVersion.ID,
159162
Transition: database.WorkspaceTransitionCreate,
160163
})
@@ -170,7 +173,7 @@ func TestWorkspaces(t *testing.T) {
170173
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
171174
projectHistory := setupProjectVersion(t, server.Client, user, project)
172175

173-
_, err := server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{
176+
_, err := server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
174177
ProjectHistoryID: projectHistory.ID,
175178
Transition: database.WorkspaceTransitionCreate,
176179
})
@@ -184,13 +187,13 @@ func TestWorkspaces(t *testing.T) {
184187
project, workspace := setupProjectAndWorkspace(t, server.Client, user)
185188
projectHistory := setupProjectVersion(t, server.Client, user, project)
186189

187-
_, err := server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{
190+
_, err := server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
188191
ProjectHistoryID: projectHistory.ID,
189192
Transition: database.WorkspaceTransitionCreate,
190193
})
191194
require.NoError(t, err)
192195

193-
_, err = server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{
196+
_, err = server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
194197
ProjectHistoryID: projectHistory.ID,
195198
Transition: database.WorkspaceTransitionCreate,
196199
})
@@ -203,7 +206,7 @@ func TestWorkspaces(t *testing.T) {
203206
user := server.RandomInitialUser(t)
204207
_, workspace := setupProjectAndWorkspace(t, server.Client, user)
205208

206-
_, err := server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceBuildRequest{
209+
_, err := server.Client.CreateWorkspaceVersion(context.Background(), "", workspace.Name, coderd.CreateWorkspaceHistoryRequest{
207210
ProjectHistoryID: uuid.New(),
208211
Transition: database.WorkspaceTransitionCreate,
209212
})

0 commit comments

Comments
 (0)