Skip to content

Commit 5b01f61

Browse files
authored
feat: Add APIs for querying workspaces (#61)
* Add SQL migration * Add query functions for workspaces * Add create routes * Add tests for codersdk * Add workspace parameter route * Add workspace query * Move workspace function * Add querying for workspace history * Fix query * Fix syntax error * Move workspace routes * Fix version * Add CLI tests * Fix syntax error * Remove error * Fix history error * Add new user test * Fix test * Lower target to 70% * Improve comments * Add comment
1 parent 139828d commit 5b01f61

27 files changed

+2511
-81
lines changed

codecov.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ coverage:
1616
status:
1717
project:
1818
default:
19-
target: 75%
19+
target: 70%
2020
informational: yes
2121

2222
ignore:

coderd/coderd.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ func New(options *Options) http.Handler {
2626
users := &users{
2727
Database: options.Database,
2828
}
29+
workspaces := &workspaces{
30+
Database: options.Database,
31+
}
2932

3033
r := chi.NewRouter()
3134
r.Route("/api/v2", func(r chi.Router) {
@@ -36,14 +39,15 @@ func New(options *Options) http.Handler {
3639
})
3740
r.Post("/login", users.loginWithPassword)
3841
r.Post("/logout", users.logout)
42+
// Used for setup.
43+
r.Post("/user", users.createInitialUser)
3944
r.Route("/users", func(r chi.Router) {
40-
r.Post("/", users.createInitialUser)
41-
45+
r.Use(
46+
httpmw.ExtractAPIKey(options.Database, nil),
47+
)
48+
r.Post("/", users.createUser)
4249
r.Group(func(r chi.Router) {
43-
r.Use(
44-
httpmw.ExtractAPIKey(options.Database, nil),
45-
httpmw.ExtractUserParam(options.Database),
46-
)
50+
r.Use(httpmw.ExtractUserParam(options.Database))
4751
r.Get("/{user}", users.user)
4852
r.Get("/{user}/organizations", users.userOrganizations)
4953
})
@@ -58,11 +62,33 @@ func New(options *Options) http.Handler {
5862
r.Get("/", projects.allProjectsForOrganization)
5963
r.Post("/", projects.createProject)
6064
r.Route("/{project}", func(r chi.Router) {
61-
r.Use(httpmw.ExtractProjectParameter(options.Database))
65+
r.Use(httpmw.ExtractProjectParam(options.Database))
6266
r.Get("/", projects.project)
63-
r.Route("/versions", func(r chi.Router) {
64-
r.Get("/", projects.projectVersions)
65-
r.Post("/", projects.createProjectVersion)
67+
r.Route("/history", func(r chi.Router) {
68+
r.Get("/", projects.allProjectHistory)
69+
r.Post("/", projects.createProjectHistory)
70+
})
71+
r.Get("/workspaces", workspaces.allWorkspacesForProject)
72+
})
73+
})
74+
})
75+
76+
// Listing operations specific to resources should go under
77+
// their respective routes. eg. /orgs/<name>/workspaces
78+
r.Route("/workspaces", func(r chi.Router) {
79+
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
80+
r.Get("/", workspaces.listAllWorkspaces)
81+
r.Route("/{user}", func(r chi.Router) {
82+
r.Use(httpmw.ExtractUserParam(options.Database))
83+
r.Get("/", workspaces.listAllWorkspaces)
84+
r.Post("/", workspaces.createWorkspaceForUser)
85+
r.Route("/{workspace}", func(r chi.Router) {
86+
r.Use(httpmw.ExtractWorkspaceParam(options.Database))
87+
r.Get("/", workspaces.singleWorkspace)
88+
r.Route("/history", func(r chi.Router) {
89+
r.Post("/", workspaces.createWorkspaceHistory)
90+
r.Get("/", workspaces.listAllWorkspaceHistory)
91+
r.Get("/latest", workspaces.latestWorkspaceHistory)
6692
})
6793
})
6894
})

coderd/projects.go

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import (
2424
// abstracted for ease of change later on.
2525
type Project database.Project
2626

27-
// ProjectVersion is the JSON representation of a Coder project version.
28-
type ProjectVersion struct {
27+
// ProjectHistory is the JSON representation of Coder project version history.
28+
type ProjectHistory struct {
2929
ID uuid.UUID `json:"id"`
3030
ProjectID uuid.UUID `json:"project_id"`
3131
CreatedAt time.Time `json:"created_at"`
@@ -42,7 +42,6 @@ type CreateProjectRequest struct {
4242

4343
// CreateProjectVersionRequest enables callers to create a new Project Version.
4444
type CreateProjectVersionRequest struct {
45-
Name string `json:"name,omitempty" validate:"username"`
4645
StorageMethod database.ProjectStorageMethod `json:"storage_method" validate:"oneof=inline-archive,required"`
4746
StorageSource []byte `json:"storage_source" validate:"max=1048576,required"`
4847
}
@@ -51,7 +50,7 @@ type projects struct {
5150
Database database.Store
5251
}
5352

54-
// allProjects lists all projects across organizations for a user.
53+
// Lists all projects the authenticated user has access to.
5554
func (p *projects) allProjects(rw http.ResponseWriter, r *http.Request) {
5655
apiKey := httpmw.APIKey(r)
5756
organizations, err := p.Database.GetOrganizationsByUserID(r.Context(), apiKey.UserID)
@@ -79,7 +78,7 @@ func (p *projects) allProjects(rw http.ResponseWriter, r *http.Request) {
7978
render.JSON(rw, r, projects)
8079
}
8180

82-
// allProjectsForOrganization lists all projects for a specific organization.
81+
// Lists all projects in an organization.
8382
func (p *projects) allProjectsForOrganization(rw http.ResponseWriter, r *http.Request) {
8483
organization := httpmw.OrganizationParam(r)
8584
projects, err := p.Database.GetProjectsByOrganizationIDs(r.Context(), []string{organization.ID})
@@ -96,7 +95,7 @@ func (p *projects) allProjectsForOrganization(rw http.ResponseWriter, r *http.Re
9695
render.JSON(rw, r, projects)
9796
}
9897

99-
// createProject makes a new project in an organization.
98+
// Creates a new project in an organization.
10099
func (p *projects) createProject(rw http.ResponseWriter, r *http.Request) {
101100
var createProject CreateProjectRequest
102101
if !httpapi.Read(rw, r, &createProject) {
@@ -142,16 +141,16 @@ func (p *projects) createProject(rw http.ResponseWriter, r *http.Request) {
142141
render.JSON(rw, r, project)
143142
}
144143

145-
// project returns a single project parsed from the URL path.
144+
// Returns a single project.
146145
func (*projects) project(rw http.ResponseWriter, r *http.Request) {
147146
project := httpmw.ProjectParam(r)
148147

149148
render.Status(r, http.StatusOK)
150149
render.JSON(rw, r, project)
151150
}
152151

153-
// projectVersions lists versions for a single project.
154-
func (p *projects) projectVersions(rw http.ResponseWriter, r *http.Request) {
152+
// Lists history for a single project.
153+
func (p *projects) allProjectHistory(rw http.ResponseWriter, r *http.Request) {
155154
project := httpmw.ProjectParam(r)
156155

157156
history, err := p.Database.GetProjectHistoryByProjectID(r.Context(), project.ID)
@@ -164,15 +163,18 @@ func (p *projects) projectVersions(rw http.ResponseWriter, r *http.Request) {
164163
})
165164
return
166165
}
167-
versions := make([]ProjectVersion, 0)
166+
apiHistory := make([]ProjectHistory, 0)
168167
for _, version := range history {
169-
versions = append(versions, convertProjectHistory(version))
168+
apiHistory = append(apiHistory, convertProjectHistory(version))
170169
}
171170
render.Status(r, http.StatusOK)
172-
render.JSON(rw, r, versions)
171+
render.JSON(rw, r, apiHistory)
173172
}
174173

175-
func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request) {
174+
// Creates a new version of the project. An import job is queued to parse
175+
// the storage method provided. Once completed, the import job will specify
176+
// the version as latest.
177+
func (p *projects) createProjectHistory(rw http.ResponseWriter, r *http.Request) {
176178
var createProjectVersion CreateProjectVersionRequest
177179
if !httpapi.Read(rw, r, &createProjectVersion) {
178180
return
@@ -204,6 +206,8 @@ func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request)
204206
Name: namesgenerator.GetRandomName(1),
205207
StorageMethod: createProjectVersion.StorageMethod,
206208
StorageSource: createProjectVersion.StorageSource,
209+
// TODO: Make this do something!
210+
ImportJobID: uuid.New(),
207211
})
208212
if err != nil {
209213
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
@@ -218,8 +222,8 @@ func (p *projects) createProjectVersion(rw http.ResponseWriter, r *http.Request)
218222
render.JSON(rw, r, convertProjectHistory(history))
219223
}
220224

221-
func convertProjectHistory(history database.ProjectHistory) ProjectVersion {
222-
return ProjectVersion{
225+
func convertProjectHistory(history database.ProjectHistory) ProjectHistory {
226+
return ProjectHistory{
223227
ID: history.ID,
224228
ProjectID: history.ProjectID,
225229
CreatedAt: history.CreatedAt,

coderd/projects_test.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func TestProjects(t *testing.T) {
104104
Provisioner: database.ProvisionerTypeTerraform,
105105
})
106106
require.NoError(t, err)
107-
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
107+
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
108108
require.NoError(t, err)
109109
require.Len(t, versions, 0)
110110
})
@@ -127,13 +127,12 @@ func TestProjects(t *testing.T) {
127127
require.NoError(t, err)
128128
_, err = writer.Write(make([]byte, 1<<10))
129129
require.NoError(t, err)
130-
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
131-
Name: "moo",
130+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
132131
StorageMethod: database.ProjectStorageMethodInlineArchive,
133132
StorageSource: buffer.Bytes(),
134133
})
135134
require.NoError(t, err)
136-
versions, err := server.Client.ProjectVersions(context.Background(), user.Organization, project.Name)
135+
versions, err := server.Client.ProjectHistory(context.Background(), user.Organization, project.Name)
137136
require.NoError(t, err)
138137
require.Len(t, versions, 1)
139138
})
@@ -156,8 +155,7 @@ func TestProjects(t *testing.T) {
156155
require.NoError(t, err)
157156
_, err = writer.Write(make([]byte, 1<<21))
158157
require.NoError(t, err)
159-
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
160-
Name: "moo",
158+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
161159
StorageMethod: database.ProjectStorageMethodInlineArchive,
162160
StorageSource: buffer.Bytes(),
163161
})
@@ -173,8 +171,7 @@ func TestProjects(t *testing.T) {
173171
Provisioner: database.ProvisionerTypeTerraform,
174172
})
175173
require.NoError(t, err)
176-
_, err = server.Client.CreateProjectVersion(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
177-
Name: "moo",
174+
_, err = server.Client.CreateProjectHistory(context.Background(), user.Organization, project.Name, coderd.CreateProjectVersionRequest{
178175
StorageMethod: database.ProjectStorageMethodInlineArchive,
179176
StorageSource: []byte{},
180177
})

coderd/users.go

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,31 @@ import (
1919
"github.com/coder/coder/httpmw"
2020
)
2121

22-
// User is the JSON representation of a Coder user.
22+
// User represents a user in Coder.
2323
type User struct {
2424
ID string `json:"id" validate:"required"`
2525
Email string `json:"email" validate:"required"`
2626
CreatedAt time.Time `json:"created_at" validate:"required"`
2727
Username string `json:"username" validate:"required"`
2828
}
2929

30-
// CreateInitialUserRequest enables callers to create a new user.
30+
// CreateInitialUserRequest provides options to create the initial
31+
// user for a Coder deployment. The organization provided will be
32+
// created as well.
3133
type CreateInitialUserRequest struct {
3234
Email string `json:"email" validate:"required,email"`
3335
Username string `json:"username" validate:"required,username"`
3436
Password string `json:"password" validate:"required"`
3537
Organization string `json:"organization" validate:"required,username"`
3638
}
3739

40+
// CreateUserRequest provides options for creating a new user.
41+
type CreateUserRequest struct {
42+
Email string `json:"email" validate:"required,email"`
43+
Username string `json:"username" validate:"required,username"`
44+
Password string `json:"password" validate:"required"`
45+
}
46+
3847
// LoginWithPasswordRequest enables callers to authenticate with email and password.
3948
type LoginWithPasswordRequest struct {
4049
Email string `json:"email" validate:"required,email"`
@@ -123,20 +132,66 @@ func (users *users) createInitialUser(rw http.ResponseWriter, r *http.Request) {
123132
}
124133

125134
render.Status(r, http.StatusCreated)
126-
render.JSON(rw, r, user)
135+
render.JSON(rw, r, convertUser(user))
136+
}
137+
138+
// Creates a new user.
139+
func (users *users) createUser(rw http.ResponseWriter, r *http.Request) {
140+
var createUser CreateUserRequest
141+
if !httpapi.Read(rw, r, &createUser) {
142+
return
143+
}
144+
_, err := users.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
145+
Username: createUser.Username,
146+
Email: createUser.Email,
147+
})
148+
if err == nil {
149+
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
150+
Message: "user already exists",
151+
})
152+
return
153+
}
154+
if !errors.Is(err, sql.ErrNoRows) {
155+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
156+
Message: fmt.Sprintf("get user: %s", err),
157+
})
158+
return
159+
}
160+
161+
hashedPassword, err := userpassword.Hash(createUser.Password)
162+
if err != nil {
163+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
164+
Message: fmt.Sprintf("hash password: %s", err.Error()),
165+
})
166+
return
167+
}
168+
169+
user, err := users.Database.InsertUser(r.Context(), database.InsertUserParams{
170+
ID: uuid.NewString(),
171+
Email: createUser.Email,
172+
HashedPassword: []byte(hashedPassword),
173+
Username: createUser.Username,
174+
LoginType: database.LoginTypeBuiltIn,
175+
CreatedAt: database.Now(),
176+
UpdatedAt: database.Now(),
177+
})
178+
if err != nil {
179+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
180+
Message: fmt.Sprintf("create user: %s", err.Error()),
181+
})
182+
return
183+
}
184+
185+
render.Status(r, http.StatusCreated)
186+
render.JSON(rw, r, convertUser(user))
127187
}
128188

