mirror of https://github.com/coder/coder.git
chore: organize http handlers (#1486)
They're currently randomly in a bunch of different files. This cleans up the handler functions to be in the file of the type they return.
This commit is contained in:
parent
9d4182b189
commit
680de709a5
|
@ -1,17 +1,8 @@
|
|||
package coderd
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
|
@ -23,612 +14,6 @@ func (*api) organization(rw http.ResponseWriter, r *http.Request) {
|
|||
httpapi.Write(rw, http.StatusOK, convertOrganization(organization))
|
||||
}
|
||||
|
||||
func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
daemons, err := api.Database.GetProvisionerDaemons(r.Context())
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner daemons: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if daemons == nil {
|
||||
daemons = []database.ProvisionerDaemon{}
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, daemons)
|
||||
}
|
||||
|
||||
// Creates a new version of a template. An import job is queued to parse the storage method provided.
|
||||
func (api *api) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
var req codersdk.CreateTemplateVersionRequest
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
if req.TemplateID != uuid.Nil {
|
||||
_, err := api.Database.GetTemplateByID(r.Context(), req.TemplateID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "template does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "file not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get file: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var templateVersion database.TemplateVersion
|
||||
var provisionerJob database.ProvisionerJob
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
jobID := uuid.New()
|
||||
for _, parameterValue := range req.ParameterValues {
|
||||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Scope: database.ParameterScopeImportJob,
|
||||
ScopeID: jobID,
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
SourceValue: parameterValue.SourceValue,
|
||||
DestinationScheme: parameterValue.DestinationScheme,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert parameter value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
provisionerJob, err = api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
|
||||
ID: jobID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
OrganizationID: organization.ID,
|
||||
InitiatorID: apiKey.UserID,
|
||||
Provisioner: req.Provisioner,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: file.Hash,
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
Input: []byte{'{', '}'},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
|
||||
var templateID uuid.NullUUID
|
||||
if req.TemplateID != uuid.Nil {
|
||||
templateID = uuid.NullUUID{
|
||||
UUID: req.TemplateID,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
templateVersion, err = api.Database.InsertTemplateVersion(r.Context(), database.InsertTemplateVersionParams{
|
||||
ID: uuid.New(),
|
||||
TemplateID: templateID,
|
||||
OrganizationID: organization.ID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
Description: "",
|
||||
JobID: provisionerJob.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert template version: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, convertTemplateVersion(templateVersion, convertProvisionerJob(provisionerJob)))
|
||||
}
|
||||
|
||||
// Create a new template in an organization.
|
||||
func (api *api) postTemplatesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
var createTemplate codersdk.CreateTemplateRequest
|
||||
if !httpapi.Read(rw, r, &createTemplate) {
|
||||
return
|
||||
}
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
_, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{
|
||||
OrganizationID: organization.ID,
|
||||
Name: createTemplate.Name,
|
||||
})
|
||||
if err == nil {
|
||||
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
||||
Message: fmt.Sprintf("template %q already exists", createTemplate.Name),
|
||||
Errors: []httpapi.Error{{
|
||||
Field: "name",
|
||||
Detail: "this value is already in use and should be unique",
|
||||
}},
|
||||
})
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), createTemplate.VersionID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "template version does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version by id: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
importJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get import job by id: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var template codersdk.Template
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
now := database.Now()
|
||||
dbTemplate, err := db.InsertTemplate(r.Context(), database.InsertTemplateParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
OrganizationID: organization.ID,
|
||||
Name: createTemplate.Name,
|
||||
Provisioner: importJob.Provisioner,
|
||||
ActiveVersionID: templateVersion.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert template: %s", err)
|
||||
}
|
||||
|
||||
err = db.UpdateTemplateVersionByID(r.Context(), database.UpdateTemplateVersionByIDParams{
|
||||
ID: templateVersion.ID,
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: dbTemplate.ID,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert template version: %s", err)
|
||||
}
|
||||
|
||||
for _, parameterValue := range createTemplate.ParameterValues {
|
||||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Scope: database.ParameterScopeTemplate,
|
||||
ScopeID: dbTemplate.ID,
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
SourceValue: parameterValue.SourceValue,
|
||||
DestinationScheme: parameterValue.DestinationScheme,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert parameter value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
template = convertTemplate(dbTemplate, 0)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, template)
|
||||
}
|
||||
|
||||
func (api *api) templatesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
templates, err := api.Database.GetTemplatesByOrganization(r.Context(), database.GetTemplatesByOrganizationParams{
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get templates: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
templateIDs := make([]uuid.UUID, 0, len(templates))
|
||||
for _, template := range templates {
|
||||
templateIDs = append(templateIDs, template.ID)
|
||||
}
|
||||
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), templateIDs)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts))
|
||||
}
|
||||
|
||||
func (api *api) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
templateName := chi.URLParam(r, "templatename")
|
||||
template, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{
|
||||
OrganizationID: organization.ID,
|
||||
Name: templateName,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no template found by name %q in the %q organization", templateName, organization.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template by organization and name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
count := uint32(0)
|
||||
if len(workspaceCounts) > 0 {
|
||||
count = uint32(workspaceCounts[0].Count)
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count))
|
||||
}
|
||||
|
||||
func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
workspaces, err := api.Database.GetWorkspacesByOrganizationID(r.Context(), database.GetWorkspacesByOrganizationIDParams{
|
||||
OrganizationID: organization.ID,
|
||||
Deleted: false,
|
||||
})
|
||||
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, err := convertWorkspaces(r.Context(), api.Database, workspaces)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("convert workspaces: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
||||
}
|
||||
|
||||
func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) {
|
||||
owner := httpmw.UserParam(r)
|
||||
workspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{
|
||||
OwnerID: owner.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, err := convertWorkspaces(r.Context(), api.Database, workspaces)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("convert workspaces: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
||||
}
|
||||
|
||||
func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
owner := httpmw.UserParam(r)
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
workspaceName := chi.URLParam(r, "workspace")
|
||||
|
||||
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
|
||||
OwnerID: owner.ID,
|
||||
Name: workspaceName,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no workspace found by name %q", workspaceName),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if workspace.OrganizationID != organization.ID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("workspace is not owned by organization %q", organization.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace,
|
||||
convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
|
||||
}
|
||||
|
||||
// Create a new workspace for the currently authenticated user.
|
||||
func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
var createWorkspace codersdk.CreateWorkspaceRequest
|
||||
if !httpapi.Read(rw, r, &createWorkspace) {
|
||||
return
|
||||
}
|
||||
apiKey := httpmw.APIKey(r)
|
||||
template, err := api.Database.GetTemplateByID(r.Context(), createWorkspace.TemplateID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: fmt.Sprintf("template %q doesn't exist", createWorkspace.TemplateID.String()),
|
||||
Errors: []httpapi.Error{{
|
||||
Field: "template_id",
|
||||
Detail: "template not found",
|
||||
}},
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
if organization.ID != template.OrganizationID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("template is not in organization %q", organization.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
|
||||
OrganizationID: template.OrganizationID,
|
||||
UserID: apiKey.UserID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "you aren't allowed to access templates 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.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
|
||||
OwnerID: apiKey.UserID,
|
||||
Name: createWorkspace.Name,
|
||||
})
|
||||
if err == nil {
|
||||
// If the workspace already exists, don't allow creation.
|
||||
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("find template for conflicting workspace name %q: %s", createWorkspace.Name, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
// The template 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 template", createWorkspace.Name, template.Name),
|
||||
Errors: []httpapi.Error{{
|
||||
Field: "name",
|
||||
Detail: "this value is already in use and should be unique",
|
||||
}},
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), template.ActiveVersionID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status
|
||||
switch templateVersionJobStatus {
|
||||
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
|
||||
httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus),
|
||||
})
|
||||
return
|
||||
case codersdk.ProvisionerJobFailed:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided template version %q has failed to import. You cannot create workspaces using it!", templateVersion.Name),
|
||||
})
|
||||
return
|
||||
case codersdk.ProvisionerJobCanceled:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "The provided template version was canceled during import. You cannot create workspaces using it!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var provisionerJob database.ProvisionerJob
|
||||
var workspaceBuild database.WorkspaceBuild
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
workspaceBuildID := uuid.New()
|
||||
// Workspaces are created without any versions.
|
||||
workspace, err = db.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
OwnerID: apiKey.UserID,
|
||||
OrganizationID: template.OrganizationID,
|
||||
TemplateID: template.ID,
|
||||
Name: createWorkspace.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace: %w", err)
|
||||
}
|
||||
for _, parameterValue := range createWorkspace.ParameterValues {
|
||||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Scope: database.ParameterScopeWorkspace,
|
||||
ScopeID: workspace.ID,
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
SourceValue: parameterValue.SourceValue,
|
||||
DestinationScheme: parameterValue.DestinationScheme,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert parameter value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
input, err := json.Marshal(workspaceProvisionJob{
|
||||
WorkspaceBuildID: workspaceBuildID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal provision job: %w", err)
|
||||
}
|
||||
provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
InitiatorID: apiKey.UserID,
|
||||
OrganizationID: template.OrganizationID,
|
||||
Provisioner: template.Provisioner,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
StorageMethod: templateVersionJob.StorageMethod,
|
||||
StorageSource: templateVersionJob.StorageSource,
|
||||
Input: input,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
|
||||
ID: workspaceBuildID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
WorkspaceID: workspace.ID,
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
InitiatorID: apiKey.UserID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
JobID: provisionerJob.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace build: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("create workspace: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
user, err := api.Database.GetUserByID(r.Context(), apiKey.UserID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get user: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace,
|
||||
convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template, user))
|
||||
}
|
||||
|
||||
// convertOrganization consumes the database representation and outputs an API friendly representation.
|
||||
func convertOrganization(organization database.Organization) codersdk.Organization {
|
||||
return codersdk.Organization{
|
||||
|
|
|
@ -5,242 +5,71 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
)
|
||||
|
||||
func TestProvisionerDaemonsByOrganization(t *testing.T) {
|
||||
func TestOrganizationsByUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_, err := client.ProvisionerDaemonsByOrganization(context.Background(), uuid.New())
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.ProvisionerDaemonsByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
orgs, err := client.OrganizationsByUser(context.Background(), codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, orgs)
|
||||
require.Len(t, orgs, 1)
|
||||
}
|
||||
|
||||
func TestPostTemplateVersionsByOrganization(t *testing.T) {
|
||||
func TestOrganizationByUserAndName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("InvalidTemplate", func(t *testing.T) {
|
||||
t.Run("NoExist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
templateID := uuid.New()
|
||||
_, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
TemplateID: templateID,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: "hash",
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.OrganizationByName(context.Background(), codersdk.Me, "nothing")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("FileNotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: "hash",
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("WithParameters", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
data, err := echo.Tar(&echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
Provision: echo.ProvisionComplete,
|
||||
ProvisionDryRun: echo.ProvisionComplete,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
|
||||
require.NoError(t, err)
|
||||
_, err = client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: file.Hash,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
ParameterValues: []codersdk.CreateParameterRequest{{
|
||||
Name: "example",
|
||||
SourceValue: "value",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostTemplatesByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
})
|
||||
|
||||
t.Run("AlreadyExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.CreateTemplate(context.Background(), user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||
Name: template.Name,
|
||||
VersionID: version.ID,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoVersion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateTemplate(context.Background(), user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||
Name: "test",
|
||||
VersionID: uuid.New(),
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplatesByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ListEmpty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, templates)
|
||||
require.Len(t, templates, 0)
|
||||
})
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates, 1)
|
||||
})
|
||||
t.Run("ListMultiple", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateByOrganizationAndName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.TemplateByName(context.Background(), user.OrganizationID, "something")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.TemplateByName(context.Background(), user.OrganizationID, template.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("InvalidTemplate", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: uuid.New(),
|
||||
Name: "workspace",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoTemplateAccess", func(t *testing.T) {
|
||||
t.Run("NoMember", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: "another",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
version := coderdtest.CreateTemplateVersion(t, other, org.ID, nil)
|
||||
template := coderdtest.CreateTemplate(t, other, org.ID, version.ID)
|
||||
|
||||
_, err = client.CreateWorkspace(context.Background(), first.OrganizationID, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: "workspace",
|
||||
})
|
||||
require.Error(t, err)
|
||||
_, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("AlreadyExists", func(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: workspace.Name,
|
||||
org, err := client.Organization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
_, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostOrganizationsByUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Conflict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
org, err := client.Organization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
_, err = client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: org.Name,
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||
|
@ -249,84 +78,10 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
|
|||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspacesByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ListEmpty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
workspaces, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspacesByOwner(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ListEmpty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceByOwnerAndName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, "something")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, workspace.Name)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: "new",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -32,6 +32,23 @@ import (
|
|||
sdkproto "github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
func (api *api) provisionerDaemonsByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
daemons, err := api.Database.GetProvisionerDaemons(r.Context())
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner daemons: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if daemons == nil {
|
||||
daemons = []database.ProvisionerDaemon{}
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, daemons)
|
||||
}
|
||||
|
||||
// Serves the provisioner daemon protobuf API over a WebSocket.
|
||||
func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request) {
|
||||
api.websocketWaitMutex.Lock()
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
|
@ -46,3 +47,21 @@ func TestProvisionerDaemons(t *testing.T) {
|
|||
}, 5*time.Second, 25*time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvisionerDaemonsByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NoAuth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_, err := client.ProvisionerDaemonsByOrganization(context.Background(), uuid.New())
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.ProvisionerDaemonsByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
|
@ -73,132 +74,181 @@ func (api *api) deleteTemplate(rw http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
}
|
||||
|
||||
func (api *api) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) {
|
||||
template := httpmw.TemplateParam(r)
|
||||
|
||||
paginationParams, ok := parsePagination(rw, r)
|
||||
if !ok {
|
||||
// Create a new template in an organization.
|
||||
func (api *api) postTemplatesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
var createTemplate codersdk.CreateTemplateRequest
|
||||
if !httpapi.Read(rw, r, &createTemplate) {
|
||||
return
|
||||
}
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
_, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{
|
||||
OrganizationID: organization.ID,
|
||||
Name: createTemplate.Name,
|
||||
})
|
||||
if err == nil {
|
||||
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
||||
Message: fmt.Sprintf("template %q already exists", createTemplate.Name),
|
||||
Errors: []httpapi.Error{{
|
||||
Field: "name",
|
||||
Detail: "this value is already in use and should be unique",
|
||||
}},
|
||||
})
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), createTemplate.VersionID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "template version does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version by id: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
importJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get import job by id: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
apiVersion := []codersdk.TemplateVersion{}
|
||||
versions, err := api.Database.GetTemplateVersionsByTemplateID(r.Context(), database.GetTemplateVersionsByTemplateIDParams{
|
||||
TemplateID: template.ID,
|
||||
AfterID: paginationParams.AfterID,
|
||||
LimitOpt: int32(paginationParams.Limit),
|
||||
OffsetOpt: int32(paginationParams.Offset),
|
||||
var template codersdk.Template
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
now := database.Now()
|
||||
dbTemplate, err := db.InsertTemplate(r.Context(), database.InsertTemplateParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
OrganizationID: organization.ID,
|
||||
Name: createTemplate.Name,
|
||||
Provisioner: importJob.Provisioner,
|
||||
ActiveVersionID: templateVersion.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert template: %s", err)
|
||||
}
|
||||
|
||||
err = db.UpdateTemplateVersionByID(r.Context(), database.UpdateTemplateVersionByIDParams{
|
||||
ID: templateVersion.ID,
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: dbTemplate.ID,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert template version: %s", err)
|
||||
}
|
||||
|
||||
for _, parameterValue := range createTemplate.ParameterValues {
|
||||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Scope: database.ParameterScopeTemplate,
|
||||
ScopeID: dbTemplate.ID,
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
SourceValue: parameterValue.SourceValue,
|
||||
DestinationScheme: parameterValue.DestinationScheme,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert parameter value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
template = convertTemplate(dbTemplate, 0)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, template)
|
||||
}
|
||||
|
||||
func (api *api) templatesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
templates, err := api.Database.GetTemplatesByOrganization(r.Context(), database.GetTemplatesByOrganizationParams{
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusOK, apiVersion)
|
||||
return
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version: %s", err),
|
||||
Message: fmt.Sprintf("get templates: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobIDs := make([]uuid.UUID, 0, len(versions))
|
||||
for _, version := range versions {
|
||||
jobIDs = append(jobIDs, version.JobID)
|
||||
templateIDs := make([]uuid.UUID, 0, len(templates))
|
||||
for _, template := range templates {
|
||||
templateIDs = append(templateIDs, template.ID)
|
||||
}
|
||||
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), templateIDs)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get jobs: %s", err),
|
||||
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobByID := map[string]database.ProvisionerJob{}
|
||||
for _, job := range jobs {
|
||||
jobByID[job.ID.String()] = job
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
job, exists := jobByID[version.JobID.String()]
|
||||
if !exists {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("job %q doesn't exist for version %q", version.JobID, version.ID),
|
||||
httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts))
|
||||
}
|
||||
|
||||
func (api *api) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
templateName := chi.URLParam(r, "templatename")
|
||||
template, err := api.Database.GetTemplateByOrganizationAndName(r.Context(), database.GetTemplateByOrganizationAndNameParams{
|
||||
OrganizationID: organization.ID,
|
||||
Name: templateName,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no template found by name %q in the %q organization", templateName, organization.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
apiVersion = append(apiVersion, convertTemplateVersion(version, convertProvisionerJob(job)))
|
||||
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template by organization and name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, apiVersion)
|
||||
}
|
||||
|
||||
func (api *api) templateVersionByName(rw http.ResponseWriter, r *http.Request) {
|
||||
template := httpmw.TemplateParam(r)
|
||||
templateVersionName := chi.URLParam(r, "templateversionname")
|
||||
templateVersion, err := api.Database.GetTemplateVersionByTemplateIDAndName(r.Context(), database.GetTemplateVersionByTemplateIDAndNameParams{
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: template.ID,
|
||||
Valid: true,
|
||||
},
|
||||
Name: templateVersionName,
|
||||
})
|
||||
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no template version found by name %q", templateVersionName),
|
||||
})
|
||||
return
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job)))
|
||||
}
|
||||
count := uint32(0)
|
||||
if len(workspaceCounts) > 0 {
|
||||
count = uint32(workspaceCounts[0].Count)
|
||||
}
|
||||
|
||||
func (api *api) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
var req codersdk.UpdateActiveTemplateVersion
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
template := httpmw.TemplateParam(r)
|
||||
version, err := api.Database.GetTemplateVersionByID(r.Context(), req.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "template version not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if version.TemplateID.UUID.String() != template.ID.String() {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "The provided template version doesn't belong to the specified template.",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = api.Database.UpdateTemplateActiveVersionByID(r.Context(), database.UpdateTemplateActiveVersionByIDParams{
|
||||
ID: template.ID,
|
||||
ActiveVersionID: req.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("update active template version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
Message: "Updated the active template version!",
|
||||
})
|
||||
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count))
|
||||
}
|
||||
|
||||
func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow) []codersdk.Template {
|
||||
|
|
|
@ -6,13 +6,10 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
)
|
||||
|
||||
func TestTemplate(t *testing.T) {
|
||||
|
@ -29,6 +26,103 @@ func TestTemplate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPostTemplatesByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
})
|
||||
|
||||
t.Run("AlreadyExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.CreateTemplate(context.Background(), user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||
Name: template.Name,
|
||||
VersionID: version.ID,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoVersion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateTemplate(context.Background(), user.OrganizationID, codersdk.CreateTemplateRequest{
|
||||
Name: "test",
|
||||
VersionID: uuid.New(),
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplatesByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ListEmpty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, templates)
|
||||
require.Len(t, templates, 0)
|
||||
})
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates, 1)
|
||||
})
|
||||
t.Run("ListMultiple", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
templates, err := client.TemplatesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateByOrganizationAndName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.TemplateByName(context.Background(), user.OrganizationID, "something")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.TemplateByName(context.Background(), user.OrganizationID, template.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -57,181 +151,3 @@ func TestDeleteTemplate(t *testing.T) {
|
|||
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateVersionsByTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
versions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, versions, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateVersionByName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.TemplateVersionByName(context.Background(), template.ID, "nothing")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.TemplateVersionByName(context.Background(), template.ID, version.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchActiveTemplateVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: uuid.New(),
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("DoesNotBelong", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version.ID,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPaginatedTemplateVersions creates a list of template versions and paginate.
|
||||
func TestPaginatedTemplateVersions(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{APIRateLimit: -1})
|
||||
// Prepare database.
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
// Populate database with template versions.
|
||||
total := 9
|
||||
for i := 0; i < total; i++ {
|
||||
data, err := echo.Tar(nil)
|
||||
require.NoError(t, err)
|
||||
file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
|
||||
require.NoError(t, err)
|
||||
templateVersion, err := client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
TemplateID: template.ID,
|
||||
StorageSource: file.Hash,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, templateVersion.ID)
|
||||
}
|
||||
|
||||
templateVersions, err := client.TemplateVersionsByTemplate(ctx,
|
||||
codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templateVersions, 10, "wrong number of template versions created")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
pagination codersdk.Pagination
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []codersdk.TemplateVersion
|
||||
}{
|
||||
{
|
||||
name: "Single result",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 1}},
|
||||
want: templateVersions[:1],
|
||||
},
|
||||
{
|
||||
name: "Single result, second page",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 1, Offset: 1}},
|
||||
want: templateVersions[1:2],
|
||||
},
|
||||
{
|
||||
name: "Last two results",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 8}},
|
||||
want: templateVersions[8:10],
|
||||
},
|
||||
{
|
||||
name: "AfterID returns next two results",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, AfterID: templateVersions[1].ID}},
|
||||
want: templateVersions[2:4],
|
||||
},
|
||||
{
|
||||
name: "No result after last AfterID",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, AfterID: templateVersions[9].ID}},
|
||||
want: []codersdk.TemplateVersion{},
|
||||
},
|
||||
{
|
||||
name: "No result after last Offset",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 10}},
|
||||
want: []codersdk.TemplateVersion{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := client.TemplateVersionsByTemplate(tt.args.ctx, codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
Pagination: tt.args.pagination,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,11 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
|
@ -134,6 +139,242 @@ func (api *api) templateVersionParameters(rw http.ResponseWriter, r *http.Reques
|
|||
httpapi.Write(rw, http.StatusOK, values)
|
||||
}
|
||||
|
||||
func (api *api) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) {
|
||||
template := httpmw.TemplateParam(r)
|
||||
|
||||
paginationParams, ok := parsePagination(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
apiVersion := []codersdk.TemplateVersion{}
|
||||
versions, err := api.Database.GetTemplateVersionsByTemplateID(r.Context(), database.GetTemplateVersionsByTemplateIDParams{
|
||||
TemplateID: template.ID,
|
||||
AfterID: paginationParams.AfterID,
|
||||
LimitOpt: int32(paginationParams.Limit),
|
||||
OffsetOpt: int32(paginationParams.Offset),
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusOK, apiVersion)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobIDs := make([]uuid.UUID, 0, len(versions))
|
||||
for _, version := range versions {
|
||||
jobIDs = append(jobIDs, version.JobID)
|
||||
}
|
||||
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get jobs: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobByID := map[string]database.ProvisionerJob{}
|
||||
for _, job := range jobs {
|
||||
jobByID[job.ID.String()] = job
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
job, exists := jobByID[version.JobID.String()]
|
||||
if !exists {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("job %q doesn't exist for version %q", version.JobID, version.ID),
|
||||
})
|
||||
return
|
||||
}
|
||||
apiVersion = append(apiVersion, convertTemplateVersion(version, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, apiVersion)
|
||||
}
|
||||
|
||||
func (api *api) templateVersionByName(rw http.ResponseWriter, r *http.Request) {
|
||||
template := httpmw.TemplateParam(r)
|
||||
templateVersionName := chi.URLParam(r, "templateversionname")
|
||||
templateVersion, err := api.Database.GetTemplateVersionByTemplateIDAndName(r.Context(), database.GetTemplateVersionByTemplateIDAndNameParams{
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: template.ID,
|
||||
Valid: true,
|
||||
},
|
||||
Name: templateVersionName,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no template version found by name %q", templateVersionName),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
func (api *api) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
var req codersdk.UpdateActiveTemplateVersion
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
template := httpmw.TemplateParam(r)
|
||||
version, err := api.Database.GetTemplateVersionByID(r.Context(), req.ID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "template version not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if version.TemplateID.UUID.String() != template.ID.String() {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "The provided template version doesn't belong to the specified template.",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = api.Database.UpdateTemplateActiveVersionByID(r.Context(), database.UpdateTemplateActiveVersionByIDParams{
|
||||
ID: template.ID,
|
||||
ActiveVersionID: req.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("update active template version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
Message: "Updated the active template version!",
|
||||
})
|
||||
}
|
||||
|
||||
// Creates a new version of a template. An import job is queued to parse the storage method provided.
|
||||
func (api *api) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
var req codersdk.CreateTemplateVersionRequest
|
||||
if !httpapi.Read(rw, r, &req) {
|
||||
return
|
||||
}
|
||||
if req.TemplateID != uuid.Nil {
|
||||
_, err := api.Database.GetTemplateByID(r.Context(), req.TemplateID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "template does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: "file not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get file: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var templateVersion database.TemplateVersion
|
||||
var provisionerJob database.ProvisionerJob
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
jobID := uuid.New()
|
||||
for _, parameterValue := range req.ParameterValues {
|
||||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Scope: database.ParameterScopeImportJob,
|
||||
ScopeID: jobID,
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
SourceValue: parameterValue.SourceValue,
|
||||
DestinationScheme: parameterValue.DestinationScheme,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert parameter value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
provisionerJob, err = api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
|
||||
ID: jobID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
OrganizationID: organization.ID,
|
||||
InitiatorID: apiKey.UserID,
|
||||
Provisioner: req.Provisioner,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: file.Hash,
|
||||
Type: database.ProvisionerJobTypeTemplateVersionImport,
|
||||
Input: []byte{'{', '}'},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
|
||||
var templateID uuid.NullUUID
|
||||
if req.TemplateID != uuid.Nil {
|
||||
templateID = uuid.NullUUID{
|
||||
UUID: req.TemplateID,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
templateVersion, err = api.Database.InsertTemplateVersion(r.Context(), database.InsertTemplateVersionParams{
|
||||
ID: uuid.New(),
|
||||
TemplateID: templateID,
|
||||
OrganizationID: organization.ID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
Description: "",
|
||||
JobID: provisionerJob.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert template version: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, convertTemplateVersion(templateVersion, convertProvisionerJob(provisionerJob)))
|
||||
}
|
||||
|
||||
func (api *api) templateVersionResources(rw http.ResponseWriter, r *http.Request) {
|
||||
templateVersion := httpmw.TemplateVersionParam(r)
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||
|
|
|
@ -7,9 +7,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
|
@ -27,6 +29,65 @@ func TestTemplateVersion(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPostTemplateVersionsByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("InvalidTemplate", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
templateID := uuid.New()
|
||||
_, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
TemplateID: templateID,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: "hash",
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("FileNotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: "hash",
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("WithParameters", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
data, err := echo.Tar(&echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
Provision: echo.ProvisionComplete,
|
||||
ProvisionDryRun: echo.ProvisionComplete,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
|
||||
require.NoError(t, err)
|
||||
_, err = client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
StorageSource: file.Hash,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
ParameterValues: []codersdk.CreateParameterRequest{{
|
||||
Name: "example",
|
||||
SourceValue: "value",
|
||||
SourceScheme: database.ParameterSourceSchemeData,
|
||||
DestinationScheme: database.ParameterDestinationSchemeProvisionerVariable,
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCancelTemplateVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("AlreadyCompleted", func(t *testing.T) {
|
||||
|
@ -280,3 +341,181 @@ func TestTemplateVersionLogs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateVersionsByTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
versions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, versions, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateVersionByName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.TemplateVersionByName(context.Background(), template.ID, "nothing")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_, err := client.TemplateVersionByName(context.Background(), template.ID, version.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchActiveTemplateVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: uuid.New(),
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("DoesNotBelong", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version.ID,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
err := client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPaginatedTemplateVersions creates a list of template versions and paginate.
|
||||
func TestPaginatedTemplateVersions(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{APIRateLimit: -1})
|
||||
// Prepare database.
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
// Populate database with template versions.
|
||||
total := 9
|
||||
for i := 0; i < total; i++ {
|
||||
data, err := echo.Tar(nil)
|
||||
require.NoError(t, err)
|
||||
file, err := client.Upload(context.Background(), codersdk.ContentTypeTar, data)
|
||||
require.NoError(t, err)
|
||||
templateVersion, err := client.CreateTemplateVersion(ctx, user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
TemplateID: template.ID,
|
||||
StorageSource: file.Hash,
|
||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
||||
Provisioner: database.ProvisionerTypeEcho,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, templateVersion.ID)
|
||||
}
|
||||
|
||||
templateVersions, err := client.TemplateVersionsByTemplate(ctx,
|
||||
codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templateVersions, 10, "wrong number of template versions created")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
pagination codersdk.Pagination
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []codersdk.TemplateVersion
|
||||
}{
|
||||
{
|
||||
name: "Single result",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 1}},
|
||||
want: templateVersions[:1],
|
||||
},
|
||||
{
|
||||
name: "Single result, second page",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 1, Offset: 1}},
|
||||
want: templateVersions[1:2],
|
||||
},
|
||||
{
|
||||
name: "Last two results",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 8}},
|
||||
want: templateVersions[8:10],
|
||||
},
|
||||
{
|
||||
name: "AfterID returns next two results",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, AfterID: templateVersions[1].ID}},
|
||||
want: templateVersions[2:4],
|
||||
},
|
||||
{
|
||||
name: "No result after last AfterID",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, AfterID: templateVersions[9].ID}},
|
||||
want: []codersdk.TemplateVersion{},
|
||||
},
|
||||
{
|
||||
name: "No result after last Offset",
|
||||
args: args{ctx: ctx, pagination: codersdk.Pagination{Limit: 2, Offset: 10}},
|
||||
want: []codersdk.TemplateVersion{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got, err := client.TemplateVersionsByTemplate(tt.args.ctx, codersdk.TemplateVersionsByTemplateRequest{
|
||||
TemplateID: template.ID,
|
||||
Pagination: tt.args.pagination,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -488,7 +488,7 @@ func (api *api) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques
|
|||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "you are not a member of that organization",
|
||||
Message: fmt.Sprintf("no organization found by name %q", organizationName),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -776,73 +776,6 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest)
|
|||
})
|
||||
}
|
||||
|
||||
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
roles := httpmw.UserRoles(r)
|
||||
|
||||
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get organizations: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
organizationIDs := make([]uuid.UUID, 0)
|
||||
for _, organization := range organizations {
|
||||
err = api.Authorizer.AuthorizeByRoleName(r.Context(), user.ID.String(), roles.Roles, rbac.ActionRead, rbac.ResourceWorkspace.All().InOrg(organization.ID))
|
||||
var apiErr *rbac.UnauthorizedError
|
||||
if xerrors.As(err, &apiErr) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("authorize: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
organizationIDs = append(organizationIDs, organization.ID)
|
||||
}
|
||||
|
||||
workspaceIDs := map[uuid.UUID]struct{}{}
|
||||
allWorkspaces, err := api.Database.GetWorkspacesByOrganizationIDs(r.Context(), database.GetWorkspacesByOrganizationIDsParams{
|
||||
Ids: organizationIDs,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspaces for organizations: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, ws := range allWorkspaces {
|
||||
workspaceIDs[ws.ID] = struct{}{}
|
||||
}
|
||||
userWorkspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{
|
||||
OwnerID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspaces for user: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, ws := range userWorkspaces {
|
||||
_, exists := workspaceIDs[ws.ID]
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
allWorkspaces = append(allWorkspaces, ws)
|
||||
}
|
||||
|
||||
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allWorkspaces)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("convert workspaces: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
||||
}
|
||||
|
||||
func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User {
|
||||
convertedUser := codersdk.User{
|
||||
ID: user.ID,
|
||||
|
|
|
@ -562,81 +562,6 @@ func TestGetUsers(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestOrganizationsByUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
orgs, err := client.OrganizationsByUser(context.Background(), codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, orgs)
|
||||
require.Len(t, orgs, 1)
|
||||
}
|
||||
|
||||
func TestOrganizationByUserAndName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NoExist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.OrganizationByName(context.Background(), codersdk.Me, "nothing")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoMember", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: "another",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
org, err := client.Organization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
_, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostOrganizationsByUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Conflict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
org, err := client.Organization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
_, err = client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: org.Name,
|
||||
})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: "new",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostAPIKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("InvalidUser", func(t *testing.T) {
|
||||
|
|
|
@ -2,9 +2,16 @@ package coderd
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
|
@ -24,6 +31,251 @@ func (api *api) workspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
|||
httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
|
||||
builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace builds: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobIDs := make([]uuid.UUID, 0, len(builds))
|
||||
for _, version := range builds {
|
||||
jobIDs = append(jobIDs, version.JobID)
|
||||
}
|
||||
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get jobs: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobByID := map[string]database.ProvisionerJob{}
|
||||
for _, job := range jobs {
|
||||
jobByID[job.ID.String()] = job
|
||||
}
|
||||
|
||||
apiBuilds := make([]codersdk.WorkspaceBuild, 0)
|
||||
for _, build := range builds {
|
||||
job, exists := jobByID[build.JobID.String()]
|
||||
if !exists {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("job %q doesn't exist for build %q", build.JobID, build.ID),
|
||||
})
|
||||
return
|
||||
}
|
||||
apiBuilds = append(apiBuilds, convertWorkspaceBuild(build, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, apiBuilds)
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
workspaceBuildName := chi.URLParam(r, "workspacebuildname")
|
||||
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{
|
||||
WorkspaceID: workspace.ID,
|
||||
Name: workspaceBuildName,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no workspace build found by name %q", workspaceBuildName),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
var createBuild codersdk.CreateWorkspaceBuildRequest
|
||||
if !httpapi.Read(rw, r, &createBuild) {
|
||||
return
|
||||
}
|
||||
if createBuild.TemplateVersionID == uuid.Nil {
|
||||
latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get latest workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
createBuild.TemplateVersionID = latestBuild.TemplateVersionID
|
||||
}
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), createBuild.TemplateVersionID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "template version not found",
|
||||
Errors: []httpapi.Error{{
|
||||
Field: "template_version_id",
|
||||
Detail: "template version not found",
|
||||
}},
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status
|
||||
switch templateVersionJobStatus {
|
||||
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
|
||||
httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus),
|
||||
})
|
||||
return
|
||||
case codersdk.ProvisionerJobFailed:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String),
|
||||
})
|
||||
return
|
||||
case codersdk.ProvisionerJobCanceled:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "The provided template version was canceled during import. You cannot builds workspaces with it!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
template, err := api.Database.GetTemplateByID(r.Context(), templateVersion.TemplateID.UUID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Store prior history ID if it exists to update it after we create new!
|
||||
priorHistoryID := uuid.NullUUID{}
|
||||
priorHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if err == nil {
|
||||
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.JobID)
|
||||
if err == nil && convertProvisionerJob(priorJob).Status.Active() {
|
||||
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
||||
Message: "a workspace build is already active",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
priorHistoryID = uuid.NullUUID{
|
||||
UUID: priorHistory.ID,
|
||||
Valid: true,
|
||||
}
|
||||
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get prior workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var workspaceBuild database.WorkspaceBuild
|
||||
var provisionerJob database.ProvisionerJob
|
||||
// This must happen in a transaction to ensure history can be inserted, and
|
||||
// the prior history can update it's "after" column to point at the new.
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
workspaceBuildID := uuid.New()
|
||||
input, err := json.Marshal(workspaceProvisionJob{
|
||||
WorkspaceBuildID: workspaceBuildID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal provision job: %w", err)
|
||||
}
|
||||
provisionerJob, err = db.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
InitiatorID: apiKey.UserID,
|
||||
OrganizationID: template.OrganizationID,
|
||||
Provisioner: template.Provisioner,
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
StorageMethod: templateVersionJob.StorageMethod,
|
||||
StorageSource: templateVersionJob.StorageSource,
|
||||
Input: input,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
state := createBuild.ProvisionerState
|
||||
if len(state) == 0 {
|
||||
state = priorHistory.ProvisionerState
|
||||
}
|
||||
|
||||
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
|
||||
ID: workspaceBuildID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
WorkspaceID: workspace.ID,
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
BeforeID: priorHistoryID,
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
ProvisionerState: state,
|
||||
InitiatorID: apiKey.UserID,
|
||||
Transition: createBuild.Transition,
|
||||
JobID: provisionerJob.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace build: %w", err)
|
||||
}
|
||||
|
||||
if priorHistoryID.Valid {
|
||||
// Update the prior history entries "after" column.
|
||||
err = db.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{
|
||||
ID: priorHistory.ID,
|
||||
ProvisionerState: priorHistory.ProvisionerState,
|
||||
UpdatedAt: database.Now(),
|
||||
AfterID: uuid.NullUUID{
|
||||
UUID: workspaceBuild.ID,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update prior workspace build: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob)))
|
||||
}
|
||||
|
||||
func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
|
|
|
@ -27,6 +27,22 @@ func TestWorkspaceBuild(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWorkspaceBuilds(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_, err := client.WorkspaceBuilds(context.Background(), workspace.ID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCancelWorkspaceBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
|
@ -61,81 +62,256 @@ func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
|
|||
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
|
||||
builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
||||
func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
workspaces, err := api.Database.GetWorkspacesByOrganizationID(r.Context(), database.GetWorkspacesByOrganizationIDParams{
|
||||
OrganizationID: organization.ID,
|
||||
Deleted: false,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace builds: %s", err),
|
||||
Message: fmt.Sprintf("get workspaces: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobIDs := make([]uuid.UUID, 0, len(builds))
|
||||
for _, version := range builds {
|
||||
jobIDs = append(jobIDs, version.JobID)
|
||||
}
|
||||
jobs, err := api.Database.GetProvisionerJobsByIDs(r.Context(), jobIDs)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get jobs: %s", err),
|
||||
Message: fmt.Sprintf("convert workspaces: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
jobByID := map[string]database.ProvisionerJob{}
|
||||
for _, job := range jobs {
|
||||
jobByID[job.ID.String()] = job
|
||||
}
|
||||
|
||||
apiBuilds := make([]codersdk.WorkspaceBuild, 0)
|
||||
for _, build := range builds {
|
||||
job, exists := jobByID[build.JobID.String()]
|
||||
if !exists {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("job %q doesn't exist for build %q", build.JobID, build.ID),
|
||||
})
|
||||
return
|
||||
}
|
||||
apiBuilds = append(apiBuilds, convertWorkspaceBuild(build, convertProvisionerJob(job)))
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, apiBuilds)
|
||||
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
||||
}
|
||||
|
||||
func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := httpmw.APIKey(r)
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
var createBuild codersdk.CreateWorkspaceBuildRequest
|
||||
if !httpapi.Read(rw, r, &createBuild) {
|
||||
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
roles := httpmw.UserRoles(r)
|
||||
|
||||
organizations, err := api.Database.GetOrganizationsByUserID(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get organizations: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
if createBuild.TemplateVersionID == uuid.Nil {
|
||||
latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
organizationIDs := make([]uuid.UUID, 0)
|
||||
for _, organization := range organizations {
|
||||
err = api.Authorizer.AuthorizeByRoleName(r.Context(), user.ID.String(), roles.Roles, rbac.ActionRead, rbac.ResourceWorkspace.All().InOrg(organization.ID))
|
||||
var apiErr *rbac.UnauthorizedError
|
||||
if xerrors.As(err, &apiErr) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get latest workspace build: %s", err),
|
||||
Message: fmt.Sprintf("authorize: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
createBuild.TemplateVersionID = latestBuild.TemplateVersionID
|
||||
organizationIDs = append(organizationIDs, organization.ID)
|
||||
}
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), createBuild.TemplateVersionID)
|
||||
|
||||
workspaceIDs := map[uuid.UUID]struct{}{}
|
||||
allWorkspaces, err := api.Database.GetWorkspacesByOrganizationIDs(r.Context(), database.GetWorkspacesByOrganizationIDsParams{
|
||||
Ids: organizationIDs,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspaces for organizations: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, ws := range allWorkspaces {
|
||||
workspaceIDs[ws.ID] = struct{}{}
|
||||
}
|
||||
userWorkspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{
|
||||
OwnerID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspaces for user: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, ws := range userWorkspaces {
|
||||
_, exists := workspaceIDs[ws.ID]
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
allWorkspaces = append(allWorkspaces, ws)
|
||||
}
|
||||
|
||||
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, allWorkspaces)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("convert workspaces: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
||||
}
|
||||
|
||||
func (api *api) workspacesByOwner(rw http.ResponseWriter, r *http.Request) {
|
||||
owner := httpmw.UserParam(r)
|
||||
workspaces, err := api.Database.GetWorkspacesByOwnerID(r.Context(), database.GetWorkspacesByOwnerIDParams{
|
||||
OwnerID: owner.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, err := convertWorkspaces(r.Context(), api.Database, workspaces)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("convert workspaces: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
||||
}
|
||||
|
||||
func (api *api) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
owner := httpmw.UserParam(r)
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
workspaceName := chi.URLParam(r, "workspace")
|
||||
|
||||
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
|
||||
OwnerID: owner.ID,
|
||||
Name: workspaceName,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no workspace found by name %q", workspaceName),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if workspace.OrganizationID != organization.ID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("workspace is not owned by organization %q", organization.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
build, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertWorkspace(workspace,
|
||||
convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
|
||||
}
|
||||
|
||||
// Create a new workspace for the currently authenticated user.
|
||||
func (api *api) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
var createWorkspace codersdk.CreateWorkspaceRequest
|
||||
if !httpapi.Read(rw, r, &createWorkspace) {
|
||||
return
|
||||
}
|
||||
apiKey := httpmw.APIKey(r)
|
||||
template, err := api.Database.GetTemplateByID(r.Context(), createWorkspace.TemplateID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: "template version not found",
|
||||
Message: fmt.Sprintf("template %q doesn't exist", createWorkspace.TemplateID.String()),
|
||||
Errors: []httpapi.Error{{
|
||||
Field: "template_version_id",
|
||||
Detail: "template version not found",
|
||||
Field: "template_id",
|
||||
Detail: "template not found",
|
||||
}},
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
if organization.ID != template.OrganizationID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("template is not in organization %q", organization.Name),
|
||||
})
|
||||
return
|
||||
}
|
||||
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
|
||||
OrganizationID: template.OrganizationID,
|
||||
UserID: apiKey.UserID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "you aren't allowed to access templates 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.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
|
||||
OwnerID: apiKey.UserID,
|
||||
Name: createWorkspace.Name,
|
||||
})
|
||||
if err == nil {
|
||||
// If the workspace already exists, don't allow creation.
|
||||
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("find template for conflicting workspace name %q: %s", createWorkspace.Name, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
// The template 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 template", createWorkspace.Name, template.Name),
|
||||
Errors: []httpapi.Error{{
|
||||
Field: "name",
|
||||
Detail: "this value is already in use and should be unique",
|
||||
}},
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), template.ActiveVersionID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template version: %s", err),
|
||||
|
@ -145,7 +321,7 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
Message: fmt.Sprintf("get template version job: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -158,53 +334,50 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
case codersdk.ProvisionerJobFailed:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: fmt.Sprintf("The provided template version %q has failed to import: %q. You cannot build workspaces with it!", templateVersion.Name, templateVersionJob.Error.String),
|
||||
Message: fmt.Sprintf("The provided template version %q has failed to import. You cannot create workspaces using it!", templateVersion.Name),
|
||||
})
|
||||
return
|
||||
case codersdk.ProvisionerJobCanceled:
|
||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
||||
Message: "The provided template version was canceled during import. You cannot builds workspaces with it!",
|
||||
Message: "The provided template version was canceled during import. You cannot create workspaces using it!",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
template, err := api.Database.GetTemplateByID(r.Context(), templateVersion.TemplateID.UUID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get template: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Store prior history ID if it exists to update it after we create new!
|
||||
priorHistoryID := uuid.NullUUID{}
|
||||
priorHistory, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
||||
if err == nil {
|
||||
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.JobID)
|
||||
if err == nil && convertProvisionerJob(priorJob).Status.Active() {
|
||||
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
||||
Message: "a workspace build is already active",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
priorHistoryID = uuid.NullUUID{
|
||||
UUID: priorHistory.ID,
|
||||
Valid: true,
|
||||
}
|
||||
} else if !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get prior workspace build: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var workspaceBuild database.WorkspaceBuild
|
||||
var provisionerJob database.ProvisionerJob
|
||||
// This must happen in a transaction to ensure history can be inserted, and
|
||||
// the prior history can update it's "after" column to point at the new.
|
||||
var workspaceBuild database.WorkspaceBuild
|
||||
err = api.Database.InTx(func(db database.Store) error {
|
||||
workspaceBuildID := uuid.New()
|
||||
// Workspaces are created without any versions.
|
||||
workspace, err = db.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
OwnerID: apiKey.UserID,
|
||||
OrganizationID: template.OrganizationID,
|
||||
TemplateID: template.ID,
|
||||
Name: createWorkspace.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace: %w", err)
|
||||
}
|
||||
for _, parameterValue := range createWorkspace.ParameterValues {
|
||||
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
||||
ID: uuid.New(),
|
||||
Name: parameterValue.Name,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Scope: database.ParameterScopeWorkspace,
|
||||
ScopeID: workspace.ID,
|
||||
SourceScheme: parameterValue.SourceScheme,
|
||||
SourceValue: parameterValue.SourceValue,
|
||||
DestinationScheme: parameterValue.DestinationScheme,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert parameter value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
input, err := json.Marshal(workspaceProvisionJob{
|
||||
WorkspaceBuildID: workspaceBuildID,
|
||||
})
|
||||
|
@ -226,84 +399,38 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
||||
}
|
||||
state := createBuild.ProvisionerState
|
||||
if len(state) == 0 {
|
||||
state = priorHistory.ProvisionerState
|
||||
}
|
||||
|
||||
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
|
||||
ID: workspaceBuildID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
WorkspaceID: workspace.ID,
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
BeforeID: priorHistoryID,
|
||||
Name: namesgenerator.GetRandomName(1),
|
||||
ProvisionerState: state,
|
||||
InitiatorID: apiKey.UserID,
|
||||
Transition: createBuild.Transition,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
JobID: provisionerJob.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert workspace build: %w", err)
|
||||
}
|
||||
|
||||
if priorHistoryID.Valid {
|
||||
// Update the prior history entries "after" column.
|
||||
err = db.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{
|
||||
ID: priorHistory.ID,
|
||||
ProvisionerState: priorHistory.ProvisionerState,
|
||||
UpdatedAt: database.Now(),
|
||||
AfterID: uuid.NullUUID{
|
||||
UUID: workspaceBuild.ID,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update prior workspace build: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(provisionerJob)))
|
||||
}
|
||||
|
||||
func (api *api) workspaceBuildByName(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
workspaceBuildName := chi.URLParam(r, "workspacebuildname")
|
||||
workspaceBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDAndName(r.Context(), database.GetWorkspaceBuildByWorkspaceIDAndNameParams{
|
||||
WorkspaceID: workspace.ID,
|
||||
Name: workspaceBuildName,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
||||
Message: fmt.Sprintf("no workspace build found by name %q", workspaceBuildName),
|
||||
Message: fmt.Sprintf("create workspace: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
user, err := api.Database.GetUserByID(r.Context(), apiKey.UserID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get workspace build by name: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get provisioner job: %s", err),
|
||||
Message: fmt.Sprintf("get user: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(job)))
|
||||
httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace,
|
||||
convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template, user))
|
||||
}
|
||||
|
||||
func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -31,18 +31,145 @@ func TestWorkspace(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWorkspaceBuilds(t *testing.T) {
|
||||
func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
t.Run("InvalidTemplate", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: uuid.New(),
|
||||
Name: "workspace",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("NoTemplateAccess", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||
Name: "another",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
version := coderdtest.CreateTemplateVersion(t, other, org.ID, nil)
|
||||
template := coderdtest.CreateTemplate(t, other, org.ID, version.ID)
|
||||
|
||||
_, err = client.CreateWorkspace(context.Background(), first.OrganizationID, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: "workspace",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("AlreadyExists", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_, err := client.WorkspaceBuilds(context.Background(), workspace.ID)
|
||||
_, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: template.ID,
|
||||
Name: workspace.Name,
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspacesByOrganization(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ListEmpty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
workspaces, err := client.WorkspacesByOrganization(context.Background(), user.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspacesByOwner(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("ListEmpty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
_ = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
workspaces, err := client.WorkspacesByOwner(context.Background(), user.OrganizationID, codersdk.Me)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, workspaces, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceByOwnerAndName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
_, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, "something")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.NewProvisionerDaemon(t, client)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
||||
_, err := client.WorkspaceByOwnerAndName(context.Background(), user.OrganizationID, codersdk.Me, workspace.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue