Files
coder/coderd/workspaces.go
Kyle Carberry 1796dc6c2f chore: Add test helpers to improve coverage (#166)
* chore: Rename ProjectHistory to ProjectVersion

Version more accurately represents version storage. This
forks from the WorkspaceHistory name, but I think it's
easier to understand Workspace history.

* Rename files

* Standardize tests a bit more

* Remove Server struct from coderdtest

* Improve test coverage for workspace history

* Fix linting errors

* Fix coderd test leak

* Fix coderd test leak

* Improve workspace history logs

* Standardize test structure for codersdk

* Fix linting errors

* Fix WebSocket compression

* Update coderd/workspaces.go

Co-authored-by: Bryan <bryan@coder.com>

* Add test for listing project parameters

* Cache npm dependencies with setup node

* Remove windows npm cache key

Co-authored-by: Bryan <bryan@coder.com>
2022-02-05 18:24:51 -06:00

178 lines
5.4 KiB
Go

package coderd
import (
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// Workspace is a per-user deployment of a project. It tracks
// project versions, and can be updated.
type Workspace database.Workspace
// CreateWorkspaceRequest provides options for creating a new workspace.
type CreateWorkspaceRequest struct {
ProjectID uuid.UUID `json:"project_id" validate:"required"`
Name string `json:"name" validate:"username,required"`
}
// Returns all workspaces across all projects and organizations.
func (api *api) workspaces(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
workspaces, err := api.Database.GetWorkspacesByUserID(r.Context(), apiKey.UserID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
})
return
}
apiWorkspaces := make([]Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiWorkspaces)
}
// Create a new workspace for the currently authenticated user.
func (api *api) postWorkspaceByUser(rw http.ResponseWriter, r *http.Request) {
var createWorkspace CreateWorkspaceRequest
if !httpapi.Read(rw, r, &createWorkspace) {
return
}
apiKey := httpmw.APIKey(r)
project, err := api.Database.GetProjectByID(r.Context(), createWorkspace.ProjectID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("project %q doesn't exist", createWorkspace.ProjectID.String()),
Errors: []httpapi.Error{{
Field: "project_id",
Code: "not_found",
}},
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err),
})
return
}
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
OrganizationID: project.OrganizationID,
UserID: apiKey.UserID,
})
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "you aren't allowed to access projects in that organization",
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization member: %s", err),
})
return
}
workspace, err := api.Database.GetWorkspaceByUserIDAndName(r.Context(), database.GetWorkspaceByUserIDAndNameParams{
OwnerID: apiKey.UserID,
Name: createWorkspace.Name,
})
if err == nil {
// If the workspace already exists, don't allow creation.
project, err := api.Database.GetProjectByID(r.Context(), workspace.ProjectID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("find project for conflicting workspace name %q: %s", createWorkspace.Name, err),
})
return
}
// The project is fetched for clarity to the user on where the conflicting name may be.
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: fmt.Sprintf("workspace %q already exists in the %q project", createWorkspace.Name, project.Name),
Errors: []httpapi.Error{{
Field: "name",
Code: "exists",
}},
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspace by name: %s", err.Error()),
})
return
}
// Workspaces are created without any versions.
workspace, err = api.Database.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
ID: uuid.New(),
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
OwnerID: apiKey.UserID,
ProjectID: project.ID,
Name: createWorkspace.Name,
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("insert workspace: %s", err),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertWorkspace(workspace))
}
// Returns a single workspace.
func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspace(workspace))
}
// Returns all workspaces for a specific project.
func (api *api) workspacesByProject(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
project := httpmw.ProjectParam(r)
workspaces, err := api.Database.GetWorkspacesByProjectAndUserID(r.Context(), database.GetWorkspacesByProjectAndUserIDParams{
OwnerID: apiKey.UserID,
ProjectID: project.ID,
})
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get workspaces: %s", err),
})
return
}
apiWorkspaces := make([]Workspace, 0, len(workspaces))
for _, workspace := range workspaces {
apiWorkspaces = append(apiWorkspaces, convertWorkspace(workspace))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiWorkspaces)
}
// Converts the internal workspace representation to a public external-facing model.
func convertWorkspace(workspace database.Workspace) Workspace {
return Workspace(workspace)
}