Files
coder/coderd/workspaces.go
Kyle Carberry 177eba87b6 refactor: Move all HTTP routes to top-level struct (#130)
* feat: Add history middleware parameters

These will be used for streaming logs, checking status,
and other operations related to workspace and project
history.

* refactor: Move all HTTP routes to top-level struct

Nesting all structs behind their respective structures
is leaky, and promotes naming conflicts between handlers.

Our HTTP routes cannot have conflicts, so neither should
function naming.
2022-02-01 22:15:26 +00:00

169 lines
5.3 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 singleWorkspace.
func (*api) workspaceByUser(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspace(workspace))
}
// Converts the internal workspace representation to a public external-facing model.
func convertWorkspace(workspace database.Workspace) Workspace {
return Workspace(workspace)
}
// Converts the internal history representation to a public external-facing model.
func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory) WorkspaceHistory {
//nolint:unconvert
return WorkspaceHistory(WorkspaceHistory{
ID: workspaceHistory.ID,
CreatedAt: workspaceHistory.CreatedAt,
UpdatedAt: workspaceHistory.UpdatedAt,
CompletedAt: workspaceHistory.CompletedAt.Time,
WorkspaceID: workspaceHistory.WorkspaceID,
ProjectHistoryID: workspaceHistory.ProjectHistoryID,
BeforeID: workspaceHistory.BeforeID.UUID,
AfterID: workspaceHistory.AfterID.UUID,
Transition: workspaceHistory.Transition,
Initiator: workspaceHistory.Initiator,
})
}