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