mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
* 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>
178 lines
5.4 KiB
Go
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)
|
|
}
|