2022-03-07 17:40:54 +00:00
package coderd
import (
2022-03-22 19:17:50 +00:00
"database/sql"
2022-05-16 19:36:27 +00:00
"encoding/json"
"errors"
2022-03-07 17:40:54 +00:00
"fmt"
"net/http"
2022-05-16 19:36:27 +00:00
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"golang.org/x/xerrors"
2022-03-25 21:07:45 +00:00
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
2022-05-18 23:15:19 +00:00
"github.com/coder/coder/coderd/rbac"
2022-03-22 19:17:50 +00:00
"github.com/coder/coder/codersdk"
2022-03-07 17:40:54 +00:00
)
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuild ( rw http . ResponseWriter , r * http . Request ) {
2022-03-07 17:40:54 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-05-18 23:15:19 +00:00
workspace , err := api . Database . GetWorkspaceByID ( r . Context ( ) , workspaceBuild . WorkspaceID )
if err != nil {
httpapi . Write ( rw , http . StatusInternalServerError , httpapi . Response {
Message : "no workspace exists for this job" ,
} )
return
}
if ! api . Authorize ( rw , r , rbac . ActionRead , rbac . ResourceWorkspace .
InOrg ( workspace . OrganizationID ) . WithOwner ( workspace . OwnerID . String ( ) ) . WithID ( workspace . ID . String ( ) ) ) {
return
}
2022-03-07 17:40:54 +00:00
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
}
2022-04-12 15:17:33 +00:00
httpapi . Write ( rw , http . StatusOK , convertWorkspaceBuild ( workspaceBuild , convertProvisionerJob ( job ) ) )
2022-03-07 17:40:54 +00:00
}
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuilds ( rw http . ResponseWriter , r * http . Request ) {
2022-05-16 19:36:27 +00:00
workspace := httpmw . WorkspaceParam ( r )
2022-05-18 23:15:19 +00:00
if ! api . Authorize ( rw , r , rbac . ActionRead , rbac . ResourceWorkspace .
InOrg ( workspace . OrganizationID ) . WithOwner ( workspace . OwnerID . String ( ) ) . WithID ( workspace . ID . String ( ) ) ) {
return
}
2022-05-18 16:33:33 +00:00
paginationParams , ok := parsePagination ( rw , r )
if ! ok {
return
}
req := database . GetWorkspaceBuildByWorkspaceIDParams {
WorkspaceID : workspace . ID ,
AfterID : paginationParams . AfterID ,
OffsetOpt : int32 ( paginationParams . Offset ) ,
LimitOpt : int32 ( paginationParams . Limit ) ,
}
builds , err := api . Database . GetWorkspaceBuildByWorkspaceID ( r . Context ( ) , req )
2022-05-16 19:36:27 +00:00
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 )
}
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuildByName ( rw http . ResponseWriter , r * http . Request ) {
2022-05-16 19:36:27 +00:00
workspace := httpmw . WorkspaceParam ( r )
2022-05-18 23:15:19 +00:00
if ! api . Authorize ( rw , r , rbac . ActionRead , rbac . ResourceWorkspace .
InOrg ( workspace . OrganizationID ) . WithOwner ( workspace . OwnerID . String ( ) ) . WithID ( workspace . ID . String ( ) ) ) {
return
}
2022-05-16 19:36:27 +00:00
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 ) ) )
}
2022-05-26 03:14:08 +00:00
func ( api * API ) postWorkspaceBuilds ( rw http . ResponseWriter , r * http . Request ) {
2022-05-16 19:36:27 +00:00
apiKey := httpmw . APIKey ( r )
workspace := httpmw . WorkspaceParam ( r )
var createBuild codersdk . CreateWorkspaceBuildRequest
if ! httpapi . Read ( rw , r , & createBuild ) {
return
}
2022-05-18 23:15:19 +00:00
// Rbac action depends on the transition
var action rbac . Action
switch createBuild . Transition {
2022-05-19 18:04:44 +00:00
case codersdk . WorkspaceTransitionDelete :
2022-05-18 23:15:19 +00:00
action = rbac . ActionDelete
2022-05-19 18:04:44 +00:00
case codersdk . WorkspaceTransitionStart , codersdk . WorkspaceTransitionStop :
2022-05-18 23:15:19 +00:00
action = rbac . ActionUpdate
default :
httpapi . Write ( rw , http . StatusInternalServerError , httpapi . Response {
Message : fmt . Sprintf ( "transition not supported: %q" , createBuild . Transition ) ,
} )
return
}
if ! api . Authorize ( rw , r , action , rbac . ResourceWorkspace .
InOrg ( workspace . OrganizationID ) . WithOwner ( workspace . OwnerID . String ( ) ) . WithID ( workspace . ID . String ( ) ) ) {
return
}
2022-05-16 19:36:27 +00:00
if createBuild . TemplateVersionID == uuid . Nil {
2022-05-18 16:33:33 +00:00
latestBuild , err := api . Database . GetLatestWorkspaceBuildByWorkspaceID ( r . Context ( ) , workspace . ID )
2022-05-16 19:36:27 +00:00
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
}
2022-05-18 16:33:33 +00:00
// Store prior build number to compute new build number
var priorBuildNum int32
priorHistory , err := api . Database . GetLatestWorkspaceBuildByWorkspaceID ( r . Context ( ) , workspace . ID )
2022-05-16 19:36:27 +00:00
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
}
2022-05-18 16:33:33 +00:00
priorBuildNum = priorHistory . BuildNumber
2022-05-16 19:36:27 +00:00
} 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 ,
2022-05-18 16:33:33 +00:00
BuildNumber : priorBuildNum + 1 ,
2022-05-16 19:36:27 +00:00
Name : namesgenerator . GetRandomName ( 1 ) ,
ProvisionerState : state ,
InitiatorID : apiKey . UserID ,
2022-05-19 18:04:44 +00:00
Transition : database . WorkspaceTransition ( createBuild . Transition ) ,
2022-05-16 19:36:27 +00:00
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 : err . Error ( ) ,
} )
return
}
httpapi . Write ( rw , http . StatusCreated , convertWorkspaceBuild ( workspaceBuild , convertProvisionerJob ( provisionerJob ) ) )
}
2022-05-26 03:14:08 +00:00
func ( api * API ) patchCancelWorkspaceBuild ( rw http . ResponseWriter , r * http . Request ) {
2022-03-22 19:17:50 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-05-18 23:15:19 +00:00
workspace , err := api . Database . GetWorkspaceByID ( r . Context ( ) , workspaceBuild . WorkspaceID )
if err != nil {
httpapi . Write ( rw , http . StatusInternalServerError , httpapi . Response {
Message : "no workspace exists for this job" ,
} )
return
}
if ! api . Authorize ( rw , r , rbac . ActionUpdate , rbac . ResourceWorkspace .
InOrg ( workspace . OrganizationID ) . WithOwner ( workspace . OwnerID . String ( ) ) . WithID ( workspace . ID . String ( ) ) ) {
return
}
2022-03-22 19:17:50 +00:00
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
}
if job . CompletedAt . Valid {
httpapi . Write ( rw , http . StatusPreconditionFailed , httpapi . Response {
Message : "Job has already completed!" ,
} )
return
}
if job . CanceledAt . Valid {
httpapi . Write ( rw , http . StatusPreconditionFailed , httpapi . Response {
Message : "Job has already been marked as canceled!" ,
} )
return
}
err = api . Database . UpdateProvisionerJobWithCancelByID ( r . Context ( ) , database . UpdateProvisionerJobWithCancelByIDParams {
ID : job . ID ,
CanceledAt : sql . NullTime {
Time : database . Now ( ) ,
Valid : true ,
} ,
} )
if err != nil {
httpapi . Write ( rw , http . StatusInternalServerError , httpapi . Response {
Message : fmt . Sprintf ( "update provisioner job: %s" , err ) ,
} )
return
}
httpapi . Write ( rw , http . StatusOK , httpapi . Response {
Message : "Job has been marked as canceled..." ,
} )
}
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuildResources ( rw http . ResponseWriter , r * http . Request ) {
2022-03-07 17:40:54 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-05-18 23:15:19 +00:00
workspace , err := api . Database . GetWorkspaceByID ( r . Context ( ) , workspaceBuild . WorkspaceID )
if err != nil {
httpapi . Write ( rw , http . StatusInternalServerError , httpapi . Response {
Message : "no workspace exists for this job" ,
} )
return
}
if ! api . Authorize ( rw , r , rbac . ActionRead , rbac . ResourceWorkspace .
InOrg ( workspace . OrganizationID ) . WithOwner ( workspace . OwnerID . String ( ) ) . WithID ( workspace . ID . String ( ) ) ) {
return
}
2022-03-07 17:40:54 +00:00
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
}
api . provisionerJobResources ( rw , r , job )
}
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuildLogs ( rw http . ResponseWriter , r * http . Request ) {
2022-03-07 17:40:54 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-05-18 23:15:19 +00:00
workspace , err := api . Database . GetWorkspaceByID ( r . Context ( ) , workspaceBuild . WorkspaceID )
if err != nil {
httpapi . Write ( rw , http . StatusInternalServerError , httpapi . Response {
Message : "no workspace exists for this job" ,
} )
return
}
if ! api . Authorize ( rw , r , rbac . ActionRead , rbac . ResourceWorkspace .
InOrg ( workspace . OrganizationID ) . WithOwner ( workspace . OwnerID . String ( ) ) . WithID ( workspace . ID . String ( ) ) ) {
return
}
2022-03-07 17:40:54 +00:00
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
}
api . provisionerJobLogs ( rw , r , job )
}
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuildState ( rw http . ResponseWriter , r * http . Request ) {
2022-05-02 22:51:58 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-05-18 23:15:19 +00:00
workspace , err := api . Database . GetWorkspaceByID ( r . Context ( ) , workspaceBuild . WorkspaceID )
if err != nil {
httpapi . Write ( rw , http . StatusInternalServerError , httpapi . Response {
Message : "no workspace exists for this job" ,
} )
return
}
if ! api . Authorize ( rw , r , rbac . ActionRead , rbac . ResourceWorkspace .
InOrg ( workspace . OrganizationID ) . WithOwner ( workspace . OwnerID . String ( ) ) . WithID ( workspace . ID . String ( ) ) ) {
return
}
2022-05-02 22:51:58 +00:00
rw . Header ( ) . Set ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
_ , _ = rw . Write ( workspaceBuild . ProvisionerState )
}
2022-03-22 19:17:50 +00:00
func convertWorkspaceBuild ( workspaceBuild database . WorkspaceBuild , job codersdk . ProvisionerJob ) codersdk . WorkspaceBuild {
2022-03-07 17:40:54 +00:00
//nolint:unconvert
2022-03-22 19:17:50 +00:00
return codersdk . WorkspaceBuild {
2022-04-06 17:42:40 +00:00
ID : workspaceBuild . ID ,
CreatedAt : workspaceBuild . CreatedAt ,
UpdatedAt : workspaceBuild . UpdatedAt ,
WorkspaceID : workspaceBuild . WorkspaceID ,
TemplateVersionID : workspaceBuild . TemplateVersionID ,
2022-05-18 16:33:33 +00:00
BuildNumber : workspaceBuild . BuildNumber ,
2022-04-06 17:42:40 +00:00
Name : workspaceBuild . Name ,
2022-05-19 18:04:44 +00:00
Transition : codersdk . WorkspaceTransition ( workspaceBuild . Transition ) ,
2022-04-06 17:42:40 +00:00
InitiatorID : workspaceBuild . InitiatorID ,
Job : job ,
2022-05-26 17:08:11 +00:00
Deadline : workspaceBuild . Deadline ,
2022-03-22 19:17:50 +00:00
}
2022-03-07 17:40:54 +00:00
}
2022-04-11 21:06:15 +00:00
func convertWorkspaceResource ( resource database . WorkspaceResource , agents [ ] codersdk . WorkspaceAgent ) codersdk . WorkspaceResource {
2022-03-22 19:17:50 +00:00
return codersdk . WorkspaceResource {
2022-03-07 17:40:54 +00:00
ID : resource . ID ,
CreatedAt : resource . CreatedAt ,
JobID : resource . JobID ,
2022-05-19 18:04:44 +00:00
Transition : codersdk . WorkspaceTransition ( resource . Transition ) ,
2022-03-07 17:40:54 +00:00
Type : resource . Type ,
Name : resource . Name ,
2022-04-11 21:06:15 +00:00
Agents : agents ,
2022-03-07 17:40:54 +00:00
}
}