diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 942bdd3dbe923..458d5f670611d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7715,8 +7715,7 @@ const docTemplate = `{ "codersdk.CreateWorkspaceRequest": { "type": "object", "required": [ - "name", - "template_id" + "name" ], "properties": { "autostart_schedule": { @@ -7726,13 +7725,19 @@ const docTemplate = `{ "type": "string" }, "rich_parameter_values": { - "description": "ParameterValues allows for additional parameters to be provided\nduring the initial provision.", + "description": "RichParameterValues allows for additional parameters to be provided\nduring the initial provision.", "type": "array", "items": { "$ref": "#/definitions/codersdk.WorkspaceBuildParameter" } }, "template_id": { + "description": "TemplateID specifies which template should be used for creating the workspace.", + "type": "string", + "format": "uuid" + }, + "template_version_id": { + "description": "TemplateVersionID can be used to specify a specific version of a template for creating the workspace.", "type": "string", "format": "uuid" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e867976f0091b..2baca8a6510f9 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6872,7 +6872,7 @@ }, "codersdk.CreateWorkspaceRequest": { "type": "object", - "required": ["name", "template_id"], + "required": ["name"], "properties": { "autostart_schedule": { "type": "string" @@ -6881,13 +6881,19 @@ "type": "string" }, "rich_parameter_values": { - "description": "ParameterValues allows for additional parameters to be provided\nduring the initial provision.", + "description": "RichParameterValues allows for additional parameters to be provided\nduring the initial provision.", "type": "array", "items": { "$ref": "#/definitions/codersdk.WorkspaceBuildParameter" } }, "template_id": { + "description": "TemplateID specifies which template should be used for creating the workspace.", + "type": "string", + "format": "uuid" + }, + "template_version_id": { + "description": "TemplateVersionID can be used to specify a specific version of a template for creating the workspace.", "type": "string", "format": "uuid" }, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 53e8308b5d65e..41f7ae1c83842 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -333,10 +333,35 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req return } - template, err := api.Database.GetTemplateByID(ctx, createWorkspace.TemplateID) + // 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 + } + + templateID = templateVersion.TemplateID.UUID + } + + template, err := api.Database.GetTemplateByID(ctx, templateID) if errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Template %q doesn't exist.", createWorkspace.TemplateID.String()), + Message: fmt.Sprintf("Template %q doesn't exist.", templateID.String()), Validations: []codersdk.ValidationError{{ Field: "template_id", Detail: "template not found", @@ -454,6 +479,10 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req Initiator(apiKey.UserID). ActiveVersion(). RichParameterValues(createWorkspace.RichParameterValues) + if createWorkspace.TemplateVersionID != uuid.Nil { + builder = builder.VersionID(createWorkspace.TemplateVersionID) + } + workspaceBuild, provisionerJob, err = builder.Build( ctx, db, func(action rbac.Action, object rbac.Objecter) bool { return api.Authorize(r, action, object) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index e3749d72953fe..a32960d6d4daf 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -491,6 +491,62 @@ func TestPostWorkspacesByOrganization(t *testing.T) { }, testutil.WaitMedium, testutil.IntervalFast) }) + t.Run("CreateFromVersion", func(t *testing.T) { + t.Parallel() + auditor := audit.NewMock() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) + user := coderdtest.CreateFirstUser(t, client) + versionDefault := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionDefault.ID) + versionTest := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, template.ID) + coderdtest.AwaitTemplateVersionJob(t, client, versionDefault.ID) + coderdtest.AwaitTemplateVersionJob(t, client, versionTest.ID) + defaultWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, uuid.Nil, + func(c *codersdk.CreateWorkspaceRequest) { c.TemplateVersionID = versionDefault.ID }, + ) + testWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, uuid.Nil, + func(c *codersdk.CreateWorkspaceRequest) { c.TemplateVersionID = versionTest.ID }, + ) + defaultWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, defaultWorkspace.LatestBuild.ID) + testWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, testWorkspace.LatestBuild.ID) + + require.Equal(t, testWorkspaceBuild.TemplateVersionID, versionTest.ID) + require.Equal(t, defaultWorkspaceBuild.TemplateVersionID, versionDefault.ID) + require.Eventually(t, func() bool { + if len(auditor.AuditLogs()) < 6 { + return false + } + return auditor.AuditLogs()[4].Action == database.AuditActionCreate + }, testutil.WaitMedium, testutil.IntervalFast) + }) + + t.Run("InvalidCombinationOfTemplateAndTemplateVersion", func(t *testing.T) { + t.Parallel() + auditor := audit.NewMock() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor}) + user := coderdtest.CreateFirstUser(t, client) + versionTest := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + versionDefault := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionDefault.ID) + coderdtest.AwaitTemplateVersionJob(t, client, versionTest.ID) + coderdtest.AwaitTemplateVersionJob(t, client, versionDefault.ID) + + name, se := cryptorand.String(8) + require.NoError(t, se) + req := codersdk.CreateWorkspaceRequest{ + // Deny setting both of these ID fields, even if they might correlate. + // Allowing both to be set would just create extra work for everyone involved. + TemplateID: template.ID, + TemplateVersionID: versionTest.ID, + Name: name, + AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"), + TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()), + } + _, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.Me, req) + + require.Error(t, err) + }) + t.Run("CreateWithDeletedTemplate", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index c139afdfa2633..aaa052a39d5f3 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -124,12 +124,16 @@ type CreateTemplateRequest struct { } // CreateWorkspaceRequest provides options for creating a new workspace. +// Either TemplateID or TemplateVersionID must be specified. They cannot both be present. type CreateWorkspaceRequest struct { - TemplateID uuid.UUID `json:"template_id" validate:"required" format:"uuid"` + // TemplateID specifies which template should be used for creating the workspace. + TemplateID uuid.UUID `json:"template_id,omitempty" validate:"required_without=TemplateVersionID,excluded_with=TemplateVersionID" format:"uuid"` + // TemplateVersionID can be used to specify a specific version of a template for creating the workspace. + TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" validate:"required_without=TemplateID,excluded_with=TemplateID" format:"uuid"` Name string `json:"name" validate:"workspace_name,required"` AutostartSchedule *string `json:"autostart_schedule"` TTLMillis *int64 `json:"ttl_ms,omitempty"` - // ParameterValues allows for additional parameters to be provided + // RichParameterValues allows for additional parameters to be provided // during the initial provision. RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` } diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b3c7cb7eb4c94..b810d768aeaf3 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1757,19 +1757,21 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in } ], "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "ttl_ms": 0 } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -| ----------------------- | ----------------------------------------------------------------------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------- | -| `autostart_schedule` | string | false | | | -| `name` | string | true | | | -| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values allows for additional parameters to be provided during the initial provision. | -| `template_id` | string | true | | | -| `ttl_ms` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +| ----------------------- | ----------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------- | +| `autostart_schedule` | string | false | | | +| `name` | string | true | | | +| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values allows for additional parameters to be provided during the initial provision. | +| `template_id` | string | false | | Template ID specifies which template should be used for creating the workspace. | +| `template_version_id` | string | false | | Template version ID can be used to specify a specific version of a template for creating the workspace. | +| `ttl_ms` | integer | false | | | ## codersdk.DAUEntry diff --git a/docs/api/workspaces.md b/docs/api/workspaces.md index 3021fbefcff7c..fd9bb1c817a83 100644 --- a/docs/api/workspaces.md +++ b/docs/api/workspaces.md @@ -27,6 +27,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member } ], "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", + "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "ttl_ms": 0 } ``` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 86c268a14fd10..feac78e69983c 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -272,7 +272,8 @@ export interface CreateWorkspaceProxyRequest { // From codersdk/organizations.go export interface CreateWorkspaceRequest { - readonly template_id: string + readonly template_id?: string + readonly template_version_id?: string readonly name: string readonly autostart_schedule?: string readonly ttl_ms?: number