2022-03-07 17:40:54 +00:00
package coderd
import (
2022-09-16 18:54:23 +00:00
"context"
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-06-10 16:08:50 +00:00
"strconv"
2022-10-09 20:01:18 +00:00
"time"
2022-03-07 17:40:54 +00:00
2022-05-16 19:36:27 +00:00
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
2022-08-01 21:53:05 +00:00
"golang.org/x/exp/slices"
2022-05-16 19:36:27 +00:00
"golang.org/x/xerrors"
2022-03-25 21:07:45 +00:00
"github.com/coder/coder/coderd/database"
2023-03-10 18:09:28 +00:00
"github.com/coder/coder/coderd/database/dbauthz"
2022-03-25 21:07:45 +00:00
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
2022-11-08 01:10:49 +00:00
"github.com/coder/coder/coderd/provisionerdserver"
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
)
2023-01-05 14:27:10 +00:00
// @Summary Get workspace build
// @ID get-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {object} codersdk.WorkspaceBuild
// @Router /workspacebuilds/{workspacebuild} [get]
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuild ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-03-07 17:40:54 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-06-01 23:49:43 +00:00
workspace := httpmw . WorkspaceParam ( r )
2022-05-18 23:15:19 +00:00
2023-03-10 16:39:02 +00:00
data , err := api . workspaceBuildsData ( ctx , [ ] database . Workspace { workspace } , [ ] database . WorkspaceBuild { workspaceBuild } )
2022-03-07 17:40:54 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-16 18:54:23 +00:00
Message : "Internal error getting workspace build data." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-03-07 17:40:54 +00:00
} )
return
}
2022-04-12 15:17:33 +00:00
2023-03-10 18:09:28 +00:00
// Ensure we have the job and template version for the workspace build.
// Otherwise we risk a panic in the api.convertWorkspaceBuild call below.
if len ( data . jobs ) == 0 {
httpapi . Write ( ctx , rw , http . StatusNotFound , codersdk . Response {
Message : "Internal error getting workspace build data." ,
Detail : "No job found for workspace build." ,
} )
return
}
if len ( data . templateVersions ) == 0 {
httpapi . Write ( ctx , rw , http . StatusNotFound , codersdk . Response {
Message : "Internal error getting workspace build data." ,
Detail : "No template version found for workspace build." ,
} )
return
}
2022-09-16 18:54:23 +00:00
apiBuild , err := api . convertWorkspaceBuild (
workspaceBuild ,
workspace ,
data . jobs [ 0 ] ,
data . users ,
data . resources ,
data . metadata ,
data . agents ,
data . apps ,
2022-11-28 19:53:56 +00:00
data . templateVersions [ 0 ] ,
2022-09-16 18:54:23 +00:00
)
2022-06-07 14:28:47 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-16 18:54:23 +00:00
Message : "Internal error converting workspace build." ,
2022-06-07 14:28:47 +00:00
Detail : err . Error ( ) ,
} )
return
}
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , apiBuild )
2022-03-07 17:40:54 +00:00
}
2023-01-05 14:27:10 +00:00
// @Summary Get workspace builds by workspace ID
// @ID get-workspace-builds-by-workspace-id
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
2023-01-13 11:27:21 +00:00
// @Param workspace path string true "Workspace ID" format(uuid)
2023-01-05 14:27:10 +00:00
// @Param after_id query string false "After ID" format(uuid)
// @Param limit query int false "Page limit"
// @Param offset query int false "Page offset"
// @Param since query string false "Since timestamp" format(date-time)
// @Success 200 {array} codersdk.WorkspaceBuild
2023-01-13 11:27:21 +00:00
// @Router /workspaces/{workspace}/builds [get]
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuilds ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-05-16 19:36:27 +00:00
workspace := httpmw . WorkspaceParam ( r )
2022-05-18 16:33:33 +00:00
paginationParams , ok := parsePagination ( rw , r )
if ! ok {
return
}
2022-06-02 14:01:45 +00:00
2022-10-09 20:01:18 +00:00
var since time . Time
sinceParam := r . URL . Query ( ) . Get ( "since" )
if sinceParam != "" {
var err error
since , err = time . Parse ( time . RFC3339 , sinceParam )
if err != nil {
httpapi . Write ( r . Context ( ) , rw , http . StatusBadRequest , codersdk . Response {
Message : "bad `since` format, must be RFC3339" ,
Detail : err . Error ( ) ,
} )
return
}
}
2023-03-10 16:39:02 +00:00
var workspaceBuilds [ ] database . WorkspaceBuild
2022-06-02 14:01:45 +00:00
// Ensure all db calls happen in the same tx
err := api . Database . InTx ( func ( store database . Store ) error {
var err error
if paginationParams . AfterID != uuid . Nil {
// See if the record exists first. If the record does not exist, the pagination
// query will not work.
2022-09-21 22:07:00 +00:00
_ , err := store . GetWorkspaceBuildByID ( ctx , paginationParams . AfterID )
2022-06-02 14:01:45 +00:00
if err != nil && xerrors . Is ( err , sql . ErrNoRows ) {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : fmt . Sprintf ( "Record at \"after_id\" (%q) does not exist." , paginationParams . AfterID . String ( ) ) ,
2022-06-02 14:01:45 +00:00
} )
return err
} else if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching workspace build at \"after_id\"." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-06-02 14:01:45 +00:00
} )
return err
}
}
2022-10-09 20:01:18 +00:00
req := database . GetWorkspaceBuildsByWorkspaceIDParams {
2022-06-02 14:01:45 +00:00
WorkspaceID : workspace . ID ,
AfterID : paginationParams . AfterID ,
OffsetOpt : int32 ( paginationParams . Offset ) ,
LimitOpt : int32 ( paginationParams . Limit ) ,
2022-10-09 20:01:18 +00:00
Since : database . Time ( since ) ,
2022-06-02 14:01:45 +00:00
}
2022-10-09 20:01:18 +00:00
workspaceBuilds , err = store . GetWorkspaceBuildsByWorkspaceID ( ctx , req )
2022-06-02 14:01:45 +00:00
if xerrors . Is ( err , sql . ErrNoRows ) {
err = nil
}
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching workspace build." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-06-02 14:01:45 +00:00
} )
return err
}
return nil
2022-11-14 17:57:33 +00:00
} , nil )
2022-05-16 19:36:27 +00:00
if err != nil {
return
}
2022-06-02 14:01:45 +00:00
2022-09-21 22:07:00 +00:00
data , err := api . workspaceBuildsData ( ctx , [ ] database . Workspace { workspace } , workspaceBuilds )
2022-05-16 19:36:27 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-16 18:54:23 +00:00
Message : "Internal error getting workspace build data." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-05-16 19:36:27 +00:00
} )
return
}
2022-09-16 18:54:23 +00:00
apiBuilds , err := api . convertWorkspaceBuilds (
workspaceBuilds ,
[ ] database . Workspace { workspace } ,
data . jobs ,
data . users ,
data . resources ,
data . metadata ,
data . agents ,
data . apps ,
2022-11-28 19:53:56 +00:00
data . templateVersions ,
2022-09-16 18:54:23 +00:00
)
2022-06-07 14:28:47 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-16 18:54:23 +00:00
Message : "Internal error converting workspace build." ,
2022-06-07 14:28:47 +00:00
Detail : err . Error ( ) ,
} )
return
}
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , apiBuilds )
2022-05-16 19:36:27 +00:00
}
2023-01-11 13:08:04 +00:00
// @Summary Get workspace build by user, workspace name, and build number
// @ID get-workspace-build-by-user-workspace-name-and-build-number
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param user path string true "User ID, name, or me"
// @Param workspacename path string true "Workspace name"
// @Param buildnumber path string true "Build number" format(number)
// @Success 200 {object} codersdk.WorkspaceBuild
// @Router /users/{user}/workspace/{workspacename}/builds/{buildnumber} [get]
2022-06-10 16:08:50 +00:00
func ( api * API ) workspaceBuildByBuildNumber ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-06-10 16:08:50 +00:00
owner := httpmw . UserParam ( r )
workspaceName := chi . URLParam ( r , "workspacename" )
buildNumber , err := strconv . ParseInt ( chi . URLParam ( r , "buildnumber" ) , 10 , 32 )
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-06-10 16:08:50 +00:00
Message : "Failed to parse build number as integer." ,
Detail : err . Error ( ) ,
} )
return
}
2022-09-21 22:07:00 +00:00
workspace , err := api . Database . GetWorkspaceByOwnerIDAndName ( ctx , database . GetWorkspaceByOwnerIDAndNameParams {
2022-06-10 16:08:50 +00:00
OwnerID : owner . ID ,
Name : workspaceName ,
} )
if errors . Is ( err , sql . ErrNoRows ) {
2022-06-14 15:14:05 +00:00
httpapi . ResourceNotFound ( rw )
2022-06-10 16:08:50 +00:00
return
}
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-10 16:08:50 +00:00
Message : "Internal error fetching workspace by name." ,
Detail : err . Error ( ) ,
} )
return
}
2022-09-21 22:07:00 +00:00
workspaceBuild , err := api . Database . GetWorkspaceBuildByWorkspaceIDAndBuildNumber ( ctx , database . GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams {
2022-06-10 16:08:50 +00:00
WorkspaceID : workspace . ID ,
BuildNumber : int32 ( buildNumber ) ,
} )
if errors . Is ( err , sql . ErrNoRows ) {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusNotFound , codersdk . Response {
2022-06-10 16:08:50 +00:00
Message : fmt . Sprintf ( "Workspace %q Build %d does not exist." , workspaceName , buildNumber ) ,
} )
return
}
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-10 16:08:50 +00:00
Message : "Internal error fetching workspace build." ,
Detail : err . Error ( ) ,
} )
return
}
2023-03-10 16:39:02 +00:00
data , err := api . workspaceBuildsData ( ctx , [ ] database . Workspace { workspace } , [ ] database . WorkspaceBuild { workspaceBuild } )
2022-06-10 16:08:50 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-16 18:54:23 +00:00
Message : "Internal error getting workspace build data." ,
2022-06-10 16:08:50 +00:00
Detail : err . Error ( ) ,
} )
return
}
2022-09-16 18:54:23 +00:00
apiBuild , err := api . convertWorkspaceBuild (
workspaceBuild ,
workspace ,
data . jobs [ 0 ] ,
data . users ,
data . resources ,
data . metadata ,
data . agents ,
data . apps ,
2022-11-28 19:53:56 +00:00
data . templateVersions [ 0 ] ,
2022-09-16 18:54:23 +00:00
)
2022-06-10 16:08:50 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-16 18:54:23 +00:00
Message : "Internal error converting workspace build." ,
2022-06-10 16:08:50 +00:00
Detail : err . Error ( ) ,
} )
return
}
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , apiBuild )
2022-06-10 16:08:50 +00:00
}
2023-01-05 14:27:10 +00:00
// Azure supports instance identity verification:
// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#tabgroup_14
//
// @Summary Create workspace build
// @ID create-workspace-build
// @Security CoderSessionToken
2023-01-13 11:27:21 +00:00
// @Accept json
2023-01-05 14:27:10 +00:00
// @Produce json
// @Tags Builds
2023-01-13 11:27:21 +00:00
// @Param workspace path string true "Workspace ID" format(uuid)
2023-01-05 14:27:10 +00:00
// @Param request body codersdk.CreateWorkspaceBuildRequest true "Create workspace build request"
// @Success 200 {object} codersdk.WorkspaceBuild
2023-01-13 11:27:21 +00:00
// @Router /workspaces/{workspace}/builds [post]
2023-02-02 15:44:57 +00:00
// nolint:gocyclo
2022-05-26 03:14:08 +00:00
func ( api * API ) postWorkspaceBuilds ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-05-16 19:36:27 +00:00
apiKey := httpmw . APIKey ( r )
workspace := httpmw . WorkspaceParam ( r )
var createBuild codersdk . CreateWorkspaceBuildRequest
2022-09-21 22:07:00 +00:00
if ! httpapi . Read ( ctx , rw , r , & createBuild ) {
2022-05-16 19:36:27 +00:00
return
}
2022-05-18 23:15:19 +00:00
2023-03-21 14:10:22 +00:00
// Doing this up front saves a lot of work if the user doesn't have permission.
// This is checked again in the dbauthz layer, but the check is cached
// and will be a noop later.
2022-05-18 23:15:19 +00:00
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 :
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : fmt . Sprintf ( "Transition %q not supported." , createBuild . Transition ) ,
2022-05-18 23:15:19 +00:00
} )
return
}
2022-08-09 18:16:53 +00:00
if ! api . Authorize ( r , action , workspace ) {
2022-06-14 15:14:05 +00:00
httpapi . ResourceNotFound ( rw )
2022-05-18 23:15:19 +00:00
return
}
2022-10-25 19:34:48 +00:00
if createBuild . TemplateVersionID == uuid . Nil {
2022-11-22 18:22:56 +00:00
latestBuild , latestBuildErr := api . Database . GetLatestWorkspaceBuildByWorkspaceID ( ctx , workspace . ID )
2022-10-25 19:34:48 +00:00
if latestBuildErr != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching the latest workspace build." ,
2022-10-25 19:34:48 +00:00
Detail : latestBuildErr . Error ( ) ,
2022-05-16 19:36:27 +00:00
} )
return
}
createBuild . TemplateVersionID = latestBuild . TemplateVersionID
}
2022-09-06 17:07:00 +00:00
2022-09-21 22:07:00 +00:00
templateVersion , err := api . Database . GetTemplateVersionByID ( ctx , createBuild . TemplateVersionID )
2022-05-16 19:36:27 +00:00
if errors . Is ( err , sql . ErrNoRows ) {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Template version not found." ,
2022-07-13 00:15:02 +00:00
Validations : [ ] codersdk . ValidationError { {
2022-05-16 19:36:27 +00:00
Field : "template_version_id" ,
Detail : "template version not found" ,
} } ,
} )
return
}
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching template version." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-05-16 19:36:27 +00:00
} )
return
}
2022-09-06 17:07:00 +00:00
2022-09-21 22:07:00 +00:00
template , err := api . Database . GetTemplateByID ( ctx , templateVersion . TemplateID . UUID )
2022-09-06 17:07:00 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-06 17:07:00 +00:00
Message : "Failed to get template" ,
Detail : err . Error ( ) ,
} )
return
}
var state [ ] byte
// If custom state, deny request since user could be corrupting or leaking
// cloud state.
if createBuild . ProvisionerState != nil || createBuild . Orphan {
if ! api . Authorize ( r , rbac . ActionUpdate , template . RBACObject ( ) ) {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusForbidden , codersdk . Response {
2022-09-06 17:07:00 +00:00
Message : "Only template managers may provide custom state" ,
} )
return
}
state = createBuild . ProvisionerState
}
if createBuild . Orphan {
if createBuild . Transition != codersdk . WorkspaceTransitionDelete {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-09-06 17:07:00 +00:00
Message : "Orphan is only permitted when deleting a workspace." ,
Detail : err . Error ( ) ,
} )
return
}
if createBuild . ProvisionerState != nil && createBuild . Orphan {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-09-06 17:07:00 +00:00
Message : "ProvisionerState cannot be set alongside Orphan since state intent is unclear." ,
} )
return
}
state = [ ] byte { }
}
2022-09-21 22:07:00 +00:00
templateVersionJob , err := api . Database . GetProvisionerJobByID ( ctx , templateVersion . JobID )
2022-05-16 19:36:27 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching provisioner job." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-05-16 19:36:27 +00:00
} )
return
}
templateVersionJobStatus := convertProvisionerJob ( templateVersionJob ) . Status
switch templateVersionJobStatus {
case codersdk . ProvisionerJobPending , codersdk . ProvisionerJobRunning :
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusNotAcceptable , codersdk . Response {
2022-05-16 19:36:27 +00:00
Message : fmt . Sprintf ( "The provided template version is %s. Wait for it to complete importing!" , templateVersionJobStatus ) ,
} )
return
case codersdk . ProvisionerJobFailed :
2023-01-13 14:30:48 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-05-16 19:36:27 +00:00
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 :
2023-01-13 14:30:48 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-05-16 19:36:27 +00:00
Message : "The provided template version was canceled during import. You cannot builds workspaces with it!" ,
} )
return
}
2022-11-16 22:34:06 +00:00
tags := provisionerdserver . MutateTags ( workspace . OwnerID , templateVersionJob . Tags )
2022-05-18 16:33:33 +00:00
// Store prior build number to compute new build number
var priorBuildNum int32
2022-09-21 22:07:00 +00:00
priorHistory , err := api . Database . GetLatestWorkspaceBuildByWorkspaceID ( ctx , workspace . ID )
2022-05-16 19:36:27 +00:00
if err == nil {
2022-09-21 22:07:00 +00:00
priorJob , err := api . Database . GetProvisionerJobByID ( ctx , priorHistory . JobID )
2022-05-16 19:36:27 +00:00
if err == nil && convertProvisionerJob ( priorJob ) . Status . Active ( ) {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusConflict , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "A workspace build is already active." ,
2022-05-16 19:36:27 +00:00
} )
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 ) {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching prior workspace build." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-05-16 19:36:27 +00:00
} )
return
}
2022-09-06 17:07:00 +00:00
if state == nil {
state = priorHistory . ProvisionerState
}
2023-01-24 13:22:00 +00:00
dbTemplateVersionParameters , err := api . Database . GetTemplateVersionParameters ( ctx , createBuild . TemplateVersionID )
2023-01-23 14:01:22 +00:00
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching template version parameters." ,
Detail : err . Error ( ) ,
} )
return
}
2023-01-24 13:22:00 +00:00
templateVersionParameters , err := convertTemplateVersionParameters ( dbTemplateVersionParameters )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error converting template version parameters." ,
Detail : err . Error ( ) ,
} )
return
}
2023-02-07 08:36:13 +00:00
lastBuildParameters , err := api . Database . GetWorkspaceBuildParameters ( ctx , priorHistory . ID )
2023-01-24 13:22:00 +00:00
if err != nil {
2023-02-07 08:36:13 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching prior workspace build parameters." ,
2023-01-24 13:22:00 +00:00
Detail : err . Error ( ) ,
} )
return
}
2023-02-07 08:36:13 +00:00
apiLastBuildParameters := convertWorkspaceBuildParameters ( lastBuildParameters )
2023-01-23 14:01:22 +00:00
2023-03-14 12:02:44 +00:00
legacyParameters , err := api . Database . ParameterValues ( ctx , database . ParameterValuesParams {
Scopes : [ ] database . ParameterScope { database . ParameterScopeWorkspace } ,
ScopeIds : [ ] uuid . UUID { workspace . ID } ,
} )
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Error fetching previous legacy parameters." ,
Detail : err . Error ( ) ,
} )
return
}
// Rich parameters migration: include legacy variables to the last build parameters
for _ , templateVersionParameter := range templateVersionParameters {
// Check if parameter is defined in previous build
if _ , found := findWorkspaceBuildParameter ( apiLastBuildParameters , templateVersionParameter . Name ) ; found {
continue
}
// Check if legacy variable is defined
for _ , legacyParameter := range legacyParameters {
if legacyParameter . Name != templateVersionParameter . LegacyVariableName {
continue
}
apiLastBuildParameters = append ( apiLastBuildParameters , codersdk . WorkspaceBuildParameter {
Name : templateVersionParameter . Name ,
Value : legacyParameter . SourceValue ,
} )
break
}
}
2023-02-07 08:36:13 +00:00
err = codersdk . ValidateWorkspaceBuildParameters ( templateVersionParameters , createBuild . RichParameterValues , apiLastBuildParameters )
2023-01-23 14:01:22 +00:00
if err != nil {
2023-02-07 08:36:13 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Error validating workspace build parameters." ,
2023-01-23 14:01:22 +00:00
Detail : err . Error ( ) ,
} )
return
}
var parameters [ ] codersdk . WorkspaceBuildParameter
for _ , templateVersionParameter := range templateVersionParameters {
// Check if parameter value is in request
if buildParameter , found := findWorkspaceBuildParameter ( createBuild . RichParameterValues , templateVersionParameter . Name ) ; found {
if ! templateVersionParameter . Mutable {
2023-04-04 12:22:46 +00:00
if _ , found := findWorkspaceBuildParameter ( apiLastBuildParameters , templateVersionParameter . Name ) ; found {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : fmt . Sprintf ( "Parameter %q is not mutable, so it can't be updated after creating a workspace." , templateVersionParameter . Name ) ,
} )
return
}
2023-01-23 14:01:22 +00:00
}
parameters = append ( parameters , * buildParameter )
continue
2023-01-17 15:24:45 +00:00
}
2023-01-23 14:01:22 +00:00
// Check if parameter is defined in previous build
if buildParameter , found := findWorkspaceBuildParameter ( apiLastBuildParameters , templateVersionParameter . Name ) ; found {
parameters = append ( parameters , * buildParameter )
2023-01-17 15:24:45 +00:00
}
}
2023-03-30 14:00:33 +00:00
if createBuild . LogLevel != "" && ! api . Authorize ( r , rbac . ActionUpdate , template ) {
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
Message : "Workspace builds with a custom log level are restricted to template authors only." ,
} )
return
}
2023-03-10 16:39:02 +00:00
var workspaceBuild database . WorkspaceBuild
2022-05-16 19:36:27 +00:00
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 {
2022-06-27 16:19:10 +00:00
// Write/Update any new params
now := database . Now ( )
for _ , param := range createBuild . ParameterValues {
2023-02-02 15:44:57 +00:00
for _ , exists := range legacyParameters {
2022-06-27 16:19:10 +00:00
// If the param exists, delete the old param before inserting the new one
if exists . Name == param . Name {
2022-09-21 22:07:00 +00:00
err = db . DeleteParameterValueByID ( ctx , exists . ID )
2022-06-27 16:19:10 +00:00
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
return xerrors . Errorf ( "Failed to delete old param %q: %w" , exists . Name , err )
}
}
}
2022-09-21 22:07:00 +00:00
_ , err = db . InsertParameterValue ( ctx , database . InsertParameterValueParams {
2022-06-27 16:19:10 +00:00
ID : uuid . New ( ) ,
Name : param . Name ,
CreatedAt : now ,
UpdatedAt : now ,
Scope : database . ParameterScopeWorkspace ,
ScopeID : workspace . ID ,
SourceScheme : database . ParameterSourceScheme ( param . SourceScheme ) ,
SourceValue : param . SourceValue ,
DestinationScheme : database . ParameterDestinationScheme ( param . DestinationScheme ) ,
} )
if err != nil {
return xerrors . Errorf ( "insert parameter value: %w" , err )
}
}
2022-05-16 19:36:27 +00:00
workspaceBuildID := uuid . New ( )
2022-11-08 01:10:49 +00:00
input , err := json . Marshal ( provisionerdserver . WorkspaceProvisionJob {
2022-05-16 19:36:27 +00:00
WorkspaceBuildID : workspaceBuildID ,
2023-03-30 14:00:33 +00:00
LogLevel : string ( createBuild . LogLevel ) ,
2022-05-16 19:36:27 +00:00
} )
if err != nil {
return xerrors . Errorf ( "marshal provision job: %w" , err )
}
2022-09-21 22:07:00 +00:00
provisionerJob , err = db . InsertProvisionerJob ( ctx , database . InsertProvisionerJobParams {
2022-05-16 19:36:27 +00:00
ID : uuid . New ( ) ,
CreatedAt : database . Now ( ) ,
UpdatedAt : database . Now ( ) ,
InitiatorID : apiKey . UserID ,
OrganizationID : template . OrganizationID ,
Provisioner : template . Provisioner ,
Type : database . ProvisionerJobTypeWorkspaceBuild ,
StorageMethod : templateVersionJob . StorageMethod ,
2022-10-13 23:02:52 +00:00
FileID : templateVersionJob . FileID ,
2022-05-16 19:36:27 +00:00
Input : input ,
2022-11-16 22:34:06 +00:00
Tags : tags ,
2022-05-16 19:36:27 +00:00
} )
if err != nil {
return xerrors . Errorf ( "insert provisioner job: %w" , err )
}
2023-03-10 16:39:02 +00:00
workspaceBuild , err = db . InsertWorkspaceBuild ( ctx , database . InsertWorkspaceBuildParams {
2022-05-16 19:36:27 +00:00
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
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 ,
2022-06-17 17:41:11 +00:00
Reason : database . BuildReasonInitiator ,
2022-05-16 19:36:27 +00:00
} )
if err != nil {
return xerrors . Errorf ( "insert workspace build: %w" , err )
}
2023-01-17 15:24:45 +00:00
names := make ( [ ] string , 0 , len ( parameters ) )
values := make ( [ ] string , 0 , len ( parameters ) )
for _ , param := range parameters {
names = append ( names , param . Name )
values = append ( values , param . Value )
}
err = db . InsertWorkspaceBuildParameters ( ctx , database . InsertWorkspaceBuildParametersParams {
WorkspaceBuildID : workspaceBuildID ,
Name : names ,
Value : values ,
} )
if err != nil {
2023-01-23 14:01:22 +00:00
return xerrors . Errorf ( "insert workspace build parameters: %w" , err )
2023-01-17 15:24:45 +00:00
}
2022-05-16 19:36:27 +00:00
return nil
2022-11-14 17:57:33 +00:00
} , nil )
2022-05-16 19:36:27 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error inserting workspace build." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-05-16 19:36:27 +00:00
} )
return
}
2022-10-10 18:03:54 +00:00
users , err := api . Database . GetUsersByIDs ( ctx , [ ] uuid . UUID {
workspace . OwnerID ,
workspaceBuild . InitiatorID ,
2022-06-09 01:23:35 +00:00
} )
2022-06-07 14:28:47 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:52:44 +00:00
Message : "Internal error getting user." ,
2022-06-07 14:28:47 +00:00
Detail : err . Error ( ) ,
} )
return
}
2022-09-16 18:54:23 +00:00
apiBuild , err := api . convertWorkspaceBuild (
workspaceBuild ,
workspace ,
provisionerJob ,
users ,
[ ] database . WorkspaceResource { } ,
[ ] database . WorkspaceResourceMetadatum { } ,
[ ] database . WorkspaceAgent { } ,
[ ] database . WorkspaceApp { } ,
2022-11-28 19:53:56 +00:00
database . TemplateVersion { } ,
2022-09-16 18:54:23 +00:00
)
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-09-16 18:54:23 +00:00
Message : "Internal error converting workspace build." ,
Detail : err . Error ( ) ,
} )
return
}
2022-11-07 15:25:18 +00:00
api . publishWorkspaceUpdate ( ctx , workspace . ID )
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusCreated , apiBuild )
2022-05-16 19:36:27 +00:00
}
2023-01-05 14:27:10 +00:00
// @Summary Cancel workspace build
// @ID cancel-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {object} codersdk.Response
// @Router /workspacebuilds/{workspacebuild}/cancel [patch]
2022-05-26 03:14:08 +00:00
func ( api * API ) patchCancelWorkspaceBuild ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-03-22 19:17:50 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-09-21 22:07:00 +00:00
workspace , err := api . Database . GetWorkspaceByID ( ctx , workspaceBuild . WorkspaceID )
2022-05-18 23:15:19 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "No workspace exists for this job." ,
2022-05-18 23:15:19 +00:00
} )
return
}
2022-11-21 10:43:53 +00:00
valid , err := api . verifyUserCanCancelWorkspaceBuilds ( ctx , httpmw . APIKey ( r ) . UserID , workspace . TemplateID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error verifying permission to cancel workspace build." ,
Detail : err . Error ( ) ,
} )
return
}
if ! valid {
httpapi . Write ( ctx , rw , http . StatusForbidden , codersdk . Response {
Message : "User is not allowed to cancel workspace builds. Owner role is required." ,
} )
return
}
2022-09-21 22:07:00 +00:00
job , err := api . Database . GetProvisionerJobByID ( ctx , workspaceBuild . JobID )
2022-03-22 19:17:50 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching provisioner job." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-03-22 19:17:50 +00:00
} )
return
}
if job . CompletedAt . Valid {
2023-01-13 14:30:48 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-03-22 19:17:50 +00:00
Message : "Job has already completed!" ,
} )
return
}
if job . CanceledAt . Valid {
2023-01-13 14:30:48 +00:00
httpapi . Write ( ctx , rw , http . StatusBadRequest , codersdk . Response {
2022-03-22 19:17:50 +00:00
Message : "Job has already been marked as canceled!" ,
} )
return
}
2022-09-21 22:07:00 +00:00
err = api . Database . UpdateProvisionerJobWithCancelByID ( ctx , database . UpdateProvisionerJobWithCancelByIDParams {
2022-03-22 19:17:50 +00:00
ID : job . ID ,
CanceledAt : sql . NullTime {
Time : database . Now ( ) ,
Valid : true ,
} ,
2022-10-03 16:43:11 +00:00
CompletedAt : sql . NullTime {
Time : database . Now ( ) ,
// If the job is running, don't mark it completed!
Valid : ! job . WorkerID . Valid ,
} ,
2022-03-22 19:17:50 +00:00
} )
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error updating provisioner job." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-03-22 19:17:50 +00:00
} )
return
}
2022-11-07 15:25:18 +00:00
api . publishWorkspaceUpdate ( ctx , workspace . ID )
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusOK , codersdk . Response {
2022-03-22 19:17:50 +00:00
Message : "Job has been marked as canceled..." ,
} )
}
2022-11-21 10:43:53 +00:00
func ( api * API ) verifyUserCanCancelWorkspaceBuilds ( ctx context . Context , userID uuid . UUID , templateID uuid . UUID ) ( bool , error ) {
template , err := api . Database . GetTemplateByID ( ctx , templateID )
if err != nil {
return false , xerrors . New ( "no template exists for this workspace" )
}
if template . AllowUserCancelWorkspaceJobs {
return true , nil // all users can cancel workspace builds
}
user , err := api . Database . GetUserByID ( ctx , userID )
if err != nil {
return false , xerrors . New ( "user does not exist" )
}
return slices . Contains ( user . RBACRoles , rbac . RoleOwner ( ) ) , nil // only user with "owner" role can cancel workspace builds
}
2023-01-05 14:27:10 +00:00
// @Summary Get workspace resources for workspace build
// @ID get-workspace-resources-for-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {array} codersdk.WorkspaceResource
// @Router /workspacebuilds/{workspacebuild}/resources [get]
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuildResources ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-03-07 17:40:54 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-05-18 23:15:19 +00:00
2022-09-21 22:07:00 +00:00
job , err := api . Database . GetProvisionerJobByID ( ctx , workspaceBuild . JobID )
2022-03-07 17:40:54 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching provisioner job." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-03-07 17:40:54 +00:00
} )
return
}
api . provisionerJobResources ( rw , r , job )
}
2023-01-17 15:24:45 +00:00
// @Summary Get build parameters for workspace build
// @ID get-build-parameters-for-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {array} codersdk.WorkspaceBuildParameter
// @Router /workspacebuilds/{workspacebuild}/parameters [get]
func ( api * API ) workspaceBuildParameters ( rw http . ResponseWriter , r * http . Request ) {
ctx := r . Context ( )
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
parameters , err := api . Database . GetWorkspaceBuildParameters ( ctx , workspaceBuild . ID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Internal error fetching workspace build parameters." ,
Detail : err . Error ( ) ,
} )
return
}
apiParameters := convertWorkspaceBuildParameters ( parameters )
httpapi . Write ( ctx , rw , http . StatusOK , apiParameters )
}
2023-01-05 14:27:10 +00:00
// @Summary Get workspace build logs
// @ID get-workspace-build-logs
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Param before query int false "Before Unix timestamp"
// @Param after query int false "After Unix timestamp"
// @Param follow query bool false "Follow log stream"
// @Success 200 {array} codersdk.ProvisionerJobLog
// @Router /workspacebuilds/{workspacebuild}/logs [get]
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuildLogs ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-03-07 17:40:54 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-05-18 23:15:19 +00:00
2022-09-21 22:07:00 +00:00
job , err := api . Database . GetProvisionerJobByID ( ctx , workspaceBuild . JobID )
2022-03-07 17:40:54 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "Internal error fetching provisioner job." ,
2022-06-03 21:48:09 +00:00
Detail : err . Error ( ) ,
2022-03-07 17:40:54 +00:00
} )
return
}
api . provisionerJobLogs ( rw , r , job )
}
2023-01-05 14:27:10 +00:00
// @Summary Get provisioner state for workspace build
// @ID get-provisioner-state-for-workspace-build
// @Security CoderSessionToken
// @Produce json
// @Tags Builds
// @Param workspacebuild path string true "Workspace build ID"
// @Success 200 {object} codersdk.WorkspaceBuild
// @Router /workspacebuilds/{workspacebuild}/state [get]
2022-05-26 03:14:08 +00:00
func ( api * API ) workspaceBuildState ( rw http . ResponseWriter , r * http . Request ) {
2022-09-21 22:07:00 +00:00
ctx := r . Context ( )
2022-05-02 22:51:58 +00:00
workspaceBuild := httpmw . WorkspaceBuildParam ( r )
2022-09-21 22:07:00 +00:00
workspace , err := api . Database . GetWorkspaceByID ( ctx , workspaceBuild . WorkspaceID )
2022-05-18 23:15:19 +00:00
if err != nil {
2022-09-21 22:07:00 +00:00
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
2022-06-07 14:33:06 +00:00
Message : "No workspace exists for this job." ,
2022-05-18 23:15:19 +00:00
} )
return
}
2023-03-07 04:24:32 +00:00
template , err := api . Database . GetTemplateByID ( ctx , workspace . TemplateID )
if err != nil {
httpapi . Write ( ctx , rw , http . StatusInternalServerError , codersdk . Response {
Message : "Failed to get template" ,
Detail : err . Error ( ) ,
} )
return
}
2022-05-18 23:15:19 +00:00
2023-03-07 04:24:32 +00:00
// You must have update permissions on the template to get the state.
// This matches a push!
if ! api . Authorize ( r , rbac . ActionUpdate , template . RBACObject ( ) ) {
2022-06-14 15:14:05 +00:00
httpapi . ResourceNotFound ( rw )
2022-05-18 23:15:19 +00:00
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-09-16 18:54:23 +00:00
type workspaceBuildsData struct {
2022-11-28 19:53:56 +00:00
users [ ] database . User
jobs [ ] database . ProvisionerJob
templateVersions [ ] database . TemplateVersion
resources [ ] database . WorkspaceResource
metadata [ ] database . WorkspaceResourceMetadatum
agents [ ] database . WorkspaceAgent
apps [ ] database . WorkspaceApp
2022-09-16 18:54:23 +00:00
}
2023-03-10 16:39:02 +00:00
func ( api * API ) workspaceBuildsData ( ctx context . Context , workspaces [ ] database . Workspace , workspaceBuilds [ ] database . WorkspaceBuild ) ( workspaceBuildsData , error ) {
2022-09-16 18:54:23 +00:00
userIDs := make ( [ ] uuid . UUID , 0 , len ( workspaceBuilds ) )
for _ , build := range workspaceBuilds {
userIDs = append ( userIDs , build . InitiatorID )
}
for _ , workspace := range workspaces {
userIDs = append ( userIDs , workspace . OwnerID )
}
2022-10-10 18:03:54 +00:00
users , err := api . Database . GetUsersByIDs ( ctx , userIDs )
2022-09-16 18:54:23 +00:00
if err != nil {
return workspaceBuildsData { } , xerrors . Errorf ( "get users: %w" , err )
}
jobIDs := make ( [ ] uuid . UUID , 0 , len ( workspaceBuilds ) )
for _ , build := range workspaceBuilds {
jobIDs = append ( jobIDs , build . JobID )
}
jobs , err := api . Database . GetProvisionerJobsByIDs ( ctx , jobIDs )
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return workspaceBuildsData { } , xerrors . Errorf ( "get provisioner jobs: %w" , err )
}
2022-11-28 19:53:56 +00:00
templateVersionIDs := make ( [ ] uuid . UUID , 0 , len ( workspaceBuilds ) )
for _ , build := range workspaceBuilds {
templateVersionIDs = append ( templateVersionIDs , build . TemplateVersionID )
}
2023-03-10 18:09:28 +00:00
// nolint:gocritic // Getting template versions by ID is a system function.
templateVersions , err := api . Database . GetTemplateVersionsByIDs ( dbauthz . AsSystemRestricted ( ctx ) , templateVersionIDs )
2022-11-28 19:53:56 +00:00
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return workspaceBuildsData { } , xerrors . Errorf ( "get template versions: %w" , err )
}
2023-03-10 18:09:28 +00:00
// nolint:gocritic // Getting workspace resources by job ID is a system function.
resources , err := api . Database . GetWorkspaceResourcesByJobIDs ( dbauthz . AsSystemRestricted ( ctx ) , jobIDs )
2022-09-16 18:54:23 +00:00
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return workspaceBuildsData { } , xerrors . Errorf ( "get workspace resources by job: %w" , err )
}
if len ( resources ) == 0 {
return workspaceBuildsData {
2022-11-28 19:53:56 +00:00
users : users ,
jobs : jobs ,
templateVersions : templateVersions ,
2022-09-16 18:54:23 +00:00
} , nil
}
resourceIDs := make ( [ ] uuid . UUID , 0 )
for _ , resource := range resources {
resourceIDs = append ( resourceIDs , resource . ID )
}
2023-03-10 18:09:28 +00:00
// nolint:gocritic // Getting workspace resource metadata by resource ID is a system function.
metadata , err := api . Database . GetWorkspaceResourceMetadataByResourceIDs ( dbauthz . AsSystemRestricted ( ctx ) , resourceIDs )
2022-09-16 18:54:23 +00:00
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return workspaceBuildsData { } , xerrors . Errorf ( "fetching resource metadata: %w" , err )
}
2023-03-10 18:09:28 +00:00
// nolint:gocritic // Getting workspace agents by resource IDs is a system function.
agents , err := api . Database . GetWorkspaceAgentsByResourceIDs ( dbauthz . AsSystemRestricted ( ctx ) , resourceIDs )
2022-09-16 18:54:23 +00:00
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return workspaceBuildsData { } , xerrors . Errorf ( "get workspace agents: %w" , err )
}
if len ( resources ) == 0 {
return workspaceBuildsData {
2022-11-28 19:53:56 +00:00
users : users ,
jobs : jobs ,
templateVersions : templateVersions ,
resources : resources ,
metadata : metadata ,
2022-09-16 18:54:23 +00:00
} , nil
}
agentIDs := make ( [ ] uuid . UUID , 0 )
for _ , agent := range agents {
agentIDs = append ( agentIDs , agent . ID )
}
2023-03-10 18:09:28 +00:00
// nolint:gocritic // Getting workspace apps by agent IDs is a system function.
apps , err := api . Database . GetWorkspaceAppsByAgentIDs ( dbauthz . AsSystemRestricted ( ctx ) , agentIDs )
2022-09-16 18:54:23 +00:00
if err != nil && ! errors . Is ( err , sql . ErrNoRows ) {
return workspaceBuildsData { } , xerrors . Errorf ( "fetching workspace apps: %w" , err )
2022-06-01 23:49:43 +00:00
}
2022-06-09 01:23:35 +00:00
2022-09-16 18:54:23 +00:00
return workspaceBuildsData {
2022-11-28 19:53:56 +00:00
users : users ,
jobs : jobs ,
templateVersions : templateVersions ,
resources : resources ,
metadata : metadata ,
agents : agents ,
apps : apps ,
2022-09-16 18:54:23 +00:00
} , nil
}
func ( api * API ) convertWorkspaceBuilds (
2023-03-10 16:39:02 +00:00
workspaceBuilds [ ] database . WorkspaceBuild ,
2022-09-16 18:54:23 +00:00
workspaces [ ] database . Workspace ,
jobs [ ] database . ProvisionerJob ,
users [ ] database . User ,
workspaceResources [ ] database . WorkspaceResource ,
resourceMetadata [ ] database . WorkspaceResourceMetadatum ,
resourceAgents [ ] database . WorkspaceAgent ,
agentApps [ ] database . WorkspaceApp ,
2022-11-28 19:53:56 +00:00
templateVersions [ ] database . TemplateVersion ,
2022-09-16 18:54:23 +00:00
) ( [ ] codersdk . WorkspaceBuild , error ) {
workspaceByID := map [ uuid . UUID ] database . Workspace { }
for _ , workspace := range workspaces {
workspaceByID [ workspace . ID ] = workspace
}
jobByID := map [ uuid . UUID ] database . ProvisionerJob { }
for _ , job := range jobs {
jobByID [ job . ID ] = job
2022-06-09 01:23:35 +00:00
}
2022-11-28 19:53:56 +00:00
templateVersionByID := map [ uuid . UUID ] database . TemplateVersion { }
for _ , templateVersion := range templateVersions {
templateVersionByID [ templateVersion . ID ] = templateVersion
}
2022-06-09 01:23:35 +00:00
2023-01-11 17:18:06 +00:00
// Should never be nil for API consistency
apiBuilds := [ ] codersdk . WorkspaceBuild { }
2022-09-16 18:54:23 +00:00
for _ , build := range workspaceBuilds {
job , exists := jobByID [ build . JobID ]
if ! exists {
return nil , xerrors . New ( "build job not found" )
}
workspace , exists := workspaceByID [ build . WorkspaceID ]
if ! exists {
return nil , xerrors . New ( "workspace not found" )
}
2022-11-28 19:53:56 +00:00
templateVersion , exists := templateVersionByID [ build . TemplateVersionID ]
if ! exists {
return nil , xerrors . New ( "template version not found" )
}
2022-09-16 18:54:23 +00:00
apiBuild , err := api . convertWorkspaceBuild (
build ,
workspace ,
job ,
users ,
workspaceResources ,
resourceMetadata ,
resourceAgents ,
agentApps ,
2022-11-28 19:53:56 +00:00
templateVersion ,
2022-09-16 18:54:23 +00:00
)
if err != nil {
return nil , xerrors . Errorf ( "converting workspace build: %w" , err )
}
apiBuilds = append ( apiBuilds , apiBuild )
}
return apiBuilds , nil
}
func ( api * API ) convertWorkspaceBuild (
2023-03-10 16:39:02 +00:00
build database . WorkspaceBuild ,
2022-09-16 18:54:23 +00:00
workspace database . Workspace ,
job database . ProvisionerJob ,
users [ ] database . User ,
workspaceResources [ ] database . WorkspaceResource ,
resourceMetadata [ ] database . WorkspaceResourceMetadatum ,
resourceAgents [ ] database . WorkspaceAgent ,
agentApps [ ] database . WorkspaceApp ,
2022-11-28 19:53:56 +00:00
templateVersion database . TemplateVersion ,
2022-09-16 18:54:23 +00:00
) ( codersdk . WorkspaceBuild , error ) {
userByID := map [ uuid . UUID ] database . User { }
for _ , user := range users {
userByID [ user . ID ] = user
}
resourcesByJobID := map [ uuid . UUID ] [ ] database . WorkspaceResource { }
for _ , resource := range workspaceResources {
resourcesByJobID [ resource . JobID ] = append ( resourcesByJobID [ resource . JobID ] , resource )
}
metadataByResourceID := map [ uuid . UUID ] [ ] database . WorkspaceResourceMetadatum { }
for _ , metadata := range resourceMetadata {
metadataByResourceID [ metadata . WorkspaceResourceID ] = append ( metadataByResourceID [ metadata . WorkspaceResourceID ] , metadata )
}
agentsByResourceID := map [ uuid . UUID ] [ ] database . WorkspaceAgent { }
for _ , agent := range resourceAgents {
agentsByResourceID [ agent . ResourceID ] = append ( agentsByResourceID [ agent . ResourceID ] , agent )
}
appsByAgentID := map [ uuid . UUID ] [ ] database . WorkspaceApp { }
for _ , app := range agentApps {
appsByAgentID [ app . AgentID ] = append ( appsByAgentID [ app . AgentID ] , app )
}
owner , exists := userByID [ workspace . OwnerID ]
if ! exists {
return codersdk . WorkspaceBuild { } , xerrors . Errorf ( "owner not found for workspace: %q" , workspace . Name )
}
initiator , exists := userByID [ build . InitiatorID ]
if ! exists {
return codersdk . WorkspaceBuild { } , xerrors . Errorf ( "build initiator not found for workspace: %q" , workspace . Name )
}
resources := resourcesByJobID [ job . ID ]
apiResources := make ( [ ] codersdk . WorkspaceResource , 0 )
for _ , resource := range resources {
agents := agentsByResourceID [ resource . ID ]
apiAgents := make ( [ ] codersdk . WorkspaceAgent , 0 )
for _ , agent := range agents {
apps := appsByAgentID [ agent . ID ]
2023-03-07 21:10:01 +00:00
apiAgent , err := convertWorkspaceAgent (
api . DERPMap , * api . TailnetCoordinator . Load ( ) , agent , convertApps ( apps ) , api . AgentInactiveDisconnectTimeout ,
api . DeploymentValues . AgentFallbackTroubleshootingURL . String ( ) ,
)
2022-09-16 18:54:23 +00:00
if err != nil {
return codersdk . WorkspaceBuild { } , xerrors . Errorf ( "converting workspace agent: %w" , err )
}
apiAgents = append ( apiAgents , apiAgent )
}
metadata := append ( make ( [ ] database . WorkspaceResourceMetadatum , 0 ) , metadataByResourceID [ resource . ID ] ... )
apiResources = append ( apiResources , convertWorkspaceResource ( resource , apiAgents , metadata ) )
2022-06-09 01:23:35 +00:00
}
2022-10-03 16:43:11 +00:00
apiJob := convertProvisionerJob ( job )
transition := codersdk . WorkspaceTransition ( build . Transition )
2022-03-22 19:17:50 +00:00
return codersdk . WorkspaceBuild {
2022-11-28 19:53:56 +00:00
ID : build . ID ,
CreatedAt : build . CreatedAt ,
UpdatedAt : build . UpdatedAt ,
WorkspaceOwnerID : workspace . OwnerID ,
WorkspaceOwnerName : owner . Username ,
WorkspaceID : build . WorkspaceID ,
WorkspaceName : workspace . Name ,
TemplateVersionID : build . TemplateVersionID ,
TemplateVersionName : templateVersion . Name ,
BuildNumber : build . BuildNumber ,
Transition : transition ,
InitiatorID : build . InitiatorID ,
InitiatorUsername : initiator . Username ,
Job : apiJob ,
Deadline : codersdk . NewNullTime ( build . Deadline , ! build . Deadline . IsZero ( ) ) ,
2023-03-07 14:14:58 +00:00
MaxDeadline : codersdk . NewNullTime ( build . MaxDeadline , ! build . MaxDeadline . IsZero ( ) ) ,
2022-11-28 19:53:56 +00:00
Reason : codersdk . BuildReason ( build . Reason ) ,
Resources : apiResources ,
Status : convertWorkspaceStatus ( apiJob . Status , transition ) ,
DailyCost : build . DailyCost ,
2022-09-16 18:54:23 +00:00
} , nil
2022-03-07 17:40:54 +00:00
}
2022-08-01 21:53:05 +00:00
func convertWorkspaceResource ( resource database . WorkspaceResource , agents [ ] codersdk . WorkspaceAgent , metadata [ ] database . WorkspaceResourceMetadatum ) codersdk . WorkspaceResource {
var convertedMetadata [ ] codersdk . WorkspaceResourceMetadata
2022-12-14 19:08:22 +00:00
for _ , field := range metadata {
convertedMetadata = append ( convertedMetadata , codersdk . WorkspaceResourceMetadata {
Key : field . Key ,
Value : field . Value . String ,
Sensitive : field . Sensitive ,
} )
2022-08-01 21:53:05 +00:00
}
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-09-09 19:38:00 +00:00
Hide : resource . Hide ,
2022-09-13 14:32:59 +00:00
Icon : resource . Icon ,
2022-04-11 21:06:15 +00:00
Agents : agents ,
2022-08-01 21:53:05 +00:00
Metadata : convertedMetadata ,
2022-11-14 17:57:33 +00:00
DailyCost : resource . DailyCost ,
2022-03-07 17:40:54 +00:00
}
}
2022-10-03 16:43:11 +00:00
func convertWorkspaceStatus ( jobStatus codersdk . ProvisionerJobStatus , transition codersdk . WorkspaceTransition ) codersdk . WorkspaceStatus {
switch jobStatus {
case codersdk . ProvisionerJobPending :
return codersdk . WorkspaceStatusPending
case codersdk . ProvisionerJobRunning :
switch transition {
case codersdk . WorkspaceTransitionStart :
return codersdk . WorkspaceStatusStarting
case codersdk . WorkspaceTransitionStop :
return codersdk . WorkspaceStatusStopping
case codersdk . WorkspaceTransitionDelete :
return codersdk . WorkspaceStatusDeleting
}
case codersdk . ProvisionerJobSucceeded :
switch transition {
case codersdk . WorkspaceTransitionStart :
return codersdk . WorkspaceStatusRunning
case codersdk . WorkspaceTransitionStop :
return codersdk . WorkspaceStatusStopped
case codersdk . WorkspaceTransitionDelete :
return codersdk . WorkspaceStatusDeleted
}
case codersdk . ProvisionerJobCanceling :
return codersdk . WorkspaceStatusCanceling
case codersdk . ProvisionerJobCanceled :
return codersdk . WorkspaceStatusCanceled
case codersdk . ProvisionerJobFailed :
return codersdk . WorkspaceStatusFailed
}
// return error status since we should never get here
return codersdk . WorkspaceStatusFailed
}
2023-01-17 15:24:45 +00:00
func convertWorkspaceBuildParameters ( parameters [ ] database . WorkspaceBuildParameter ) [ ] codersdk . WorkspaceBuildParameter {
2023-02-19 00:32:09 +00:00
apiParameters := make ( [ ] codersdk . WorkspaceBuildParameter , 0 , len ( parameters ) )
2023-01-17 15:24:45 +00:00
for _ , p := range parameters {
apiParameter := codersdk . WorkspaceBuildParameter {
Name : p . Name ,
Value : p . Value ,
}
apiParameters = append ( apiParameters , apiParameter )
}
return apiParameters
}
2023-01-23 14:01:22 +00:00
func findWorkspaceBuildParameter ( params [ ] codersdk . WorkspaceBuildParameter , parameterName string ) ( * codersdk . WorkspaceBuildParameter , bool ) {
for _ , p := range params {
if p . Name == parameterName {
return & p , true
}
}
return nil , false
}