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, }) }