mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
* Add SQL migration * Add query functions for workspaces * Add create routes * Add tests for codersdk * Add workspace parameter route * Add workspace query * Move workspace function * Add querying for workspace history * Fix query * Fix syntax error * Move workspace routes * Fix version * Add CLI tests * Fix syntax error * Remove error * Fix history error * Add new user test * Fix test * Lower target to 70% * Improve comments * Add comment
234 lines
7.2 KiB
Go
234 lines
7.2 KiB
Go
package coderd
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/render"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/moby/moby/pkg/namesgenerator"
|
|
|
|
"github.com/coder/coder/database"
|
|
"github.com/coder/coder/httpapi"
|
|
"github.com/coder/coder/httpmw"
|
|
)
|
|
|
|
// Project is the JSON representation of a Coder project.
|
|
// This type matches the database object for now, but is
|
|
// abstracted for ease of change later on.
|
|
type Project database.Project
|
|
|
|
// ProjectHistory is the JSON representation of Coder project version history.
|
|
type ProjectHistory struct {
|
|
ID uuid.UUID `json:"id"`
|
|
ProjectID uuid.UUID `json:"project_id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Name string `json:"name"`
|
|
StorageMethod database.ProjectStorageMethod `json:"storage_method"`
|
|
}
|
|
|
|
// CreateProjectRequest enables callers to create a new Project.
|
|
type CreateProjectRequest struct {
|
|
Name string `json:"name" validate:"username,required"`
|
|
Provisioner database.ProvisionerType `json:"provisioner" validate:"oneof=terraform cdr-basic,required"`
|
|
}
|
|
|
|
// CreateProjectVersionRequest enables callers to create a new Project Version.
|
|
type CreateProjectVersionRequest struct {
|
|
StorageMethod database.ProjectStorageMethod `json:"storage_method" validate:"oneof=inline-archive,required"`
|
|
StorageSource []byte `json:"storage_source" validate:"max=1048576,required"`
|
|
}
|
|
|
|
type projects struct {
|
|
Database database.Store
|
|
}
|
|
|
|
// Lists all projects the authenticated user has access to.
|
|
func (p *projects) allProjects(rw http.ResponseWriter, r *http.Request) {
|
|
apiKey := httpmw.APIKey(r)
|
|
organizations, err := p.Database.GetOrganizationsByUserID(r.Context(), apiKey.UserID)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get organizations: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
organizationIDs := make([]string, 0, len(organizations))
|
|
for _, organization := range organizations {
|
|
organizationIDs = append(organizationIDs, organization.ID)
|
|
}
|
|
projects, err := p.Database.GetProjectsByOrganizationIDs(r.Context(), organizationIDs)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get projects: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(rw, r, projects)
|
|
}
|
|
|
|
// Lists all projects in an organization.
|
|
func (p *projects) allProjectsForOrganization(rw http.ResponseWriter, r *http.Request) {
|
|
organization := httpmw.OrganizationParam(r)
|
|
projects, err := p.Database.GetProjectsByOrganizationIDs(r.Context(), []string{organization.ID})
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get projects: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(rw, r, projects)
|
|
}
|
|
|
|
// Creates a new project in an organization.
|
|
func (p *projects) createProject(rw http.ResponseWriter, r *http.Request) {
|
|
var createProject CreateProjectRequest
|
|
if !httpapi.Read(rw, r, &createProject) {
|
|
return
|
|
}
|
|
organization := httpmw.OrganizationParam(r)
|
|
_, err := p.Database.GetProjectByOrganizationAndName(r.Context(), database.GetProjectByOrganizationAndNameParams{
|
|
OrganizationID: organization.ID,
|
|
Name: createProject.Name,
|
|
})
|
|
if err == nil {
|
|
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
|
Message: fmt.Sprintf("project %q already exists", createProject.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 project by name: %s", err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
project, err := p.Database.InsertProject(r.Context(), database.InsertProjectParams{
|
|
ID: uuid.New(),
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
OrganizationID: organization.ID,
|
|
Name: createProject.Name,
|
|
Provisioner: createProject.Provisioner,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("insert project: %s", err),
|
|
})
|
|
return
|
|
}
|
|
render.Status(r, http.StatusCreated)
|
|
render.JSON(rw, r, project)
|
|
}
|
|
|
|
// Returns a single project.
|
|
func (*projects) project(rw http.ResponseWriter, r *http.Request) {
|
|
project := httpmw.ProjectParam(r)
|
|
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(rw, r, project)
|
|
}
|
|
|
|
// Lists history for a single project.
|
|
func (p *projects) allProjectHistory(rw http.ResponseWriter, r *http.Request) {
|
|
project := httpmw.ProjectParam(r)
|
|
|
|
history, err := p.Database.GetProjectHistoryByProjectID(r.Context(), project.ID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get project history: %s", err),
|
|
})
|
|
return
|
|
}
|
|
apiHistory := make([]ProjectHistory, 0)
|
|
for _, version := range history {
|
|
apiHistory = append(apiHistory, convertProjectHistory(version))
|
|
}
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(rw, r, apiHistory)
|
|
}
|
|
|
|
// Creates a new version of the project. An import job is queued to parse
|
|
// the storage method provided. Once completed, the import job will specify
|
|
// the version as latest.
|
|
func (p *projects) createProjectHistory(rw http.ResponseWriter, r *http.Request) {
|
|
var createProjectVersion CreateProjectVersionRequest
|
|
if !httpapi.Read(rw, r, &createProjectVersion) {
|
|
return
|
|
}
|
|
|
|
switch createProjectVersion.StorageMethod {
|
|
case database.ProjectStorageMethodInlineArchive:
|
|
tarReader := tar.NewReader(bytes.NewReader(createProjectVersion.StorageSource))
|
|
_, err := tarReader.Next()
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: "the archive must be a tar",
|
|
})
|
|
return
|
|
}
|
|
default:
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
Message: fmt.Sprintf("unsupported storage method %s", createProjectVersion.StorageMethod),
|
|
})
|
|
return
|
|
}
|
|
|
|
project := httpmw.ProjectParam(r)
|
|
history, err := p.Database.InsertProjectHistory(r.Context(), database.InsertProjectHistoryParams{
|
|
ID: uuid.New(),
|
|
ProjectID: project.ID,
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
Name: namesgenerator.GetRandomName(1),
|
|
StorageMethod: createProjectVersion.StorageMethod,
|
|
StorageSource: createProjectVersion.StorageSource,
|
|
// TODO: Make this do something!
|
|
ImportJobID: uuid.New(),
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("insert project history: %s", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
// TODO: A job to process the new version should occur here.
|
|
|
|
render.Status(r, http.StatusCreated)
|
|
render.JSON(rw, r, convertProjectHistory(history))
|
|
}
|
|
|
|
func convertProjectHistory(history database.ProjectHistory) ProjectHistory {
|
|
return ProjectHistory{
|
|
ID: history.ID,
|
|
ProjectID: history.ProjectID,
|
|
CreatedAt: history.CreatedAt,
|
|
UpdatedAt: history.UpdatedAt,
|
|
Name: history.Name,
|
|
}
|
|
}
|