mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
* Nest jobs under an organization * Rename project parameter to parameter schema * Update references when computing project parameters * Add files endpoint * Allow one-off project import jobs * Allow variables to be injected that are not defined by the schema * Update API to use jobs first * Fix CLI tests * Fix linting * Fix hex length for files table * Reduce memory allocation for windows
167 lines
5.3 KiB
Go
167 lines
5.3 KiB
Go
package coderd
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/render"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/coder/coder/database"
|
|
"github.com/coder/coder/httpapi"
|
|
"github.com/coder/coder/httpmw"
|
|
)
|
|
|
|
type ProvisionerJobStatus string
|
|
|
|
// Completed returns whether the job is still processing.
|
|
func (p ProvisionerJobStatus) Completed() bool {
|
|
return p == ProvisionerJobStatusSucceeded || p == ProvisionerJobStatusFailed || p == ProvisionerJobStatusCancelled
|
|
}
|
|
|
|
const (
|
|
ProvisionerJobStatusPending ProvisionerJobStatus = "pending"
|
|
ProvisionerJobStatusRunning ProvisionerJobStatus = "running"
|
|
ProvisionerJobStatusSucceeded ProvisionerJobStatus = "succeeded"
|
|
ProvisionerJobStatusCancelled ProvisionerJobStatus = "canceled"
|
|
ProvisionerJobStatusFailed ProvisionerJobStatus = "failed"
|
|
)
|
|
|
|
type ProvisionerJob struct {
|
|
ID uuid.UUID `json:"id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
CancelledAt *time.Time `json:"canceled_at,omitempty"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
Status ProvisionerJobStatus `json:"status"`
|
|
Error string `json:"error,omitempty"`
|
|
Provisioner database.ProvisionerType `json:"provisioner"`
|
|
WorkerID *uuid.UUID `json:"worker_id,omitempty"`
|
|
}
|
|
|
|
type CreateProjectImportJobRequest struct {
|
|
StorageMethod database.ProvisionerStorageMethod `json:"storage_method" validate:"oneof=file,required"`
|
|
StorageSource string `json:"storage_source" validate:"required"`
|
|
Provisioner database.ProvisionerType `json:"provisioner" validate:"oneof=terraform echo,required"`
|
|
|
|
AdditionalParameters []ParameterValue `json:"parameter_values"`
|
|
SkipParameterSchemas bool `json:"skip_parameter_schemas"`
|
|
SkipResources bool `json:"skip_resources"`
|
|
}
|
|
|
|
func (*api) provisionerJobByOrganization(rw http.ResponseWriter, r *http.Request) {
|
|
job := httpmw.ProvisionerJobParam(r)
|
|
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(rw, r, convertProvisionerJob(job))
|
|
}
|
|
|
|
func (api *api) postProvisionerImportJobByOrganization(rw http.ResponseWriter, r *http.Request) {
|
|
apiKey := httpmw.APIKey(r)
|
|
organization := httpmw.OrganizationParam(r)
|
|
var req CreateProjectImportJobRequest
|
|
if !httpapi.Read(rw, r, &req) {
|
|
return
|
|
}
|
|
file, err := api.Database.GetFileByHash(r.Context(), req.StorageSource)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
httpapi.Write(rw, http.StatusBadRequest, 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
|
|
}
|
|
|
|
input, err := json.Marshal(projectVersionImportJob{
|
|
// AdditionalParameters: req.AdditionalParameters,
|
|
OrganizationID: organization.ID,
|
|
SkipParameterSchemas: req.SkipParameterSchemas,
|
|
SkipResources: req.SkipResources,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("marshal job: %s", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
job, err := api.Database.InsertProvisionerJob(r.Context(), database.InsertProvisionerJobParams{
|
|
ID: uuid.New(),
|
|
CreatedAt: database.Now(),
|
|
UpdatedAt: database.Now(),
|
|
OrganizationID: organization.ID,
|
|
InitiatorID: apiKey.UserID,
|
|
Provisioner: req.Provisioner,
|
|
StorageMethod: database.ProvisionerStorageMethodFile,
|
|
StorageSource: file.Hash,
|
|
Type: database.ProvisionerJobTypeProjectVersionImport,
|
|
Input: input,
|
|
})
|
|
if err != nil {
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
Message: fmt.Sprintf("insert provisioner job: %s", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
render.Status(r, http.StatusCreated)
|
|
render.JSON(rw, r, convertProvisionerJob(job))
|
|
}
|
|
|
|
func convertProvisionerJob(provisionerJob database.ProvisionerJob) ProvisionerJob {
|
|
job := ProvisionerJob{
|
|
ID: provisionerJob.ID,
|
|
CreatedAt: provisionerJob.CreatedAt,
|
|
UpdatedAt: provisionerJob.UpdatedAt,
|
|
Error: provisionerJob.Error.String,
|
|
Provisioner: provisionerJob.Provisioner,
|
|
}
|
|
// Applying values optional to the struct.
|
|
if provisionerJob.StartedAt.Valid {
|
|
job.StartedAt = &provisionerJob.StartedAt.Time
|
|
}
|
|
if provisionerJob.CancelledAt.Valid {
|
|
job.CancelledAt = &provisionerJob.CancelledAt.Time
|
|
}
|
|
if provisionerJob.CompletedAt.Valid {
|
|
job.CompletedAt = &provisionerJob.CompletedAt.Time
|
|
}
|
|
if provisionerJob.WorkerID.Valid {
|
|
job.WorkerID = &provisionerJob.WorkerID.UUID
|
|
}
|
|
|
|
switch {
|
|
case provisionerJob.CancelledAt.Valid:
|
|
job.Status = ProvisionerJobStatusCancelled
|
|
case !provisionerJob.StartedAt.Valid:
|
|
job.Status = ProvisionerJobStatusPending
|
|
case provisionerJob.CompletedAt.Valid:
|
|
job.Status = ProvisionerJobStatusSucceeded
|
|
case database.Now().Sub(provisionerJob.UpdatedAt) > 30*time.Second:
|
|
job.Status = ProvisionerJobStatusFailed
|
|
job.Error = "Worker failed to update job in time."
|
|
default:
|
|
job.Status = ProvisionerJobStatusRunning
|
|
}
|
|
|
|
if !provisionerJob.CancelledAt.Valid && job.Error != "" {
|
|
job.Status = ProvisionerJobStatusFailed
|
|
}
|
|
|
|
return job
|
|
}
|
|
|
|
func provisionerJobLogsChannel(jobID uuid.UUID) string {
|
|
return fmt.Sprintf("provisioner-log-logs:%s", jobID)
|
|
}
|