mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
It's possible for a workspace to become in an invalid state. This is something we'll detect for jobs, and allow monitoring of. These commands will allow admins to manually reconcile state.
439 lines
14 KiB
Go
439 lines
14 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"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)
|
|
}
|
|
state := createBuild.ProvisionerState
|
|
if state == nil || 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) 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 convertWorkspaces(ctx context.Context, db database.Store, workspaces []database.Workspace) ([]codersdk.Workspace, error) {
|
|
workspaceIDs := make([]uuid.UUID, 0, len(workspaces))
|
|
templateIDs := make([]uuid.UUID, 0, len(workspaces))
|
|
for _, workspace := range workspaces {
|
|
workspaceIDs = append(workspaceIDs, workspace.ID)
|
|
templateIDs = append(templateIDs, workspace.TemplateID)
|
|
}
|
|
workspaceBuilds, err := db.GetWorkspaceBuildsByWorkspaceIDsWithoutAfter(ctx, workspaceIDs)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get workspace builds: %w", err)
|
|
}
|
|
templates, err := db.GetTemplatesByIDs(ctx, templateIDs)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get templates: %w", err)
|
|
}
|
|
jobIDs := make([]uuid.UUID, 0, len(workspaceBuilds))
|
|
for _, build := range workspaceBuilds {
|
|
jobIDs = append(jobIDs, build.JobID)
|
|
}
|
|
jobs, err := db.GetProvisionerJobsByIDs(ctx, jobIDs)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get provisioner jobs: %w", err)
|
|
}
|
|
|
|
buildByWorkspaceID := map[uuid.UUID]database.WorkspaceBuild{}
|
|
for _, workspaceBuild := range workspaceBuilds {
|
|
buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild
|
|
}
|
|
templateByID := map[uuid.UUID]database.Template{}
|
|
for _, template := range templates {
|
|
templateByID[template.ID] = template
|
|
}
|
|
jobByID := map[uuid.UUID]database.ProvisionerJob{}
|
|
for _, job := range jobs {
|
|
jobByID[job.ID] = job
|
|
}
|
|
apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces))
|
|
for _, workspace := range workspaces {
|
|
build, exists := buildByWorkspaceID[workspace.ID]
|
|
if !exists {
|
|
return nil, xerrors.Errorf("build not found for workspace %q", workspace.Name)
|
|
}
|
|
template, exists := templateByID[workspace.TemplateID]
|
|
if !exists {
|
|
return nil, xerrors.Errorf("template not found for workspace %q", workspace.Name)
|
|
}
|
|
job, exists := jobByID[build.JobID]
|
|
if !exists {
|
|
return nil, xerrors.Errorf("build job not found for workspace: %q", err)
|
|
}
|
|
apiWorkspaces = append(apiWorkspaces,
|
|
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template))
|
|
}
|
|
return apiWorkspaces, nil
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|