129189
// Returns the parameterized user requested. All validation
130190
// is completed in the middleware for this route.
131191
func (*users) user(rw http.ResponseWriter, r *http.Request) {
132192
user := httpmw.UserParam(r)
133193

134-
render.JSON(rw, r, User{
135-
ID: user.ID,
136-
Email: user.Email,
137-
CreatedAt: user.CreatedAt,
138-
Username: user.Username,
139-
})
194+
render.JSON(rw, r, convertUser(user))
140195
}
141196

142197
// Returns organizations the parameterized user has access to.
@@ -265,3 +320,12 @@ func generateAPIKeyIDSecret() (id string, secret string, err error) {
265320
}
266321
return id, secret, nil
267322
}
323+
324+
func convertUser(user database.User) User {
325+
return User{
326+
ID: user.ID,
327+
Email: user.Email,
328+
CreatedAt: user.CreatedAt,
329+
Username: user.Username,
330+
}
331+
}

coderd/users_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,30 @@ func TestUsers(t *testing.T) {
7676
require.NoError(t, err)
7777
require.Len(t, orgs, 1)
7878
})
79+
80+
t.Run("CreateUser", func(t *testing.T) {
81+
t.Parallel()
82+
server := coderdtest.New(t)
83+
_ = server.RandomInitialUser(t)
84+
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
85+
Email: "wow@ok.io",
86+
Username: "tomato",
87+
Password: "bananas",
88+
})
89+
require.NoError(t, err)
90+
})
91+
92+
t.Run("CreateUserConflict", func(t *testing.T) {
93+
t.Parallel()
94+
server := coderdtest.New(t)
95+
user := server.RandomInitialUser(t)
96+
_, err := server.Client.CreateUser(context.Background(), coderd.CreateUserRequest{
97+
Email: "wow@ok.io",
98+
Username: user.Username,
99+
Password: "bananas",
100+
})
101+
require.Error(t, err)
102+
})
79103
}
80104

81105
func TestLogout(t *testing.T) {

0 commit comments

Comments
 (0)