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
|
|
|
"golang.org/x/xerrors"
|
2022-05-18 21:16:26 +00:00
|
|
|
|
|
|
|
"cdr.dev/slog"
|
2023-08-18 18:55:43 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
2023-11-09 05:24:56 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
2023-08-18 18:55:43 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
2023-09-01 16:50:12 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
2023-09-19 06:25:57 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/database/provisionerjobs"
|
2023-08-18 18:55:43 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/httpapi"
|
|
|
|
"github.com/coder/coder/v2/coderd/httpmw"
|
|
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
2023-09-04 13:48:25 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/schedule/cron"
|
2023-08-18 18:55:43 +00:00
|
|
|
"github.com/coder/coder/v2/coderd/searchquery"
|
|
|
|
"github.com/coder/coder/v2/coderd/telemetry"
|
|
|
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
|
|
"github.com/coder/coder/v2/coderd/wsbuilder"
|
|
|
|
"github.com/coder/coder/v2/codersdk"
|
|
|
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
2022-01-25 19:52:58 +00:00
|
|
|
)
|
|
|
|
|
2022-07-29 14:01:17 +00:00
|
|
|
var (
|
|
|
|
ttlMin = time.Minute //nolint:revive // min here means 'minimum' not 'minutes'
|
2023-07-19 13:43:10 +00:00
|
|
|
ttlMax = 30 * 24 * time.Hour
|
2022-07-29 14:01:17 +00:00
|
|
|
|
2022-11-09 19:36:25 +00:00
|
|
|
errTTLMin = xerrors.New("time until shutdown must be at least one minute")
|
2023-07-19 13:43:10 +00:00
|
|
|
errTTLMax = xerrors.New("time until shutdown must be less than 30 days")
|
2022-11-09 19:36:25 +00:00
|
|
|
errDeadlineTooSoon = xerrors.New("new deadline must be at least 30 minutes in the future")
|
|
|
|
errDeadlineBeforeStart = xerrors.New("new deadline must be before workspace start time")
|
2022-07-29 14:01:17 +00:00
|
|
|
)
|
|
|
|
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Summary Get workspace metadata by ID
|
|
|
|
// @ID get-workspace-metadata-by-id
|
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Produce json
|
|
|
|
// @Tags Workspaces
|
2023-01-13 11:27:21 +00:00
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Param include_deleted query bool false "Return data instead of HTTP 404 if the workspace is deleted"
|
|
|
|
// @Success 200 {object} codersdk.Workspace
|
2023-01-13 11:27:21 +00:00
|
|
|
// @Router /workspaces/{workspace} [get]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) workspace(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
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
2024-01-24 13:39:19 +00:00
|
|
|
apiKey := httpmw.APIKey(r)
|
2022-05-19 14:29:10 +00:00
|
|
|
|
|
|
|
var (
|
2022-06-10 14:58:42 +00:00
|
|
|
deletedStr = r.URL.Query().Get("include_deleted")
|
2022-05-19 14:29:10 +00:00
|
|
|
showDeleted = false
|
|
|
|
)
|
|
|
|
if deletedStr != "" {
|
|
|
|
var err error
|
|
|
|
showDeleted, err = strconv.ParseBool(deletedStr)
|
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-10 14:58:42 +00:00
|
|
|
Message: fmt.Sprintf("Invalid boolean value %q for \"include_deleted\" query param.", deletedStr),
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{
|
2022-06-03 21:48:09 +00:00
|
|
|
{Field: "deleted", Detail: "Must be a valid boolean"},
|
|
|
|
},
|
2022-05-19 14:29:10 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if workspace.Deleted && !showDeleted {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusGone, codersdk.Response{
|
2022-06-07 14:33:06 +00:00
|
|
|
Message: fmt.Sprintf("Workspace %q was deleted, you can view this workspace by specifying '?deleted=true' and trying again.", workspace.ID.String()),
|
2022-05-19 14:29:10 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-21 22:07:00 +00:00
|
|
|
data, err := api.workspaceData(ctx, []database.Workspace{workspace})
|
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-09-16 18:54:23 +00:00
|
|
|
Message: "Internal error fetching workspace resources.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-03-22 19:17:50 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-14 01:41:21 +00:00
|
|
|
|
2023-08-17 18:22:03 +00:00
|
|
|
if len(data.templates) == 0 {
|
|
|
|
httpapi.Forbidden(rw)
|
|
|
|
return
|
|
|
|
}
|
2024-01-30 17:07:06 +00:00
|
|
|
owner, ok := userByID(workspace.OwnerID, data.users)
|
2023-10-10 12:55:28 +00:00
|
|
|
if !ok {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspace resources.",
|
|
|
|
Detail: "unable to find workspace owner's username",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2024-01-24 14:13:14 +00:00
|
|
|
|
|
|
|
w, err := convertWorkspace(
|
2024-01-24 13:39:19 +00:00
|
|
|
apiKey.UserID,
|
2022-09-16 18:54:23 +00:00
|
|
|
workspace,
|
|
|
|
data.builds[0],
|
|
|
|
data.templates[0],
|
2024-01-30 17:07:06 +00:00
|
|
|
owner.Username,
|
|
|
|
owner.AvatarURL,
|
2023-12-15 18:38:47 +00:00
|
|
|
api.Options.AllowWorkspaceRenames,
|
2024-01-24 14:13:14 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error converting workspace.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, w)
|
2022-03-07 17:40:54 +00:00
|
|
|
}
|
|
|
|
|
2023-01-13 11:27:21 +00:00
|
|
|
// workspaces returns all workspaces a user can read.
|
|
|
|
// Optional filters with query params
|
|
|
|
//
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Summary List workspaces
|
2023-01-13 11:27:21 +00:00
|
|
|
// @ID list-workspaces
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Produce json
|
|
|
|
// @Tags Workspaces
|
2023-12-19 23:42:07 +00:00
|
|
|
// @Param q query string false "Search query in the format `key:value`. Available keys are: owner, template, name, status, has-agent, dormant, last_used_after, last_used_before."
|
2023-07-10 18:57:09 +00:00
|
|
|
// @Param limit query int false "Page limit"
|
|
|
|
// @Param offset query int false "Page offset"
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Success 200 {object} codersdk.WorkspacesResponse
|
|
|
|
// @Router /workspaces [get]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) {
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx := r.Context()
|
2022-05-18 15:09:07 +00:00
|
|
|
apiKey := httpmw.APIKey(r)
|
2022-05-16 19:36:27 +00:00
|
|
|
|
2022-10-13 16:41:13 +00:00
|
|
|
page, ok := parsePagination(rw, r)
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-14 13:46:33 +00:00
|
|
|
queryStr := r.URL.Query().Get("q")
|
2023-10-11 00:00:09 +00:00
|
|
|
filter, errs := searchquery.Workspaces(queryStr, page, api.AgentInactiveDisconnectTimeout)
|
2022-06-14 13:46:33 +00:00
|
|
|
if len(errs) > 0 {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-14 13:46:33 +00:00
|
|
|
Message: "Invalid workspace search query.",
|
|
|
|
Validations: errs,
|
|
|
|
})
|
|
|
|
return
|
2022-05-18 15:09:07 +00:00
|
|
|
}
|
2022-06-14 13:46:33 +00:00
|
|
|
|
|
|
|
if filter.OwnerUsername == "me" {
|
2022-05-18 15:09:07 +00:00
|
|
|
filter.OwnerID = apiKey.UserID
|
2022-06-14 13:46:33 +00:00
|
|
|
filter.OwnerUsername = ""
|
2022-05-18 15:09:07 +00:00
|
|
|
}
|
|
|
|
|
2022-11-28 18:12:34 +00:00
|
|
|
// Workspaces do not have ACL columns.
|
|
|
|
prepared, err := api.HTTPAuth.AuthorizeSQLFilter(r, rbac.ActionRead, rbac.ResourceWorkspace.Type)
|
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-10-04 15:35:33 +00:00
|
|
|
Message: "Internal error preparing sql filter.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-16 19:36:27 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-24 13:39:19 +00:00
|
|
|
// To show the requester's favorite workspaces first, we pass their userID and compare it to
|
|
|
|
// the workspace owner_id when ordering the rows.
|
|
|
|
filter.RequesterID = apiKey.UserID
|
|
|
|
|
2024-03-05 08:24:43 +00:00
|
|
|
// We need the technical row to present the correct count on every page.
|
|
|
|
filter.WithSummary = true
|
|
|
|
|
2022-11-28 18:12:34 +00:00
|
|
|
workspaceRows, err := api.Database.GetAuthorizedWorkspaces(ctx, filter, prepared)
|
2022-08-11 22:07:48 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-08-11 22:07:48 +00:00
|
|
|
Message: "Internal error fetching workspaces.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2024-03-05 08:24:43 +00:00
|
|
|
if len(workspaceRows) == 0 {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspaces.",
|
|
|
|
Detail: "Workspace summary row is missing.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(workspaceRows) == 1 {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspacesResponse{
|
|
|
|
Workspaces: []codersdk.Workspace{},
|
|
|
|
Count: int(workspaceRows[0].Count),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// Skip technical summary row
|
|
|
|
workspaceRows = workspaceRows[:len(workspaceRows)-1]
|
|
|
|
|
2022-11-16 15:16:37 +00:00
|
|
|
if len(workspaceRows) == 0 {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspacesResponse{
|
|
|
|
Workspaces: []codersdk.Workspace{},
|
|
|
|
Count: 0,
|
2022-05-16 19:36:27 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-11-16 15:16:37 +00:00
|
|
|
|
|
|
|
workspaces := database.ConvertWorkspaceRows(workspaceRows)
|
2022-09-16 18:54:23 +00:00
|
|
|
|
2022-11-10 18:25:46 +00:00
|
|
|
data, err := api.workspaceData(ctx, workspaces)
|
2022-10-20 17:23:14 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-11-10 18:25:46 +00:00
|
|
|
Message: "Internal error fetching workspace resources.",
|
2022-10-20 17:23:14 +00:00
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-24 13:39:19 +00:00
|
|
|
wss, err := convertWorkspaces(apiKey.UserID, workspaces, data)
|
2022-10-20 17:23:14 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-11-10 18:25:46 +00:00
|
|
|
Message: "Internal error converting workspaces.",
|
2022-10-20 17:23:14 +00:00
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-10 18:25:46 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.WorkspacesResponse{
|
2023-10-11 00:00:09 +00:00
|
|
|
Workspaces: wss,
|
2022-11-16 15:16:37 +00:00
|
|
|
Count: int(workspaceRows[0].Count),
|
2022-10-20 17:23:14 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-01-11 13:08:04 +00:00
|
|
|
// @Summary Get workspace metadata by user and workspace name
|
|
|
|
// @ID get-workspace-metadata-by-user-and-workspace-name
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Produce json
|
|
|
|
// @Tags Workspaces
|
2023-01-11 13:08:04 +00:00
|
|
|
// @Param user path string true "User ID, name, or me"
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Param workspacename path string true "Workspace name"
|
|
|
|
// @Param include_deleted query bool false "Return data instead of HTTP 404 if the workspace is deleted"
|
|
|
|
// @Success 200 {object} codersdk.Workspace
|
|
|
|
// @Router /users/{user}/workspace/{workspacename} [get]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) workspaceByOwnerAndName(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
|
|
|
owner := httpmw.UserParam(r)
|
2022-05-18 23:15:19 +00:00
|
|
|
workspaceName := chi.URLParam(r, "workspacename")
|
2024-01-24 13:39:19 +00:00
|
|
|
apiKey := httpmw.APIKey(r)
|
2022-05-16 19:36:27 +00:00
|
|
|
|
2022-06-08 18:04:05 +00:00
|
|
|
includeDeleted := false
|
|
|
|
if s := r.URL.Query().Get("include_deleted"); s != "" {
|
|
|
|
var err error
|
|
|
|
includeDeleted, err = strconv.ParseBool(s)
|
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
2022-06-08 18:04:05 +00:00
|
|
|
Message: fmt.Sprintf("Invalid boolean value %q for \"include_deleted\" query param.", s),
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{
|
2022-06-08 18:04:05 +00:00
|
|
|
{Field: "include_deleted", Detail: "Must be a valid boolean"},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-21 22:07:00 +00:00
|
|
|
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{
|
2022-05-16 19:36:27 +00:00
|
|
|
OwnerID: owner.ID,
|
|
|
|
Name: workspaceName,
|
|
|
|
})
|
2022-06-08 18:04:05 +00:00
|
|
|
if includeDeleted && errors.Is(err, sql.ErrNoRows) {
|
2022-09-21 22:07:00 +00:00
|
|
|
workspace, err = api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{
|
2022-06-08 18:04:05 +00:00
|
|
|
OwnerID: owner.ID,
|
|
|
|
Name: workspaceName,
|
|
|
|
Deleted: includeDeleted,
|
|
|
|
})
|
|
|
|
}
|
2023-04-13 18:06:16 +00:00
|
|
|
if httpapi.Is404Error(err) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-05-16 19:36:27 +00:00
|
|
|
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 workspace by name.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-16 19:36:27 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-05-17 18:43:19 +00:00
|
|
|
|
2022-09-21 22:07:00 +00:00
|
|
|
data, err := api.workspaceData(ctx, []database.Workspace{workspace})
|
2022-06-09 01:23:35 +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 fetching workspace resources.",
|
2022-06-09 01:23:35 +00:00
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-10 11:26:11 +00:00
|
|
|
if len(data.builds) == 0 || len(data.templates) == 0 {
|
|
|
|
httpapi.ResourceNotFound(rw)
|
|
|
|
return
|
|
|
|
}
|
2024-01-30 17:07:06 +00:00
|
|
|
owner, ok := userByID(workspace.OwnerID, data.users)
|
2023-10-10 12:55:28 +00:00
|
|
|
if !ok {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspace resources.",
|
|
|
|
Detail: "unable to find workspace owner's username",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2024-01-24 14:13:14 +00:00
|
|
|
w, err := convertWorkspace(
|
2024-01-24 13:39:19 +00:00
|
|
|
apiKey.UserID,
|
2022-09-16 18:54:23 +00:00
|
|
|
workspace,
|
|
|
|
data.builds[0],
|
|
|
|
data.templates[0],
|
2024-01-30 17:07:06 +00:00
|
|
|
owner.Username,
|
|
|
|
owner.AvatarURL,
|
2023-12-15 18:38:47 +00:00
|
|
|
api.Options.AllowWorkspaceRenames,
|
2024-01-24 14:13:14 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error converting workspace.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, w)
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
|
|
|
|
2023-01-13 11:27:21 +00:00
|
|
|
// Create a new workspace for the currently authenticated user.
|
|
|
|
//
|
2023-01-11 11:16:09 +00:00
|
|
|
// @Summary Create user workspace by organization
|
2024-04-02 15:11:24 +00:00
|
|
|
// @Description Create a new workspace using a template. The request must
|
|
|
|
// @Description specify either the Template ID or the Template Version ID,
|
|
|
|
// @Description not both. If the Template ID is specified, the active version
|
|
|
|
// @Description of the template will be used.
|
2023-01-11 11:16:09 +00:00
|
|
|
// @ID create-user-workspace-by-organization
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Security CoderSessionToken
|
2023-02-09 14:02:03 +00:00
|
|
|
// @Accept json
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Produce json
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param organization path string true "Organization ID" format(uuid)
|
2023-01-11 11:16:09 +00:00
|
|
|
// @Param user path string true "Username, UUID, or me"
|
2023-02-09 14:02:03 +00:00
|
|
|
// @Param request body codersdk.CreateWorkspaceRequest true "Create workspace request"
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Success 200 {object} codersdk.Workspace
|
|
|
|
// @Router /organizations/{organization}/members/{user}/workspaces [post]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
|
2022-09-10 16:07:45 +00:00
|
|
|
var (
|
2023-01-30 18:43:29 +00:00
|
|
|
ctx = r.Context()
|
|
|
|
organization = httpmw.OrganizationParam(r)
|
|
|
|
apiKey = httpmw.APIKey(r)
|
|
|
|
auditor = api.Auditor.Load()
|
2023-10-11 05:41:14 +00:00
|
|
|
member = httpmw.OrganizationMemberParam(r)
|
2023-01-30 18:43:29 +00:00
|
|
|
workspaceResourceInfo = audit.AdditionalFields{
|
2023-10-11 05:41:14 +00:00
|
|
|
WorkspaceOwner: member.Username,
|
2023-01-30 18:43:29 +00:00
|
|
|
}
|
2022-09-10 16:07:45 +00:00
|
|
|
)
|
2022-12-09 17:19:30 +00:00
|
|
|
|
2023-01-30 18:43:29 +00:00
|
|
|
wriBytes, err := json.Marshal(workspaceResourceInfo)
|
|
|
|
if err != nil {
|
|
|
|
api.Logger.Warn(ctx, "marshal workspace owner name")
|
|
|
|
}
|
|
|
|
|
|
|
|
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
|
|
|
Audit: *auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionCreate,
|
|
|
|
AdditionalFields: wriBytes,
|
2024-02-26 14:27:33 +00:00
|
|
|
OrganizationID: organization.ID,
|
2023-01-30 18:43:29 +00:00
|
|
|
})
|
|
|
|
|
2022-09-10 16:07:45 +00:00
|
|
|
defer commitAudit()
|
|
|
|
|
2023-03-21 14:10:22 +00:00
|
|
|
// Do this upfront to save work.
|
2022-06-14 15:14:05 +00:00
|
|
|
if !api.Authorize(r, rbac.ActionCreate,
|
2023-10-11 05:41:14 +00:00
|
|
|
rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(member.UserID.String())) {
|
2022-06-14 15:14:05 +00:00
|
|
|
httpapi.ResourceNotFound(rw)
|
2022-05-27 16:19:13 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-16 19:36:27 +00:00
|
|
|
var createWorkspace codersdk.CreateWorkspaceRequest
|
2022-09-21 22:07:00 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &createWorkspace) {
|
2022-05-16 19:36:27 +00:00
|
|
|
return
|
|
|
|
}
|
2022-05-27 16:19:13 +00:00
|
|
|
|
2023-08-31 21:07:58 +00:00
|
|
|
// If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it.
|
|
|
|
templateID := createWorkspace.TemplateID
|
|
|
|
if templateID == uuid.Nil {
|
|
|
|
templateVersion, err := api.Database.GetTemplateVersionByID(ctx, createWorkspace.TemplateVersionID)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
|
|
Message: fmt.Sprintf("Template version %q doesn't exist.", templateID.String()),
|
|
|
|
Validations: []codersdk.ValidationError{{
|
|
|
|
Field: "template_version_id",
|
|
|
|
Detail: "template not found",
|
|
|
|
}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching template version.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2023-10-11 14:26:22 +00:00
|
|
|
if templateVersion.Archived {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Archived template versions cannot be used to make a workspace.",
|
|
|
|
Validations: []codersdk.ValidationError{
|
|
|
|
{
|
|
|
|
Field: "template_version_id",
|
|
|
|
Detail: "template version archived",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2023-08-31 21:07:58 +00:00
|
|
|
|
|
|
|
templateID = templateVersion.TemplateID.UUID
|
|
|
|
}
|
|
|
|
|
|
|
|
template, err := api.Database.GetTemplateByID(ctx, templateID)
|
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{
|
2023-08-31 21:07:58 +00:00
|
|
|
Message: fmt.Sprintf("Template %q doesn't exist.", templateID.String()),
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{{
|
2022-05-16 19:36:27 +00:00
|
|
|
Field: "template_id",
|
|
|
|
Detail: "template 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.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-16 19:36:27 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-11-10 22:53:14 +00:00
|
|
|
if template.Deleted {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
|
|
|
Message: fmt.Sprintf("Template %q has been deleted!", template.Name),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-06-14 13:46:33 +00:00
|
|
|
|
2023-11-20 19:16:18 +00:00
|
|
|
templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template)
|
|
|
|
if templateAccessControl.IsDeprecated() {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
|
|
Message: fmt.Sprintf("Template %q has been deprecated, and cannot be used to create a new workspace.", template.Name),
|
|
|
|
// Pass the deprecated message to the user.
|
|
|
|
Detail: templateAccessControl.Deprecated,
|
|
|
|
Validations: nil,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-06-14 13:46:33 +00:00
|
|
|
if organization.ID != template.OrganizationID {
|
2023-08-30 21:14:24 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
2022-06-14 13:46:33 +00:00
|
|
|
Message: fmt.Sprintf("Template is not in organization %q.", organization.Name),
|
2022-05-16 19:36:27 +00:00
|
|
|
})
|
2022-01-25 19:52:58 +00:00
|
|
|
return
|
|
|
|
}
|
2022-05-16 19:36:27 +00:00
|
|
|
|
2022-11-09 19:36:25 +00:00
|
|
|
dbAutostartSchedule, err := validWorkspaceSchedule(createWorkspace.AutostartSchedule)
|
2022-06-07 12:37:45 +00:00
|
|
|
if err != nil {
|
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: "Invalid Autostart Schedule.",
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{{Field: "schedule", Detail: err.Error()}},
|
2022-06-07 12:37:45 +00:00
|
|
|
})
|
|
|
|
return
|
2022-05-23 22:31:41 +00:00
|
|
|
}
|
|
|
|
|
2023-07-20 13:35:41 +00:00
|
|
|
templateSchedule, err := (*api.TemplateScheduleStore.Load()).Get(ctx, api.Database, template.ID)
|
2023-03-07 14:14:58 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching template schedule.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-20 15:37:57 +00:00
|
|
|
dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, templateSchedule.DefaultTTL)
|
2022-05-30 19:19:17 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
2022-07-27 21:20:02 +00:00
|
|
|
Message: "Invalid Workspace Time to Shutdown.",
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{{Field: "ttl_ms", Detail: err.Error()}},
|
2022-05-30 19:19:17 +00:00
|
|
|
})
|
|
|
|
return
|
2022-05-23 22:31:41 +00:00
|
|
|
}
|
|
|
|
|
2023-10-06 09:27:12 +00:00
|
|
|
// back-compatibility: default to "never" if not included.
|
|
|
|
dbAU := database.AutomaticUpdatesNever
|
|
|
|
if createWorkspace.AutomaticUpdates != "" {
|
|
|
|
dbAU, err = validWorkspaceAutomaticUpdates(createWorkspace.AutomaticUpdates)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
|
|
Message: "Invalid Workspace Automatic Updates setting.",
|
|
|
|
Validations: []codersdk.ValidationError{{Field: "automatic_updates", Detail: err.Error()}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-14 14:27:06 +00:00
|
|
|
// TODO: This should be a system call as the actor might not be able to
|
|
|
|
// read other workspaces. Ideally we check the error on create and look for
|
|
|
|
// a postgres conflict error.
|
2022-09-21 22:07:00 +00:00
|
|
|
workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{
|
2023-10-11 05:41:14 +00:00
|
|
|
OwnerID: member.UserID,
|
2022-05-16 19:36:27 +00:00
|
|
|
Name: createWorkspace.Name,
|
|
|
|
})
|
|
|
|
if err == nil {
|
|
|
|
// If the workspace already exists, don't allow creation.
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
2022-08-26 09:28:38 +00:00
|
|
|
Message: fmt.Sprintf("Workspace %q already exists.", createWorkspace.Name),
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{{
|
2022-05-16 19:36:27 +00:00
|
|
|
Field: "name",
|
2022-08-02 17:19:00 +00:00
|
|
|
Detail: "This value is already in use and should be unique.",
|
2022-01-25 19:52:58 +00:00
|
|
|
}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-09-10 16:07:45 +00:00
|
|
|
if err != nil && !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: fmt.Sprintf("Internal error fetching workspace by name %q.", createWorkspace.Name),
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-05-16 19:36:27 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-30 18:01:20 +00:00
|
|
|
var (
|
2023-05-23 08:06:33 +00:00
|
|
|
provisionerJob *database.ProvisionerJob
|
|
|
|
workspaceBuild *database.WorkspaceBuild
|
2022-09-30 18:01:20 +00:00
|
|
|
)
|
2022-05-16 19:36:27 +00:00
|
|
|
err = api.Database.InTx(func(db database.Store) error {
|
2023-09-01 16:50:12 +00:00
|
|
|
now := dbtime.Now()
|
2022-05-16 19:36:27 +00:00
|
|
|
// Workspaces are created without any versions.
|
2022-09-21 22:07:00 +00:00
|
|
|
workspace, err = db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
|
2022-05-23 22:31:41 +00:00
|
|
|
ID: uuid.New(),
|
|
|
|
CreatedAt: now,
|
|
|
|
UpdatedAt: now,
|
2023-10-11 05:41:14 +00:00
|
|
|
OwnerID: member.UserID,
|
2022-05-23 22:31:41 +00:00
|
|
|
OrganizationID: template.OrganizationID,
|
|
|
|
TemplateID: template.ID,
|
|
|
|
Name: createWorkspace.Name,
|
|
|
|
AutostartSchedule: dbAutostartSchedule,
|
|
|
|
Ttl: dbTTL,
|
2023-04-04 13:24:04 +00:00
|
|
|
// The workspaces page will sort by last used at, and it's useful to
|
|
|
|
// have the newly created workspace at the top of the list!
|
2023-10-06 09:27:12 +00:00
|
|
|
LastUsedAt: dbtime.Now(),
|
|
|
|
AutomaticUpdates: dbAU,
|
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
|
|
|
}
|
2023-01-17 15:24:45 +00:00
|
|
|
|
2023-05-23 08:06:33 +00:00
|
|
|
builder := wsbuilder.New(workspace, database.WorkspaceTransitionStart).
|
|
|
|
Reason(database.BuildReasonInitiator).
|
|
|
|
Initiator(apiKey.UserID).
|
|
|
|
ActiveVersion().
|
|
|
|
RichParameterValues(createWorkspace.RichParameterValues)
|
2023-08-31 21:07:58 +00:00
|
|
|
if createWorkspace.TemplateVersionID != uuid.Nil {
|
|
|
|
builder = builder.VersionID(createWorkspace.TemplateVersionID)
|
|
|
|
}
|
|
|
|
|
2023-05-23 08:06:33 +00:00
|
|
|
workspaceBuild, provisionerJob, err = builder.Build(
|
2023-09-19 06:25:57 +00:00
|
|
|
ctx,
|
|
|
|
db,
|
|
|
|
func(action rbac.Action, object rbac.Objecter) bool {
|
2023-05-23 08:06:33 +00:00
|
|
|
return api.Authorize(r, action, object)
|
2023-10-18 20:08:02 +00:00
|
|
|
},
|
|
|
|
audit.WorkspaceBuildBaggageFromRequest(r),
|
|
|
|
)
|
2023-05-23 08:06:33 +00:00
|
|
|
return err
|
2022-11-14 17:57:33 +00:00
|
|
|
}, nil)
|
2023-05-23 08:06:33 +00:00
|
|
|
var bldErr wsbuilder.BuildError
|
|
|
|
if xerrors.As(err, &bldErr) {
|
|
|
|
httpapi.Write(ctx, rw, bldErr.Status, codersdk.Response{
|
|
|
|
Message: bldErr.Message,
|
|
|
|
Detail: bldErr.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-01-25 19:52:58 +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 creating workspace.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-03-07 17:40:54 +00:00
|
|
|
})
|
|
|
|
return
|
2022-02-06 00:24:51 +00:00
|
|
|
}
|
2023-09-19 06:25:57 +00:00
|
|
|
err = provisionerjobs.PostJob(api.Pubsub, *provisionerJob)
|
|
|
|
if err != nil {
|
|
|
|
// Client probably doesn't care about this error, so just log it.
|
|
|
|
api.Logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err))
|
|
|
|
}
|
2022-09-10 16:07:45 +00:00
|
|
|
aReq.New = workspace
|
|
|
|
|
2022-06-17 05:26:40 +00:00
|
|
|
api.Telemetry.Report(&telemetry.Snapshot{
|
|
|
|
Workspaces: []telemetry.Workspace{telemetry.ConvertWorkspace(workspace)},
|
2023-05-23 08:06:33 +00:00
|
|
|
WorkspaceBuilds: []telemetry.WorkspaceBuild{telemetry.ConvertWorkspaceBuild(*workspaceBuild)},
|
2022-06-17 05:26:40 +00:00
|
|
|
})
|
|
|
|
|
2022-09-16 18:54:23 +00:00
|
|
|
apiBuild, err := api.convertWorkspaceBuild(
|
2023-05-23 08:06:33 +00:00
|
|
|
*workspaceBuild,
|
2022-09-16 18:54:23 +00:00
|
|
|
workspace,
|
2023-06-20 20:07:18 +00:00
|
|
|
database.GetProvisionerJobsByIDsWithQueuePositionRow{
|
|
|
|
ProvisionerJob: *provisionerJob,
|
|
|
|
QueuePosition: 0,
|
|
|
|
},
|
2023-10-11 05:41:14 +00:00
|
|
|
member.Username,
|
2024-01-30 17:07:06 +00:00
|
|
|
member.AvatarURL,
|
2022-09-16 18:54:23 +00:00
|
|
|
[]database.WorkspaceResource{},
|
|
|
|
[]database.WorkspaceResourceMetadatum{},
|
|
|
|
[]database.WorkspaceAgent{},
|
|
|
|
[]database.WorkspaceApp{},
|
2023-09-25 21:47:17 +00:00
|
|
|
[]database.WorkspaceAgentScript{},
|
|
|
|
[]database.WorkspaceAgentLogSource{},
|
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
|
|
|
|
}
|
|
|
|
|
2024-01-24 14:13:14 +00:00
|
|
|
w, err := convertWorkspace(
|
2024-01-24 13:39:19 +00:00
|
|
|
apiKey.UserID,
|
2022-09-16 18:54:23 +00:00
|
|
|
workspace,
|
|
|
|
apiBuild,
|
|
|
|
template,
|
2023-10-11 05:41:14 +00:00
|
|
|
member.Username,
|
2024-01-30 17:07:06 +00:00
|
|
|
member.AvatarURL,
|
2023-12-15 18:38:47 +00:00
|
|
|
api.Options.AllowWorkspaceRenames,
|
2024-01-24 14:13:14 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error converting workspace.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusCreated, w)
|
2022-02-06 00:24:51 +00:00
|
|
|
}
|
|
|
|
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Summary Update workspace metadata by ID
|
|
|
|
// @ID update-workspace-metadata-by-id
|
|
|
|
// @Security CoderSessionToken
|
2022-12-21 14:37:30 +00:00
|
|
|
// @Accept json
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Param request body codersdk.UpdateWorkspaceRequest true "Metadata update request"
|
|
|
|
// @Success 204
|
|
|
|
// @Router /workspaces/{workspace} [patch]
|
2022-08-26 09:28:38 +00:00
|
|
|
func (api *API) patchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
2022-09-10 16:07:45 +00:00
|
|
|
var (
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx = r.Context()
|
2022-09-10 16:07:45 +00:00
|
|
|
workspace = httpmw.WorkspaceParam(r)
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor = api.Auditor.Load()
|
2022-09-10 16:07:45 +00:00
|
|
|
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
2024-02-26 14:27:33 +00:00
|
|
|
Audit: *auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
|
|
|
OrganizationID: workspace.OrganizationID,
|
2022-09-10 16:07:45 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = workspace
|
|
|
|
|
2022-08-26 09:28:38 +00:00
|
|
|
var req codersdk.UpdateWorkspaceRequest
|
2022-09-21 22:07:00 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
2022-08-26 09:28:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if req.Name == "" || req.Name == workspace.Name {
|
2022-09-10 16:07:45 +00:00
|
|
|
aReq.New = workspace
|
2022-08-26 09:28:38 +00:00
|
|
|
// Nothing changed, optionally this could be an error.
|
|
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
|
|
return
|
|
|
|
}
|
2022-09-10 16:07:45 +00:00
|
|
|
|
2022-08-26 09:28:38 +00:00
|
|
|
// The reason we double check here is in case more fields can be
|
|
|
|
// patched in the future, it's enough if one changes.
|
|
|
|
name := workspace.Name
|
|
|
|
if req.Name != "" || req.Name != workspace.Name {
|
2023-12-15 18:38:47 +00:00
|
|
|
if !api.Options.AllowWorkspaceRenames {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
|
|
Message: "Workspace renames are not allowed.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-08-26 09:28:38 +00:00
|
|
|
name = req.Name
|
|
|
|
}
|
|
|
|
|
2022-09-21 22:07:00 +00:00
|
|
|
newWorkspace, err := api.Database.UpdateWorkspace(ctx, database.UpdateWorkspaceParams{
|
2022-08-26 09:28:38 +00:00
|
|
|
ID: workspace.ID,
|
|
|
|
Name: name,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
// The query protects against updating deleted workspaces and
|
|
|
|
// the existence of the workspace is checked in the request,
|
|
|
|
// if we get ErrNoRows it means the workspace was deleted.
|
|
|
|
//
|
|
|
|
// We could do this check earlier but we'd need to start a
|
|
|
|
// transaction.
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusMethodNotAllowed, codersdk.Response{
|
2022-08-26 09:28:38 +00:00
|
|
|
Message: fmt.Sprintf("Workspace %q is deleted and cannot be updated.", workspace.Name),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// Check if the name was already in use.
|
2022-08-29 22:00:52 +00:00
|
|
|
if database.IsUniqueViolation(err) {
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
2022-08-26 09:28:38 +00:00
|
|
|
Message: fmt.Sprintf("Workspace %q already exists.", req.Name),
|
|
|
|
Validations: []codersdk.ValidationError{{
|
|
|
|
Field: "name",
|
|
|
|
Detail: "This value is already in use and should be unique.",
|
|
|
|
}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
2022-08-26 09:28:38 +00:00
|
|
|
Message: "Internal error updating workspace.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-07 15:25:18 +00:00
|
|
|
api.publishWorkspaceUpdate(ctx, workspace.ID)
|
|
|
|
|
2022-09-10 16:07:45 +00:00
|
|
|
aReq.New = newWorkspace
|
2022-08-26 09:28:38 +00:00
|
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Summary Update workspace autostart schedule by ID
|
|
|
|
// @ID update-workspace-autostart-schedule-by-id
|
|
|
|
// @Security CoderSessionToken
|
2022-12-21 14:37:30 +00:00
|
|
|
// @Accept json
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Param request body codersdk.UpdateWorkspaceAutostartRequest true "Schedule update request"
|
|
|
|
// @Success 204
|
|
|
|
// @Router /workspaces/{workspace}/autostart [put]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
|
2022-09-10 16:07:45 +00:00
|
|
|
var (
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx = r.Context()
|
2022-09-10 16:07:45 +00:00
|
|
|
workspace = httpmw.WorkspaceParam(r)
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor = api.Auditor.Load()
|
2022-09-10 16:07:45 +00:00
|
|
|
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
2024-02-26 14:27:33 +00:00
|
|
|
Audit: *auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
|
|
|
OrganizationID: workspace.OrganizationID,
|
2022-09-10 16:07:45 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = workspace
|
|
|
|
|
2022-04-07 09:03:35 +00:00
|
|
|
var req codersdk.UpdateWorkspaceAutostartRequest
|
2022-09-21 22:07:00 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
2022-04-07 09:03:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-11-09 19:36:25 +00:00
|
|
|
dbSched, err := validWorkspaceSchedule(req.Schedule)
|
2022-06-07 12:37:45 +00:00
|
|
|
if err != nil {
|
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: "Invalid autostart schedule.",
|
2022-07-13 00:15:02 +00:00
|
|
|
Validations: []codersdk.ValidationError{{Field: "schedule", Detail: err.Error()}},
|
2022-06-02 10:23:34 +00:00
|
|
|
})
|
|
|
|
return
|
2022-04-07 09:03:35 +00:00
|
|
|
}
|
|
|
|
|
2023-04-04 12:48:35 +00:00
|
|
|
// Check if the template allows users to configure autostart.
|
2023-07-20 13:35:41 +00:00
|
|
|
templateSchedule, err := (*api.TemplateScheduleStore.Load()).Get(ctx, api.Database, workspace.TemplateID)
|
2023-04-04 12:48:35 +00:00
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error getting template schedule options.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !templateSchedule.UserAutostartEnabled {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
|
|
Message: "Autostart is not allowed for workspaces using this template.",
|
|
|
|
Validations: []codersdk.ValidationError{{Field: "schedule", Detail: "Autostart is not allowed for workspaces using this template."}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-21 22:07:00 +00:00
|
|
|
err = api.Database.UpdateWorkspaceAutostart(ctx, database.UpdateWorkspaceAutostartParams{
|
2022-04-07 09:03:35 +00:00
|
|
|
ID: workspace.ID,
|
|
|
|
AutostartSchedule: dbSched,
|
|
|
|
})
|
|
|
|
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 workspace autostart schedule.",
|
2022-06-03 21:48:09 +00:00
|
|
|
Detail: err.Error(),
|
2022-04-07 09:03:35 +00:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-09-10 16:07:45 +00:00
|
|
|
|
|
|
|
newWorkspace := workspace
|
|
|
|
newWorkspace.AutostartSchedule = dbSched
|
|
|
|
aReq.New = newWorkspace
|
|
|
|
|
|
|
|
rw.WriteHeader(http.StatusNoContent)
|
2022-04-07 09:03:35 +00:00
|
|
|
}
|
|
|
|
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Summary Update workspace TTL by ID
|
|
|
|
// @ID update-workspace-ttl-by-id
|
|
|
|
// @Security CoderSessionToken
|
2022-12-21 14:37:30 +00:00
|
|
|
// @Accept json
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Param request body codersdk.UpdateWorkspaceTTLRequest true "Workspace TTL update request"
|
|
|
|
// @Success 204
|
|
|
|
// @Router /workspaces/{workspace}/ttl [put]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
|
2022-09-10 16:07:45 +00:00
|
|
|
var (
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx = r.Context()
|
2022-09-10 16:07:45 +00:00
|
|
|
workspace = httpmw.WorkspaceParam(r)
|
2022-09-20 04:11:01 +00:00
|
|
|
auditor = api.Auditor.Load()
|
2022-09-10 16:07:45 +00:00
|
|
|
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
2024-02-26 14:27:33 +00:00
|
|
|
Audit: *auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
|
|
|
OrganizationID: workspace.OrganizationID,
|
2022-09-10 16:07:45 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
defer commitAudit()
|
2022-09-28 21:50:21 +00:00
|
|
|
aReq.Old = workspace
|
2022-09-10 16:07:45 +00:00
|
|
|
|
2022-05-19 19:09:27 +00:00
|
|
|
var req codersdk.UpdateWorkspaceTTLRequest
|
2022-09-21 22:07:00 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
2022-04-07 09:03:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-09-10 16:07:45 +00:00
|
|
|
var dbTTL sql.NullInt64
|
|
|
|
|
2022-06-14 16:09:24 +00:00
|
|
|
err := api.Database.InTx(func(s database.Store) error {
|
2023-07-20 13:35:41 +00:00
|
|
|
templateSchedule, err := (*api.TemplateScheduleStore.Load()).Get(ctx, s, workspace.TemplateID)
|
2023-03-07 14:14:58 +00:00
|
|
|
if err != nil {
|
|
|
|
return xerrors.Errorf("get template schedule: %w", err)
|
|
|
|
}
|
2023-04-04 12:48:35 +00:00
|
|
|
if !templateSchedule.UserAutostopEnabled {
|
|
|
|
return codersdk.ValidationError{Field: "ttl_ms", Detail: "Custom autostop TTL is not allowed for workspaces using this template."}
|
|
|
|
}
|
2023-03-07 14:14:58 +00:00
|
|
|
|
|
|
|
// don't override 0 ttl with template default here because it indicates
|
2023-04-04 12:48:35 +00:00
|
|
|
// disabled autostop
|
2022-12-05 22:19:30 +00:00
|
|
|
var validityErr error
|
2024-03-20 15:37:57 +00:00
|
|
|
dbTTL, validityErr = validWorkspaceTTLMillis(req.TTLMillis, 0)
|
2022-12-05 22:19:30 +00:00
|
|
|
if validityErr != nil {
|
|
|
|
return codersdk.ValidationError{Field: "ttl_ms", Detail: validityErr.Error()}
|
2022-06-14 16:09:24 +00:00
|
|
|
}
|
2022-09-21 22:07:00 +00:00
|
|
|
if err := s.UpdateWorkspaceTTL(ctx, database.UpdateWorkspaceTTLParams{
|
2022-06-09 21:10:24 +00:00
|
|
|
ID: workspace.ID,
|
|
|
|
Ttl: dbTTL,
|
|
|
|
}); err != nil {
|
2022-07-27 21:20:02 +00:00
|
|
|
return xerrors.Errorf("update workspace time until shutdown: %w", err)
|
2022-06-09 21:10:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2022-11-14 17:57:33 +00:00
|
|
|
}, nil)
|
2022-04-07 09:03:35 +00:00
|
|
|
if err != nil {
|
2022-07-27 21:20:02 +00:00
|
|
|
resp := codersdk.Response{
|
|
|
|
Message: "Error updating workspace time until shutdown.",
|
2022-06-14 16:09:24 +00:00
|
|
|
}
|
2022-07-27 21:20:02 +00:00
|
|
|
var validErr codersdk.ValidationError
|
|
|
|
if errors.As(err, &validErr) {
|
|
|
|
resp.Validations = []codersdk.ValidationError{validErr}
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, resp)
|
2022-07-27 21:20:02 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
resp.Detail = err.Error()
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, resp)
|
2022-04-07 09:03:35 +00:00
|
|
|
return
|
|
|
|
}
|
2022-06-09 21:10:24 +00:00
|
|
|
|
2022-09-10 16:07:45 +00:00
|
|
|
newWorkspace := workspace
|
|
|
|
newWorkspace.Ttl = dbTTL
|
|
|
|
aReq.New = newWorkspace
|
|
|
|
|
|
|
|
rw.WriteHeader(http.StatusNoContent)
|
2022-04-07 09:03:35 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 18:25:54 +00:00
|
|
|
// @Summary Update workspace dormancy status by id.
|
|
|
|
// @ID update-workspace-dormancy-status-by-id
|
2023-06-28 21:12:49 +00:00
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Accept json
|
|
|
|
// @Produce json
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
2023-08-24 18:25:54 +00:00
|
|
|
// @Param request body codersdk.UpdateWorkspaceDormancy true "Make a workspace dormant or active"
|
2023-08-04 00:46:02 +00:00
|
|
|
// @Success 200 {object} codersdk.Workspace
|
2023-08-24 18:25:54 +00:00
|
|
|
// @Router /workspaces/{workspace}/dormant [put]
|
|
|
|
func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
|
2023-10-05 18:41:07 +00:00
|
|
|
var (
|
|
|
|
ctx = r.Context()
|
|
|
|
workspace = httpmw.WorkspaceParam(r)
|
2024-01-24 13:39:19 +00:00
|
|
|
apiKey = httpmw.APIKey(r)
|
2023-10-05 18:41:07 +00:00
|
|
|
oldWorkspace = workspace
|
|
|
|
auditor = api.Auditor.Load()
|
|
|
|
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
2024-02-26 14:27:33 +00:00
|
|
|
Audit: *auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
|
|
|
OrganizationID: workspace.OrganizationID,
|
2023-10-05 18:41:07 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
aReq.Old = oldWorkspace
|
|
|
|
defer commitAudit()
|
2023-06-28 21:12:49 +00:00
|
|
|
|
2023-08-24 18:25:54 +00:00
|
|
|
var req codersdk.UpdateWorkspaceDormancy
|
2023-06-28 21:12:49 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the workspace is already in the desired state do nothing!
|
2023-08-24 18:25:54 +00:00
|
|
|
if workspace.DormantAt.Valid == req.Dormant {
|
2023-06-28 21:12:49 +00:00
|
|
|
httpapi.Write(ctx, rw, http.StatusNotModified, codersdk.Response{
|
|
|
|
Message: "Nothing to do!",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-24 18:25:54 +00:00
|
|
|
dormantAt := sql.NullTime{
|
|
|
|
Valid: req.Dormant,
|
2023-06-28 21:12:49 +00:00
|
|
|
}
|
2023-08-24 18:25:54 +00:00
|
|
|
if req.Dormant {
|
2023-09-01 16:50:12 +00:00
|
|
|
dormantAt.Time = dbtime.Now()
|
2023-06-28 21:12:49 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 18:25:54 +00:00
|
|
|
workspace, err := api.Database.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
|
|
|
|
ID: workspace.ID,
|
|
|
|
DormantAt: dormantAt,
|
2023-06-28 21:12:49 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error updating workspace locked status.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-04 00:46:02 +00:00
|
|
|
data, err := api.workspaceData(ctx, []database.Workspace{workspace})
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspace resources.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2024-01-30 17:07:06 +00:00
|
|
|
owner, ok := userByID(workspace.OwnerID, data.users)
|
2023-10-10 12:55:28 +00:00
|
|
|
if !ok {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspace resources.",
|
|
|
|
Detail: "unable to find workspace owner's username",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2023-08-04 00:46:02 +00:00
|
|
|
|
2023-08-17 18:22:03 +00:00
|
|
|
if len(data.templates) == 0 {
|
|
|
|
httpapi.Forbidden(rw)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-05 18:41:07 +00:00
|
|
|
aReq.New = workspace
|
2024-01-24 14:13:14 +00:00
|
|
|
|
|
|
|
w, err := convertWorkspace(
|
2024-01-24 13:39:19 +00:00
|
|
|
apiKey.UserID,
|
2023-08-04 00:46:02 +00:00
|
|
|
workspace,
|
|
|
|
data.builds[0],
|
|
|
|
data.templates[0],
|
2024-01-30 17:07:06 +00:00
|
|
|
owner.Username,
|
|
|
|
owner.AvatarURL,
|
2023-12-15 18:38:47 +00:00
|
|
|
api.Options.AllowWorkspaceRenames,
|
2024-01-24 14:13:14 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error converting workspace.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, w)
|
2023-06-28 21:12:49 +00:00
|
|
|
}
|
|
|
|
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Summary Extend workspace deadline by ID
|
|
|
|
// @ID extend-workspace-deadline-by-id
|
|
|
|
// @Security CoderSessionToken
|
2022-12-21 14:37:30 +00:00
|
|
|
// @Accept json
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Produce json
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Param request body codersdk.PutExtendWorkspaceRequest true "Extend deadline update request"
|
|
|
|
// @Success 200 {object} codersdk.Response
|
|
|
|
// @Router /workspaces/{workspace}/extend [put]
|
2022-05-26 17:08:11 +00:00
|
|
|
func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx := r.Context()
|
2022-05-26 17:08:11 +00:00
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
|
|
|
|
|
|
var req codersdk.PutExtendWorkspaceRequest
|
2022-09-21 22:07:00 +00:00
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
2022-05-26 17:08:11 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-30 19:19:17 +00:00
|
|
|
code := http.StatusOK
|
2022-07-13 00:15:02 +00:00
|
|
|
resp := codersdk.Response{}
|
2022-05-26 17:08:11 +00:00
|
|
|
|
|
|
|
err := api.Database.InTx(func(s database.Store) error {
|
2022-09-21 22:07:00 +00:00
|
|
|
build, err := s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
2022-05-26 17:08:11 +00:00
|
|
|
if err != nil {
|
|
|
|
code = http.StatusInternalServerError
|
2022-06-14 21:39:15 +00:00
|
|
|
resp.Message = "Error fetching workspace build."
|
2022-05-26 17:08:11 +00:00
|
|
|
return xerrors.Errorf("get latest workspace build: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-09-21 22:07:00 +00:00
|
|
|
job, err := s.GetProvisionerJobByID(ctx, build.JobID)
|
2022-06-14 21:39:15 +00:00
|
|
|
if err != nil {
|
|
|
|
code = http.StatusInternalServerError
|
|
|
|
resp.Message = "Error fetching workspace provisioner job."
|
|
|
|
return xerrors.Errorf("get provisioner job: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-05-26 17:08:11 +00:00
|
|
|
if build.Transition != database.WorkspaceTransitionStart {
|
|
|
|
code = http.StatusConflict
|
2022-06-07 14:33:06 +00:00
|
|
|
resp.Message = "Workspace must be started, current status: " + string(build.Transition)
|
2022-05-26 17:08:11 +00:00
|
|
|
return xerrors.Errorf("workspace must be started, current status: %s", build.Transition)
|
|
|
|
}
|
|
|
|
|
2022-06-14 21:39:15 +00:00
|
|
|
if !job.CompletedAt.Valid {
|
|
|
|
code = http.StatusConflict
|
|
|
|
resp.Message = "Workspace is still building!"
|
|
|
|
return xerrors.Errorf("workspace is still building")
|
|
|
|
}
|
|
|
|
|
|
|
|
if build.Deadline.IsZero() {
|
|
|
|
code = http.StatusConflict
|
|
|
|
resp.Message = "Workspace shutdown is manual."
|
|
|
|
return xerrors.Errorf("workspace shutdown is manual")
|
|
|
|
}
|
|
|
|
|
2022-05-27 19:04:33 +00:00
|
|
|
newDeadline := req.Deadline.UTC()
|
2022-11-09 19:36:25 +00:00
|
|
|
if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline); err != nil {
|
2022-07-29 14:01:17 +00:00
|
|
|
// NOTE(Cian): Putting the error in the Message field on request from the FE folks.
|
|
|
|
// Normally, we would put the validation error in Validations, but this endpoint is
|
|
|
|
// not tied to a form or specific named user input on the FE.
|
2022-05-26 17:08:11 +00:00
|
|
|
code = http.StatusBadRequest
|
2022-07-29 14:01:17 +00:00
|
|
|
resp.Message = "Cannot extend workspace: " + err.Error()
|
2022-05-30 19:19:17 +00:00
|
|
|
return err
|
2022-05-26 17:08:11 +00:00
|
|
|
}
|
2023-03-07 14:14:58 +00:00
|
|
|
if !build.MaxDeadline.IsZero() && newDeadline.After(build.MaxDeadline) {
|
|
|
|
code = http.StatusBadRequest
|
|
|
|
resp.Message = "Cannot extend workspace beyond max deadline."
|
|
|
|
return xerrors.New("Cannot extend workspace: deadline is beyond max deadline imposed by template")
|
|
|
|
}
|
2022-05-26 17:08:11 +00:00
|
|
|
|
2023-09-22 15:22:07 +00:00
|
|
|
if err := s.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
|
|
|
|
ID: build.ID,
|
|
|
|
UpdatedAt: dbtime.Now(),
|
|
|
|
Deadline: newDeadline,
|
|
|
|
MaxDeadline: build.MaxDeadline,
|
2022-05-26 17:08:11 +00:00
|
|
|
}); err != nil {
|
2022-05-30 19:19:17 +00:00
|
|
|
code = http.StatusInternalServerError
|
2022-06-07 14:33:06 +00:00
|
|
|
resp.Message = "Failed to extend workspace deadline."
|
2022-05-26 17:08:11 +00:00
|
|
|
return xerrors.Errorf("update workspace build: %w", err)
|
|
|
|
}
|
2022-06-07 14:33:06 +00:00
|
|
|
resp.Message = "Deadline updated to " + newDeadline.Format(time.RFC3339) + "."
|
2022-05-26 17:08:11 +00:00
|
|
|
|
|
|
|
return nil
|
2022-11-14 17:57:33 +00:00
|
|
|
}, nil)
|
2022-05-26 17:08:11 +00:00
|
|
|
if err != nil {
|
2022-09-21 22:07:00 +00:00
|
|
|
api.Logger.Info(ctx, "extending workspace", slog.Error(err))
|
2022-05-26 17:08:11 +00:00
|
|
|
}
|
2022-11-28 15:59:43 +00:00
|
|
|
api.publishWorkspaceUpdate(ctx, workspace.ID)
|
2022-09-21 22:07:00 +00:00
|
|
|
httpapi.Write(ctx, rw, code, resp)
|
2022-05-26 17:08:11 +00:00
|
|
|
}
|
|
|
|
|
2024-03-20 16:44:12 +00:00
|
|
|
// @Summary Post Workspace Usage by ID
|
|
|
|
// @ID post-workspace-usage-by-id
|
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Success 204
|
|
|
|
// @Router /workspaces/{workspace}/usage [post]
|
|
|
|
func (api *API) postWorkspaceUsage(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
|
|
|
if !api.Authorize(r, rbac.ActionUpdate, workspace) {
|
|
|
|
httpapi.Forbidden(rw)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
api.workspaceUsageTracker.Add(workspace.ID)
|
|
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
2024-01-24 13:39:19 +00:00
|
|
|
// @Summary Favorite workspace by ID.
|
|
|
|
// @ID favorite-workspace-by-id
|
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Success 204
|
|
|
|
// @Router /workspaces/{workspace}/favorite [put]
|
|
|
|
func (api *API) putFavoriteWorkspace(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
ctx = r.Context()
|
|
|
|
workspace = httpmw.WorkspaceParam(r)
|
|
|
|
apiKey = httpmw.APIKey(r)
|
|
|
|
auditor = api.Auditor.Load()
|
|
|
|
)
|
|
|
|
|
|
|
|
if apiKey.UserID != workspace.OwnerID {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
|
|
|
Message: "You can only favorite workspaces that you own.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
2024-02-26 14:27:33 +00:00
|
|
|
Audit: *auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
|
|
|
OrganizationID: workspace.OrganizationID,
|
2024-01-24 13:39:19 +00:00
|
|
|
})
|
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = workspace
|
|
|
|
|
|
|
|
err := api.Database.FavoriteWorkspace(ctx, workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error setting workspace as favorite",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
aReq.New = workspace
|
|
|
|
aReq.New.Favorite = true
|
|
|
|
|
|
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
|
|
|
// @Summary Unfavorite workspace by ID.
|
|
|
|
// @ID unfavorite-workspace-by-id
|
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Success 204
|
|
|
|
// @Router /workspaces/{workspace}/favorite [delete]
|
|
|
|
func (api *API) deleteFavoriteWorkspace(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
ctx = r.Context()
|
|
|
|
workspace = httpmw.WorkspaceParam(r)
|
|
|
|
apiKey = httpmw.APIKey(r)
|
|
|
|
auditor = api.Auditor.Load()
|
|
|
|
)
|
|
|
|
|
|
|
|
if apiKey.UserID != workspace.OwnerID {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
|
|
|
Message: "You can only un-favorite workspaces that you own.",
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
2024-02-26 14:27:33 +00:00
|
|
|
Audit: *auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
|
|
|
OrganizationID: workspace.OrganizationID,
|
2024-01-24 13:39:19 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = workspace
|
|
|
|
|
|
|
|
err := api.Database.UnfavoriteWorkspace(ctx, workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error unsetting workspace as favorite",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
aReq.New = workspace
|
|
|
|
aReq.New.Favorite = false
|
|
|
|
|
|
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
2023-10-06 09:27:12 +00:00
|
|
|
// @Summary Update workspace automatic updates by ID
|
|
|
|
// @ID update-workspace-automatic-updates-by-id
|
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Accept json
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Param request body codersdk.UpdateWorkspaceAutomaticUpdatesRequest true "Automatic updates request"
|
|
|
|
// @Success 204
|
|
|
|
// @Router /workspaces/{workspace}/autoupdates [put]
|
|
|
|
func (api *API) putWorkspaceAutoupdates(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
ctx = r.Context()
|
|
|
|
workspace = httpmw.WorkspaceParam(r)
|
|
|
|
auditor = api.Auditor.Load()
|
|
|
|
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
|
2024-02-26 14:27:33 +00:00
|
|
|
Audit: *auditor,
|
|
|
|
Log: api.Logger,
|
|
|
|
Request: r,
|
|
|
|
Action: database.AuditActionWrite,
|
|
|
|
OrganizationID: workspace.OrganizationID,
|
2023-10-06 09:27:12 +00:00
|
|
|
})
|
|
|
|
)
|
|
|
|
defer commitAudit()
|
|
|
|
aReq.Old = workspace
|
|
|
|
|
|
|
|
var req codersdk.UpdateWorkspaceAutomaticUpdatesRequest
|
|
|
|
if !httpapi.Read(ctx, rw, r, &req) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !database.AutomaticUpdates(req.AutomaticUpdates).Valid() {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
|
|
Message: "Invalid request",
|
|
|
|
Validations: []codersdk.ValidationError{{Field: "automatic_updates", Detail: "must be always or never"}},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err := api.Database.UpdateWorkspaceAutomaticUpdates(ctx, database.UpdateWorkspaceAutomaticUpdatesParams{
|
|
|
|
ID: workspace.ID,
|
|
|
|
AutomaticUpdates: database.AutomaticUpdates(req.AutomaticUpdates),
|
|
|
|
})
|
|
|
|
if httpapi.Is404Error(err) {
|
|
|
|
httpapi.ResourceNotFound(rw)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error updating workspace automatic updates setting",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
newWorkspace := workspace
|
|
|
|
newWorkspace.AutomaticUpdates = database.AutomaticUpdates(req.AutomaticUpdates)
|
|
|
|
aReq.New = newWorkspace
|
|
|
|
|
|
|
|
rw.WriteHeader(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
2023-11-09 05:24:56 +00:00
|
|
|
// @Summary Resolve workspace autostart by id.
|
|
|
|
// @ID resolve-workspace-autostart-by-id
|
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Produce json
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Success 200 {object} codersdk.ResolveAutostartResponse
|
|
|
|
// @Router /workspaces/{workspace}/resolve-autostart [get]
|
|
|
|
func (api *API) resolveAutostart(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
var (
|
|
|
|
ctx = r.Context()
|
|
|
|
workspace = httpmw.WorkspaceParam(r)
|
|
|
|
)
|
|
|
|
|
|
|
|
template, err := api.Database.GetTemplateByID(ctx, workspace.TemplateID)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.InternalServerError(rw, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template)
|
|
|
|
useActiveVersion := templateAccessControl.RequireActiveVersion || workspace.AutomaticUpdates == database.AutomaticUpdatesAlways
|
|
|
|
if !useActiveVersion {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ResolveAutostartResponse{})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
build, err := api.Database.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching latest workspace build.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if build.TemplateVersionID == template.ActiveVersionID {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ResolveAutostartResponse{})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
version, err := api.Database.GetTemplateVersionByID(ctx, template.ActiveVersionID)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching template version.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dbVersionParams, err := api.Database.GetTemplateVersionParameters(ctx, version.ID)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching template version parameters.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dbBuildParams, err := api.Database.GetWorkspaceBuildParameters(ctx, build.ID)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error fetching latest workspace build parameters.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
versionParams, err := db2sdk.TemplateVersionParameters(dbVersionParams)
|
|
|
|
if err != nil {
|
|
|
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
|
|
Message: "Internal error converting template version parameters.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
resolver := codersdk.ParameterResolver{
|
|
|
|
Rich: db2sdk.WorkspaceBuildParameters(dbBuildParams),
|
|
|
|
}
|
|
|
|
|
|
|
|
var response codersdk.ResolveAutostartResponse
|
|
|
|
for _, param := range versionParams {
|
|
|
|
_, err := resolver.ValidateResolve(param, nil)
|
|
|
|
// There's a parameter mismatch if we get an error back from the
|
|
|
|
// resolver.
|
|
|
|
response.ParameterMismatch = err != nil
|
|
|
|
if response.ParameterMismatch {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
httpapi.Write(ctx, rw, http.StatusOK, response)
|
|
|
|
}
|
|
|
|
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Summary Watch workspace by ID
|
2023-01-13 11:27:21 +00:00
|
|
|
// @ID watch-workspace-by-id
|
2022-12-19 17:43:46 +00:00
|
|
|
// @Security CoderSessionToken
|
|
|
|
// @Produce text/event-stream
|
|
|
|
// @Tags Workspaces
|
|
|
|
// @Param workspace path string true "Workspace ID" format(uuid)
|
|
|
|
// @Success 200 {object} codersdk.Response
|
|
|
|
// @Router /workspaces/{workspace}/watch [get]
|
2022-05-26 03:14:08 +00:00
|
|
|
func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
|
2022-09-21 22:07:00 +00:00
|
|
|
ctx := r.Context()
|
2022-05-18 21:16:26 +00:00
|
|
|
workspace := httpmw.WorkspaceParam(r)
|
2024-01-24 13:39:19 +00:00
|
|
|
apiKey := httpmw.APIKey(r)
|
2022-05-18 21:16:26 +00:00
|
|
|
|
2022-11-01 14:57:38 +00:00
|
|
|
sendEvent, senderClosed, err := httpapi.ServerSentEventSender(rw, r)
|
2022-05-18 21:16:26 +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 setting up server-sent events.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
})
|
2022-05-18 21:16:26 +00:00
|
|
|
return
|
|
|
|
}
|
2022-11-01 14:57:38 +00:00
|
|
|
// Prevent handler from returning until the sender is closed.
|
|
|
|
defer func() {
|
|
|
|
<-senderClosed
|
|
|
|
}()
|
2022-05-18 21:16:26 +00:00
|
|
|
|
2024-02-14 17:14:49 +00:00
|
|
|
sendUpdate := func(_ context.Context, _ []byte) {
|
2022-11-07 15:25:18 +00:00
|
|
|
workspace, err := api.Database.GetWorkspaceByID(ctx, workspace.ID)
|
|
|
|
if err != nil {
|
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypeError,
|
|
|
|
Data: codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspace.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
data, err := api.workspaceData(ctx, []database.Workspace{workspace})
|
|
|
|
if err != nil {
|
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypeError,
|
|
|
|
Data: codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspace data.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2023-08-17 18:22:03 +00:00
|
|
|
if len(data.templates) == 0 {
|
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypeError,
|
|
|
|
Data: codersdk.Response{
|
|
|
|
Message: "Forbidden reading template of selected workspace.",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-11-07 15:25:18 +00:00
|
|
|
|
2024-01-30 17:07:06 +00:00
|
|
|
owner, ok := userByID(workspace.OwnerID, data.users)
|
2023-10-10 12:55:28 +00:00
|
|
|
if !ok {
|
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypeError,
|
|
|
|
Data: codersdk.Response{
|
|
|
|
Message: "Internal error fetching workspace resources.",
|
|
|
|
Detail: "unable to find workspace owner's username",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2024-01-24 14:13:14 +00:00
|
|
|
|
|
|
|
w, err := convertWorkspace(
|
|
|
|
apiKey.UserID,
|
|
|
|
workspace,
|
|
|
|
data.builds[0],
|
|
|
|
data.templates[0],
|
2024-01-30 17:07:06 +00:00
|
|
|
owner.Username,
|
|
|
|
owner.AvatarURL,
|
2024-01-24 14:13:14 +00:00
|
|
|
api.Options.AllowWorkspaceRenames,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypeError,
|
|
|
|
Data: codersdk.Response{
|
|
|
|
Message: "Internal error converting workspace.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2022-11-07 15:25:18 +00:00
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypeData,
|
2024-01-24 14:13:14 +00:00
|
|
|
Data: w,
|
2022-11-07 15:25:18 +00:00
|
|
|
})
|
2022-11-09 17:01:34 +00:00
|
|
|
}
|
|
|
|
|
2023-07-14 23:07:48 +00:00
|
|
|
cancelWorkspaceSubscribe, err := api.Pubsub.Subscribe(codersdk.WorkspaceNotifyChannel(workspace.ID), sendUpdate)
|
2022-11-07 15:25:18 +00:00
|
|
|
if err != nil {
|
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypeError,
|
|
|
|
Data: codersdk.Response{
|
|
|
|
Message: "Internal error subscribing to workspace events.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-11-09 17:01:34 +00:00
|
|
|
defer cancelWorkspaceSubscribe()
|
|
|
|
|
|
|
|
// This is required to show whether the workspace is up-to-date.
|
|
|
|
cancelTemplateSubscribe, err := api.Pubsub.Subscribe(watchTemplateChannel(workspace.TemplateID), sendUpdate)
|
|
|
|
if err != nil {
|
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypeError,
|
|
|
|
Data: codersdk.Response{
|
|
|
|
Message: "Internal error subscribing to template events.",
|
|
|
|
Detail: err.Error(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer cancelTemplateSubscribe()
|
2022-11-07 15:25:18 +00:00
|
|
|
|
|
|
|
// An initial ping signals to the request that the server is now ready
|
|
|
|
// and the client can begin servicing a channel with data.
|
|
|
|
_ = sendEvent(ctx, codersdk.ServerSentEvent{
|
|
|
|
Type: codersdk.ServerSentEventTypePing,
|
|
|
|
})
|
2023-08-28 18:14:17 +00:00
|
|
|
// Send updated workspace info after connection is established. This avoids
|
|
|
|
// missing updates if the client connects after an update.
|
|
|
|
sendUpdate(ctx, nil)
|
2022-11-07 15:25:18 +00:00
|
|
|
|
2022-05-18 21:16:26 +00:00
|
|
|
for {
|
|
|
|
select {
|
2022-09-21 22:07:00 +00:00
|
|
|
case <-ctx.Done():
|
2022-09-16 18:54:23 +00:00
|
|
|
return
|
2022-11-01 14:57:38 +00:00
|
|
|
case <-senderClosed:
|
|
|
|
return
|
2022-05-18 21:16:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-16 18:54:23 +00:00
|
|
|
type workspaceData struct {
|
2023-12-15 18:38:47 +00:00
|
|
|
templates []database.Template
|
|
|
|
builds []codersdk.WorkspaceBuild
|
|
|
|
users []database.User
|
|
|
|
allowRenames bool
|
2022-09-16 18:54:23 +00:00
|
|
|
}
|
|
|
|
|
2023-08-17 18:22:03 +00:00
|
|
|
// workspacesData only returns the data the caller can access. If the caller
|
|
|
|
// does not have the correct perms to read a given template, the template will
|
|
|
|
// not be returned.
|
|
|
|
// So the caller must check the templates & users exist before using them.
|
2022-09-16 18:54:23 +00:00
|
|
|
func (api *API) workspaceData(ctx context.Context, workspaces []database.Workspace) (workspaceData, error) {
|
2022-04-25 21:11:03 +00:00
|
|
|
workspaceIDs := make([]uuid.UUID, 0, len(workspaces))
|
|
|
|
templateIDs := make([]uuid.UUID, 0, len(workspaces))
|
|
|
|
for _, workspace := range workspaces {
|
|
|
|
workspaceIDs = append(workspaceIDs, workspace.ID)
|
|
|
|
templateIDs = append(templateIDs, workspace.TemplateID)
|
2022-06-09 01:23:35 +00:00
|
|
|
}
|
|
|
|
|
2022-09-16 18:54:23 +00:00
|
|
|
templates, err := api.Database.GetTemplatesWithFilter(ctx, database.GetTemplatesWithFilterParams{
|
2022-09-12 23:24:20 +00:00
|
|
|
IDs: templateIDs,
|
2022-06-14 13:46:33 +00:00
|
|
|
})
|
2022-09-16 18:54:23 +00:00
|
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return workspaceData{}, xerrors.Errorf("get templates: %w", err)
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
2022-09-16 18:54:23 +00:00
|
|
|
|
2023-08-01 17:26:22 +00:00
|
|
|
// This query must be run as system restricted to be efficient.
|
|
|
|
// nolint:gocritic
|
|
|
|
builds, err := api.Database.GetLatestWorkspaceBuildsByWorkspaceIDs(dbauthz.AsSystemRestricted(ctx), workspaceIDs)
|
2022-09-16 18:54:23 +00:00
|
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
|
|
return workspaceData{}, xerrors.Errorf("get workspace builds: %w", err)
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
2022-09-16 18:54:23 +00:00
|
|
|
|
|
|
|
data, err := api.workspaceBuildsData(ctx, workspaces, builds)
|
2022-05-14 01:41:21 +00:00
|
|
|
if err != nil {
|
2022-09-16 18:54:23 +00:00
|
|
|
return workspaceData{}, xerrors.Errorf("get workspace builds data: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
apiBuilds, err := api.convertWorkspaceBuilds(
|
|
|
|
builds,
|
|
|
|
workspaces,
|
|
|
|
data.jobs,
|
|
|
|
data.users,
|
|
|
|
data.resources,
|
|
|
|
data.metadata,
|
|
|
|
data.agents,
|
|
|
|
data.apps,
|
2023-09-25 21:47:17 +00:00
|
|
|
data.scripts,
|
|
|
|
data.logSources,
|
2022-11-28 19:53:56 +00:00
|
|
|
data.templateVersions,
|
2022-09-16 18:54:23 +00:00
|
|
|
)
|
2022-04-25 21:11:03 +00:00
|
|
|
if err != nil {
|
2022-09-16 18:54:23 +00:00
|
|
|
return workspaceData{}, xerrors.Errorf("convert workspace builds: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return workspaceData{
|
2023-12-15 18:38:47 +00:00
|
|
|
templates: templates,
|
|
|
|
builds: apiBuilds,
|
|
|
|
users: data.users,
|
|
|
|
allowRenames: api.Options.AllowWorkspaceRenames,
|
2022-09-16 18:54:23 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2024-01-24 13:39:19 +00:00
|
|
|
func convertWorkspaces(requesterID uuid.UUID, workspaces []database.Workspace, data workspaceData) ([]codersdk.Workspace, error) {
|
2022-09-16 18:54:23 +00:00
|
|
|
buildByWorkspaceID := map[uuid.UUID]codersdk.WorkspaceBuild{}
|
|
|
|
for _, workspaceBuild := range data.builds {
|
|
|
|
buildByWorkspaceID[workspaceBuild.WorkspaceID] = workspaceBuild
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
|
|
|
templateByID := map[uuid.UUID]database.Template{}
|
2022-09-16 18:54:23 +00:00
|
|
|
for _, template := range data.templates {
|
2022-04-25 21:11:03 +00:00
|
|
|
templateByID[template.ID] = template
|
|
|
|
}
|
2022-05-14 01:41:21 +00:00
|
|
|
userByID := map[uuid.UUID]database.User{}
|
2022-09-16 18:54:23 +00:00
|
|
|
for _, user := range data.users {
|
2022-05-14 01:41:21 +00:00
|
|
|
userByID[user.ID] = user
|
|
|
|
}
|
2022-09-16 18:54:23 +00:00
|
|
|
|
2022-04-25 21:11:03 +00:00
|
|
|
apiWorkspaces := make([]codersdk.Workspace, 0, len(workspaces))
|
|
|
|
for _, workspace := range workspaces {
|
2023-08-17 18:22:03 +00:00
|
|
|
// If any data is missing from the workspace, just skip returning
|
|
|
|
// this workspace. This is not ideal, but the user cannot read
|
|
|
|
// all the workspace's data, so do not show them.
|
|
|
|
// Ideally we could just return some sort of "unknown" for the missing
|
|
|
|
// fields?
|
2022-04-25 21:11:03 +00:00
|
|
|
build, exists := buildByWorkspaceID[workspace.ID]
|
|
|
|
if !exists {
|
2023-08-17 18:22:03 +00:00
|
|
|
continue
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
|
|
|
template, exists := templateByID[workspace.TemplateID]
|
|
|
|
if !exists {
|
2023-08-17 18:22:03 +00:00
|
|
|
continue
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
2022-06-09 01:23:35 +00:00
|
|
|
owner, exists := userByID[workspace.OwnerID]
|
2022-05-14 01:41:21 +00:00
|
|
|
if !exists {
|
2023-08-17 18:22:03 +00:00
|
|
|
continue
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
2022-09-16 18:54:23 +00:00
|
|
|
|
2024-01-24 14:13:14 +00:00
|
|
|
w, err := convertWorkspace(
|
2024-01-24 13:39:19 +00:00
|
|
|
requesterID,
|
2022-09-16 18:54:23 +00:00
|
|
|
workspace,
|
|
|
|
build,
|
|
|
|
template,
|
2023-10-10 12:55:28 +00:00
|
|
|
owner.Username,
|
2024-01-30 17:07:06 +00:00
|
|
|
owner.AvatarURL,
|
2023-12-15 18:38:47 +00:00
|
|
|
data.allowRenames,
|
2024-01-24 14:13:14 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, xerrors.Errorf("convert workspace: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
apiWorkspaces = append(apiWorkspaces, w)
|
2022-04-25 21:11:03 +00:00
|
|
|
}
|
2023-05-22 20:51:29 +00:00
|
|
|
return apiWorkspaces, nil
|
|
|
|
}
|
|
|
|
|
2022-06-01 23:49:43 +00:00
|
|
|
func convertWorkspace(
|
2024-01-24 13:39:19 +00:00
|
|
|
requesterID uuid.UUID,
|
2022-06-01 23:49:43 +00:00
|
|
|
workspace database.Workspace,
|
2022-09-16 18:54:23 +00:00
|
|
|
workspaceBuild codersdk.WorkspaceBuild,
|
2022-06-01 23:49:43 +00:00
|
|
|
template database.Template,
|
2024-01-30 17:07:06 +00:00
|
|
|
username string,
|
|
|
|
avatarURL string,
|
2023-12-15 18:38:47 +00:00
|
|
|
allowRenames bool,
|
2024-01-24 14:13:14 +00:00
|
|
|
) (codersdk.Workspace, error) {
|
|
|
|
if requesterID == uuid.Nil {
|
|
|
|
return codersdk.Workspace{}, xerrors.Errorf("developer error: requesterID cannot be uuid.Nil!")
|
|
|
|
}
|
2022-06-02 10:23:34 +00:00
|
|
|
var autostartSchedule *string
|
|
|
|
if workspace.AutostartSchedule.Valid {
|
|
|
|
autostartSchedule = &workspace.AutostartSchedule.String
|
|
|
|
}
|
|
|
|
|
2023-08-24 18:25:54 +00:00
|
|
|
var dormantAt *time.Time
|
|
|
|
if workspace.DormantAt.Valid {
|
|
|
|
dormantAt = &workspace.DormantAt.Time
|
2023-06-28 21:12:49 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 18:25:54 +00:00
|
|
|
var deletingAt *time.Time
|
2023-07-21 03:01:11 +00:00
|
|
|
if workspace.DeletingAt.Valid {
|
2023-08-24 18:25:54 +00:00
|
|
|
deletingAt = &workspace.DeletingAt.Time
|
2023-07-21 03:01:11 +00:00
|
|
|
}
|
|
|
|
|
2023-07-10 09:40:11 +00:00
|
|
|
failingAgents := []uuid.UUID{}
|
|
|
|
for _, resource := range workspaceBuild.Resources {
|
|
|
|
for _, agent := range resource.Agents {
|
|
|
|
if !agent.Health.Healthy {
|
|
|
|
failingAgents = append(failingAgents, agent.ID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-21 03:01:11 +00:00
|
|
|
ttlMillis := convertWorkspaceTTLMillis(workspace.Ttl)
|
2024-04-11 19:08:51 +00:00
|
|
|
// If the template doesn't allow a workspace-configured value, then report the
|
|
|
|
// template value instead.
|
|
|
|
if !template.AllowUserAutostop {
|
|
|
|
ttlMillis = convertWorkspaceTTLMillis(sql.NullInt64{Valid: true, Int64: template.DefaultTTL})
|
|
|
|
}
|
2023-06-28 21:12:49 +00:00
|
|
|
|
2024-01-24 13:39:19 +00:00
|
|
|
// Only show favorite status if you own the workspace.
|
|
|
|
requesterFavorite := workspace.OwnerID == requesterID && workspace.Favorite
|
|
|
|
|
2022-03-22 19:17:50 +00:00
|
|
|
return codersdk.Workspace{
|
2022-11-21 10:43:53 +00:00
|
|
|
ID: workspace.ID,
|
|
|
|
CreatedAt: workspace.CreatedAt,
|
|
|
|
UpdatedAt: workspace.UpdatedAt,
|
|
|
|
OwnerID: workspace.OwnerID,
|
2024-01-30 17:07:06 +00:00
|
|
|
OwnerName: username,
|
|
|
|
OwnerAvatarURL: avatarURL,
|
2023-03-21 14:10:22 +00:00
|
|
|
OrganizationID: workspace.OrganizationID,
|
2022-11-21 10:43:53 +00:00
|
|
|
TemplateID: workspace.TemplateID,
|
|
|
|
LatestBuild: workspaceBuild,
|
|
|
|
TemplateName: template.Name,
|
|
|
|
TemplateIcon: template.Icon,
|
|
|
|
TemplateDisplayName: template.DisplayName,
|
|
|
|
TemplateAllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
2023-08-27 16:26:20 +00:00
|
|
|
TemplateActiveVersionID: template.ActiveVersionID,
|
2023-10-19 23:21:52 +00:00
|
|
|
TemplateRequireActiveVersion: template.RequireActiveVersion,
|
2022-11-21 10:43:53 +00:00
|
|
|
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
|
|
|
|
Name: workspace.Name,
|
|
|
|
AutostartSchedule: autostartSchedule,
|
|
|
|
TTLMillis: ttlMillis,
|
|
|
|
LastUsedAt: workspace.LastUsedAt,
|
2023-08-24 18:25:54 +00:00
|
|
|
DeletingAt: deletingAt,
|
|
|
|
DormantAt: dormantAt,
|
2023-07-10 09:40:11 +00:00
|
|
|
Health: codersdk.WorkspaceHealth{
|
|
|
|
Healthy: len(failingAgents) == 0,
|
|
|
|
FailingAgents: failingAgents,
|
|
|
|
},
|
2023-10-06 09:27:12 +00:00
|
|
|
AutomaticUpdates: codersdk.AutomaticUpdates(workspace.AutomaticUpdates),
|
2023-12-15 18:38:47 +00:00
|
|
|
AllowRenames: allowRenames,
|
2024-01-24 13:39:19 +00:00
|
|
|
Favorite: requesterFavorite,
|
2024-01-24 14:13:14 +00:00
|
|
|
}, nil
|
2022-01-25 19:52:58 +00:00
|
|
|
}
|
2022-05-19 19:09:27 +00:00
|
|
|
|
2022-06-02 10:23:34 +00:00
|
|
|
func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 {
|
2022-05-19 19:09:27 +00:00
|
|
|
if !i.Valid {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-02 10:23:34 +00:00
|
|
|
millis := time.Duration(i.Int64).Milliseconds()
|
|
|
|
return &millis
|
2022-05-19 19:09:27 +00:00
|
|
|
}
|
2022-05-27 19:04:33 +00:00
|
|
|
|
2024-03-20 15:37:57 +00:00
|
|
|
func validWorkspaceTTLMillis(millis *int64, templateDefault time.Duration) (sql.NullInt64, error) {
|
2022-06-02 10:23:34 +00:00
|
|
|
if ptr.NilOrZero(millis) {
|
2023-03-07 14:14:58 +00:00
|
|
|
if templateDefault == 0 {
|
2022-11-09 19:36:25 +00:00
|
|
|
return sql.NullInt64{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return sql.NullInt64{
|
2023-03-07 14:14:58 +00:00
|
|
|
Int64: int64(templateDefault),
|
2022-11-09 19:36:25 +00:00
|
|
|
Valid: true,
|
|
|
|
}, nil
|
2022-05-30 19:19:17 +00:00
|
|
|
}
|
|
|
|
|
2022-06-02 10:23:34 +00:00
|
|
|
dur := time.Duration(*millis) * time.Millisecond
|
|
|
|
truncated := dur.Truncate(time.Minute)
|
2022-07-29 14:01:17 +00:00
|
|
|
if truncated < ttlMin {
|
|
|
|
return sql.NullInt64{}, errTTLMin
|
2022-05-30 19:19:17 +00:00
|
|
|
}
|
|
|
|
|
2022-07-29 14:01:17 +00:00
|
|
|
if truncated > ttlMax {
|
|
|
|
return sql.NullInt64{}, errTTLMax
|
2022-05-30 19:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return sql.NullInt64{
|
|
|
|
Valid: true,
|
|
|
|
Int64: int64(truncated),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-10-06 09:27:12 +00:00
|
|
|
func validWorkspaceAutomaticUpdates(updates codersdk.AutomaticUpdates) (database.AutomaticUpdates, error) {
|
|
|
|
if updates == "" {
|
|
|
|
return database.AutomaticUpdatesNever, nil
|
|
|
|
}
|
|
|
|
dbAU := database.AutomaticUpdates(updates)
|
|
|
|
if !dbAU.Valid() {
|
|
|
|
return "", xerrors.New("Automatic updates must be always or never")
|
|
|
|
}
|
|
|
|
return dbAU, nil
|
|
|
|
}
|
|
|
|
|
2022-11-09 19:36:25 +00:00
|
|
|
func validWorkspaceDeadline(startedAt, newDeadline time.Time) error {
|
2022-06-14 21:39:15 +00:00
|
|
|
soon := time.Now().Add(29 * time.Minute)
|
|
|
|
if newDeadline.Before(soon) {
|
2022-07-29 14:01:17 +00:00
|
|
|
return errDeadlineTooSoon
|
2022-05-30 19:19:17 +00:00
|
|
|
}
|
|
|
|
|
2022-06-14 21:39:15 +00:00
|
|
|
// No idea how this could happen.
|
|
|
|
if newDeadline.Before(startedAt) {
|
2022-07-29 14:01:17 +00:00
|
|
|
return errDeadlineBeforeStart
|
2022-05-30 19:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2022-05-27 19:04:33 +00:00
|
|
|
}
|
2022-06-02 10:23:34 +00:00
|
|
|
|
2022-11-09 19:36:25 +00:00
|
|
|
func validWorkspaceSchedule(s *string) (sql.NullString, error) {
|
2022-06-02 10:23:34 +00:00
|
|
|
if ptr.NilOrEmpty(s) {
|
|
|
|
return sql.NullString{}, nil
|
|
|
|
}
|
|
|
|
|
2023-09-04 13:48:25 +00:00
|
|
|
_, err := cron.Weekly(*s)
|
2022-06-02 10:23:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return sql.NullString{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return sql.NullString{
|
|
|
|
Valid: true,
|
|
|
|
String: *s,
|
|
|
|
}, nil
|
|
|
|
}
|
2022-06-14 13:46:33 +00:00
|
|
|
|
2022-11-07 15:25:18 +00:00
|
|
|
func (api *API) publishWorkspaceUpdate(ctx context.Context, workspaceID uuid.UUID) {
|
2023-07-14 23:07:48 +00:00
|
|
|
err := api.Pubsub.Publish(codersdk.WorkspaceNotifyChannel(workspaceID), []byte{})
|
2022-11-07 15:25:18 +00:00
|
|
|
if err != nil {
|
|
|
|
api.Logger.Warn(ctx, "failed to publish workspace update",
|
|
|
|
slog.F("workspace_id", workspaceID), slog.Error(err))
|
|
|
|
}
|
|
|
|
}
|
2023-06-16 14:14:22 +00:00
|
|
|
|
2023-07-28 15:57:23 +00:00
|
|
|
func (api *API) publishWorkspaceAgentLogsUpdate(ctx context.Context, workspaceAgentID uuid.UUID, m agentsdk.LogsNotifyMessage) {
|
2023-06-16 14:14:22 +00:00
|
|
|
b, err := json.Marshal(m)
|
|
|
|
if err != nil {
|
2023-07-28 15:57:23 +00:00
|
|
|
api.Logger.Warn(ctx, "failed to marshal logs notify message", slog.F("workspace_agent_id", workspaceAgentID), slog.Error(err))
|
2023-06-16 14:14:22 +00:00
|
|
|
}
|
2023-07-28 15:57:23 +00:00
|
|
|
err = api.Pubsub.Publish(agentsdk.LogsNotifyChannel(workspaceAgentID), b)
|
2023-06-16 14:14:22 +00:00
|
|
|
if err != nil {
|
2023-07-28 15:57:23 +00:00
|
|
|
api.Logger.Warn(ctx, "failed to publish workspace agent logs update", slog.F("workspace_agent_id", workspaceAgentID), slog.Error(err))
|
2023-06-16 14:14:22 +00:00
|
|
|
}
|
|
|
|
}
|