mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
+20
-11
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+1
-1
@@ -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": {
|
||||
|
||||
Generated
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+53
-151
@@ -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,58 +791,50 @@ 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,
|
||||
UserAutostartEnabled: resolved.allowUserAutostart,
|
||||
UserAutostopEnabled: resolved.allowUserAutostop,
|
||||
DefaultTTL: defaultTTL,
|
||||
ActivityBump: activityBump,
|
||||
AutostopRequirement: schedule.TemplateAutostopRequirement{
|
||||
DaysOfWeek: autostopRequirementDaysOfWeekParsed,
|
||||
Weeks: req.AutostopRequirement.Weeks,
|
||||
DaysOfWeek: resolved.autostopRequirementDaysOfWeekParsed,
|
||||
Weeks: resolved.autostopRequirementWeeks,
|
||||
},
|
||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||
DaysOfWeek: autostartRequirementDaysOfWeekParsed,
|
||||
DaysOfWeek: resolved.autostartRequirementDaysOfWeekParsed,
|
||||
},
|
||||
FailureTTL: failureTTL,
|
||||
TimeTilDormant: inactivityTTL,
|
||||
TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
|
||||
UpdateWorkspaceLastUsedAt: updateWorkspaceLastUsedAt,
|
||||
UpdateWorkspaceDormantAt: req.UpdateWorkspaceDormantAt,
|
||||
UpdateWorkspaceDormantAt: resolved.updateWorkspaceDormantAtIntent,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set template schedule options: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+162
-76
@@ -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,
|
||||
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,
|
||||
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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Generated
+11
-6
@@ -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;
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user