2022-01-25 19:52:58 +00:00
|
|
|
package coderd
|
|
|
|
|
|
|
|
import (
|
2022-04-25 21:11:03 +00:00
|
|
|
"context"
|
2022-01-25 19:52:58 +00:00
|
|
|
"database/sql"
|
2022-03-07 17:40:54 +00:00
|
|
|
"encoding/json"
|
2022-01-25 19:52:58 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2022-05-19 14:29:10 +00:00
|
|
|
"strconv"
|
2022-05-18 21:16:26 +00:00
|
|
|
"time"
|
2022-01-25 19:52:58 +00:00
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
2022-01-25 19:52:58 +00:00
|
|
|
"github.com/google/uuid"
|
2022-03-07 17:40:54 +00:00
|
|
|
"github.com/moby/moby/pkg/namesgenerator"
|
2022-05-14 01:41:21 +00:00
|
|
|
"golang.org/x/sync/errgroup"
|
2022-03-07 17:40:54 +00:00
|
|
|
"golang.org/x/xerrors"
|
2022-05-18 21:16:26 +00:00
|
|
|
"nhooyr.io/websocket"
|
|
|
|
"nhooyr.io/websocket/wsjson"
|
|
|
|
|
|
|
|
"cdr.dev/slog"
|
2022-01-25 19:52:58 +00:00
|
|
|
|
2022-05-11 22:03:02 +00:00
|
|
|
"github.com/coder/coder/coderd/autobuild/schedule"
|
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-16 19:36:27 +00:00
|
|
|
"github.com/coder/coder/coderd/rbac"
|
2022-03-22 19:17:50 +00:00
|
|
|
"github.com/coder/coder/codersdk"
|
2022-01-25 19:52:58 +00:00
|
|
|
)
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) workspace(rw http.ResponseWriter, r *http.Request) {
|
2022-03-07 17:40:54 +00:00
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
2022-05-24 13:43:34 +00:00
|
|
|
if !api.Authorize(rw, r, rbac.ActionRead, workspace) {
|
2022-05-19 14:29:10 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// The `deleted` query parameter (which defaults to `false`) MUST match the
|
|
|
|
// `Deleted` field on the workspace otherwise you will get a 410 Gone.
|
|
|
|
var (
|
|
|
|
deletedStr = r.URL.Query().Get("deleted")
|
|
|
|
showDeleted = false
|
|
|
|
)
|
|
|
|
if deletedStr != "" {
|
|
|
|
var err error
|
|
|
|
showDeleted, err = strconv.ParseBool(deletedStr)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("invalid bool for 'deleted' query param: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if workspace.Deleted && !showDeleted {
|
|
|
|
httpapi.Write(rw, http.StatusGone, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("workspace %q was deleted, you can view this workspace by specifying '?deleted=true' and trying again", workspace.ID.String()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !workspace.Deleted && showDeleted {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("workspace %q is not deleted, please remove '?deleted=true' and try again", workspace.ID.String()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-18 16:33:33 +00:00
|
|
|
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
2022-03-22 19:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get workspace build: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-14 01:41:21 +00:00
|
|
|
var (
|
|
|
|
group errgroup.Group
|
|
|
|
job database.ProvisionerJob
|
|
|
|
template database.Template
|
|
|
|
owner database.User
|
|
|
|
)
|
|
|
|
group.Go(func() (err error) {
|
|
|
|
job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
group.Go(func() (err error) {
|
|
|
|
template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
group.Go(func() (err error) {
|
|
|
|
owner, err = api.Database.GetUserByID(r.Context(), workspace.OwnerID)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
err = group.Wait()
|
2022-03-22 19:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-05-14 01:41:21 +00:00
|
|
|
Message: fmt.Sprintf("fetch resource: %s", err),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-14 01:41:21 +00:00
|
|
|
|
|
|
|
httpapi.Write(rw, http.StatusOK,
|
|
|
|
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) workspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
2022-05-16 19:36:27 +00:00
|
|
|
organization := httpmw.OrganizationParam(r)
|
2022-05-18 15:09:07 +00:00
|
|
|
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
|
2022-05-16 19:36:27 +00:00
|
|
|
OrganizationID: organization.ID,
|
|
|
|
Deleted: false,
|
|
|
|
})
|
2022-03-22 19:17:50 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
err = nil
|
|
|
|
}
|
2022-03-07 17:40:54 +00:00
|
|
|
if err != nil {
|
2022-03-22 19:17:50 +00:00
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-05-16 19:36:27 +00:00
|
|
|
Message: fmt.Sprintf("get workspaces: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-05-24 13:43:34 +00:00
|
|
|
// Rbac filter
|
|
|
|
workspaces = AuthorizeFilter(api, r, rbac.ActionRead, workspaces)
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-05-24 13:43:34 +00:00
|
|
|
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces)
|
2022-05-16 19:36:27 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("convert workspaces: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
|
|
|
}
|
|
|
|
|
2022-05-18 15:09:07 +00:00
|
|
|
// workspaces returns all workspaces a user can read.
|
|
|
|
// Optional filters with query params
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
2022-05-18 15:09:07 +00:00
|
|
|
apiKey := httpmw.APIKey(r)
|
2022-05-16 19:36:27 +00:00
|
|
|
|
2022-05-18 15:09:07 +00:00
|
|
|
// Empty strings mean no filter
|
|
|
|
orgFilter := r.URL.Query().Get("organization_id")
|
2022-05-19 16:08:55 +00:00
|
|
|
ownerFilter := r.URL.Query().Get("owner")
|
2022-05-18 15:09:07 +00:00
|
|
|
|
|
|
|
filter := database.GetWorkspacesWithFilterParams{Deleted: false}
|
|
|
|
if orgFilter != "" {
|
|
|
|
orgID, err := uuid.Parse(orgFilter)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("organization_id must be a uuid: %s", err.Error()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
filter.OrganizationID = orgID
|
|
|
|
}
|
|
|
|
if ownerFilter == "me" {
|
|
|
|
filter.OwnerID = apiKey.UserID
|
|
|
|
} else if ownerFilter != "" {
|
|
|
|
userID, err := uuid.Parse(ownerFilter)
|
|
|
|
if err != nil {
|
|
|
|
// Maybe it's a username
|
|
|
|
user, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
|
|
|
|
// Why not just accept 1 arg and use it for both in the sql?
|
|
|
|
Username: ownerFilter,
|
|
|
|
Email: ownerFilter,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: "owner must be a uuid or username",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
userID = user.ID
|
|
|
|
}
|
|
|
|
filter.OwnerID = userID
|
|
|
|
}
|
|
|
|
|
2022-05-24 13:43:34 +00:00
|
|
|
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), filter)
|
2022-05-16 19:36:27 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get workspaces for user: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-24 13:43:34 +00:00
|
|
|
// Only return workspaces the user can read
|
|
|
|
workspaces = AuthorizeFilter(api, r, rbac.ActionRead, workspaces)
|
|
|
|
|
|
|
|
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces)
|
2022-05-16 19:36:27 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("convert workspaces: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) workspacesByOwner(rw http.ResponseWriter, r *http.Request) {
|
2022-05-16 19:36:27 +00:00
|
|
|
owner := httpmw.UserParam(r)
|
2022-05-18 15:09:07 +00:00
|
|
|
workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), database.GetWorkspacesWithFilterParams{
|
2022-05-16 19:36:27 +00:00
|
|
|
OwnerID: owner.ID,
|
2022-05-18 15:09:07 +00:00
|
|
|
Deleted: false,
|
2022-05-16 19:36:27 +00:00
|
|
|
})
|
2022-03-22 19:17:50 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
err = nil
|
|
|
|
}
|
2022-01-25 19:52:58 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-05-16 19:36:27 +00:00
|
|
|
Message: fmt.Sprintf("get workspaces: %s", err),
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-05-24 13:43:34 +00:00
|
|
|
// Only return workspaces the user can read
|
|
|
|
workspaces = AuthorizeFilter(api, r, rbac.ActionRead, workspaces)
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-05-24 13:43:34 +00:00
|
|
|
apiWorkspaces, err := convertWorkspaces(r.Context(), api.Database, workspaces)
|
2022-05-16 19:36:27 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("convert workspaces: %s", err),
|
|
|
|
})
|
|
|
|
return
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
2022-05-16 19:36:27 +00:00
|
|
|
httpapi.Write(rw, http.StatusOK, apiWorkspaces)
|
|
|
|
}
|
2022-01-25 19:52:58 +00:00
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) {
|
2022-05-16 19:36:27 +00:00
|
|
|
owner := httpmw.UserParam(r)
|
|
|
|
organization := httpmw.OrganizationParam(r)
|
2022-05-18 23:15:19 +00:00
|
|
|
workspaceName := chi.URLParam(r, "workspacename")
|
2022-05-16 19:36:27 +00:00
|
|
|
|
|
|
|
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
|
|
|
|
OwnerID: owner.ID,
|
|
|
|
Name: workspaceName,
|
|
|
|
})
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
2022-05-17 18:43:19 +00:00
|
|
|
// Do not leak information if the workspace exists or not
|
|
|
|
httpapi.Forbidden(rw)
|
2022-05-16 19:36:27 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get workspace by name: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if workspace.OrganizationID != organization.ID {
|
|
|
|
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("workspace is not owned by organization %q", organization.Name),
|
|
|
|
})
|
|
|
|
return
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
2022-03-07 17:40:54 +00:00
|
|
|
|
2022-05-24 13:43:34 +00:00
|
|
|
if !api.Authorize(rw, r, rbac.ActionRead, workspace) {
|
2022-05-17 18:43:19 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-18 16:33:33 +00:00
|
|
|
build, 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 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 provisioner 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, owner))
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
|
|
|
|
2022-05-16 19:36:27 +00:00
|
|
|
// Create a new workspace for the currently authenticated user.
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
2022-05-27 16:19:13 +00:00
|
|
|
organization := httpmw.OrganizationParam(r)
|
|
|
|
apiKey := httpmw.APIKey(r)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionCreate,
|
|
|
|
rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(apiKey.UserID.String())) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-16 19:36:27 +00:00
|
|
|
var createWorkspace codersdk.CreateWorkspaceRequest
|
|
|
|
if !httpapi.Read(rw, r, &createWorkspace) {
|
|
|
|
return
|
|
|
|
}
|
2022-05-27 16:19:13 +00:00
|
|
|
|
2022-05-16 19:36:27 +00:00
|
|
|
template, err := api.Database.GetTemplateByID(r.Context(), createWorkspace.TemplateID)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("template %q doesn't exist", createWorkspace.TemplateID.String()),
|
|
|
|
Errors: []httpapi.Error{{
|
|
|
|
Field: "template_id",
|
|
|
|
Detail: "template not found",
|
|
|
|
}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get template: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-27 16:19:13 +00:00
|
|
|
|
2022-05-16 19:36:27 +00:00
|
|
|
if organization.ID != template.OrganizationID {
|
|
|
|
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("template is not in organization %q", organization.Name),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
_, err = api.Database.GetOrganizationMemberByUserID(r.Context(), database.GetOrganizationMemberByUserIDParams{
|
|
|
|
OrganizationID: template.OrganizationID,
|
|
|
|
UserID: apiKey.UserID,
|
|
|
|
})
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
|
|
|
Message: "you aren't allowed to access templates in that organization",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get organization member: %s", err),
|
|
|
|
})
|
2022-01-25 19:52:58 +00:00
|
|
|
return
|
|
|
|
}
|
2022-05-16 19:36:27 +00:00
|
|
|
|
2022-05-23 22:31:41 +00:00
|
|
|
var dbAutostartSchedule sql.NullString
|
|
|
|
if createWorkspace.AutostartSchedule != nil {
|
|
|
|
_, err := schedule.Weekly(*createWorkspace.AutostartSchedule)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("parse autostart schedule: %s", err.Error()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
dbAutostartSchedule.Valid = true
|
|
|
|
dbAutostartSchedule.String = *createWorkspace.AutostartSchedule
|
|
|
|
}
|
|
|
|
|
|
|
|
var dbTTL sql.NullInt64
|
|
|
|
if createWorkspace.TTL != nil && *createWorkspace.TTL > 0 {
|
|
|
|
dbTTL.Valid = true
|
|
|
|
dbTTL.Int64 = int64(*createWorkspace.TTL)
|
|
|
|
}
|
|
|
|
|
2022-05-16 19:36:27 +00:00
|
|
|
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(r.Context(), database.GetWorkspaceByOwnerIDAndNameParams{
|
|
|
|
OwnerID: apiKey.UserID,
|
|
|
|
Name: createWorkspace.Name,
|
|
|
|
})
|
|
|
|
if err == nil {
|
|
|
|
// If the workspace already exists, don't allow creation.
|
|
|
|
template, err := api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
2022-03-22 19:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-05-16 19:36:27 +00:00
|
|
|
Message: fmt.Sprintf("find template for conflicting workspace name %q: %s", createWorkspace.Name, err),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-16 19:36:27 +00:00
|
|
|
// The template is fetched for clarity to the user on where the conflicting name may be.
|
|
|
|
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("workspace %q already exists in the %q template", createWorkspace.Name, template.Name),
|
2022-01-25 19:52:58 +00:00
|
|
|
Errors: []httpapi.Error{{
|
2022-05-16 19:36:27 +00:00
|
|
|
Field: "name",
|
|
|
|
Detail: "this value is already in use and should be unique",
|
2022-01-25 19:52:58 +00:00
|
|
|
}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-16 19:36:27 +00:00
|
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get workspace by name: %s", err.Error()),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templateVersion, err := api.Database.GetTemplateVersionByID(r.Context(), template.ActiveVersionID)
|
2022-01-25 19:52:58 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("get template version: %s", err),
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
templateVersionJob, err := api.Database.GetProvisionerJobByID(r.Context(), templateVersion.JobID)
|
2022-03-07 17:40:54 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-05-16 19:36:27 +00:00
|
|
|
Message: fmt.Sprintf("get template version job: %s", err),
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-04-06 17:42:40 +00:00
|
|
|
templateVersionJobStatus := convertProvisionerJob(templateVersionJob).Status
|
|
|
|
switch templateVersionJobStatus {
|
2022-03-22 19:17:50 +00:00
|
|
|
case codersdk.ProvisionerJobPending, codersdk.ProvisionerJobRunning:
|
2022-03-07 17:40:54 +00:00
|
|
|
httpapi.Write(rw, http.StatusNotAcceptable, httpapi.Response{
|
2022-04-06 17:42:40 +00:00
|
|
|
Message: fmt.Sprintf("The provided template version is %s. Wait for it to complete importing!", templateVersionJobStatus),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
2022-03-22 19:17:50 +00:00
|
|
|
case codersdk.ProvisionerJobFailed:
|
2022-03-07 17:40:54 +00:00
|
|
|
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
2022-05-16 19:36:27 +00:00
|
|
|
Message: fmt.Sprintf("The provided template version %q has failed to import. You cannot create workspaces using it!", templateVersion.Name),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
2022-03-22 19:17:50 +00:00
|
|
|
case codersdk.ProvisionerJobCanceled:
|
2022-03-07 17:40:54 +00:00
|
|
|
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
|
2022-05-16 19:36:27 +00:00
|
|
|
Message: "The provided template version was canceled during import. You cannot create workspaces using it!",
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-16 19:36:27 +00:00
|
|
|
var provisionerJob database.ProvisionerJob
|
|
|
|
var workspaceBuild database.WorkspaceBuild
|
|
|
|
err = api.Database.InTx(func(db database.Store) error {
|
2022-05-23 22:31:41 +00:00
|
|
|
now := database.Now()
|
2022-05-16 19:36:27 +00:00
|
|
|
workspaceBuildID := uuid.New()
|
|
|
|
// Workspaces are created without any versions.
|
|
|
|
workspace, err = db.InsertWorkspace(r.Context(), database.InsertWorkspaceParams{
|
2022-05-23 22:31:41 +00:00
|
|
|
ID: uuid.New(),
|
|
|
|
CreatedAt: now,
|
|
|
|
UpdatedAt: now,
|
|
|
|
OwnerID: apiKey.UserID,
|
|
|
|
OrganizationID: template.OrganizationID,
|
|
|
|
TemplateID: template.ID,
|
|
|
|
Name: createWorkspace.Name,
|
|
|
|
AutostartSchedule: dbAutostartSchedule,
|
|
|
|
Ttl: dbTTL,
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
2022-05-16 19:36:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("insert workspace: %w", err)
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
2022-05-16 19:36:27 +00:00
|
|
|
for _, parameterValue := range createWorkspace.ParameterValues {
|
|
|
|
_, err = db.InsertParameterValue(r.Context(), database.InsertParameterValueParams{
|
|
|
|
ID: uuid.New(),
|
|
|
|
Name: parameterValue.Name,
|
2022-05-23 22:31:41 +00:00
|
|
|
CreatedAt: now,
|
|
|
|
UpdatedAt: now,
|
2022-05-16 19:36:27 +00:00
|
|
|
Scope: database.ParameterScopeWorkspace,
|
|
|
|
ScopeID: workspace.ID,
|
2022-05-19 18:04:44 +00:00
|
|
|
SourceScheme: database.ParameterSourceScheme(parameterValue.SourceScheme),
|
2022-05-16 19:36:27 +00:00
|
|
|
SourceValue: parameterValue.SourceValue,
|
2022-05-19 18:04:44 +00:00
|
|
|
DestinationScheme: database.ParameterDestinationScheme(parameterValue.DestinationScheme),
|
2022-05-16 19:36:27 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("insert parameter value: %w", err)
|
|
|
|
}
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
2022-01-25 19:52:58 +00:00
|
|
|
|
2022-03-07 17:40:54 +00:00
|
|
|
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(),
|
2022-05-23 22:31:41 +00:00
|
|
|
CreatedAt: now,
|
|
|
|
UpdatedAt: now,
|
2022-03-07 17:40:54 +00:00
|
|
|
InitiatorID: apiKey.UserID,
|
2022-04-06 17:42:40 +00:00
|
|
|
OrganizationID: template.OrganizationID,
|
|
|
|
Provisioner: template.Provisioner,
|
2022-03-07 17:40:54 +00:00
|
|
|
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
2022-04-06 17:42:40 +00:00
|
|
|
StorageMethod: templateVersionJob.StorageMethod,
|
|
|
|
StorageSource: templateVersionJob.StorageSource,
|
2022-03-07 17:40:54 +00:00
|
|
|
Input: input,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("insert provisioner job: %w", err)
|
|
|
|
}
|
|
|
|
workspaceBuild, err = db.InsertWorkspaceBuild(r.Context(), database.InsertWorkspaceBuildParams{
|
2022-04-06 17:42:40 +00:00
|
|
|
ID: workspaceBuildID,
|
2022-05-23 22:31:41 +00:00
|
|
|
CreatedAt: now,
|
|
|
|
UpdatedAt: now,
|
2022-04-06 17:42:40 +00:00
|
|
|
WorkspaceID: workspace.ID,
|
|
|
|
TemplateVersionID: templateVersion.ID,
|
|
|
|
Name: namesgenerator.GetRandomName(1),
|
|
|
|
InitiatorID: apiKey.UserID,
|
2022-05-16 19:36:27 +00:00
|
|
|
Transition: database.WorkspaceTransitionStart,
|
2022-04-06 17:42:40 +00:00
|
|
|
JobID: provisionerJob.ID,
|
2022-05-26 17:08:11 +00:00
|
|
|
BuildNumber: 1, // First build!
|
|
|
|
Deadline: time.Time{}, // provisionerd will set this upon success
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("insert workspace build: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
2022-01-25 19:52:58 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-05-16 19:36:27 +00:00
|
|
|
Message: fmt.Sprintf("create workspace: %s", err),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
2022-02-06 00:24:51 +00:00
|
|
|
}
|
2022-05-16 19:36:27 +00:00
|
|
|
user, err := api.Database.GetUserByID(r.Context(), apiKey.UserID)
|
2022-02-06 00:24:51 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-05-16 19:36:27 +00:00
|
|
|
Message: fmt.Sprintf("get user: %s", err),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
2022-02-06 00:24:51 +00:00
|
|
|
}
|
2022-03-07 17:40:54 +00:00
|
|
|
|
2022-05-16 19:36:27 +00:00
|
|
|
httpapi.Write(rw, http.StatusCreated, convertWorkspace(workspace,
|
|
|
|
convertWorkspaceBuild(workspaceBuild, convertProvisionerJob(templateVersionJob)), template, user))
|
2022-02-06 00:24:51 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
2022-05-18 23:15:19 +00:00
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace.
|
|
|
|
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-07 09:03:35 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
|
2022-05-18 23:15:19 +00:00
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceWorkspace.
|
|
|
|
InOrg(workspace.OrganizationID).WithOwner(workspace.OwnerID.String()).WithID(workspace.ID.String())) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-19 19:09:27 +00:00
|
|
|
var req codersdk.UpdateWorkspaceTTLRequest
|
2022-04-07 09:03:35 +00:00
|
|
|
if !httpapi.Read(rw, r, &req) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-19 19:09:27 +00:00
|
|
|
var dbTTL sql.NullInt64
|
|
|
|
if req.TTL != nil && *req.TTL > 0 {
|
|
|
|
truncated := req.TTL.Truncate(time.Minute)
|
|
|
|
dbTTL.Int64 = int64(truncated)
|
|
|
|
dbTTL.Valid = true
|
2022-04-07 09:03:35 +00:00
|
|
|
}
|
|
|
|
|
2022-05-19 19:09:27 +00:00
|
|
|
err := api.Database.UpdateWorkspaceTTL(r.Context(), database.UpdateWorkspaceTTLParams{
|
|
|
|
ID: workspace.ID,
|
|
|
|
Ttl: dbTTL,
|
2022-04-07 09:03:35 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
2022-05-19 19:09:27 +00:00
|
|
|
Message: fmt.Sprintf("update workspace ttl: %s", err),
|
2022-04-07 09:03:35 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-26 17:08:11 +00:00
|
|
|
func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
|
|
|
|
|
|
if !api.Authorize(rw, r, rbac.ActionUpdate, workspace) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var req codersdk.PutExtendWorkspaceRequest
|
|
|
|
if !httpapi.Read(rw, r, &req) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var code = http.StatusOK
|
|
|
|
|
|
|
|
err := api.Database.InTx(func(s database.Store) error {
|
|
|
|
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
code = http.StatusInternalServerError
|
|
|
|
return xerrors.Errorf("get latest workspace build: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if build.Transition != database.WorkspaceTransitionStart {
|
|
|
|
code = http.StatusConflict
|
|
|
|
return xerrors.Errorf("workspace must be started, current status: %s", build.Transition)
|
|
|
|
}
|
|
|
|
|
|
|
|
newDeadline := req.Deadline.Truncate(time.Minute).UTC()
|
|
|
|
if newDeadline.IsZero() {
|
|
|
|
// This should not be possible because the struct validation field enforces a non-zero value.
|
|
|
|
code = http.StatusBadRequest
|
|
|
|
return xerrors.New("new deadline cannot be zero")
|
|
|
|
}
|
|
|
|
|
|
|
|
if newDeadline.Before(build.Deadline) || newDeadline.Before(time.Now()) {
|
|
|
|
code = http.StatusBadRequest
|
|
|
|
return xerrors.Errorf("new deadline %q must be after existing deadline %q", newDeadline.Format(time.RFC3339), build.Deadline.Format(time.RFC3339))
|
|
|
|
}
|
|
|
|
|
|
|
|
// both newDeadline and build.Deadline are truncated to time.Minute
|
|
|
|
if newDeadline == build.Deadline {
|
|
|
|
code = http.StatusNotModified
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.UpdateWorkspaceBuildByID(r.Context(), database.UpdateWorkspaceBuildByIDParams{
|
|
|
|
ID: build.ID,
|
|
|
|
UpdatedAt: build.UpdatedAt,
|
|
|
|
ProvisionerState: build.ProvisionerState,
|
|
|
|
Deadline: newDeadline,
|
|
|
|
}); err != nil {
|
|
|
|
return xerrors.Errorf("update workspace build: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
var resp = httpapi.Response{}
|
|
|
|
if err != nil {
|
|
|
|
resp.Message = err.Error()
|
|
|
|
}
|
|
|
|
httpapi.Write(rw, code, resp)
|
|
|
|
}
|
|
|
|
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
2022-05-18 21:16:26 +00:00
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
2022-05-27 16:19:13 +00:00
|
|
|
if !api.Authorize(rw, r, rbac.ActionRead, workspace) {
|
|
|
|
return
|
|
|
|
}
|
2022-05-18 21:16:26 +00:00
|
|
|
|
|
|
|
c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
|
|
|
|
// Fix for Safari 15.1:
|
|
|
|
// There is a bug in latest Safari in which compressed web socket traffic
|
|
|
|
// isn't handled correctly. Turning off compression is a workaround:
|
|
|
|
// https://github.com/nhooyr/websocket/issues/218
|
|
|
|
CompressionMode: websocket.CompressionDisabled,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
api.Logger.Warn(r.Context(), "accept websocket connection", slog.Error(err))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer c.Close(websocket.StatusInternalError, "internal error")
|
|
|
|
|
|
|
|
// Makes the websocket connection write-only
|
|
|
|
ctx := c.CloseRead(r.Context())
|
|
|
|
|
|
|
|
// Send a heartbeat every 15 seconds to avoid the websocket being killed.
|
|
|
|
go func() {
|
|
|
|
ticker := time.NewTicker(time.Second * 15)
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
case <-ticker.C:
|
|
|
|
err := c.Ping(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
t := time.NewTicker(time.Second * 1)
|
|
|
|
defer t.Stop()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-t.C:
|
|
|
|
workspace, err := api.Database.GetWorkspaceByID(r.Context(), workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
_ = wsjson.Write(ctx, c, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get workspace: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(r.Context(), workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
_ = wsjson.Write(ctx, c, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("get workspace build: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var (
|
|
|
|
group errgroup.Group
|
|
|
|
job database.ProvisionerJob
|
|
|
|
template database.Template
|
|
|
|
owner database.User
|
|
|
|
)
|
|
|
|
group.Go(func() (err error) {
|
|
|
|
job, err = api.Database.GetProvisionerJobByID(r.Context(), build.JobID)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
group.Go(func() (err error) {
|
|
|
|
template, err = api.Database.GetTemplateByID(r.Context(), workspace.TemplateID)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
group.Go(func() (err error) {
|
|
|
|
owner, err = api.Database.GetUserByID(r.Context(), workspace.OwnerID)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
err = group.Wait()
|
|
|
|
if err != nil {
|
|
|
|
_ = wsjson.Write(ctx, c, httpapi.Response{
|
|
|
|
Message: fmt.Sprintf("fetch resource: %s", err),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
_ = wsjson.Write(ctx, c, convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, owner))
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-25 21:11:03 +00:00
|
|
|
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))
|
2022-05-14 01:41:21 +00:00
|
|
|
ownerIDs := make([]uuid.UUID, 0, len(workspaces))
|
2022-04-25 21:11:03 +00:00
|
|
|
for _, workspace := range workspaces {
|
|
|
|
workspaceIDs = append(workspaceIDs, workspace.ID)
|
|
|
|
templateIDs = append(templateIDs, workspace.TemplateID)
|
2022-05-14 01:41:21 +00:00
|
|
|
ownerIDs = append(ownerIDs, workspace.OwnerID)
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
2022-05-18 16:33:33 +00:00
|
|
|
workspaceBuilds, err := db.GetLatestWorkspaceBuildsByWorkspaceIDs(ctx, workspaceIDs)
|
2022-04-25 21:11:03 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-05-14 01:41:21 +00:00
|
|
|
users, err := db.GetUsersByIDs(ctx, ownerIDs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, xerrors.Errorf("get users: %w", err)
|
|
|
|
}
|
2022-04-25 21:11:03 +00:00
|
|
|
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 {
|
2022-05-18 16:33:33 +00:00
|
|
|
buildByWorkspaceID[workspaceBuild.WorkspaceID] = database.WorkspaceBuild{
|
|
|
|
ID: workspaceBuild.ID,
|
|
|
|
CreatedAt: workspaceBuild.CreatedAt,
|
|
|
|
UpdatedAt: workspaceBuild.UpdatedAt,
|
|
|
|
WorkspaceID: workspaceBuild.WorkspaceID,
|
|
|
|
TemplateVersionID: workspaceBuild.TemplateVersionID,
|
|
|
|
Name: workspaceBuild.Name,
|
|
|
|
BuildNumber: workspaceBuild.BuildNumber,
|
|
|
|
Transition: workspaceBuild.Transition,
|
|
|
|
InitiatorID: workspaceBuild.InitiatorID,
|
|
|
|
ProvisionerState: workspaceBuild.ProvisionerState,
|
|
|
|
JobID: workspaceBuild.JobID,
|
|
|
|
}
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
|
|
|
templateByID := map[uuid.UUID]database.Template{}
|
|
|
|
for _, template := range templates {
|
|
|
|
templateByID[template.ID] = template
|
|
|
|
}
|
2022-05-14 01:41:21 +00:00
|
|
|
userByID := map[uuid.UUID]database.User{}
|
|
|
|
for _, user := range users {
|
|
|
|
userByID[user.ID] = user
|
|
|
|
}
|
2022-04-25 21:11:03 +00:00
|
|
|
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 {
|
2022-05-14 01:41:21 +00:00
|
|
|
return nil, xerrors.Errorf("build job not found for workspace: %w", err)
|
|
|
|
}
|
|
|
|
user, exists := userByID[workspace.OwnerID]
|
|
|
|
if !exists {
|
|
|
|
return nil, xerrors.Errorf("owner not found for workspace: %q", workspace.Name)
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
|
|
|
apiWorkspaces = append(apiWorkspaces,
|
2022-05-14 01:41:21 +00:00
|
|
|
convertWorkspace(workspace, convertWorkspaceBuild(build, convertProvisionerJob(job)), template, user))
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
|
|
|
return apiWorkspaces, nil
|
|
|
|
}
|
|
|
|
|
2022-05-14 01:41:21 +00:00
|
|
|
func convertWorkspace(workspace database.Workspace, workspaceBuild codersdk.WorkspaceBuild, template database.Template, owner database.User) codersdk.Workspace {
|
2022-03-22 19:17:50 +00:00
|
|
|
return codersdk.Workspace{
|
2022-04-07 09:03:35 +00:00
|
|
|
ID: workspace.ID,
|
|
|
|
CreatedAt: workspace.CreatedAt,
|
|
|
|
UpdatedAt: workspace.UpdatedAt,
|
|
|
|
OwnerID: workspace.OwnerID,
|
2022-05-14 01:41:21 +00:00
|
|
|
OwnerName: owner.Username,
|
2022-04-07 09:03:35 +00:00
|
|
|
TemplateID: workspace.TemplateID,
|
|
|
|
LatestBuild: workspaceBuild,
|
|
|
|
TemplateName: template.Name,
|
|
|
|
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
|
|
|
|
Name: workspace.Name,
|
|
|
|
AutostartSchedule: workspace.AutostartSchedule.String,
|
2022-05-19 19:09:27 +00:00
|
|
|
TTL: convertSQLNullInt64(workspace.Ttl),
|
2022-03-22 19:17:50 +00:00
|
|
|
}
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
2022-05-19 19:09:27 +00:00
|
|
|
|
|
|
|
func convertSQLNullInt64(i sql.NullInt64) *time.Duration {
|
|
|
|
if !i.Valid {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return (*time.Duration)(&i.Int64)
|
|
|
|
}
|