mirror of
https://github.com/coder/coder.git
synced 2025-07-18 14:17:22 +00:00
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:
@ -1,17 +1,8 @@
|
|||||||
package coderd
|
package coderd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"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/database"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/coderd/httpmw"
|
"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))
|
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.
|
// convertOrganization consumes the database representation and outputs an API friendly representation.
|
||||||
func convertOrganization(organization database.Organization) codersdk.Organization {
|
func convertOrganization(organization database.Organization) codersdk.Organization {
|
||||||
return codersdk.Organization{
|
return codersdk.Organization{
|
||||||
|
@ -5,242 +5,71 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
"github.com/coder/coder/coderd/database"
|
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProvisionerDaemonsByOrganization(t *testing.T) {
|
func TestOrganizationsByUser(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("NoAuth", func(t *testing.T) {
|
client := coderdtest.New(t, nil)
|
||||||
t.Parallel()
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
client := coderdtest.New(t, nil)
|
orgs, err := client.OrganizationsByUser(context.Background(), codersdk.Me)
|
||||||
_, err := client.ProvisionerDaemonsByOrganization(context.Background(), uuid.New())
|
require.NoError(t, err)
|
||||||
require.Error(t, err)
|
require.NotNil(t, orgs)
|
||||||
})
|
require.Len(t, orgs, 1)
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostTemplateVersionsByOrganization(t *testing.T) {
|
func TestOrganizationByUserAndName(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("InvalidTemplate", func(t *testing.T) {
|
t.Run("NoExist", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
coderdtest.CreateFirstUser(t, client)
|
||||||
templateID := uuid.New()
|
_, err := client.OrganizationByName(context.Background(), codersdk.Me, "nothing")
|
||||||
_, err := client.CreateTemplateVersion(context.Background(), user.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
|
||||||
TemplateID: templateID,
|
|
||||||
StorageMethod: database.ProvisionerStorageMethodFile,
|
|
||||||
StorageSource: "hash",
|
|
||||||
Provisioner: database.ProvisionerTypeEcho,
|
|
||||||
})
|
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
require.ErrorAs(t, err, &apiErr)
|
||||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("FileNotFound", func(t *testing.T) {
|
t.Run("NoMember", 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.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
first := coderdtest.CreateFirstUser(t, client)
|
first := coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
||||||
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||||
Name: "another",
|
Name: "another",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
version := coderdtest.CreateTemplateVersion(t, other, org.ID, nil)
|
_, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name)
|
||||||
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
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
require.ErrorAs(t, err, &apiErr)
|
||||||
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
require.Equal(t, http.StatusUnauthorized, apiErr.StatusCode())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("AlreadyExists", func(t *testing.T) {
|
t.Run("Valid", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
coderdtest.NewProvisionerDaemon(t, client)
|
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
org, err := client.Organization(context.Background(), user.OrganizationID)
|
||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
require.NoError(t, err)
|
||||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
_, err = client.OrganizationByName(context.Background(), codersdk.Me, org.Name)
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
require.NoError(t, err)
|
||||||
_, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.CreateWorkspaceRequest{
|
})
|
||||||
TemplateID: template.ID,
|
}
|
||||||
Name: workspace.Name,
|
|
||||||
|
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
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
require.ErrorAs(t, err, &apiErr)
|
||||||
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
||||||
@ -249,84 +78,10 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
|
|||||||
t.Run("Create", func(t *testing.T) {
|
t.Run("Create", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
coderdtest.NewProvisionerDaemon(t, client)
|
_ = coderdtest.CreateFirstUser(t, client)
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
_, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
Name: "new",
|
||||||
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)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,23 @@ import (
|
|||||||
sdkproto "github.com/coder/coder/provisionersdk/proto"
|
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.
|
// Serves the provisioner daemon protobuf API over a WebSocket.
|
||||||
func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request) {
|
func (api *api) provisionerDaemonsListen(rw http.ResponseWriter, r *http.Request) {
|
||||||
api.websocketWaitMutex.Lock()
|
api.websocketWaitMutex.Lock()
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
@ -46,3 +47,21 @@ func TestProvisionerDaemons(t *testing.T) {
|
|||||||
}, 5*time.Second, 25*time.Millisecond)
|
}, 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/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/database"
|
"github.com/coder/coder/coderd/database"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"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) {
|
// Create a new template in an organization.
|
||||||
template := httpmw.TemplateParam(r)
|
func (api *api) postTemplatesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var createTemplate codersdk.CreateTemplateRequest
|
||||||
paginationParams, ok := parsePagination(rw, r)
|
if !httpapi.Read(rw, r, &createTemplate) {
|
||||||
if !ok {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiVersion := []codersdk.TemplateVersion{}
|
var template codersdk.Template
|
||||||
versions, err := api.Database.GetTemplateVersionsByTemplateID(r.Context(), database.GetTemplateVersionsByTemplateIDParams{
|
err = api.Database.InTx(func(db database.Store) error {
|
||||||
TemplateID: template.ID,
|
now := database.Now()
|
||||||
AfterID: paginationParams.AfterID,
|
dbTemplate, err := db.InsertTemplate(r.Context(), database.InsertTemplateParams{
|
||||||
LimitOpt: int32(paginationParams.Limit),
|
ID: uuid.New(),
|
||||||
OffsetOpt: int32(paginationParams.Offset),
|
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) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
httpapi.Write(rw, http.StatusOK, apiVersion)
|
err = nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: fmt.Sprintf("get template version: %s", err),
|
Message: fmt.Sprintf("get templates: %s", err.Error()),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jobIDs := make([]uuid.UUID, 0, len(versions))
|
templateIDs := make([]uuid.UUID, 0, len(templates))
|
||||||
for _, version := range versions {
|
for _, template := range templates {
|
||||||
jobIDs = append(jobIDs, version.JobID)
|
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 {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: fmt.Sprintf("get jobs: %s", err),
|
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jobByID := map[string]database.ProvisionerJob{}
|
|
||||||
for _, job := range jobs {
|
|
||||||
jobByID[job.ID.String()] = job
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, version := range versions {
|
httpapi.Write(rw, http.StatusOK, convertTemplates(templates, workspaceCounts))
|
||||||
job, exists := jobByID[version.JobID.String()]
|
}
|
||||||
if !exists {
|
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
func (api *api) templateByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
|
||||||
Message: fmt.Sprintf("job %q doesn't exist for version %q", version.JobID, version.ID),
|
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
|
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)
|
workspaceCounts, err := api.Database.GetWorkspaceOwnerCountsByTemplateIDs(r.Context(), []uuid.UUID{template.ID})
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
httpapi.Write(rw, http.StatusNotFound, httpapi.Response{
|
err = nil
|
||||||
Message: fmt.Sprintf("no template version found by name %q", templateVersionName),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: fmt.Sprintf("get template version by name: %s", err),
|
Message: fmt.Sprintf("get workspace counts: %s", err.Error()),
|
||||||
})
|
|
||||||
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
|
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) {
|
httpapi.Write(rw, http.StatusOK, convertTemplate(template, count))
|
||||||
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!",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow) []codersdk.Template {
|
func convertTemplates(templates []database.Template, workspaceCounts []database.GetWorkspaceOwnerCountsByTemplateIDsRow) []codersdk.Template {
|
||||||
|
@ -6,13 +6,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
"github.com/coder/coder/coderd/database"
|
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTemplate(t *testing.T) {
|
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) {
|
func TestDeleteTemplate(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@ -57,181 +151,3 @@ func TestDeleteTemplate(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusPreconditionFailed, apiErr.StatusCode())
|
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"
|
"fmt"
|
||||||
"net/http"
|
"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/database"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/coderd/httpmw"
|
"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)
|
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) {
|
func (api *api) templateVersionResources(rw http.ResponseWriter, r *http.Request) {
|
||||||
templateVersion := httpmw.TemplateVersionParam(r)
|
templateVersion := httpmw.TemplateVersionParam(r)
|
||||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
job, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||||
|
@ -7,9 +7,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/coderd/coderdtest"
|
"github.com/coder/coder/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/coderd/database"
|
||||||
"github.com/coder/coder/codersdk"
|
"github.com/coder/coder/codersdk"
|
||||||
"github.com/coder/coder/provisioner/echo"
|
"github.com/coder/coder/provisioner/echo"
|
||||||
"github.com/coder/coder/provisionersdk/proto"
|
"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) {
|
func TestPatchCancelTemplateVersion(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("AlreadyCompleted", func(t *testing.T) {
|
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) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
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
|
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 {
|
func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User {
|
||||||
convertedUser := codersdk.User{
|
convertedUser := codersdk.User{
|
||||||
ID: user.ID,
|
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) {
|
func TestPostAPIKey(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("InvalidUser", func(t *testing.T) {
|
t.Run("InvalidUser", func(t *testing.T) {
|
||||||
|
@ -2,9 +2,16 @@ package coderd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"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/database"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/coderd/httpmw"
|
"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)))
|
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) {
|
func (api *api) patchCancelWorkspaceBuild(rw http.ResponseWriter, r *http.Request) {
|
||||||
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
workspaceBuild := httpmw.WorkspaceBuildParam(r)
|
||||||
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceBuild.JobID)
|
||||||
|
@ -27,6 +27,22 @@ func TestWorkspaceBuild(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
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) {
|
func TestPatchCancelWorkspaceBuild(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/coder/coder/coderd/database"
|
"github.com/coder/coder/coderd/database"
|
||||||
"github.com/coder/coder/coderd/httpapi"
|
"github.com/coder/coder/coderd/httpapi"
|
||||||
"github.com/coder/coder/coderd/httpmw"
|
"github.com/coder/coder/coderd/httpmw"
|
||||||
|
"github.com/coder/coder/coderd/rbac"
|
||||||
"github.com/coder/coder/codersdk"
|
"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))
|
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
func (api *api) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||||
workspace := httpmw.WorkspaceParam(r)
|
organization := httpmw.OrganizationParam(r)
|
||||||
|
workspaces, err := api.Database.GetWorkspacesByOrganizationID(r.Context(), database.GetWorkspacesByOrganizationIDParams{
|
||||||
builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
OrganizationID: organization.ID,
|
||||||
|
Deleted: false,
|
||||||
|
})
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: fmt.Sprintf("get workspace builds: %s", err),
|
Message: fmt.Sprintf("get workspaces: %s", err),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jobIDs := make([]uuid.UUID, 0, len(builds))
|
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces)
|
||||||
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 {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: fmt.Sprintf("get jobs: %s", err),
|
Message: fmt.Sprintf("convert workspaces: %s", err),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jobByID := map[string]database.ProvisionerJob{}
|
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
||||||
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) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
func (api *api) workspacesByUser(rw http.ResponseWriter, r *http.Request) {
|
||||||
apiKey := httpmw.APIKey(r)
|
user := httpmw.UserParam(r)
|
||||||
workspace := httpmw.WorkspaceParam(r)
|
roles := httpmw.UserRoles(r)
|
||||||
var createBuild codersdk.CreateWorkspaceBuildRequest
|
|
||||||
if !httpapi.Read(rw, r, &createBuild) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if createBuild.TemplateVersionID == uuid.Nil {
|
organizationIDs := make([]uuid.UUID, 0)
|
||||||
latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
|
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 {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: fmt.Sprintf("get latest workspace build: %s", err),
|
Message: fmt.Sprintf("authorize: %s", err),
|
||||||
})
|
})
|
||||||
return
|
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) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
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{{
|
Errors: []httpapi.Error{{
|
||||||
Field: "template_version_id",
|
Field: "template_id",
|
||||||
Detail: "template version not found",
|
Detail: "template not found",
|
||||||
}},
|
}},
|
||||||
})
|
})
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: fmt.Sprintf("get template version: %s", err),
|
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)
|
templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
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
|
return
|
||||||
}
|
}
|
||||||
@ -158,53 +334,50 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
case codersdk.ProvisionerJobFailed:
|
case codersdk.ProvisionerJobFailed:
|
||||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
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
|
return
|
||||||
case codersdk.ProvisionerJobCanceled:
|
case codersdk.ProvisionerJobCanceled:
|
||||||
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
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
|
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
|
var provisionerJob database.ProvisionerJob
|
||||||
// This must happen in a transaction to ensure history can be inserted, and
|
var workspaceBuild database.WorkspaceBuild
|
||||||
// the prior history can update it's "after" column to point at the new.
|
|
||||||
err = api.Database.InTx(func(db database.Store) error {
|
err = api.Database.InTx(func(db database.Store) error {
|
||||||
workspaceBuildID := uuid.New()
|
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{
|
input, err := json.Marshal(workspaceProvisionJob{
|
||||||
WorkspaceBuildID: workspaceBuildID,
|
WorkspaceBuildID: workspaceBuildID,
|
||||||
})
|
})
|
||||||
@ -226,84 +399,38 @@ func (api *api) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("insert provisioner job: %w", err)
|
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{
|
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
|
||||||
ID: workspaceBuildID,
|
ID: workspaceBuildID,
|
||||||
CreatedAt: database.Now(),
|
CreatedAt: database.Now(),
|
||||||
UpdatedAt: database.Now(),
|
UpdatedAt: database.Now(),
|
||||||
WorkspaceID: workspace.ID,
|
WorkspaceID: workspace.ID,
|
||||||
TemplateVersionID: templateVersion.ID,
|
TemplateVersionID: templateVersion.ID,
|
||||||
BeforeID: priorHistoryID,
|
|
||||||
Name: namesgenerator.GetRandomName(1),
|
Name: namesgenerator.GetRandomName(1),
|
||||||
ProvisionerState: state,
|
|
||||||
InitiatorID: apiKey.UserID,
|
InitiatorID: apiKey.UserID,
|
||||||
Transition: createBuild.Transition,
|
Transition: database.WorkspaceTransitionStart,
|
||||||
JobID: provisionerJob.ID,
|
JobID: provisionerJob.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("insert workspace build: %w", err)
|
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
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: err.Error(),
|
Message: fmt.Sprintf("create workspace: %s", err),
|
||||||
})
|
|
||||||
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),
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
user, err := api.Database.GetUserByID(r.Context(), apiKey.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||||
Message: fmt.Sprintf("get workspace build by name: %s", err),
|
Message: fmt.Sprintf("get user: %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
|
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) {
|
func (api *api) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -31,18 +31,145 @@ func TestWorkspace(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorkspaceBuilds(t *testing.T) {
|
func TestPostWorkspacesByOrganization(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Run("Single", func(t *testing.T) {
|
t.Run("InvalidTemplate", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := coderdtest.New(t, nil)
|
client := coderdtest.New(t, nil)
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
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)
|
coderdtest.NewProvisionerDaemon(t, client)
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.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)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user