feat!: patchTemplateMeta to use optional fields (#24984)

Closes https://github.com/coder/coder/issues/13112

**Breaking Change**: Removed status code `StatusNotModified` when no
diffs occur in a patch. Now the patch is always applied and a template
is always returned.
This commit is contained in:
Steven Masley
2026-05-11 12:43:52 -05:00
committed by GitHub
parent 3986aa8a51
commit 19573e8aee
18 changed files with 1028 additions and 356 deletions
+20 -11
View File
@@ -8,6 +8,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
@@ -88,6 +89,10 @@ func (r *RootCmd) templateEdit() *serpent.Command {
}
// Default values
if !userSetOption(inv, "name") {
name = template.Name
}
if !userSetOption(inv, "description") {
description = template.Description
}
@@ -169,12 +174,12 @@ func (r *RootCmd) templateEdit() *serpent.Command {
}
req := codersdk.UpdateTemplateMeta{
Name: name,
Name: &name,
DisplayName: &displayName,
Description: &description,
Icon: &icon,
DefaultTTLMillis: defaultTTL.Milliseconds(),
ActivityBumpMillis: activityBump.Milliseconds(),
DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
ActivityBumpMillis: ptr.Ref(activityBump.Milliseconds()),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: autostopRequirementDaysOfWeek,
Weeks: autostopRequirementWeeks,
@@ -182,15 +187,19 @@ func (r *RootCmd) templateEdit() *serpent.Command {
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: autostartRequirementDaysOfWeek,
},
FailureTTLMillis: failureTTL.Milliseconds(),
TimeTilDormantMillis: dormancyThreshold.Milliseconds(),
TimeTilDormantAutoDeleteMillis: dormancyAutoDeletion.Milliseconds(),
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
AllowUserAutostart: allowUserAutostart,
AllowUserAutostop: allowUserAutostop,
RequireActiveVersion: requireActiveVersion,
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
TimeTilDormantMillis: ptr.Ref(dormancyThreshold.Milliseconds()),
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormancyAutoDeletion.Milliseconds()),
AllowUserCancelWorkspaceJobs: &allowUserCancelWorkspaceJobs,
AllowUserAutostart: &allowUserAutostart,
AllowUserAutostop: &allowUserAutostop,
RequireActiveVersion: &requireActiveVersion,
DeprecationMessage: deprecated,
DisableEveryoneGroupAccess: disableEveryoneGroup,
DisableEveryoneGroupAccess: &disableEveryoneGroup,
// TODO(Emyrk): now that the API accepts partial updates,
// rewrite this CLI to only set pointers for flags the user
// explicitly provided via userSetOption. The current
// fetch-then-resend-everything dance is no longer required.
}
_, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req)
+5 -4
View File
@@ -101,8 +101,7 @@ func TestTemplateEdit(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "not modified")
require.NoError(t, err)
// Assert that the template metadata did not change.
updated, err := client.Template(context.Background(), template.ID)
@@ -751,8 +750,10 @@ func TestTemplateEdit(t *testing.T) {
var req codersdk.UpdateTemplateMeta
err = json.Unmarshal(body, &req)
require.NoError(t, err)
assert.False(t, req.AllowUserAutostart)
assert.False(t, req.AllowUserAutostop)
require.NotNil(t, req.AllowUserAutostart)
assert.False(t, *req.AllowUserAutostart)
require.NotNil(t, req.AllowUserAutostop)
assert.False(t, *req.AllowUserAutostop)
r.Body = io.NopCloser(bytes.NewReader(body))
updateTemplateCalled.Add(1)
+1 -1
View File
@@ -23367,7 +23367,7 @@ const docTemplate = `{
"type": "integer"
},
"update_workspace_dormant_at": {
"description": "UpdateWorkspaceDormant updates the dormant_at field of workspaces spawned\nfrom the template. This is useful for preventing dormant workspaces being immediately\ndeleted when updating the dormant_ttl field to a new, shorter value.",
"description": "UpdateWorkspaceDormantAt updates the dormant_at field of workspaces spawned\nfrom the template. This is useful for preventing dormant workspaces being\nimmediately deleted when updating the dormant_ttl field to a new, shorter\nvalue.",
"type": "boolean"
},
"update_workspace_last_used_at": {
+1 -1
View File
@@ -21512,7 +21512,7 @@
"type": "integer"
},
"update_workspace_dormant_at": {
"description": "UpdateWorkspaceDormant updates the dormant_at field of workspaces spawned\nfrom the template. This is useful for preventing dormant workspaces being immediately\ndeleted when updating the dormant_ttl field to a new, shorter value.",
"description": "UpdateWorkspaceDormantAt updates the dormant_at field of workspaces spawned\nfrom the template. This is useful for preventing dormant workspaces being\nimmediately deleted when updating the dormant_ttl field to a new, shorter\nvalue.",
"type": "boolean"
},
"update_workspace_last_used_at": {
+3 -3
View File
@@ -550,8 +550,8 @@ func TestExecutorAutostopAIAgentActivity(t *testing.T) {
// Given: template has activity bump enabled.
_, err := client.UpdateTemplateMeta(ctx, r.Template.ID, codersdk.UpdateTemplateMeta{
DefaultTTLMillis: (2 * time.Hour).Milliseconds(),
ActivityBumpMillis: time.Hour.Milliseconds(),
DefaultTTLMillis: ptr.Ref((2 * time.Hour).Milliseconds()),
ActivityBumpMillis: ptr.Ref(time.Hour.Milliseconds()),
})
require.NoError(t, err)
@@ -1905,7 +1905,7 @@ func TestExecutorTaskWorkspace(t *testing.T) {
if defaultTTL > 0 {
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
DefaultTTLMillis: defaultTTL.Milliseconds(),
DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
})
require.NoError(t, err)
}
+70 -168
View File
@@ -35,6 +35,8 @@ import (
"github.com/coder/coder/v2/examples"
)
const defaultRequirementWeeks = 1
// Returns a single template.
//
// @Summary Get template settings by ID
@@ -679,72 +681,47 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
return
}
var (
validErrs []codersdk.ValidationError
autostopRequirementDaysOfWeekParsed uint8
autostartRequirementDaysOfWeekParsed uint8
)
if req.DefaultTTLMillis < 0 {
// resolveTemplateMetaUpdate falls back to the existing template's
// values for any pointer field that is nil in the request, so that
// omitted fields are preserved instead of being overwritten with
// Go zero values.
resolved, validErrs := resolveTemplateMetaUpdate(template, scheduleOpts, req)
if resolved.defaultTTLMillis < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
}
if req.ActivityBumpMillis < 0 {
if resolved.activityBumpMillis < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "activity_bump_ms", Detail: "Must be a positive integer."})
}
if req.AutostopRequirement == nil {
req.AutostopRequirement = &codersdk.TemplateAutostopRequirement{
DaysOfWeek: codersdk.BitmapToWeekdays(scheduleOpts.AutostopRequirement.DaysOfWeek),
Weeks: scheduleOpts.AutostopRequirement.Weeks,
}
}
if len(req.AutostopRequirement.DaysOfWeek) > 0 {
autostopRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(req.AutostopRequirement.DaysOfWeek)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.days_of_week", Detail: err.Error()})
}
}
if req.AutostartRequirement == nil {
req.AutostartRequirement = &codersdk.TemplateAutostartRequirement{
DaysOfWeek: codersdk.BitmapToWeekdays(scheduleOpts.AutostartRequirement.DaysOfWeek),
}
}
if len(req.AutostartRequirement.DaysOfWeek) > 0 {
autostartRequirementDaysOfWeekParsed, err = codersdk.WeekdaysToBitmap(req.AutostartRequirement.DaysOfWeek)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostart_requirement.days_of_week", Detail: err.Error()})
}
}
if req.AutostopRequirement.Weeks < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."})
}
if req.AutostopRequirement.Weeks == 0 {
req.AutostopRequirement.Weeks = 1
}
if template.AutostopRequirementWeeks <= 0 {
template.AutostopRequirementWeeks = 1
}
if req.AutostopRequirement.Weeks > schedule.MaxTemplateAutostopRequirementWeeks {
if resolved.autostopRequirementWeeks > schedule.MaxTemplateAutostopRequirementWeeks {
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: fmt.Sprintf("Must be less than %d.", schedule.MaxTemplateAutostopRequirementWeeks)})
}
// Defaults to the existing.
deprecationMessage := template.Deprecated
if req.DeprecationMessage != nil {
deprecationMessage = *req.DeprecationMessage
// AutostopRequirement.Weeks is allowed to be negative on input but is
// surfaced as a validation error. resolveTemplateMetaUpdate normalizes
// 0 -> 1 but preserves negatives so the caller can reject them.
if req.AutostopRequirement != nil && req.AutostopRequirement.Weeks < 0 {
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: "Must be a positive integer."})
}
if template.AutostopRequirementWeeks <= 0 {
template.AutostopRequirementWeeks = defaultRequirementWeeks
}
// The minimum valid value for a dormant TTL is 1 minute. This is
// to ensure an uninformed user does not send an unintentionally
// small number resulting in potentially catastrophic consequences.
const minTTL = 1000 * 60
if req.FailureTTLMillis < 0 || (req.FailureTTLMillis > 0 && req.FailureTTLMillis < minTTL) {
if resolved.failureTTLMillis < 0 || (resolved.failureTTLMillis > 0 && resolved.failureTTLMillis < minTTL) {
validErrs = append(validErrs, codersdk.ValidationError{Field: "failure_ttl_ms", Detail: "Value must be at least one minute."})
}
if req.TimeTilDormantMillis < 0 || (req.TimeTilDormantMillis > 0 && req.TimeTilDormantMillis < minTTL) {
if resolved.timeTilDormantMillis < 0 || (resolved.timeTilDormantMillis > 0 && resolved.timeTilDormantMillis < minTTL) {
validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_ms", Detail: "Value must be at least one minute."})
}
if req.TimeTilDormantAutoDeleteMillis < 0 || (req.TimeTilDormantAutoDeleteMillis > 0 && req.TimeTilDormantAutoDeleteMillis < minTTL) {
if resolved.timeTilDormantAutoDeleteMillis < 0 || (resolved.timeTilDormantAutoDeleteMillis > 0 && resolved.timeTilDormantAutoDeleteMillis < minTTL) {
validErrs = append(validErrs, codersdk.ValidationError{Field: "time_til_dormant_autodelete_ms", Detail: "Value must be at least one minute."})
}
// MaxPortShareLevel resolution depends on the (potentially licensed)
// PortSharer interface, so it stays out of the pure resolver.
maxPortShareLevel := template.MaxPortSharingLevel
if req.MaxPortShareLevel != nil && *req.MaxPortShareLevel != portSharer.ConvertMaxLevel(template.MaxPortSharingLevel) {
err := portSharer.ValidateTemplateMaxLevel(*req.MaxPortShareLevel)
@@ -755,19 +732,6 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
}
}
corsBehavior := template.CorsBehavior
if req.CORSBehavior != nil && *req.CORSBehavior != "" {
val := database.CorsBehavior(*req.CORSBehavior)
if !val.Valid() {
validErrs = append(validErrs, codersdk.ValidationError{
Field: "cors_behavior",
Detail: fmt.Sprintf("Invalid CORS behavior %q. Must be one of [%s]", *req.CORSBehavior, strings.Join(slice.ToStrings(database.AllCorsBehaviorValues()), ", ")),
})
} else {
corsBehavior = val
}
}
if len(validErrs) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid request to update template metadata!",
@@ -776,57 +740,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
return
}
// Defaults to the existing.
classicTemplateFlow := template.UseClassicParameterFlow
if req.UseClassicParameterFlow != nil {
classicTemplateFlow = *req.UseClassicParameterFlow
}
disableModuleCache := template.DisableModuleCache
if req.DisableModuleCache != nil {
disableModuleCache = *req.DisableModuleCache
}
displayName := ptr.NilToDefault(req.DisplayName, template.DisplayName)
description := ptr.NilToDefault(req.Description, template.Description)
icon := ptr.NilToDefault(req.Icon, template.Icon)
var updated database.Template
err = api.Database.InTx(func(tx database.Store) error {
if req.Name == template.Name &&
description == template.Description &&
displayName == template.DisplayName &&
icon == template.Icon &&
req.AllowUserAutostart == template.AllowUserAutostart &&
req.AllowUserAutostop == template.AllowUserAutostop &&
req.AllowUserCancelWorkspaceJobs == template.AllowUserCancelWorkspaceJobs &&
req.DefaultTTLMillis == time.Duration(template.DefaultTTL).Milliseconds() &&
req.ActivityBumpMillis == time.Duration(template.ActivityBump).Milliseconds() &&
autostopRequirementDaysOfWeekParsed == scheduleOpts.AutostopRequirement.DaysOfWeek &&
autostartRequirementDaysOfWeekParsed == scheduleOpts.AutostartRequirement.DaysOfWeek &&
req.AutostopRequirement.Weeks == scheduleOpts.AutostopRequirement.Weeks &&
req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() &&
req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
req.RequireActiveVersion == template.RequireActiveVersion &&
(deprecationMessage == template.Deprecated) &&
(classicTemplateFlow == template.UseClassicParameterFlow) &&
(disableModuleCache == template.DisableModuleCache) &&
maxPortShareLevel == template.MaxPortSharingLevel &&
corsBehavior == template.CorsBehavior {
return nil
}
// Users should not be able to clear the template name in the UI
name := req.Name
if name == "" {
name = template.Name
}
groupACL := template.GroupACL
if req.DisableEveryoneGroupAccess {
delete(groupACL, template.OrganizationID.String())
}
if template.MaxPortSharingLevel != maxPortShareLevel {
switch maxPortShareLevel {
case database.AppSharingLevelOwner:
@@ -846,25 +761,25 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{
ID: template.ID,
UpdatedAt: dbtime.Now(),
Name: name,
DisplayName: displayName,
Description: description,
Icon: icon,
AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
GroupACL: groupACL,
Name: resolved.name,
DisplayName: resolved.displayName,
Description: resolved.description,
Icon: resolved.icon,
AllowUserCancelWorkspaceJobs: resolved.allowUserCancelWorkspaceJobs,
GroupACL: resolved.groupACL,
MaxPortSharingLevel: maxPortShareLevel,
UseClassicParameterFlow: classicTemplateFlow,
CorsBehavior: corsBehavior,
DisableModuleCache: disableModuleCache,
UseClassicParameterFlow: resolved.useClassicTemplateFlow,
CorsBehavior: resolved.corsBehavior,
DisableModuleCache: resolved.disableModuleCache,
})
if err != nil {
return xerrors.Errorf("update template metadata: %w", err)
}
if template.RequireActiveVersion != req.RequireActiveVersion || deprecationMessage != template.Deprecated {
if template.RequireActiveVersion != resolved.requireActiveVersion || resolved.deprecationMessage != template.Deprecated {
err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, template.ID, dbauthz.TemplateAccessControl{
RequireActiveVersion: req.RequireActiveVersion,
Deprecated: deprecationMessage,
RequireActiveVersion: resolved.requireActiveVersion,
Deprecated: resolved.deprecationMessage,
})
if err != nil {
return xerrors.Errorf("set template access control: %w", err)
@@ -876,50 +791,42 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
return xerrors.Errorf("fetch updated template metadata: %w", err)
}
defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
activityBump := time.Duration(req.ActivityBumpMillis) * time.Millisecond
failureTTL := time.Duration(req.FailureTTLMillis) * time.Millisecond
inactivityTTL := time.Duration(req.TimeTilDormantMillis) * time.Millisecond
timeTilDormantAutoDelete := time.Duration(req.TimeTilDormantAutoDeleteMillis) * time.Millisecond
defaultTTL := time.Duration(resolved.defaultTTLMillis) * time.Millisecond
activityBump := time.Duration(resolved.activityBumpMillis) * time.Millisecond
failureTTL := time.Duration(resolved.failureTTLMillis) * time.Millisecond
inactivityTTL := time.Duration(resolved.timeTilDormantMillis) * time.Millisecond
timeTilDormantAutoDelete := time.Duration(resolved.timeTilDormantAutoDeleteMillis) * time.Millisecond
// updateWorkspaceLastUsedAtIntent is a one-shot intent: only run the
// side effect when the field was explicitly set to true.
var updateWorkspaceLastUsedAt workspacestats.UpdateTemplateWorkspacesLastUsedAtFunc
if req.UpdateWorkspaceLastUsedAt {
if resolved.updateWorkspaceLastUsedAtIntent {
updateWorkspaceLastUsedAt = workspacestats.UpdateTemplateWorkspacesLastUsedAt
}
if defaultTTL != time.Duration(template.DefaultTTL) ||
activityBump != time.Duration(template.ActivityBump) ||
autostopRequirementDaysOfWeekParsed != scheduleOpts.AutostopRequirement.DaysOfWeek ||
autostartRequirementDaysOfWeekParsed != scheduleOpts.AutostartRequirement.DaysOfWeek ||
req.AutostopRequirement.Weeks != scheduleOpts.AutostopRequirement.Weeks ||
failureTTL != time.Duration(template.FailureTTL) ||
inactivityTTL != time.Duration(template.TimeTilDormant) ||
timeTilDormantAutoDelete != time.Duration(template.TimeTilDormantAutoDelete) ||
req.AllowUserAutostart != template.AllowUserAutostart ||
req.AllowUserAutostop != template.AllowUserAutostop {
updated, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, updated, schedule.TemplateScheduleOptions{
// Some of these values are enterprise-only, but the
// TemplateScheduleStore will handle avoiding setting them if
// unlicensed.
UserAutostartEnabled: req.AllowUserAutostart,
UserAutostopEnabled: req.AllowUserAutostop,
DefaultTTL: defaultTTL,
ActivityBump: activityBump,
AutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: autostopRequirementDaysOfWeekParsed,
Weeks: req.AutostopRequirement.Weeks,
},
AutostartRequirement: schedule.TemplateAutostartRequirement{
DaysOfWeek: autostartRequirementDaysOfWeekParsed,
},
FailureTTL: failureTTL,
TimeTilDormant: inactivityTTL,
TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
UpdateWorkspaceLastUsedAt: updateWorkspaceLastUsedAt,
UpdateWorkspaceDormantAt: req.UpdateWorkspaceDormantAt,
})
if err != nil {
return xerrors.Errorf("set template schedule options: %w", err)
}
updated, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, updated, schedule.TemplateScheduleOptions{
// Some of these values are enterprise-only, but the
// TemplateScheduleStore will handle avoiding setting them if
// unlicensed.
UserAutostartEnabled: resolved.allowUserAutostart,
UserAutostopEnabled: resolved.allowUserAutostop,
DefaultTTL: defaultTTL,
ActivityBump: activityBump,
AutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: resolved.autostopRequirementDaysOfWeekParsed,
Weeks: resolved.autostopRequirementWeeks,
},
AutostartRequirement: schedule.TemplateAutostartRequirement{
DaysOfWeek: resolved.autostartRequirementDaysOfWeekParsed,
},
FailureTTL: failureTTL,
TimeTilDormant: inactivityTTL,
TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
UpdateWorkspaceLastUsedAt: updateWorkspaceLastUsedAt,
UpdateWorkspaceDormantAt: resolved.updateWorkspaceDormantAtIntent,
})
if err != nil {
return xerrors.Errorf("set template schedule options: %w", err)
}
return nil
@@ -927,7 +834,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
if err != nil {
if database.IsUniqueViolation(err, database.UniqueTemplatesOrganizationIDNameIndex) {
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
Message: fmt.Sprintf("Template with name %q already exists.", req.Name),
Message: fmt.Sprintf("Template with name %q already exists.", resolved.name),
Validations: []codersdk.ValidationError{{
Field: "name",
Detail: "This value is already in use and should be unique.",
@@ -945,11 +852,6 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
}
}
if updated.UpdatedAt.IsZero() {
aReq.New = template
rw.WriteHeader(http.StatusNotModified)
return
}
aReq.New = updated
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(updated))
+169
View File
@@ -0,0 +1,169 @@
package coderd
import (
"strings"
"time"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
)
// templateMetaUpdate is the resolved set of values to apply for a
// PATCH /templates/{template} request. Any field on
// codersdk.UpdateTemplateMeta that is nil falls back to the existing
// template's value so that omitted request fields are not modified.
type templateMetaUpdate struct {
name string
displayName string
description string
icon string
defaultTTLMillis int64
activityBumpMillis int64
failureTTLMillis int64
timeTilDormantMillis int64
timeTilDormantAutoDeleteMillis int64
allowUserAutostart bool
allowUserAutostop bool
allowUserCancelWorkspaceJobs bool
requireActiveVersion bool
deprecationMessage string
useClassicTemplateFlow bool
disableModuleCache bool
corsBehavior database.CorsBehavior
autostopRequirementDaysOfWeekParsed uint8
autostartRequirementDaysOfWeekParsed uint8
autostopRequirementWeeks int64
groupACL database.TemplateACL
// updateWorkspaceLastUsedAtIntent and updateWorkspaceDormantAtIntent are one-shot
// intents that trigger side effects only when the request explicitly
// sets the field to true. nil and false are no-ops.
updateWorkspaceLastUsedAtIntent bool
updateWorkspaceDormantAtIntent bool
}
// resolveTemplateMetaUpdate produces a templateMetaUpdate populated with
// either the request value (when present) or the existing template's
// value (when the request field is nil).
//
// This function validates shape, not contents: it parses the
// autostop/autostart day-of-week strings into bitmaps and ensures any
// non-empty CORS behavior is a recognized enum. Errors it returns are
// user-facing validation errors the caller must surface as 400 Bad
// Request.
//
// Range and content checks (e.g. activityBumpMillis >= 0,
// failureTTLMillis >= 1 minute, max port share level) and validation
// that depends on external interfaces (such as port-sharing licensure)
// are the caller's responsibility.
func resolveTemplateMetaUpdate(
template database.Template,
scheduleOpts schedule.TemplateScheduleOptions,
req codersdk.UpdateTemplateMeta,
) (templateMetaUpdate, []codersdk.ValidationError) {
var validErrs []codersdk.ValidationError
out := templateMetaUpdate{
name: ptr.NilToDefault(req.Name, template.Name),
displayName: ptr.NilToDefault(req.DisplayName, template.DisplayName),
description: ptr.NilToDefault(req.Description, template.Description),
icon: ptr.NilToDefault(req.Icon, template.Icon),
defaultTTLMillis: ptr.NilToDefault(req.DefaultTTLMillis, time.Duration(template.DefaultTTL).Milliseconds()),
activityBumpMillis: ptr.NilToDefault(req.ActivityBumpMillis, time.Duration(template.ActivityBump).Milliseconds()),
failureTTLMillis: ptr.NilToDefault(req.FailureTTLMillis, time.Duration(template.FailureTTL).Milliseconds()),
timeTilDormantMillis: ptr.NilToDefault(req.TimeTilDormantMillis, time.Duration(template.TimeTilDormant).Milliseconds()),
timeTilDormantAutoDeleteMillis: ptr.NilToDefault(req.TimeTilDormantAutoDeleteMillis, time.Duration(template.TimeTilDormantAutoDelete).Milliseconds()),
allowUserAutostart: ptr.NilToDefault(req.AllowUserAutostart, template.AllowUserAutostart),
allowUserAutostop: ptr.NilToDefault(req.AllowUserAutostop, template.AllowUserAutostop),
allowUserCancelWorkspaceJobs: ptr.NilToDefault(req.AllowUserCancelWorkspaceJobs, template.AllowUserCancelWorkspaceJobs),
requireActiveVersion: ptr.NilToDefault(req.RequireActiveVersion, template.RequireActiveVersion),
deprecationMessage: ptr.NilToDefault(req.DeprecationMessage, template.Deprecated),
useClassicTemplateFlow: ptr.NilToDefault(req.UseClassicParameterFlow, template.UseClassicParameterFlow),
disableModuleCache: ptr.NilToDefault(req.DisableModuleCache, template.DisableModuleCache),
groupACL: template.GroupACL,
// Default to the original values
corsBehavior: template.CorsBehavior,
autostopRequirementDaysOfWeekParsed: scheduleOpts.AutostopRequirement.DaysOfWeek,
autostopRequirementWeeks: scheduleOpts.AutostopRequirement.Weeks,
autostartRequirementDaysOfWeekParsed: scheduleOpts.AutostartRequirement.DaysOfWeek,
updateWorkspaceLastUsedAtIntent: false,
updateWorkspaceDormantAtIntent: false,
}
// Users should not be able to clear the template name. This is the only field
// that treats a zero value as omitted.
if out.name == "" {
out.name = template.Name
}
// Override autostop if provided is non-nil
if req.AutostopRequirement != nil {
bitmap, err := codersdk.WeekdaysToBitmap(req.AutostopRequirement.DaysOfWeek)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{
Field: "autostop_requirement.days_of_week",
Detail: err.Error(),
})
} else {
out.autostopRequirementDaysOfWeekParsed = bitmap
out.autostopRequirementWeeks = req.AutostopRequirement.Weeks
}
// Always force <= 0 -> 1
if out.autostopRequirementWeeks <= 0 {
out.autostopRequirementWeeks = defaultRequirementWeeks
}
}
// Override autostart if provided is non-nil
if req.AutostartRequirement != nil {
bitmap, err := codersdk.WeekdaysToBitmap(req.AutostartRequirement.DaysOfWeek)
if err != nil {
validErrs = append(validErrs, codersdk.ValidationError{
Field: "autostart_requirement.days_of_week",
Detail: err.Error(),
})
} else {
out.autostartRequirementDaysOfWeekParsed = bitmap
}
}
// Resolve CORS behavior. An empty string is treated as "do not
// change" because the existing UI-driven flow used to send empty
// strings for unset values. A non-empty invalid value is a
// validation error.
if req.CORSBehavior != nil && *req.CORSBehavior != "" {
val := database.CorsBehavior(*req.CORSBehavior)
if !val.Valid() {
validErrs = append(validErrs, codersdk.ValidationError{
Field: "cors_behavior",
Detail: "Invalid CORS behavior \"" + string(*req.CORSBehavior) +
"\". Must be one of [" + strings.Join(slice.ToStrings(database.AllCorsBehaviorValues()), ", ") + "]",
})
} else {
out.corsBehavior = val
}
}
if req.DisableEveryoneGroupAccess != nil && *req.DisableEveryoneGroupAccess {
// Remove the "everyone" group from the template. If this is set to false, the
// user needs to explicitly add the "everyone" group back to the ACL via the
// group ACL endpoints, so we don't treat false as a no-op.
delete(out.groupACL, template.OrganizationID.String())
}
// One-shot intent flags. nil and false are both no-ops; true is a
// trigger to run the side effect.
if req.UpdateWorkspaceLastUsedAt != nil && *req.UpdateWorkspaceLastUsedAt {
out.updateWorkspaceLastUsedAtIntent = true
}
if req.UpdateWorkspaceDormantAt != nil && *req.UpdateWorkspaceDormantAt {
out.updateWorkspaceDormantAtIntent = true
}
return out, validErrs
}
@@ -0,0 +1,496 @@
package coderd //nolint:testpackage // Tests the unexported resolveTemplateMetaUpdate helper.
import (
"reflect"
"testing"
"github.com/google/uuid"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
)
// baselineTemplate returns a database.Template populated with non-default
// values for every field that resolveTemplateMetaUpdate reads. Non-default
// values let single-field tests detect when a field is being silently
// overwritten with a zero value.
func baselineTemplate() database.Template {
orgID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
return database.Template{
ID: uuid.MustParse("00000000-0000-0000-0000-000000000002"),
OrganizationID: orgID,
Name: "baseline",
DisplayName: "Baseline Template",
Description: "An existing description.",
Icon: "/baseline.svg",
AllowUserAutostart: false,
AllowUserAutostop: false,
AllowUserCancelWorkspaceJobs: false,
RequireActiveVersion: true,
DefaultTTL: int64(60 * 60 * 1000 * 1000 * 1000), // 1 hour in ns
ActivityBump: int64(30 * 60 * 1000 * 1000 * 1000), // 30 minutes in ns
FailureTTL: int64(120 * 60 * 1000 * 1000 * 1000), // 2 hours in ns
TimeTilDormant: int64(240 * 60 * 1000 * 1000 * 1000), // 4 hours in ns
TimeTilDormantAutoDelete: int64(480 * 60 * 1000 * 1000 * 1000), // 8 hours in ns
AutostopRequirementDaysOfWeek: 0b0000001, // Monday
AutostopRequirementWeeks: 2,
AutostartBlockDaysOfWeek: 0b1000000, // Sunday
Deprecated: "deprecated", // non-empty so the conversion is observable
MaxPortSharingLevel: database.AppSharingLevelOrganization,
UseClassicParameterFlow: true,
CorsBehavior: database.CorsBehaviorPassthru,
DisableModuleCache: true,
GroupACL: database.TemplateACL{
orgID.String(): {"read"},
},
}
}
// baselineScheduleOpts returns schedule options matching the baseline
// template above, so that nil request fields resolve to these values.
func baselineScheduleOpts() schedule.TemplateScheduleOptions {
return schedule.TemplateScheduleOptions{
AutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b0000001,
Weeks: 2,
},
AutostartRequirement: schedule.TemplateAutostartRequirement{
DaysOfWeek: 0b1000000,
},
}
}
// baselineResolved returns the templateMetaUpdate that resolveTemplateMetaUpdate
// produces for an empty request against baselineTemplate / baselineScheduleOpts.
func baselineResolved() templateMetaUpdate {
tpl := baselineTemplate()
return templateMetaUpdate{
name: tpl.Name,
displayName: tpl.DisplayName,
description: tpl.Description,
icon: tpl.Icon,
defaultTTLMillis: tpl.DefaultTTL / 1e6,
activityBumpMillis: tpl.ActivityBump / 1e6,
failureTTLMillis: tpl.FailureTTL / 1e6,
timeTilDormantMillis: tpl.TimeTilDormant / 1e6,
timeTilDormantAutoDeleteMillis: tpl.TimeTilDormantAutoDelete / 1e6,
allowUserAutostart: tpl.AllowUserAutostart,
allowUserAutostop: tpl.AllowUserAutostop,
allowUserCancelWorkspaceJobs: tpl.AllowUserCancelWorkspaceJobs,
requireActiveVersion: tpl.RequireActiveVersion,
deprecationMessage: tpl.Deprecated,
useClassicTemplateFlow: tpl.UseClassicParameterFlow,
disableModuleCache: tpl.DisableModuleCache,
corsBehavior: tpl.CorsBehavior,
autostopRequirementDaysOfWeekParsed: 0b0000001,
autostartRequirementDaysOfWeekParsed: 0b1000000,
autostopRequirementWeeks: tpl.AutostopRequirementWeeks,
groupACL: tpl.GroupACL,
}
}
func TestResolveTemplateMetaUpdate(t *testing.T) {
t.Parallel()
type expected struct {
// override is applied to baselineResolved to produce the expected
// templateMetaUpdate. Allows each case to express only its delta.
override func(*templateMetaUpdate)
base func(template *database.Template)
// validErrFields, if non-empty, asserts the resolver produced a
// validation error for each named field.
validErrFields []string
}
tests := []struct {
name string
req codersdk.UpdateTemplateMeta
expected expected
}{
// Sanity check: an empty PATCH preserves every field.
{
name: "EmptyRequestPreservesEverything",
req: codersdk.UpdateTemplateMeta{},
expected: expected{override: func(*templateMetaUpdate) {}},
},
// One case per pointer field: each case sends only that field
// and asserts only that field changed in the resolved struct.
{
name: "Name",
req: codersdk.UpdateTemplateMeta{Name: ptr.Ref("renamed")},
expected: expected{override: func(r *templateMetaUpdate) {
r.name = "renamed"
}},
},
{
name: "NameEmptyStringFallsBackToCurrent",
req: codersdk.UpdateTemplateMeta{Name: ptr.Ref("")},
// Empty string is treated as "do not clear" because the UI
// disallows clearing the name. Resolver must keep the
// existing name.
// This is a unique case to just the `name` field.
expected: expected{override: func(*templateMetaUpdate) {}},
},
{
name: "DisplayName",
req: codersdk.UpdateTemplateMeta{DisplayName: ptr.Ref("Renamed")},
expected: expected{override: func(r *templateMetaUpdate) {
r.displayName = "Renamed"
}},
},
{
name: "Description",
req: codersdk.UpdateTemplateMeta{Description: ptr.Ref("New description")},
expected: expected{override: func(r *templateMetaUpdate) {
r.description = "New description"
}},
},
{
name: "Icon",
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("/new.svg")},
expected: expected{override: func(r *templateMetaUpdate) {
r.icon = "/new.svg"
}},
},
{
name: "DefaultTTLMillis",
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: ptr.Ref(int64(7200_000))},
expected: expected{override: func(r *templateMetaUpdate) {
r.defaultTTLMillis = 7200_000
}},
},
{
name: "DefaultTTLMillisZeroExplicit",
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: ptr.Ref(int64(0))},
expected: expected{override: func(r *templateMetaUpdate) {
r.defaultTTLMillis = 0
}},
},
{
name: "ActivityBumpMillis",
req: codersdk.UpdateTemplateMeta{ActivityBumpMillis: ptr.Ref(int64(900_000))},
expected: expected{override: func(r *templateMetaUpdate) {
r.activityBumpMillis = 900_000
}},
},
{
name: "AllowUserAutostart",
req: codersdk.UpdateTemplateMeta{AllowUserAutostart: ptr.Ref(true)},
expected: expected{override: func(r *templateMetaUpdate) {
r.allowUserAutostart = true
}},
},
{
name: "AllowUserAutostop",
req: codersdk.UpdateTemplateMeta{AllowUserAutostop: ptr.Ref(true)},
expected: expected{override: func(r *templateMetaUpdate) {
r.allowUserAutostop = true
}},
},
{
name: "AllowUserAutostop/true",
req: codersdk.UpdateTemplateMeta{AllowUserAutostop: ptr.Ref(false)},
expected: expected{
base: func(update *database.Template) {
update.AllowUserAutostop = true
},
override: func(r *templateMetaUpdate) {
r.allowUserAutostop = false
},
},
},
{
name: "AllowUserCancelWorkspaceJobs",
req: codersdk.UpdateTemplateMeta{AllowUserCancelWorkspaceJobs: ptr.Ref(true)},
expected: expected{override: func(r *templateMetaUpdate) {
r.allowUserCancelWorkspaceJobs = true
}},
},
{
name: "FailureTTLMillis",
req: codersdk.UpdateTemplateMeta{FailureTTLMillis: ptr.Ref(int64(3_600_000))},
expected: expected{override: func(r *templateMetaUpdate) {
r.failureTTLMillis = 3_600_000
}},
},
{
name: "TimeTilDormantMillis",
req: codersdk.UpdateTemplateMeta{TimeTilDormantMillis: ptr.Ref(int64(7_200_000))},
expected: expected{override: func(r *templateMetaUpdate) {
r.timeTilDormantMillis = 7_200_000
}},
},
{
name: "TimeTilDormantAutoDeleteMillis",
req: codersdk.UpdateTemplateMeta{TimeTilDormantAutoDeleteMillis: ptr.Ref(int64(14_400_000))},
expected: expected{override: func(r *templateMetaUpdate) {
r.timeTilDormantAutoDeleteMillis = 14_400_000
}},
},
{
name: "RequireActiveVersion",
req: codersdk.UpdateTemplateMeta{RequireActiveVersion: ptr.Ref(false)},
expected: expected{override: func(r *templateMetaUpdate) {
r.requireActiveVersion = false
}},
},
{
name: "DeprecationMessage",
req: codersdk.UpdateTemplateMeta{DeprecationMessage: ptr.Ref("now deprecated")},
expected: expected{override: func(r *templateMetaUpdate) {
r.deprecationMessage = "now deprecated"
}},
},
{
name: "DeprecationMessageEmptyStringClears",
req: codersdk.UpdateTemplateMeta{DeprecationMessage: ptr.Ref("")},
expected: expected{override: func(r *templateMetaUpdate) {
r.deprecationMessage = ""
}},
},
{
name: "UseClassicParameterFlow",
req: codersdk.UpdateTemplateMeta{UseClassicParameterFlow: ptr.Ref(false)},
expected: expected{override: func(r *templateMetaUpdate) {
r.useClassicTemplateFlow = false
}},
},
{
name: "DisableModuleCache",
req: codersdk.UpdateTemplateMeta{DisableModuleCache: ptr.Ref(false)},
expected: expected{override: func(r *templateMetaUpdate) {
r.disableModuleCache = false
}},
},
// CORS behavior.
{
name: "CORSBehaviorChange",
req: codersdk.UpdateTemplateMeta{
CORSBehavior: ptr.Ref(codersdk.CORSBehavior(database.CorsBehaviorSimple)),
},
expected: expected{override: func(r *templateMetaUpdate) {
r.corsBehavior = database.CorsBehaviorSimple
}},
},
{
name: "CORSBehaviorEmptyStringPreserves",
req: codersdk.UpdateTemplateMeta{
CORSBehavior: ptr.Ref(codersdk.CORSBehavior("")),
},
// Empty string is treated as "do not change" for backwards
// compatibility with older clients that always send the
// field.
expected: expected{override: func(*templateMetaUpdate) {}},
},
{
name: "CORSBehaviorInvalid",
req: codersdk.UpdateTemplateMeta{
CORSBehavior: ptr.Ref(codersdk.CORSBehavior("not-a-real-value")),
},
expected: expected{
// Invalid value: keep current and surface a validation error.
override: func(*templateMetaUpdate) {},
validErrFields: []string{"cors_behavior"},
},
},
// Autostop / autostart requirement bitmaps.
{
name: "AutostopRequirementChange",
req: codersdk.UpdateTemplateMeta{
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: []string{"friday"},
Weeks: 4,
},
},
expected: expected{override: func(r *templateMetaUpdate) {
r.autostopRequirementDaysOfWeekParsed = 0b0010000
r.autostopRequirementWeeks = 4
}},
},
{
name: "AutostopRequirementWeeksZeroNormalizesToOne",
req: codersdk.UpdateTemplateMeta{
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: []string{"monday"},
Weeks: 0,
},
},
expected: expected{override: func(r *templateMetaUpdate) {
r.autostopRequirementDaysOfWeekParsed = 0b0000001
r.autostopRequirementWeeks = 1
}},
},
{
name: "AutostopRequirementInvalidDay",
req: codersdk.UpdateTemplateMeta{
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: []string{"funday"},
Weeks: 1,
},
},
expected: expected{
override: func(r *templateMetaUpdate) {
r.autostopRequirementDaysOfWeekParsed = 1
r.autostopRequirementWeeks = 2
},
validErrFields: []string{"autostop_requirement.days_of_week"},
},
},
{
name: "AutostartRequirementChange",
req: codersdk.UpdateTemplateMeta{
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: []string{"saturday"},
},
},
expected: expected{override: func(r *templateMetaUpdate) {
r.autostartRequirementDaysOfWeekParsed = 0b0100000
}},
},
{
name: "AutostartRequirementInvalidDay",
req: codersdk.UpdateTemplateMeta{
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: []string{"funday"},
},
},
expected: expected{
override: func(r *templateMetaUpdate) {
r.autostartRequirementDaysOfWeekParsed = 64
},
validErrFields: []string{"autostart_requirement.days_of_week"},
},
},
// One-shot intent flags. nil and false should both result in
// the corresponding *Intent field being false; only true triggers it.
{
name: "DisableEveryoneGroupAccessFalseIsNoop",
req: codersdk.UpdateTemplateMeta{DisableEveryoneGroupAccess: ptr.Ref(false)},
expected: expected{override: func(*templateMetaUpdate) {
// disableEveryoneIntent stays false.
}},
},
{
name: "DisableEveryoneGroupAccessTrueWithMembership",
req: codersdk.UpdateTemplateMeta{DisableEveryoneGroupAccess: ptr.Ref(true)},
expected: expected{override: func(r *templateMetaUpdate) {
r.groupACL = database.TemplateACL{}
}},
},
{
name: "UpdateWorkspaceLastUsedAtFalseIsNoop",
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceLastUsedAt: ptr.Ref(false)},
expected: expected{override: func(*templateMetaUpdate) {}},
},
{
name: "UpdateWorkspaceLastUsedAtTrue",
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceLastUsedAt: ptr.Ref(true)},
expected: expected{override: func(r *templateMetaUpdate) {
r.updateWorkspaceLastUsedAtIntent = true
}},
},
{
name: "UpdateWorkspaceDormantAtFalseIsNoop",
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceDormantAt: ptr.Ref(false)},
expected: expected{override: func(*templateMetaUpdate) {}},
},
{
name: "UpdateWorkspaceDormantAtTrue",
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceDormantAt: ptr.Ref(true)},
expected: expected{override: func(r *templateMetaUpdate) {
r.updateWorkspaceDormantAtIntent = true
}},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tpl := baselineTemplate()
if tc.expected.base != nil {
tc.expected.base(&tpl)
}
schedOpts := baselineScheduleOpts()
got, validErrs := resolveTemplateMetaUpdate(tpl, schedOpts, tc.req)
want := baselineResolved()
tc.expected.override(&want)
if !reflect.DeepEqual(got, want) {
t.Fatalf("resolved mismatch\ngot: %+v\nwant: %+v", got, want)
}
if len(validErrs) != len(tc.expected.validErrFields) {
t.Fatalf("got %d validation errors, want %d: %+v",
len(validErrs), len(tc.expected.validErrFields), validErrs)
}
for i, field := range tc.expected.validErrFields {
if validErrs[i].Field != field {
t.Errorf("validation error %d: field = %q, want %q",
i, validErrs[i].Field, field)
}
}
})
}
}
// TestResolveTemplateMetaUpdate_NameClearedFallsBackToTemplateName covers the
// pre-existing rule that a template's name cannot be cleared from the UI.
// Even an explicit empty pointer must resolve to the existing template name.
func TestResolveTemplateMetaUpdate_NameClearedFallsBackToTemplateName(t *testing.T) {
t.Parallel()
tpl := baselineTemplate()
schedOpts := baselineScheduleOpts()
got, _ := resolveTemplateMetaUpdate(tpl, schedOpts, codersdk.UpdateTemplateMeta{
Name: ptr.Ref(""),
})
if got.name != tpl.Name {
t.Fatalf("got name = %q, want %q (preserved)", got.name, tpl.Name)
}
}
// TestResolveTemplateMetaUpdate_NilRequestUsesScheduleOptsForRequirements
// verifies that an entirely empty request returns the schedule store's
// current autostop/autostart requirement values, rather than zeros.
func TestResolveTemplateMetaUpdate_NilRequestUsesScheduleOptsForRequirements(t *testing.T) {
t.Parallel()
tpl := baselineTemplate()
schedOpts := schedule.TemplateScheduleOptions{
AutostopRequirement: schedule.TemplateAutostopRequirement{
DaysOfWeek: 0b0001100, // Wed + Thu
Weeks: 3,
},
AutostartRequirement: schedule.TemplateAutostartRequirement{
DaysOfWeek: 0b0010000, // Fri
},
}
got, validErrs := resolveTemplateMetaUpdate(tpl, schedOpts, codersdk.UpdateTemplateMeta{})
if len(validErrs) != 0 {
t.Fatalf("unexpected validation errors: %+v", validErrs)
}
if got.autostopRequirementDaysOfWeekParsed != schedOpts.AutostopRequirement.DaysOfWeek {
t.Errorf("autostop days = 0b%07b, want 0b%07b",
got.autostopRequirementDaysOfWeekParsed,
schedOpts.AutostopRequirement.DaysOfWeek)
}
if got.autostartRequirementDaysOfWeekParsed != schedOpts.AutostartRequirement.DaysOfWeek {
t.Errorf("autostart days = 0b%07b, want 0b%07b",
got.autostartRequirementDaysOfWeekParsed,
schedOpts.AutostartRequirement.DaysOfWeek)
}
if got.autostopRequirementWeeks != schedOpts.AutostopRequirement.Weeks {
t.Errorf("autostop weeks = %d, want %d",
got.autostopRequirementWeeks, schedOpts.AutostopRequirement.Weeks)
}
}
+165 -79
View File
@@ -901,13 +901,13 @@ func TestPatchTemplateMeta(t *testing.T) {
assert.Equal(t, (1 * time.Hour).Milliseconds(), template.ActivityBumpMillis)
req := codersdk.UpdateTemplateMeta{
Name: "new-template-name",
Name: ptr.Ref("new-template-name"),
DisplayName: ptr.Ref("Displayed Name 456"),
Description: ptr.Ref("lorem ipsum dolor sit amet et cetera"),
Icon: ptr.Ref("/icon/new-icon.png"),
DefaultTTLMillis: 12 * time.Hour.Milliseconds(),
ActivityBumpMillis: 3 * time.Hour.Milliseconds(),
AllowUserCancelWorkspaceJobs: false,
DefaultTTLMillis: ptr.Ref(12 * time.Hour.Milliseconds()),
ActivityBumpMillis: ptr.Ref(3 * time.Hour.Milliseconds()),
AllowUserCancelWorkspaceJobs: ptr.Ref(false),
}
// It is unfortunate we need to sleep, but the test can fail if the
// updatedAt is too close together.
@@ -918,25 +918,25 @@ func TestPatchTemplateMeta(t *testing.T) {
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, req.Name, updated.Name)
assert.Equal(t, *req.Name, updated.Name)
assert.Equal(t, *req.DisplayName, updated.DisplayName)
assert.Equal(t, *req.Description, updated.Description)
assert.Equal(t, *req.Icon, updated.Icon)
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis)
assert.False(t, req.AllowUserCancelWorkspaceJobs)
assert.Equal(t, *req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, *req.ActivityBumpMillis, updated.ActivityBumpMillis)
assert.False(t, *req.AllowUserCancelWorkspaceJobs)
// Extra paranoid: did it _really_ happen?
updated, err = client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, req.Name, updated.Name)
assert.Equal(t, *req.Name, updated.Name)
assert.Equal(t, *req.DisplayName, updated.DisplayName)
assert.Equal(t, *req.Description, updated.Description)
assert.Equal(t, *req.Icon, updated.Icon)
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis)
assert.False(t, req.AllowUserCancelWorkspaceJobs)
assert.Equal(t, *req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, *req.ActivityBumpMillis, updated.ActivityBumpMillis)
assert.False(t, *req.AllowUserCancelWorkspaceJobs)
require.Len(t, auditor.AuditLogs(), 5)
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action)
@@ -957,7 +957,7 @@ func TestPatchTemplateMeta(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template2.Name,
Name: &template2.Name,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
@@ -1052,7 +1052,7 @@ func TestPatchTemplateMeta(t *testing.T) {
// Ensure the same value port share level is a no-op
level = codersdk.WorkspaceAgentPortShareLevelPublic
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: coderdtest.RandomUsername(t),
Name: ptr.Ref(coderdtest.RandomUsername(t)),
MaxPortShareLevel: &level,
})
require.NoError(t, err)
@@ -1072,7 +1072,7 @@ func TestPatchTemplateMeta(t *testing.T) {
time.Sleep(time.Millisecond * 5)
req := codersdk.UpdateTemplateMeta{
DefaultTTLMillis: 0,
DefaultTTLMillis: ptr.Ref(int64(0)),
}
// We're too fast! Sleep so we can be sure that updatedAt is greater
@@ -1087,7 +1087,7 @@ func TestPatchTemplateMeta(t *testing.T) {
updated, err := client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, *req.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Empty(t, updated.DeprecationMessage)
assert.False(t, updated.Deprecated)
})
@@ -1106,7 +1106,7 @@ func TestPatchTemplateMeta(t *testing.T) {
time.Sleep(time.Millisecond * 5)
req := codersdk.UpdateTemplateMeta{
DefaultTTLMillis: -1,
DefaultTTLMillis: ptr.Ref(int64(-1)),
}
ctx := testutil.Context(t, testutil.WaitLong)
@@ -1163,16 +1163,16 @@ func TestPatchTemplateMeta(t *testing.T) {
defer cancel()
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: &template.Name,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: 0,
DefaultTTLMillis: ptr.Ref(int64(0)),
AutostopRequirement: &template.AutostopRequirement,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
FailureTTLMillis: failureTTL.Milliseconds(),
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(),
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
TimeTilDormantAutoDeleteMillis: ptr.Ref(timeTilDormantAutoDelete.Milliseconds()),
})
require.NoError(t, err)
@@ -1198,16 +1198,16 @@ func TestPatchTemplateMeta(t *testing.T) {
defer cancel()
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: &template.Name,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: template.DefaultTTLMillis,
DefaultTTLMillis: &template.DefaultTTLMillis,
AutostopRequirement: &template.AutostopRequirement,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
FailureTTLMillis: failureTTL.Milliseconds(),
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(),
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
TimeTilDormantAutoDeleteMillis: ptr.Ref(timeTilDormantAutoDelete.Milliseconds()),
})
require.NoError(t, err)
require.Zero(t, got.FailureTTLMillis)
@@ -1259,15 +1259,15 @@ func TestPatchTemplateMeta(t *testing.T) {
allowAutostart.Store(false)
allowAutostop.Store(false)
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: &template.Name,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: template.DefaultTTLMillis,
DefaultTTLMillis: &template.DefaultTTLMillis,
AutostopRequirement: &template.AutostopRequirement,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
AllowUserAutostart: allowAutostart.Load(),
AllowUserAutostop: allowAutostop.Load(),
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
AllowUserAutostart: ptr.Ref(allowAutostart.Load()),
AllowUserAutostop: ptr.Ref(allowAutostop.Load()),
})
require.NoError(t, err)
@@ -1290,16 +1290,15 @@ func TestPatchTemplateMeta(t *testing.T) {
defer cancel()
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
// Increase the default TTL to avoid error "not modified".
DefaultTTLMillis: template.DefaultTTLMillis + 1,
Name: &template.Name,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: ptr.Ref(template.DefaultTTLMillis + 1),
AutostopRequirement: &template.AutostopRequirement,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
AllowUserAutostart: false,
AllowUserAutostop: false,
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
AllowUserAutostart: ptr.Ref(false),
AllowUserAutostop: ptr.Ref(false),
})
require.NoError(t, err)
require.True(t, got.AllowUserAutostart)
@@ -1322,24 +1321,26 @@ func TestPatchTemplateMeta(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
req := codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: &template.Name,
Description: &template.Description,
Icon: &template.Icon,
DefaultTTLMillis: template.DefaultTTLMillis,
ActivityBumpMillis: template.ActivityBumpMillis,
DefaultTTLMillis: &template.DefaultTTLMillis,
ActivityBumpMillis: &template.ActivityBumpMillis,
AutostopRequirement: nil,
AllowUserAutostart: template.AllowUserAutostart,
AllowUserAutostop: template.AllowUserAutostop,
AllowUserAutostart: &template.AllowUserAutostart,
AllowUserAutostop: &template.AllowUserAutostop,
}
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
require.ErrorContains(t, err, "not modified")
require.NoError(t, err)
updated, err := client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Equal(t, updated.UpdatedAt, template.UpdatedAt)
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.ActivityBumpMillis, updated.ActivityBumpMillis)
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
})
t.Run("Invalid", func(t *testing.T) {
@@ -1356,7 +1357,7 @@ func TestPatchTemplateMeta(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
req := codersdk.UpdateTemplateMeta{
DefaultTTLMillis: -int64(time.Hour),
DefaultTTLMillis: ptr.Ref(-int64(time.Hour)),
}
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
var apiErr *codersdk.Error
@@ -1438,12 +1439,12 @@ func TestPatchTemplateMeta(t *testing.T) {
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
req := codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: &template.Name,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: time.Hour.Milliseconds(),
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
// wrong order
DaysOfWeek: []string{"saturday", "friday"},
@@ -1515,12 +1516,12 @@ func TestPatchTemplateMeta(t *testing.T) {
require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostopRequirement.DaysOfWeek)
require.EqualValues(t, 2, template.AutostopRequirement.Weeks)
req := codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: &template.Name,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: time.Hour.Milliseconds(),
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: []string{},
Weeks: 0,
@@ -1552,12 +1553,12 @@ func TestPatchTemplateMeta(t *testing.T) {
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
req := codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: &template.Name,
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: time.Hour.Milliseconds(),
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: []string{"monday"},
Weeks: 2,
@@ -1603,9 +1604,11 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.True(t, updated.UseClassicParameterFlow, "expected true")
// noop
req.UseClassicParameterFlow = nil
updated, err = client.UpdateTemplateMeta(ctx, template.ID, req)
_, err = client.UpdateTemplateMeta(ctx, template.ID, req)
require.NoError(t, err)
updated, err = client.Template(ctx, template.ID)
require.NoError(t, err)
assert.True(t, updated.UseClassicParameterFlow, "expected true")
@@ -1636,9 +1639,13 @@ func TestPatchTemplateMeta(t *testing.T) {
require.NoError(t, err)
assert.True(t, updated.DisableModuleCache, "expected true")
// noop - should stay true when not specified
// Sending DisableModuleCache: nil with no other changes is a true
// no-op and produces a 304 Not Modified (surfaced as an error by the
// SDK).
req.DisableModuleCache = nil
updated, err = client.UpdateTemplateMeta(ctx, template.ID, req)
_, err = client.UpdateTemplateMeta(ctx, template.ID, req)
require.NoError(t, err)
updated, err = client.Template(ctx, template.ID)
require.NoError(t, err)
assert.True(t, updated.DisableModuleCache, "expected true")
@@ -1675,7 +1682,7 @@ func TestPatchTemplateMeta(t *testing.T) {
DisplayName: &displayName,
Description: &description,
Icon: &icon,
DefaultTTLMillis: defaultTTLMillis,
DefaultTTLMillis: ptr.Ref(defaultTTLMillis),
}
type expected struct {
@@ -1694,38 +1701,41 @@ func TestPatchTemplateMeta(t *testing.T) {
tests := []testCase{
{
name: "Only update default_ttl_ms",
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: 99 * time.Hour.Milliseconds()},
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: ptr.Ref(99 * time.Hour.Milliseconds())},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 99 * time.Hour.Milliseconds()},
},
{
name: "Clear display name",
req: codersdk.UpdateTemplateMeta{DisplayName: ptr.Ref("")},
expected: expected{displayName: "", description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0},
expected: expected{displayName: "", description: reference.Description, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Clear description",
req: codersdk.UpdateTemplateMeta{Description: ptr.Ref("")},
expected: expected{displayName: reference.DisplayName, description: "", icon: reference.Icon, defaultTTLMillis: 0},
expected: expected{displayName: reference.DisplayName, description: "", icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Clear icon",
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("")},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: "", defaultTTLMillis: 0},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: "", defaultTTLMillis: defaultTTLMillis},
},
// A request whose only field is nil is a true no-op under the new
// PATCH semantics; the handler returns 304 Not Modified and the
// template values are preserved.
{
name: "Nil display name defaults to reference display name",
name: "Nil display name is a no-op",
req: codersdk.UpdateTemplateMeta{DisplayName: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Nil description defaults to reference description",
name: "Nil description is a no-op",
req: codersdk.UpdateTemplateMeta{Description: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
{
name: "Nil icon defaults to reference icon",
name: "Nil icon is a no-op",
req: codersdk.UpdateTemplateMeta{Icon: nil},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 0},
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: defaultTTLMillis},
},
}
@@ -1734,12 +1744,16 @@ func TestPatchTemplateMeta(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
defer func() {
ctx := testutil.Context(t, testutil.WaitLong)
// Restore reference after each test case
_, err := client.UpdateTemplateMeta(ctx, reference.ID, restoreReq)
require.NoError(t, err)
// Restore reference after each test case. The restore
// itself can be a no-op (and return an error) when the
// previous test case was already a no-op; that is
// expected, so we ignore the error here.
_, _ = client.UpdateTemplateMeta(ctx, reference.ID, restoreReq)
}()
ctx := testutil.Context(t, testutil.WaitLong)
updated, err := client.UpdateTemplateMeta(ctx, reference.ID, tc.req)
_, err := client.UpdateTemplateMeta(ctx, reference.ID, tc.req)
require.NoError(t, err)
updated, err := client.Template(ctx, reference.ID)
require.NoError(t, err)
assert.Equal(t, tc.expected.displayName, updated.DisplayName)
assert.Equal(t, tc.expected.description, updated.Description)
@@ -1748,6 +1762,78 @@ func TestPatchTemplateMeta(t *testing.T) {
})
}
})
// EmptyBodyPreservesAllFields ensures the PATCH endpoint treats an empty
// body as a no-op so that omitted fields do not overwrite existing values.
t.Run("EmptyBodyPreservesAllFields", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DisplayName = "Original Display"
ctr.Description = "Original description"
ctr.Icon = "/icon/original.png"
ctr.DefaultTTLMillis = ptr.Ref((24 * time.Hour).Milliseconds())
ctr.AllowUserCancelWorkspaceJobs = ptr.Ref(true)
})
ctx := testutil.Context(t, testutil.WaitLong)
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{})
require.NoError(t, err)
updated, err := client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.DisplayName, updated.DisplayName)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.AllowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs)
assert.Equal(t, template.RequireActiveVersion, updated.RequireActiveVersion)
})
// PartialUpdatePreservesOtherFields ensures sending a single field on the
// PATCH body changes only that field and leaves the others alone. This is
// the headline behavior PLAT-184 enables: previously, omitted booleans
// were silently overwritten with false because the SDK type used
// non-pointer booleans.
t.Run("PartialUpdatePreservesOtherFields", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.AllowUserCancelWorkspaceJobs = ptr.Ref(true)
ctr.DefaultTTLMillis = ptr.Ref((24 * time.Hour).Milliseconds())
})
require.True(t, template.AllowUserCancelWorkspaceJobs)
require.Equal(t, (24 * time.Hour).Milliseconds(), template.DefaultTTLMillis)
ctx := testutil.Context(t, testutil.WaitLong)
// Sending only DefaultTTLMillis must not flip AllowUserCancelWorkspaceJobs
// to false.
newTTL := (12 * time.Hour).Milliseconds()
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
DefaultTTLMillis: &newTTL,
})
require.NoError(t, err)
assert.Equal(t, newTTL, updated.DefaultTTLMillis)
assert.True(t, updated.AllowUserCancelWorkspaceJobs, "omitted bool field must not be overwritten")
// Conversely, sending only AllowUserCancelWorkspaceJobs must not zero
// out DefaultTTLMillis.
updated, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
AllowUserCancelWorkspaceJobs: ptr.Ref(false),
})
require.NoError(t, err)
assert.False(t, updated.AllowUserCancelWorkspaceJobs)
assert.Equal(t, newTTL, updated.DefaultTTLMillis, "omitted int64 field must not be overwritten")
})
}
func TestDeleteTemplate(t *testing.T) {
+1 -1
View File
@@ -667,7 +667,7 @@ func TestWorkspaceAgentAppStatus_ActivityBump(t *testing.T) {
// Configure template with activity_bump to enable deadline bumping.
_, err := client.UpdateTemplateMeta(ctx, r.Template.ID, codersdk.UpdateTemplateMeta{
ActivityBumpMillis: time.Hour.Milliseconds(),
ActivityBumpMillis: ptr.Ref(time.Hour.Milliseconds()),
})
require.NoError(t, err)
+1 -1
View File
@@ -4756,7 +4756,7 @@ func TestWorkspaceUsageTracking(t *testing.T) {
DefaultTTL: int64(8 * time.Hour),
})
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
ActivityBumpMillis: 8 * time.Hour.Milliseconds(),
ActivityBumpMillis: ptr.Ref(8 * time.Hour.Milliseconds()),
})
require.NoError(t, err)
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
+19 -19
View File
@@ -215,40 +215,43 @@ type ACLAvailable struct {
Groups []Group `json:"groups"`
}
// UpdateTemplateMeta is the request body for the PATCH /templates/{template}
// endpoint. All fields are optional. Fields that are nil are not modified.
type UpdateTemplateMeta struct {
Name string `json:"name,omitempty" validate:"omitempty,template_name"`
Name *string `json:"name,omitempty" validate:"omitempty,template_name"`
DisplayName *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
Description *string `json:"description,omitempty"`
Icon *string `json:"icon,omitempty"`
DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"`
DefaultTTLMillis *int64 `json:"default_ttl_ms,omitempty"`
// ActivityBumpMillis allows optionally specifying the activity bump
// duration for all workspaces created from this template. Defaults to 1h
// but can be set to 0 to disable activity bumping.
ActivityBumpMillis int64 `json:"activity_bump_ms,omitempty"`
ActivityBumpMillis *int64 `json:"activity_bump_ms,omitempty"`
// AutostopRequirement and AutostartRequirement can only be set if your license
// includes the advanced template scheduling feature. If you attempt to set this
// value while unlicensed, it will be ignored.
AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"`
AutostartRequirement *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"`
AllowUserAutostart bool `json:"allow_user_autostart,omitempty"`
AllowUserAutostop bool `json:"allow_user_autostop,omitempty"`
AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"`
TimeTilDormantMillis int64 `json:"time_til_dormant_ms,omitempty"`
TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms,omitempty"`
AllowUserAutostart *bool `json:"allow_user_autostart,omitempty"`
AllowUserAutostop *bool `json:"allow_user_autostop,omitempty"`
AllowUserCancelWorkspaceJobs *bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
FailureTTLMillis *int64 `json:"failure_ttl_ms,omitempty"`
TimeTilDormantMillis *int64 `json:"time_til_dormant_ms,omitempty"`
TimeTilDormantAutoDeleteMillis *int64 `json:"time_til_dormant_autodelete_ms,omitempty"`
// UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces
// spawned from the template. This is useful for preventing workspaces being
// immediately locked when updating the inactivity_ttl field to a new, shorter
// value.
UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"`
// UpdateWorkspaceDormant updates the dormant_at field of workspaces spawned
// from the template. This is useful for preventing dormant workspaces being immediately
// deleted when updating the dormant_ttl field to a new, shorter value.
UpdateWorkspaceDormantAt bool `json:"update_workspace_dormant_at"`
UpdateWorkspaceLastUsedAt *bool `json:"update_workspace_last_used_at,omitempty"`
// UpdateWorkspaceDormantAt updates the dormant_at field of workspaces spawned
// from the template. This is useful for preventing dormant workspaces being
// immediately deleted when updating the dormant_ttl field to a new, shorter
// value.
UpdateWorkspaceDormantAt *bool `json:"update_workspace_dormant_at,omitempty"`
// RequireActiveVersion mandates workspaces built using this template
// use the active version of the template. This option has no
// effect on template admins.
RequireActiveVersion bool `json:"require_active_version,omitempty"`
RequireActiveVersion *bool `json:"require_active_version,omitempty"`
// DeprecationMessage if set, will mark the template as deprecated and block
// any new workspaces from using this template.
// If passed an empty string, will remove the deprecated message, making
@@ -259,7 +262,7 @@ type UpdateTemplateMeta struct {
// If this is set to true, the template will not be available to all users,
// and must be explicitly granted to users or groups in the permissions settings
// of the template.
DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"`
DisableEveryoneGroupAccess *bool `json:"disable_everyone_group_access,omitempty"`
MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level,omitempty"`
CORSBehavior *CORSBehavior `json:"cors_behavior,omitempty"`
// UseClassicParameterFlow is a flag that switches the default behavior to use the classic
@@ -353,9 +356,6 @@ func (c *Client) UpdateTemplateMeta(ctx context.Context, templateID uuid.UUID, r
return Template{}, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotModified {
return Template{}, xerrors.New("template metadata not modified")
}
if res.StatusCode != http.StatusOK {
return Template{}, ReadBodyAsError(res)
}
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
@@ -46,7 +47,7 @@ func TestStart(t *testing.T) {
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, oldVersion.ID)
require.Equal(t, oldVersion.ID, template.ActiveVersionID)
template = coderdtest.UpdateTemplateMeta(t, templateAdminClient, template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.True(t, template.RequireActiveVersion)
+20 -20
View File
@@ -218,20 +218,20 @@ func TestTemplateEdit(t *testing.T) {
}
template, err := ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{
Name: expectedName,
Name: ptr.Ref(expectedName),
DisplayName: &expectedDisplayName,
Description: &expectedDescription,
Icon: &expectedIcon,
DefaultTTLMillis: expectedDefaultTTLMillis,
AllowUserAutostop: expectedAllowAutostop,
AllowUserAutostart: expectedAllowAutostart,
FailureTTLMillis: expectedFailureTTLMillis,
TimeTilDormantMillis: expectedDormancyMillis,
TimeTilDormantAutoDeleteMillis: expectedAutoDeleteMillis,
RequireActiveVersion: expectedRequireActiveVersion,
DefaultTTLMillis: ptr.Ref(expectedDefaultTTLMillis),
AllowUserAutostop: ptr.Ref(expectedAllowAutostop),
AllowUserAutostart: ptr.Ref(expectedAllowAutostart),
FailureTTLMillis: ptr.Ref(expectedFailureTTLMillis),
TimeTilDormantMillis: ptr.Ref(expectedDormancyMillis),
TimeTilDormantAutoDeleteMillis: ptr.Ref(expectedAutoDeleteMillis),
RequireActiveVersion: ptr.Ref(expectedRequireActiveVersion),
DeprecationMessage: ptr.Ref(deprecationMessage),
DisableEveryoneGroupAccess: expectedDisableEveryone,
AllowUserCancelWorkspaceJobs: expectedAllowCancelJobs,
DisableEveryoneGroupAccess: ptr.Ref(expectedDisableEveryone),
AllowUserCancelWorkspaceJobs: ptr.Ref(expectedAllowCancelJobs),
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: expectedAutostartDaysOfWeek,
},
@@ -266,20 +266,20 @@ func TestTemplateEdit(t *testing.T) {
expectedAutoStopWeeks = 2
template, err = ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{
Name: expectedName,
Name: ptr.Ref(expectedName),
DisplayName: &expectedDisplayName,
Description: &expectedDescription,
Icon: &expectedIcon,
DefaultTTLMillis: expectedDefaultTTLMillis,
AllowUserAutostop: expectedAllowAutostop,
AllowUserAutostart: expectedAllowAutostart,
FailureTTLMillis: expectedFailureTTLMillis,
TimeTilDormantMillis: expectedDormancyMillis,
TimeTilDormantAutoDeleteMillis: expectedAutoDeleteMillis,
RequireActiveVersion: expectedRequireActiveVersion,
DefaultTTLMillis: ptr.Ref(expectedDefaultTTLMillis),
AllowUserAutostop: ptr.Ref(expectedAllowAutostop),
AllowUserAutostart: ptr.Ref(expectedAllowAutostart),
FailureTTLMillis: ptr.Ref(expectedFailureTTLMillis),
TimeTilDormantMillis: ptr.Ref(expectedDormancyMillis),
TimeTilDormantAutoDeleteMillis: ptr.Ref(expectedAutoDeleteMillis),
RequireActiveVersion: ptr.Ref(expectedRequireActiveVersion),
DeprecationMessage: ptr.Ref(deprecationMessage),
DisableEveryoneGroupAccess: expectedDisableEveryone,
AllowUserCancelWorkspaceJobs: expectedAllowCancelJobs,
DisableEveryoneGroupAccess: ptr.Ref(expectedDisableEveryone),
AllowUserCancelWorkspaceJobs: ptr.Ref(expectedAllowCancelJobs),
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: expectedAutostartDaysOfWeek,
},
+33 -31
View File
@@ -186,14 +186,16 @@ func TestTemplates(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// OK
// OK: setting the same level is a no-op under the new PATCH semantics
// (304 Not Modified) but must not be a server error.
var level codersdk.WorkspaceAgentPortShareLevel = codersdk.WorkspaceAgentPortShareLevelPublic
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
MaxPortShareLevel: &level,
})
require.NoError(t, err)
assert.Equal(t, level, updated.MaxPortShareLevel)
template, err = client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Equal(t, level, template.MaxPortShareLevel)
// Invalid level
level = "invalid"
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
@@ -258,7 +260,7 @@ func TestTemplates(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
@@ -275,7 +277,7 @@ func TestTemplates(t *testing.T) {
// Ensure a missing field is a noop
updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: ptr.Ref(template.Icon + "something"),
@@ -312,7 +314,7 @@ func TestTemplates(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
_, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
@@ -348,12 +350,12 @@ func TestTemplates(t *testing.T) {
ctx := context.Background()
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: time.Hour.Milliseconds(),
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: []string{"monday", "saturday"},
Weeks: 3,
@@ -402,14 +404,14 @@ func TestTemplates(t *testing.T) {
)
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
FailureTTLMillis: failureTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
})
require.NoError(t, err)
require.Equal(t, failureTTL.Milliseconds(), updated.FailureTTLMillis)
@@ -471,14 +473,14 @@ func TestTemplates(t *testing.T) {
// nolint: paralleltest // context is from parent t.Run
t.Run(c.Name, func(t *testing.T) {
_, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
TimeTilDormantMillis: c.TimeTilDormantMS,
FailureTTLMillis: c.FailureTTLMS,
TimeTilDormantAutoDeleteMillis: c.DormantAutoDeleteMS,
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
TimeTilDormantMillis: ptr.Ref(c.TimeTilDormantMS),
FailureTTLMillis: ptr.Ref(c.FailureTTLMS),
TimeTilDormantAutoDeleteMillis: ptr.Ref(c.DormantAutoDeleteMS),
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
@@ -529,7 +531,7 @@ func TestTemplates(t *testing.T) {
dormantTTL := time.Minute
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
})
require.NoError(t, err)
require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
@@ -547,7 +549,7 @@ func TestTemplates(t *testing.T) {
// Disable the time_til_dormant_auto_delete on the template, then we can assert that the workspaces
// no longer have a deleting_at field.
updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: 0,
TimeTilDormantAutoDeleteMillis: ptr.Ref[int64](0),
})
require.NoError(t, err)
require.EqualValues(t, 0, updated.TimeTilDormantAutoDeleteMillis)
@@ -604,8 +606,8 @@ func TestTemplates(t *testing.T) {
dormantTTL := time.Minute
//nolint:gocritic // non-template-admin cannot update template meta
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
UpdateWorkspaceDormantAt: true,
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
UpdateWorkspaceDormantAt: ptr.Ref(true),
})
require.NoError(t, err)
require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
@@ -661,8 +663,8 @@ func TestTemplates(t *testing.T) {
inactivityTTL := time.Minute
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
UpdateWorkspaceLastUsedAt: true,
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
UpdateWorkspaceLastUsedAt: ptr.Ref(true),
})
require.NoError(t, err)
require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis)
@@ -706,14 +708,14 @@ func TestTemplates(t *testing.T) {
// Update the field and assert it persists.
updatedTemplate, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: false,
RequireActiveVersion: ptr.Ref(false),
})
require.NoError(t, err)
require.False(t, updatedTemplate.RequireActiveVersion)
// Flip it back to ensure we aren't hardcoding to a default value.
updatedTemplate, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.NoError(t, err)
require.True(t, updatedTemplate.RequireActiveVersion)
@@ -1003,12 +1005,12 @@ func TestTemplateACL(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 1, len(acl.Groups))
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DisableEveryoneGroupAccess: true,
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
DisableEveryoneGroupAccess: ptr.Ref(true),
})
require.NoError(t, err)
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
@@ -43,7 +44,7 @@ func TestWorkspaceBuild(t *testing.T) {
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, tplAv1.ID)
require.Equal(t, tplAv1.ID, tplA.ActiveVersionID)
tplA = coderdtest.UpdateTemplateMeta(t, ownerClient, tplA.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.True(t, tplA.RequireActiveVersion)
tplAv2 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
@@ -57,7 +58,7 @@ func TestWorkspaceBuild(t *testing.T) {
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, tplBv1.ID)
require.Equal(t, tplBv1.ID, tplB.ActiveVersionID)
tplB = coderdtest.UpdateTemplateMeta(t, ownerClient, tplB.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.True(t, tplB.RequireActiveVersion)
+8 -8
View File
@@ -784,7 +784,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
}).Do().Template
template := coderdtest.UpdateTemplateMeta(t, client, tpl.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantMillis: inactiveTTL.Milliseconds(),
TimeTilDormantMillis: ptr.Ref(inactiveTTL.Milliseconds()),
})
resp := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
@@ -1260,7 +1260,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
require.Len(t, stats.Transitions, 0)
_, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
})
require.NoError(t, err)
@@ -1334,7 +1334,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Now that we've validated that the workspace is eligible for autostart
// lets cause it to become dormant.
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantMillis: inactiveTTL.Milliseconds(),
TimeTilDormantMillis: ptr.Ref(inactiveTTL.Milliseconds()),
})
require.NoError(t, err)
@@ -1433,7 +1433,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Enable auto-deletion for the template.
_, err = templateAdmin.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: transitionTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: ptr.Ref(transitionTTL.Milliseconds()),
})
require.NoError(t, err)
@@ -1538,8 +1538,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Update the template to require the promoted version.
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
AllowUserAutostart: true,
RequireActiveVersion: ptr.Ref(true),
AllowUserAutostart: ptr.Ref(true),
})
require.NoError(t, err)
@@ -1832,7 +1832,7 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
templateTTL = 72 * time.Hour.Milliseconds()
ctx := testutil.Context(t, testutil.WaitShort)
template = coderdtest.UpdateTemplateMeta(t, client, template.ID, codersdk.UpdateTemplateMeta{
DefaultTTLMillis: templateTTL,
DefaultTTLMillis: ptr.Ref(templateTTL),
})
workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
@@ -4068,7 +4068,7 @@ func TestResolveAutostart(t *testing.T) {
defer cancel()
_, err := ownerClient.UpdateTemplateMeta(ctx, version1.Template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.NoError(t, err)
+11 -6
View File
@@ -8259,6 +8259,10 @@ export interface UpdateTemplateACL {
}
// From codersdk/templates.go
/**
* UpdateTemplateMeta is the request body for the PATCH /templates/{template}
* endpoint. All fields are optional. Fields that are nil are not modified.
*/
export interface UpdateTemplateMeta {
readonly name?: string;
readonly display_name?: string;
@@ -8290,13 +8294,14 @@ export interface UpdateTemplateMeta {
* immediately locked when updating the inactivity_ttl field to a new, shorter
* value.
*/
readonly update_workspace_last_used_at: boolean;
readonly update_workspace_last_used_at?: boolean;
/**
* UpdateWorkspaceDormant updates the dormant_at field of workspaces spawned
* from the template. This is useful for preventing dormant workspaces being immediately
* deleted when updating the dormant_ttl field to a new, shorter value.
* UpdateWorkspaceDormantAt updates the dormant_at field of workspaces spawned
* from the template. This is useful for preventing dormant workspaces being
* immediately deleted when updating the dormant_ttl field to a new, shorter
* value.
*/
readonly update_workspace_dormant_at: boolean;
readonly update_workspace_dormant_at?: boolean;
/**
* RequireActiveVersion mandates workspaces built using this template
* use the active version of the template. This option has no
@@ -8317,7 +8322,7 @@ export interface UpdateTemplateMeta {
* and must be explicitly granted to users or groups in the permissions settings
* of the template.
*/
readonly disable_everyone_group_access: boolean;
readonly disable_everyone_group_access?: boolean;
readonly max_port_share_level?: WorkspaceAgentPortShareLevel;
readonly cors_behavior?: CORSBehavior;
/**