Files
coder/coderd/workspacehistory.go
Kyle Carberry c65850b654 fix: Simplify provisionerd job acquire (#158)
This uses a simple channel to detect whether a
job is running or not, and moves all cancels
to be in goroutines.
2022-02-03 19:13:22 -06:00

260 lines
9.0 KiB
Go

package coderd
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/go-chi/render"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/database"
"github.com/coder/coder/httpapi"
"github.com/coder/coder/httpmw"
)
// WorkspaceHistory is an at-point representation of a workspace state.
// Iterate on before/after to determine a chronological history.
type WorkspaceHistory struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
WorkspaceID uuid.UUID `json:"workspace_id"`
ProjectHistoryID uuid.UUID `json:"project_history_id"`
BeforeID uuid.UUID `json:"before_id"`
AfterID uuid.UUID `json:"after_id"`
Name string `json:"name"`
Transition database.WorkspaceTransition `json:"transition"`
Initiator string `json:"initiator"`
Provision ProvisionerJob `json:"provision"`
}
// CreateWorkspaceHistoryRequest provides options to update the latest workspace history.
type CreateWorkspaceHistoryRequest struct {
ProjectHistoryID uuid.UUID `json:"project_history_id" validate:"required"`
Transition database.WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"`
}
func (api *api) postWorkspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) {
var createBuild CreateWorkspaceHistoryRequest
if !httpapi.Read(rw, r, &createBuild) {
return
}
user := httpmw.UserParam(r)
workspace := httpmw.WorkspaceParam(r)
projectHistory, err := api.Database.GetProjectHistoryByID(r.Context(), createBuild.ProjectHistoryID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "project history not found",
Errors: []httpapi.Error{{
Field: "project_history_id",
Code: "exists",
}},
})
return
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project history: %s", err),
})
return
}
projectHistoryJob, err := api.Database.GetProvisionerJobByID(r.Context(), projectHistory.ImportJobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
projectHistoryJobStatus := convertProvisionerJob(projectHistoryJob).Status
switch projectHistoryJobStatus {
case ProvisionerJobStatusPending, ProvisionerJobStatusRunning:
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: fmt.Sprintf("The provided project history is %s. Wait for it to complete importing!", projectHistoryJobStatus),
})
return
case ProvisionerJobStatusFailed:
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: fmt.Sprintf("The provided project history %q has failed to import. You cannot create workspaces using it!", projectHistory.Name),
})
return
case ProvisionerJobStatusCancelled:
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "The provided project history was canceled during import. You cannot create workspaces using it!",
})
}
project, err := api.Database.GetProjectByID(r.Context(), projectHistory.ProjectID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project: %s", err),
})
return
}
// Store prior history ID if it exists to update it after we create new!
priorHistoryID := uuid.NullUUID{}
priorHistory, err := api.Database.GetWorkspaceHistoryByWorkspaceIDWithoutAfter(r.Context(), workspace.ID)
if err == nil {
priorJob, err := api.Database.GetProvisionerJobByID(r.Context(), priorHistory.ProvisionJobID)
if err == nil && convertProvisionerJob(priorJob).Status.Completed() {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "a workspace build is already active",
})
return
}
priorHistoryID = uuid.NullUUID{
UUID: priorHistory.ID,
Valid: true,
}
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get prior workspace history: %s", err),
})
return
}
var provisionerJob database.ProvisionerJob
var workspaceHistory database.WorkspaceHistory
// 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 {
// Generate the ID before-hand so the provisioner job is aware of it!
workspaceHistoryID := uuid.New()
input, err := json.Marshal(workspaceProvisionJob{
WorkspaceHistoryID: workspaceHistoryID,
})
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: user.ID,
Provisioner: project.Provisioner,
Type: database.ProvisionerJobTypeWorkspaceProvision,
ProjectID: project.ID,
Input: input,
})
if err != nil {
return xerrors.Errorf("insert provisioner job: %w", err)
}
workspaceHistory, err = db.InsertWorkspaceHistory(r.Context(), database.InsertWorkspaceHistoryParams{
ID: workspaceHistoryID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
WorkspaceID: workspace.ID,
ProjectHistoryID: projectHistory.ID,
BeforeID: priorHistoryID,
Initiator: user.ID,
Transition: createBuild.Transition,
ProvisionJobID: provisionerJob.ID,
})
if err != nil {
return xerrors.Errorf("insert workspace history: %w", err)
}
if priorHistoryID.Valid {
// Update the prior history entries "after" column.
err = db.UpdateWorkspaceHistoryByID(r.Context(), database.UpdateWorkspaceHistoryByIDParams{
ID: priorHistory.ID,
UpdatedAt: database.Now(),
AfterID: uuid.NullUUID{
UUID: workspaceHistory.ID,
Valid: true,
},
})
if err != nil {
return xerrors.Errorf("update prior workspace history: %w", err)
}
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}
render.Status(r, http.StatusCreated)
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory, provisionerJob))
}
// Returns all workspace history. This is not sorted. Use before/after to chronologically sort.
func (api *api) workspaceHistoryByUser(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
histories, err := api.Database.GetWorkspaceHistoryByWorkspaceID(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 history: %s", err),
})
return
}
apiHistory := make([]WorkspaceHistory, 0, len(histories))
for _, history := range histories {
job, err := api.Database.GetProvisionerJobByID(r.Context(), history.ProvisionJobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
apiHistory = append(apiHistory, convertWorkspaceHistory(history, job))
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, apiHistory)
}
func (api *api) workspaceHistoryByName(rw http.ResponseWriter, r *http.Request) {
workspaceHistory := httpmw.WorkspaceHistoryParam(r)
job, err := api.Database.GetProvisionerJobByID(r.Context(), workspaceHistory.ProvisionJobID)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get provisioner job: %s", err),
})
return
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, convertWorkspaceHistory(workspaceHistory, job))
}
// Converts the internal history representation to a public external-facing model.
func convertWorkspaceHistory(workspaceHistory database.WorkspaceHistory, provisionerJob database.ProvisionerJob) WorkspaceHistory {
//nolint:unconvert
return WorkspaceHistory(WorkspaceHistory{
ID: workspaceHistory.ID,
CreatedAt: workspaceHistory.CreatedAt,
UpdatedAt: workspaceHistory.UpdatedAt,
WorkspaceID: workspaceHistory.WorkspaceID,
ProjectHistoryID: workspaceHistory.ProjectHistoryID,
BeforeID: workspaceHistory.BeforeID.UUID,
AfterID: workspaceHistory.AfterID.UUID,
Name: workspaceHistory.Name,
Transition: workspaceHistory.Transition,
Initiator: workspaceHistory.Initiator,
Provision: convertProvisionerJob(provisionerJob),
})
}
func workspaceHistoryLogsChannel(workspaceHistoryID uuid.UUID) string {
return fmt.Sprintf("workspace-history-logs:%s", workspaceHistoryID)
}