From 912bea7604c9ddde7232bcee7573df2e6bd2e418 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Mon, 29 Jul 2024 19:07:29 +0000 Subject: [PATCH 1/8] aaaaahhhaaaa --- coderd/apidoc/docs.go | 48 +++++++ coderd/apidoc/swagger.json | 42 ++++++ coderd/audit/request.go | 4 + coderd/coderd.go | 1 + coderd/workspaces.go | 122 +++++++++++++----- docs/api/workspaces.md | 13 +- site/src/api/api.ts | 3 +- site/src/api/queries/templates.ts | 10 +- site/src/api/queries/workspaces.ts | 7 +- .../CreateWorkspacePage.tsx | 7 +- 10 files changed, 204 insertions(+), 53 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 487aac8f7fb76..81a3233c11ae2 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2593,6 +2593,7 @@ const docTemplate = `{ ], "summary": "Create user workspace by organization", "operationId": "create-user-workspace-by-organization", + "deprecated": true, "parameters": [ { "type": "string", @@ -5845,6 +5846,53 @@ const docTemplate = `{ } } }, + "/users/{user}/workspaces": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "description": "Create a new workspace using a template. The request must\nspecify either the Template ID or the Template Version ID,\nnot both. If the Template ID is specified, the active version\nof the template will be used.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Workspaces" + ], + "summary": "Create user workspace by organization", + "operationId": "create-user-workspace", + "parameters": [ + { + "type": "string", + "description": "Username, UUID, or me", + "name": "user", + "in": "path", + "required": true + }, + { + "description": "Create workspace request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.CreateWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Workspace" + } + } + } + } + }, "/workspace-quota/{user}": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index be72fcb8d03ac..8b762cddc27d4 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2267,6 +2267,7 @@ "tags": ["Workspaces"], "summary": "Create user workspace by organization", "operationId": "create-user-workspace-by-organization", + "deprecated": true, "parameters": [ { "type": "string", @@ -5163,6 +5164,47 @@ } } }, + "/users/{user}/workspaces": { + "post": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "description": "Create a new workspace using a template. The request must\nspecify either the Template ID or the Template Version ID,\nnot both. If the Template ID is specified, the active version\nof the template will be used.", + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Workspaces"], + "summary": "Create user workspace by organization", + "operationId": "create-user-workspace", + "parameters": [ + { + "type": "string", + "description": "Username, UUID, or me", + "name": "user", + "in": "path", + "required": true + }, + { + "description": "Create workspace request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.CreateWorkspaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Workspace" + } + } + } + } + }, "/workspace-quota/{user}": { "get": { "security": [ diff --git a/coderd/audit/request.go b/coderd/audit/request.go index eecb0b81d4d8f..92b0a8a8accc2 100644 --- a/coderd/audit/request.go +++ b/coderd/audit/request.go @@ -51,6 +51,10 @@ type Request[T Auditable] struct { Action database.AuditAction } +func (r *Request[T]) UpdateOrganizationID(id uuid.UUID) { + r.params.OrganizationID = id +} + type BackgroundAuditParams[T Auditable] struct { Audit Auditor Log slog.Logger diff --git a/coderd/coderd.go b/coderd/coderd.go index a62cdae08cc49..6f8a59ad6efc6 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1043,6 +1043,7 @@ func New(options *Options) *API { r.Get("/", api.organizationsByUser) r.Get("/{organizationname}", api.organizationByUserAndName) }) + r.Post("/workspaces", api.postUserWorkspaces) r.Route("/workspace/{workspacename}", func(r chi.Router) { r.Get("/", api.workspaceByOwnerAndName) r.Get("/builds/{buildnumber}", api.workspaceBuildByBuildNumber) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index ceba543639cc3..c5f6260ec1033 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -340,6 +340,7 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) // @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. +// @Deprecated // @ID create-user-workspace-by-organization // @Security CoderSessionToken // @Accept json @@ -353,9 +354,9 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request) func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() - organization = httpmw.OrganizationParam(r) apiKey = httpmw.APIKey(r) auditor = api.Auditor.Load() + organization = httpmw.OrganizationParam(r) member = httpmw.OrganizationMemberParam(r) workspaceResourceInfo = audit.AdditionalFields{ WorkspaceOwner: member.Username, @@ -380,15 +381,78 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return } - var createWorkspace codersdk.CreateWorkspaceRequest - if !httpapi.Read(ctx, rw, r, &createWorkspace) { + var req codersdk.CreateWorkspaceRequest + if !httpapi.Read(ctx, rw, r, &req) { return } + user := database.User{ + ID: member.UserID, + Username: member.Username, + AvatarURL: member.AvatarURL, + } + + createWorkspace(ctx, aReq, apiKey.UserID, api, user, req, rw, r) +} + +// Create a new workspace for the currently authenticated user. +// +// @Summary Create user workspace by organization +// @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. +// @ID create-user-workspace +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Workspaces +// @Param user path string true "Username, UUID, or me" +// @Param request body codersdk.CreateWorkspaceRequest true "Create workspace request" +// @Success 200 {object} codersdk.Workspace +// @Router /users/{user}/workspaces [post] +func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + apiKey = httpmw.APIKey(r) + auditor = api.Auditor.Load() + user = httpmw.UserParam(r) + ) + + aReq, commitAudit := audit.InitRequest[database.Workspace](rw, &audit.RequestParams{ + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionCreate, + AdditionalFields: audit.AdditionalFields{ + WorkspaceOwner: user.Username, + }, + }) + + defer commitAudit() + + var req codersdk.CreateWorkspaceRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + createWorkspace(ctx, aReq, apiKey.UserID, api, user, req, rw, r) +} + +func createWorkspace( + ctx context.Context, + auditReq *audit.Request[database.Workspace], + initiatorID uuid.UUID, + api *API, + user database.User, + req codersdk.CreateWorkspaceRequest, + rw http.ResponseWriter, + r *http.Request, +) { // If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it. - templateID := createWorkspace.TemplateID + templateID := req.TemplateID if templateID == uuid.Nil { - templateVersion, err := api.Database.GetTemplateVersionByID(ctx, createWorkspace.TemplateVersionID) + templateVersion, err := api.Database.GetTemplateVersionByID(ctx, req.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()), @@ -446,6 +510,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req }) return } + auditReq.UpdateOrganizationID(template.OrganizationID) templateAccessControl := (*(api.AccessControlStore.Load())).GetTemplateAccessControl(template) if templateAccessControl.IsDeprecated() { @@ -458,14 +523,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return } - if organization.ID != template.OrganizationID { - httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ - Message: fmt.Sprintf("Template is not in organization %q.", organization.Name), - }) - return - } - - dbAutostartSchedule, err := validWorkspaceSchedule(createWorkspace.AutostartSchedule) + dbAutostartSchedule, err := validWorkspaceSchedule(req.AutostartSchedule) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid Autostart Schedule.", @@ -483,7 +541,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return } - dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis, templateSchedule.DefaultTTL) + dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis, templateSchedule.DefaultTTL) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid Workspace Time to Shutdown.", @@ -494,8 +552,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req // back-compatibility: default to "never" if not included. dbAU := database.AutomaticUpdatesNever - if createWorkspace.AutomaticUpdates != "" { - dbAU, err = validWorkspaceAutomaticUpdates(createWorkspace.AutomaticUpdates) + if req.AutomaticUpdates != "" { + dbAU, err = validWorkspaceAutomaticUpdates(req.AutomaticUpdates) if err != nil { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "Invalid Workspace Automatic Updates setting.", @@ -509,13 +567,13 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req // read other workspaces. Ideally we check the error on create and look for // a postgres conflict error. workspace, err := api.Database.GetWorkspaceByOwnerIDAndName(ctx, database.GetWorkspaceByOwnerIDAndNameParams{ - OwnerID: member.UserID, - Name: createWorkspace.Name, + OwnerID: user.ID, + Name: req.Name, }) if err == nil { // If the workspace already exists, don't allow creation. httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - Message: fmt.Sprintf("Workspace %q already exists.", createWorkspace.Name), + 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.", @@ -525,7 +583,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req } if err != nil && !errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: fmt.Sprintf("Internal error fetching workspace by name %q.", createWorkspace.Name), + Message: fmt.Sprintf("Internal error fetching workspace by name %q.", req.Name), Detail: err.Error(), }) return @@ -542,10 +600,10 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req ID: uuid.New(), CreatedAt: now, UpdatedAt: now, - OwnerID: member.UserID, + OwnerID: user.ID, OrganizationID: template.OrganizationID, TemplateID: template.ID, - Name: createWorkspace.Name, + Name: req.Name, AutostartSchedule: dbAutostartSchedule, Ttl: dbTTL, // The workspaces page will sort by last used at, and it's useful to @@ -559,11 +617,11 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req builder := wsbuilder.New(workspace, database.WorkspaceTransitionStart). Reason(database.BuildReasonInitiator). - Initiator(apiKey.UserID). + Initiator(initiatorID). ActiveVersion(). - RichParameterValues(createWorkspace.RichParameterValues) - if createWorkspace.TemplateVersionID != uuid.Nil { - builder = builder.VersionID(createWorkspace.TemplateVersionID) + RichParameterValues(req.RichParameterValues) + if req.TemplateVersionID != uuid.Nil { + builder = builder.VersionID(req.TemplateVersionID) } workspaceBuild, provisionerJob, err = builder.Build( @@ -596,7 +654,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req // 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)) } - aReq.New = workspace + auditReq.New = workspace api.Telemetry.Report(&telemetry.Snapshot{ Workspaces: []telemetry.Workspace{telemetry.ConvertWorkspace(workspace)}, @@ -610,8 +668,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req ProvisionerJob: *provisionerJob, QueuePosition: 0, }, - member.Username, - member.AvatarURL, + user.Username, + user.AvatarURL, []database.WorkspaceResource{}, []database.WorkspaceResourceMetadatum{}, []database.WorkspaceAgent{}, @@ -629,12 +687,12 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req } w, err := convertWorkspace( - apiKey.UserID, + initiatorID, workspace, apiBuild, template, - member.Username, - member.AvatarURL, + user.Username, + user.AvatarURL, api.Options.AllowWorkspaceRenames, ) if err != nil { diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index ddaa70c9df292..d0355f731691b 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -6,13 +6,13 @@ ```shell # Example request using curl -curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/members/{user}/workspaces \ +curl -X POST http://coder-server:8080/api/v2/users/{user}/workspaces \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` -`POST /organizations/{organization}/members/{user}/workspaces` +`POST /users/{user}/workspaces` Create a new workspace using a template. The request must specify either the Template ID or the Template Version ID, @@ -40,11 +40,10 @@ of the template will be used. ### Parameters -| Name | In | Type | Required | Description | -| -------------- | ---- | ---------------------------------------------------------------------------- | -------- | ------------------------ | -| `organization` | path | string(uuid) | true | Organization ID | -| `user` | path | string | true | Username, UUID, or me | -| `body` | body | [codersdk.CreateWorkspaceRequest](schemas.md#codersdkcreateworkspacerequest) | true | Create workspace request | +| Name | In | Type | Required | Description | +| ------ | ---- | ---------------------------------------------------------------------------- | -------- | ------------------------ | +| `user` | path | string | true | Username, UUID, or me | +| `body` | body | [codersdk.CreateWorkspaceRequest](schemas.md#codersdkcreateworkspacerequest) | true | Create workspace request | ### Example responses diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ca006a3a16997..a98ce16afc61b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1019,12 +1019,11 @@ class ApiMethods { }; createWorkspace = async ( - organizationId: string, userId = "me", workspace: TypesGen.CreateWorkspaceRequest, ): Promise => { const response = await this.axios.post( - `/api/v2/organizations/${organizationId}/members/${userId}/workspaces`, + `/api/v2/users/${userId}/workspaces`, workspace, ); diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 2d0485b8f347b..605ef54e0b35b 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -13,20 +13,20 @@ import type { import { delay } from "utils/delay"; import { getTemplateVersionFiles } from "utils/templateVersion"; -export const templateByNameKey = (organizationId: string, name: string) => [ - organizationId, +export const templateByNameKey = (organization: string, name: string) => [ + organization, "template", name, "settings", ]; export const templateByName = ( - organizationId: string, + organization: string, name: string, ): QueryOptions