mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
If a job silently failed, it wasn't possible for another one to execute. This fixes it by using the API status to return active state.
368 lines
12 KiB
Go
368 lines
12 KiB
Go
package coderd
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/moby/moby/pkg/namesgenerator"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/coderd/autostart/schedule"
|
|
"github.com/coder/coder/coderd/database"
|
|
"github.com/coder/coder/coderd/httpapi"
|
|
"github.com/coder/coder/coderd/httpmw"
|
|
"github.com/coder/coder/codersdk"
|
|
)
|
|
|
|
func (api *api) workspace(rw http.ResponseWriter, r *http.Request) {
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
|
|
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 workspace build 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))
|
|
}
|
|
|
|
func (api *api) workspaceBuilds(rw http.ResponseWriter, r *http.Request) {
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
|
|
builds, err := api.Database.GetWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("get workspace builds: %s", err),
|
|
})
|
|
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) 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)
|
|
}
|
|
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: priorHistory.ProvisionerState,
|
|
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) 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) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
|
var req codersdk.UpdateWorkspaceAutostartRequest
|
|
if !httpapi.Read(rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
var dbSched sql.NullString
|
|
if req.Schedule != "" {
|
|
validSched, err := schedule.Weekly(req.Schedule)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("invalid autostart schedule: %s", err),
|
|
})
|
|
return
|
|
}
|
|
dbSched.String = validSched.String()
|
|
dbSched.Valid = true
|
|
}
|
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
err := api.Database.UpdateWorkspaceAutostart(r.Context(), database.UpdateWorkspaceAutostartParams{
|
|
ID: workspace.ID,
|
|
AutostartSchedule: dbSched,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("update workspace autostart schedule: %s", err),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
func (api *api) putWorkspaceAutostop(rw http.ResponseWriter, r *http.Request) {
|
|
var req codersdk.UpdateWorkspaceAutostopRequest
|
|
if !httpapi.Read(rw, r, &req) {
|
|
return
|
|
}
|
|
|
|
var dbSched sql.NullString
|
|
if req.Schedule != "" {
|
|
validSched, err := schedule.Weekly(req.Schedule)
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("invalid autostop schedule: %s", err),
|
|
})
|
|
return
|
|
}
|
|
dbSched.String = validSched.String()
|
|
dbSched.Valid = true
|
|
}
|
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
err := api.Database.UpdateWorkspaceAutostop(r.Context(), database.UpdateWorkspaceAutostopParams{
|
|
ID: workspace.ID,
|
|
AutostopSchedule: dbSched,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("update workspace autostop schedule: %s", err),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
func convertWorkspace(workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template) codersdk.Workspace {
|
|
return codersdk.Workspace{
|
|
ID: workspace.ID,
|
|
CreatedAt: workspace.CreatedAt,
|
|
UpdatedAt: workspace.UpdatedAt,
|
|
OwnerID: workspace.OwnerID,
|
|
TemplateID: workspace.TemplateID,
|
|
LatestBuild: workspaceBuild,
|
|
TemplateName: template.Name,
|
|
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
|
|
Name: workspace.Name,
|
|
AutostartSchedule: workspace.AutostartSchedule.String,
|
|
AutostopSchedule: workspace.AutostopSchedule.String,
|
|
}
|
|
}
|