Skip to content

Commit eded7a4

Browse files
authored
feat: create a workspace from any template version (#9471)
1 parent 796a975 commit eded7a4

File tree

8 files changed

+121
-17
lines changed

8 files changed

+121
-17
lines changed

coderd/apidoc/docs.go

+8-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+8-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/workspaces.go

+31-2
Original file line numberDiff line numberDiff line change
@@ -333,10 +333,35 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
333333
return
334334
}
335335

336-
template, err := api.Database.GetTemplateByID(ctx, createWorkspace.TemplateID)
336+
// If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it.
337+
templateID := createWorkspace.TemplateID
338+
if templateID == uuid.Nil {
339+
templateVersion, err := api.Database.GetTemplateVersionByID(ctx, createWorkspace.TemplateVersionID)
340+
if errors.Is(err, sql.ErrNoRows) {
341+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
342+
Message: fmt.Sprintf("Template version %q doesn't exist.", templateID.String()),
343+
Validations: []codersdk.ValidationError{{
344+
Field: "template_version_id",
345+
Detail: "template not found",
346+
}},
347+
})
348+
return
349+
}
350+
if err != nil {
351+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
352+
Message: "Internal error fetching template version.",
353+
Detail: err.Error(),
354+
})
355+
return
356+
}
357+
358+
templateID = templateVersion.TemplateID.UUID
359+
}
360+
361+
template, err := api.Database.GetTemplateByID(ctx, templateID)
337362
if errors.Is(err, sql.ErrNoRows) {
338363
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
339-
Message: fmt.Sprintf("Template %q doesn't exist.", createWorkspace.TemplateID.String()),
364+
Message: fmt.Sprintf("Template %q doesn't exist.", templateID.String()),
340365
Validations: []codersdk.ValidationError{{
341366
Field: "template_id",
342367
Detail: "template not found",
@@ -454,6 +479,10 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
454479
Initiator(apiKey.UserID).
455480
ActiveVersion().
456481
RichParameterValues(createWorkspace.RichParameterValues)
482+
if createWorkspace.TemplateVersionID != uuid.Nil {
483+
builder = builder.VersionID(createWorkspace.TemplateVersionID)
484+
}
485+
457486
workspaceBuild, provisionerJob, err = builder.Build(
458487
ctx, db, func(action rbac.Action, object rbac.Objecter) bool {
459488
return api.Authorize(r, action, object)

coderd/workspaces_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,62 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
491491
}, testutil.WaitMedium, testutil.IntervalFast)
492492
})
493493

494+
t.Run("CreateFromVersion", func(t *testing.T) {
495+
t.Parallel()
496+
auditor := audit.NewMock()
497+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
498+
user := coderdtest.CreateFirstUser(t, client)
499+
versionDefault := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
500+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionDefault.ID)
501+
versionTest := coderdtest.UpdateTemplateVersion(t, client, user.OrganizationID, nil, template.ID)
502+
coderdtest.AwaitTemplateVersionJob(t, client, versionDefault.ID)
503+
coderdtest.AwaitTemplateVersionJob(t, client, versionTest.ID)
504+
defaultWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, uuid.Nil,
505+
func(c *codersdk.CreateWorkspaceRequest) { c.TemplateVersionID = versionDefault.ID },
506+
)
507+
testWorkspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, uuid.Nil,
508+
func(c *codersdk.CreateWorkspaceRequest) { c.TemplateVersionID = versionTest.ID },
509+
)
510+
defaultWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, defaultWorkspace.LatestBuild.ID)
511+
testWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, testWorkspace.LatestBuild.ID)
512+
513+
require.Equal(t, testWorkspaceBuild.TemplateVersionID, versionTest.ID)
514+
require.Equal(t, defaultWorkspaceBuild.TemplateVersionID, versionDefault.ID)
515+
require.Eventually(t, func() bool {
516+
if len(auditor.AuditLogs()) < 6 {
517+
return false
518+
}
519+
return auditor.AuditLogs()[4].Action == database.AuditActionCreate
520+
}, testutil.WaitMedium, testutil.IntervalFast)
521+
})
522+
523+
t.Run("InvalidCombinationOfTemplateAndTemplateVersion", func(t *testing.T) {
524+
t.Parallel()
525+
auditor := audit.NewMock()
526+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
527+
user := coderdtest.CreateFirstUser(t, client)
528+
versionTest := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
529+
versionDefault := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
530+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, versionDefault.ID)
531+
coderdtest.AwaitTemplateVersionJob(t, client, versionTest.ID)
532+
coderdtest.AwaitTemplateVersionJob(t, client, versionDefault.ID)
533+
534+
name, se := cryptorand.String(8)
535+
require.NoError(t, se)
536+
req := codersdk.CreateWorkspaceRequest{
537+
// Deny setting both of these ID fields, even if they might correlate.
538+
// Allowing both to be set would just create extra work for everyone involved.
539+
TemplateID: template.ID,
540+
TemplateVersionID: versionTest.ID,
541+
Name: name,
542+
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"),
543+
TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()),
544+
}
545+
_, err := client.CreateWorkspace(context.Background(), user.OrganizationID, codersdk.Me, req)
546+
547+
require.Error(t, err)
548+
})
549+
494550
t.Run("CreateWithDeletedTemplate", func(t *testing.T) {
495551
t.Parallel()
496552
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})

codersdk/organizations.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,16 @@ type CreateTemplateRequest struct {
124124
}
125125

126126
// CreateWorkspaceRequest provides options for creating a new workspace.
127+
// Either TemplateID or TemplateVersionID must be specified. They cannot both be present.
127128
type CreateWorkspaceRequest struct {
128-
TemplateID uuid.UUID `json:"template_id" validate:"required" format:"uuid"`
129+
// TemplateID specifies which template should be used for creating the workspace.
130+
TemplateID uuid.UUID `json:"template_id,omitempty" validate:"required_without=TemplateVersionID,excluded_with=TemplateVersionID" format:"uuid"`
131+
// TemplateVersionID can be used to specify a specific version of a template for creating the workspace.
132+
TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" validate:"required_without=TemplateID,excluded_with=TemplateID" format:"uuid"`
129133
Name string `json:"name" validate:"workspace_name,required"`
130134
AutostartSchedule *string `json:"autostart_schedule"`
131135
TTLMillis *int64 `json:"ttl_ms,omitempty"`
132-
// ParameterValues allows for additional parameters to be provided
136+
// RichParameterValues allows for additional parameters to be provided
133137
// during the initial provision.
134138
RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"`
135139
}

docs/api/schemas.md

+9-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/api/workspaces.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/api/typesGenerated.ts

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)