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"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/cli/cliui"
|
"github.com/coder/coder/v2/cli/cliui"
|
||||||
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/pretty"
|
"github.com/coder/pretty"
|
||||||
"github.com/coder/serpent"
|
"github.com/coder/serpent"
|
||||||
@@ -88,6 +89,10 @@ func (r *RootCmd) templateEdit() *serpent.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default values
|
// Default values
|
||||||
|
if !userSetOption(inv, "name") {
|
||||||
|
name = template.Name
|
||||||
|
}
|
||||||
|
|
||||||
if !userSetOption(inv, "description") {
|
if !userSetOption(inv, "description") {
|
||||||
description = template.Description
|
description = template.Description
|
||||||
}
|
}
|
||||||
@@ -169,12 +174,12 @@ func (r *RootCmd) templateEdit() *serpent.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
Name: name,
|
Name: &name,
|
||||||
DisplayName: &displayName,
|
DisplayName: &displayName,
|
||||||
Description: &description,
|
Description: &description,
|
||||||
Icon: &icon,
|
Icon: &icon,
|
||||||
DefaultTTLMillis: defaultTTL.Milliseconds(),
|
DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
|
||||||
ActivityBumpMillis: activityBump.Milliseconds(),
|
ActivityBumpMillis: ptr.Ref(activityBump.Milliseconds()),
|
||||||
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
||||||
DaysOfWeek: autostopRequirementDaysOfWeek,
|
DaysOfWeek: autostopRequirementDaysOfWeek,
|
||||||
Weeks: autostopRequirementWeeks,
|
Weeks: autostopRequirementWeeks,
|
||||||
@@ -182,15 +187,19 @@ func (r *RootCmd) templateEdit() *serpent.Command {
|
|||||||
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
||||||
DaysOfWeek: autostartRequirementDaysOfWeek,
|
DaysOfWeek: autostartRequirementDaysOfWeek,
|
||||||
},
|
},
|
||||||
FailureTTLMillis: failureTTL.Milliseconds(),
|
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
|
||||||
TimeTilDormantMillis: dormancyThreshold.Milliseconds(),
|
TimeTilDormantMillis: ptr.Ref(dormancyThreshold.Milliseconds()),
|
||||||
TimeTilDormantAutoDeleteMillis: dormancyAutoDeletion.Milliseconds(),
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormancyAutoDeletion.Milliseconds()),
|
||||||
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: &allowUserCancelWorkspaceJobs,
|
||||||
AllowUserAutostart: allowUserAutostart,
|
AllowUserAutostart: &allowUserAutostart,
|
||||||
AllowUserAutostop: allowUserAutostop,
|
AllowUserAutostop: &allowUserAutostop,
|
||||||
RequireActiveVersion: requireActiveVersion,
|
RequireActiveVersion: &requireActiveVersion,
|
||||||
DeprecationMessage: deprecated,
|
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)
|
_, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req)
|
||||||
|
|||||||
@@ -101,8 +101,7 @@ func TestTemplateEdit(t *testing.T) {
|
|||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
err := inv.WithContext(ctx).Run()
|
err := inv.WithContext(ctx).Run()
|
||||||
|
require.NoError(t, err)
|
||||||
require.ErrorContains(t, err, "not modified")
|
|
||||||
|
|
||||||
// Assert that the template metadata did not change.
|
// Assert that the template metadata did not change.
|
||||||
updated, err := client.Template(context.Background(), template.ID)
|
updated, err := client.Template(context.Background(), template.ID)
|
||||||
@@ -751,8 +750,10 @@ func TestTemplateEdit(t *testing.T) {
|
|||||||
var req codersdk.UpdateTemplateMeta
|
var req codersdk.UpdateTemplateMeta
|
||||||
err = json.Unmarshal(body, &req)
|
err = json.Unmarshal(body, &req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, req.AllowUserAutostart)
|
require.NotNil(t, req.AllowUserAutostart)
|
||||||
assert.False(t, req.AllowUserAutostop)
|
assert.False(t, *req.AllowUserAutostart)
|
||||||
|
require.NotNil(t, req.AllowUserAutostop)
|
||||||
|
assert.False(t, *req.AllowUserAutostop)
|
||||||
|
|
||||||
r.Body = io.NopCloser(bytes.NewReader(body))
|
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||||
updateTemplateCalled.Add(1)
|
updateTemplateCalled.Add(1)
|
||||||
|
|||||||
Generated
+1
-1
@@ -23367,7 +23367,7 @@ const docTemplate = `{
|
|||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"update_workspace_dormant_at": {
|
"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"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"update_workspace_last_used_at": {
|
"update_workspace_last_used_at": {
|
||||||
|
|||||||
Generated
+1
-1
@@ -21512,7 +21512,7 @@
|
|||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"update_workspace_dormant_at": {
|
"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"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"update_workspace_last_used_at": {
|
"update_workspace_last_used_at": {
|
||||||
|
|||||||
@@ -550,8 +550,8 @@ func TestExecutorAutostopAIAgentActivity(t *testing.T) {
|
|||||||
|
|
||||||
// Given: template has activity bump enabled.
|
// Given: template has activity bump enabled.
|
||||||
_, err := client.UpdateTemplateMeta(ctx, r.Template.ID, codersdk.UpdateTemplateMeta{
|
_, err := client.UpdateTemplateMeta(ctx, r.Template.ID, codersdk.UpdateTemplateMeta{
|
||||||
DefaultTTLMillis: (2 * time.Hour).Milliseconds(),
|
DefaultTTLMillis: ptr.Ref((2 * time.Hour).Milliseconds()),
|
||||||
ActivityBumpMillis: time.Hour.Milliseconds(),
|
ActivityBumpMillis: ptr.Ref(time.Hour.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1905,7 +1905,7 @@ func TestExecutorTaskWorkspace(t *testing.T) {
|
|||||||
|
|
||||||
if defaultTTL > 0 {
|
if defaultTTL > 0 {
|
||||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
DefaultTTLMillis: defaultTTL.Milliseconds(),
|
DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-168
@@ -35,6 +35,8 @@ import (
|
|||||||
"github.com/coder/coder/v2/examples"
|
"github.com/coder/coder/v2/examples"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultRequirementWeeks = 1
|
||||||
|
|
||||||
// Returns a single template.
|
// Returns a single template.
|
||||||
//
|
//
|
||||||
// @Summary Get template settings by ID
|
// @Summary Get template settings by ID
|
||||||
@@ -679,72 +681,47 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// resolveTemplateMetaUpdate falls back to the existing template's
|
||||||
validErrs []codersdk.ValidationError
|
// values for any pointer field that is nil in the request, so that
|
||||||
autostopRequirementDaysOfWeekParsed uint8
|
// omitted fields are preserved instead of being overwritten with
|
||||||
autostartRequirementDaysOfWeekParsed uint8
|
// Go zero values.
|
||||||
)
|
resolved, validErrs := resolveTemplateMetaUpdate(template, scheduleOpts, req)
|
||||||
if req.DefaultTTLMillis < 0 {
|
|
||||||
|
if resolved.defaultTTLMillis < 0 {
|
||||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."})
|
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."})
|
validErrs = append(validErrs, codersdk.ValidationError{Field: "activity_bump_ms", Detail: "Must be a positive integer."})
|
||||||
}
|
}
|
||||||
|
if resolved.autostopRequirementWeeks > schedule.MaxTemplateAutostopRequirementWeeks {
|
||||||
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 {
|
|
||||||
validErrs = append(validErrs, codersdk.ValidationError{Field: "autostop_requirement.weeks", Detail: fmt.Sprintf("Must be less than %d.", 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.
|
// AutostopRequirement.Weeks is allowed to be negative on input but is
|
||||||
deprecationMessage := template.Deprecated
|
// surfaced as a validation error. resolveTemplateMetaUpdate normalizes
|
||||||
if req.DeprecationMessage != nil {
|
// 0 -> 1 but preserves negatives so the caller can reject them.
|
||||||
deprecationMessage = *req.DeprecationMessage
|
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
|
// The minimum valid value for a dormant TTL is 1 minute. This is
|
||||||
// to ensure an uninformed user does not send an unintentionally
|
// to ensure an uninformed user does not send an unintentionally
|
||||||
// small number resulting in potentially catastrophic consequences.
|
// small number resulting in potentially catastrophic consequences.
|
||||||
const minTTL = 1000 * 60
|
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."})
|
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."})
|
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."})
|
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
|
maxPortShareLevel := template.MaxPortSharingLevel
|
||||||
if req.MaxPortShareLevel != nil && *req.MaxPortShareLevel != portSharer.ConvertMaxLevel(template.MaxPortSharingLevel) {
|
if req.MaxPortShareLevel != nil && *req.MaxPortShareLevel != portSharer.ConvertMaxLevel(template.MaxPortSharingLevel) {
|
||||||
err := portSharer.ValidateTemplateMaxLevel(*req.MaxPortShareLevel)
|
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 {
|
if len(validErrs) > 0 {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
Message: "Invalid request to update template metadata!",
|
Message: "Invalid request to update template metadata!",
|
||||||
@@ -776,57 +740,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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
|
var updated database.Template
|
||||||
err = api.Database.InTx(func(tx database.Store) error {
|
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 {
|
if template.MaxPortSharingLevel != maxPortShareLevel {
|
||||||
switch maxPortShareLevel {
|
switch maxPortShareLevel {
|
||||||
case database.AppSharingLevelOwner:
|
case database.AppSharingLevelOwner:
|
||||||
@@ -846,25 +761,25 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{
|
err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{
|
||||||
ID: template.ID,
|
ID: template.ID,
|
||||||
UpdatedAt: dbtime.Now(),
|
UpdatedAt: dbtime.Now(),
|
||||||
Name: name,
|
Name: resolved.name,
|
||||||
DisplayName: displayName,
|
DisplayName: resolved.displayName,
|
||||||
Description: description,
|
Description: resolved.description,
|
||||||
Icon: icon,
|
Icon: resolved.icon,
|
||||||
AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: resolved.allowUserCancelWorkspaceJobs,
|
||||||
GroupACL: groupACL,
|
GroupACL: resolved.groupACL,
|
||||||
MaxPortSharingLevel: maxPortShareLevel,
|
MaxPortSharingLevel: maxPortShareLevel,
|
||||||
UseClassicParameterFlow: classicTemplateFlow,
|
UseClassicParameterFlow: resolved.useClassicTemplateFlow,
|
||||||
CorsBehavior: corsBehavior,
|
CorsBehavior: resolved.corsBehavior,
|
||||||
DisableModuleCache: disableModuleCache,
|
DisableModuleCache: resolved.disableModuleCache,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("update template metadata: %w", err)
|
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{
|
err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, template.ID, dbauthz.TemplateAccessControl{
|
||||||
RequireActiveVersion: req.RequireActiveVersion,
|
RequireActiveVersion: resolved.requireActiveVersion,
|
||||||
Deprecated: deprecationMessage,
|
Deprecated: resolved.deprecationMessage,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("set template access control: %w", err)
|
return xerrors.Errorf("set template access control: %w", err)
|
||||||
@@ -876,50 +791,42 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
return xerrors.Errorf("fetch updated template metadata: %w", err)
|
return xerrors.Errorf("fetch updated template metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultTTL := time.Duration(req.DefaultTTLMillis) * time.Millisecond
|
defaultTTL := time.Duration(resolved.defaultTTLMillis) * time.Millisecond
|
||||||
activityBump := time.Duration(req.ActivityBumpMillis) * time.Millisecond
|
activityBump := time.Duration(resolved.activityBumpMillis) * time.Millisecond
|
||||||
failureTTL := time.Duration(req.FailureTTLMillis) * time.Millisecond
|
failureTTL := time.Duration(resolved.failureTTLMillis) * time.Millisecond
|
||||||
inactivityTTL := time.Duration(req.TimeTilDormantMillis) * time.Millisecond
|
inactivityTTL := time.Duration(resolved.timeTilDormantMillis) * time.Millisecond
|
||||||
timeTilDormantAutoDelete := time.Duration(req.TimeTilDormantAutoDeleteMillis) * 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
|
var updateWorkspaceLastUsedAt workspacestats.UpdateTemplateWorkspacesLastUsedAtFunc
|
||||||
if req.UpdateWorkspaceLastUsedAt {
|
if resolved.updateWorkspaceLastUsedAtIntent {
|
||||||
updateWorkspaceLastUsedAt = workspacestats.UpdateTemplateWorkspacesLastUsedAt
|
updateWorkspaceLastUsedAt = workspacestats.UpdateTemplateWorkspacesLastUsedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
if defaultTTL != time.Duration(template.DefaultTTL) ||
|
updated, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, updated, schedule.TemplateScheduleOptions{
|
||||||
activityBump != time.Duration(template.ActivityBump) ||
|
// Some of these values are enterprise-only, but the
|
||||||
autostopRequirementDaysOfWeekParsed != scheduleOpts.AutostopRequirement.DaysOfWeek ||
|
// TemplateScheduleStore will handle avoiding setting them if
|
||||||
autostartRequirementDaysOfWeekParsed != scheduleOpts.AutostartRequirement.DaysOfWeek ||
|
// unlicensed.
|
||||||
req.AutostopRequirement.Weeks != scheduleOpts.AutostopRequirement.Weeks ||
|
UserAutostartEnabled: resolved.allowUserAutostart,
|
||||||
failureTTL != time.Duration(template.FailureTTL) ||
|
UserAutostopEnabled: resolved.allowUserAutostop,
|
||||||
inactivityTTL != time.Duration(template.TimeTilDormant) ||
|
DefaultTTL: defaultTTL,
|
||||||
timeTilDormantAutoDelete != time.Duration(template.TimeTilDormantAutoDelete) ||
|
ActivityBump: activityBump,
|
||||||
req.AllowUserAutostart != template.AllowUserAutostart ||
|
AutostopRequirement: schedule.TemplateAutostopRequirement{
|
||||||
req.AllowUserAutostop != template.AllowUserAutostop {
|
DaysOfWeek: resolved.autostopRequirementDaysOfWeekParsed,
|
||||||
updated, err = (*api.TemplateScheduleStore.Load()).Set(ctx, tx, updated, schedule.TemplateScheduleOptions{
|
Weeks: resolved.autostopRequirementWeeks,
|
||||||
// Some of these values are enterprise-only, but the
|
},
|
||||||
// TemplateScheduleStore will handle avoiding setting them if
|
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
||||||
// unlicensed.
|
DaysOfWeek: resolved.autostartRequirementDaysOfWeekParsed,
|
||||||
UserAutostartEnabled: req.AllowUserAutostart,
|
},
|
||||||
UserAutostopEnabled: req.AllowUserAutostop,
|
FailureTTL: failureTTL,
|
||||||
DefaultTTL: defaultTTL,
|
TimeTilDormant: inactivityTTL,
|
||||||
ActivityBump: activityBump,
|
TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
|
||||||
AutostopRequirement: schedule.TemplateAutostopRequirement{
|
UpdateWorkspaceLastUsedAt: updateWorkspaceLastUsedAt,
|
||||||
DaysOfWeek: autostopRequirementDaysOfWeekParsed,
|
UpdateWorkspaceDormantAt: resolved.updateWorkspaceDormantAtIntent,
|
||||||
Weeks: req.AutostopRequirement.Weeks,
|
})
|
||||||
},
|
if err != nil {
|
||||||
AutostartRequirement: schedule.TemplateAutostartRequirement{
|
return xerrors.Errorf("set template schedule options: %w", err)
|
||||||
DaysOfWeek: autostartRequirementDaysOfWeekParsed,
|
|
||||||
},
|
|
||||||
FailureTTL: failureTTL,
|
|
||||||
TimeTilDormant: inactivityTTL,
|
|
||||||
TimeTilDormantAutoDelete: timeTilDormantAutoDelete,
|
|
||||||
UpdateWorkspaceLastUsedAt: updateWorkspaceLastUsedAt,
|
|
||||||
UpdateWorkspaceDormantAt: req.UpdateWorkspaceDormantAt,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return xerrors.Errorf("set template schedule options: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -927,7 +834,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if database.IsUniqueViolation(err, database.UniqueTemplatesOrganizationIDNameIndex) {
|
if database.IsUniqueViolation(err, database.UniqueTemplatesOrganizationIDNameIndex) {
|
||||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
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{{
|
Validations: []codersdk.ValidationError{{
|
||||||
Field: "name",
|
Field: "name",
|
||||||
Detail: "This value is already in use and should be unique.",
|
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
|
aReq.New = updated
|
||||||
|
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, api.convertTemplate(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+165
-79
@@ -901,13 +901,13 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
assert.Equal(t, (1 * time.Hour).Milliseconds(), template.ActivityBumpMillis)
|
assert.Equal(t, (1 * time.Hour).Milliseconds(), template.ActivityBumpMillis)
|
||||||
|
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
Name: "new-template-name",
|
Name: ptr.Ref("new-template-name"),
|
||||||
DisplayName: ptr.Ref("Displayed Name 456"),
|
DisplayName: ptr.Ref("Displayed Name 456"),
|
||||||
Description: ptr.Ref("lorem ipsum dolor sit amet et cetera"),
|
Description: ptr.Ref("lorem ipsum dolor sit amet et cetera"),
|
||||||
Icon: ptr.Ref("/icon/new-icon.png"),
|
Icon: ptr.Ref("/icon/new-icon.png"),
|
||||||
DefaultTTLMillis: 12 * time.Hour.Milliseconds(),
|
DefaultTTLMillis: ptr.Ref(12 * time.Hour.Milliseconds()),
|
||||||
ActivityBumpMillis: 3 * time.Hour.Milliseconds(),
|
ActivityBumpMillis: ptr.Ref(3 * time.Hour.Milliseconds()),
|
||||||
AllowUserCancelWorkspaceJobs: false,
|
AllowUserCancelWorkspaceJobs: ptr.Ref(false),
|
||||||
}
|
}
|
||||||
// It is unfortunate we need to sleep, but the test can fail if the
|
// It is unfortunate we need to sleep, but the test can fail if the
|
||||||
// updatedAt is too close together.
|
// updatedAt is too close together.
|
||||||
@@ -918,25 +918,25 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
updated, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
|
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.DisplayName, updated.DisplayName)
|
||||||
assert.Equal(t, *req.Description, updated.Description)
|
assert.Equal(t, *req.Description, updated.Description)
|
||||||
assert.Equal(t, *req.Icon, updated.Icon)
|
assert.Equal(t, *req.Icon, updated.Icon)
|
||||||
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
|
assert.Equal(t, *req.DefaultTTLMillis, updated.DefaultTTLMillis)
|
||||||
assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis)
|
assert.Equal(t, *req.ActivityBumpMillis, updated.ActivityBumpMillis)
|
||||||
assert.False(t, req.AllowUserCancelWorkspaceJobs)
|
assert.False(t, *req.AllowUserCancelWorkspaceJobs)
|
||||||
|
|
||||||
// Extra paranoid: did it _really_ happen?
|
// Extra paranoid: did it _really_ happen?
|
||||||
updated, err = client.Template(ctx, template.ID)
|
updated, err = client.Template(ctx, template.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
|
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.DisplayName, updated.DisplayName)
|
||||||
assert.Equal(t, *req.Description, updated.Description)
|
assert.Equal(t, *req.Description, updated.Description)
|
||||||
assert.Equal(t, *req.Icon, updated.Icon)
|
assert.Equal(t, *req.Icon, updated.Icon)
|
||||||
assert.Equal(t, req.DefaultTTLMillis, updated.DefaultTTLMillis)
|
assert.Equal(t, *req.DefaultTTLMillis, updated.DefaultTTLMillis)
|
||||||
assert.Equal(t, req.ActivityBumpMillis, updated.ActivityBumpMillis)
|
assert.Equal(t, *req.ActivityBumpMillis, updated.ActivityBumpMillis)
|
||||||
assert.False(t, req.AllowUserCancelWorkspaceJobs)
|
assert.False(t, *req.AllowUserCancelWorkspaceJobs)
|
||||||
|
|
||||||
require.Len(t, auditor.AuditLogs(), 5)
|
require.Len(t, auditor.AuditLogs(), 5)
|
||||||
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action)
|
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action)
|
||||||
@@ -957,7 +957,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
|
||||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template2.Name,
|
Name: &template2.Name,
|
||||||
})
|
})
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
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
|
// Ensure the same value port share level is a no-op
|
||||||
level = codersdk.WorkspaceAgentPortShareLevelPublic
|
level = codersdk.WorkspaceAgentPortShareLevelPublic
|
||||||
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: coderdtest.RandomUsername(t),
|
Name: ptr.Ref(coderdtest.RandomUsername(t)),
|
||||||
MaxPortShareLevel: &level,
|
MaxPortShareLevel: &level,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -1072,7 +1072,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
|
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
DefaultTTLMillis: 0,
|
DefaultTTLMillis: ptr.Ref(int64(0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're too fast! Sleep so we can be sure that updatedAt is greater
|
// 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)
|
updated, err := client.Template(ctx, template.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Greater(t, updated.UpdatedAt, template.UpdatedAt)
|
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.Empty(t, updated.DeprecationMessage)
|
||||||
assert.False(t, updated.Deprecated)
|
assert.False(t, updated.Deprecated)
|
||||||
})
|
})
|
||||||
@@ -1106,7 +1106,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
|
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
DefaultTTLMillis: -1,
|
DefaultTTLMillis: ptr.Ref(int64(-1)),
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
@@ -1163,16 +1163,16 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: &template.Name,
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
DefaultTTLMillis: 0,
|
DefaultTTLMillis: ptr.Ref(int64(0)),
|
||||||
AutostopRequirement: &template.AutostopRequirement,
|
AutostopRequirement: &template.AutostopRequirement,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
|
||||||
FailureTTLMillis: failureTTL.Milliseconds(),
|
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
|
||||||
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
|
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
|
||||||
TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(),
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(timeTilDormantAutoDelete.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1198,16 +1198,16 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: &template.Name,
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
DefaultTTLMillis: template.DefaultTTLMillis,
|
DefaultTTLMillis: &template.DefaultTTLMillis,
|
||||||
AutostopRequirement: &template.AutostopRequirement,
|
AutostopRequirement: &template.AutostopRequirement,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
|
||||||
FailureTTLMillis: failureTTL.Milliseconds(),
|
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
|
||||||
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
|
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
|
||||||
TimeTilDormantAutoDeleteMillis: timeTilDormantAutoDelete.Milliseconds(),
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(timeTilDormantAutoDelete.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Zero(t, got.FailureTTLMillis)
|
require.Zero(t, got.FailureTTLMillis)
|
||||||
@@ -1259,15 +1259,15 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
allowAutostart.Store(false)
|
allowAutostart.Store(false)
|
||||||
allowAutostop.Store(false)
|
allowAutostop.Store(false)
|
||||||
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: &template.Name,
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
DefaultTTLMillis: template.DefaultTTLMillis,
|
DefaultTTLMillis: &template.DefaultTTLMillis,
|
||||||
AutostopRequirement: &template.AutostopRequirement,
|
AutostopRequirement: &template.AutostopRequirement,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
|
||||||
AllowUserAutostart: allowAutostart.Load(),
|
AllowUserAutostart: ptr.Ref(allowAutostart.Load()),
|
||||||
AllowUserAutostop: allowAutostop.Load(),
|
AllowUserAutostop: ptr.Ref(allowAutostop.Load()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1290,16 +1290,15 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
got, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: &template.Name,
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
// Increase the default TTL to avoid error "not modified".
|
DefaultTTLMillis: ptr.Ref(template.DefaultTTLMillis + 1),
|
||||||
DefaultTTLMillis: template.DefaultTTLMillis + 1,
|
|
||||||
AutostopRequirement: &template.AutostopRequirement,
|
AutostopRequirement: &template.AutostopRequirement,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
|
||||||
AllowUserAutostart: false,
|
AllowUserAutostart: ptr.Ref(false),
|
||||||
AllowUserAutostop: false,
|
AllowUserAutostop: ptr.Ref(false),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, got.AllowUserAutostart)
|
require.True(t, got.AllowUserAutostart)
|
||||||
@@ -1322,24 +1321,26 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: &template.Name,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
DefaultTTLMillis: template.DefaultTTLMillis,
|
DefaultTTLMillis: &template.DefaultTTLMillis,
|
||||||
ActivityBumpMillis: template.ActivityBumpMillis,
|
ActivityBumpMillis: &template.ActivityBumpMillis,
|
||||||
AutostopRequirement: nil,
|
AutostopRequirement: nil,
|
||||||
AllowUserAutostart: template.AllowUserAutostart,
|
AllowUserAutostart: &template.AllowUserAutostart,
|
||||||
AllowUserAutostop: template.AllowUserAutostop,
|
AllowUserAutostop: &template.AllowUserAutostop,
|
||||||
}
|
}
|
||||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
require.ErrorContains(t, err, "not modified")
|
require.NoError(t, err)
|
||||||
updated, err := client.Template(ctx, template.ID)
|
updated, err := client.Template(ctx, template.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, updated.UpdatedAt, template.UpdatedAt)
|
|
||||||
assert.Equal(t, template.Name, updated.Name)
|
assert.Equal(t, template.Name, updated.Name)
|
||||||
assert.Equal(t, template.Description, updated.Description)
|
assert.Equal(t, template.Description, updated.Description)
|
||||||
assert.Equal(t, template.Icon, updated.Icon)
|
assert.Equal(t, template.Icon, updated.Icon)
|
||||||
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
|
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) {
|
t.Run("Invalid", func(t *testing.T) {
|
||||||
@@ -1356,7 +1357,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
DefaultTTLMillis: -int64(time.Hour),
|
DefaultTTLMillis: ptr.Ref(-int64(time.Hour)),
|
||||||
}
|
}
|
||||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, req)
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
@@ -1438,12 +1439,12 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
||||||
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: &template.Name,
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
|
||||||
DefaultTTLMillis: time.Hour.Milliseconds(),
|
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
|
||||||
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
||||||
// wrong order
|
// wrong order
|
||||||
DaysOfWeek: []string{"saturday", "friday"},
|
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.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostopRequirement.DaysOfWeek)
|
||||||
require.EqualValues(t, 2, template.AutostopRequirement.Weeks)
|
require.EqualValues(t, 2, template.AutostopRequirement.Weeks)
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: &template.Name,
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
|
||||||
DefaultTTLMillis: time.Hour.Milliseconds(),
|
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
|
||||||
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
||||||
DaysOfWeek: []string{},
|
DaysOfWeek: []string{},
|
||||||
Weeks: 0,
|
Weeks: 0,
|
||||||
@@ -1552,12 +1553,12 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
require.Empty(t, template.AutostopRequirement.DaysOfWeek)
|
||||||
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
require.EqualValues(t, 1, template.AutostopRequirement.Weeks)
|
||||||
req := codersdk.UpdateTemplateMeta{
|
req := codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: &template.Name,
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: &template.AllowUserCancelWorkspaceJobs,
|
||||||
DefaultTTLMillis: time.Hour.Milliseconds(),
|
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
|
||||||
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
||||||
DaysOfWeek: []string{"monday"},
|
DaysOfWeek: []string{"monday"},
|
||||||
Weeks: 2,
|
Weeks: 2,
|
||||||
@@ -1603,9 +1604,11 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, updated.UseClassicParameterFlow, "expected true")
|
assert.True(t, updated.UseClassicParameterFlow, "expected true")
|
||||||
|
|
||||||
// noop
|
|
||||||
req.UseClassicParameterFlow = nil
|
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)
|
require.NoError(t, err)
|
||||||
assert.True(t, updated.UseClassicParameterFlow, "expected true")
|
assert.True(t, updated.UseClassicParameterFlow, "expected true")
|
||||||
|
|
||||||
@@ -1636,9 +1639,13 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, updated.DisableModuleCache, "expected true")
|
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
|
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)
|
require.NoError(t, err)
|
||||||
assert.True(t, updated.DisableModuleCache, "expected true")
|
assert.True(t, updated.DisableModuleCache, "expected true")
|
||||||
|
|
||||||
@@ -1675,7 +1682,7 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
DisplayName: &displayName,
|
DisplayName: &displayName,
|
||||||
Description: &description,
|
Description: &description,
|
||||||
Icon: &icon,
|
Icon: &icon,
|
||||||
DefaultTTLMillis: defaultTTLMillis,
|
DefaultTTLMillis: ptr.Ref(defaultTTLMillis),
|
||||||
}
|
}
|
||||||
|
|
||||||
type expected struct {
|
type expected struct {
|
||||||
@@ -1694,38 +1701,41 @@ func TestPatchTemplateMeta(t *testing.T) {
|
|||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
{
|
{
|
||||||
name: "Only update default_ttl_ms",
|
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()},
|
expected: expected{displayName: reference.DisplayName, description: reference.Description, icon: reference.Icon, defaultTTLMillis: 99 * time.Hour.Milliseconds()},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Clear display name",
|
name: "Clear display name",
|
||||||
req: codersdk.UpdateTemplateMeta{DisplayName: ptr.Ref("")},
|
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",
|
name: "Clear description",
|
||||||
req: codersdk.UpdateTemplateMeta{Description: ptr.Ref("")},
|
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",
|
name: "Clear icon",
|
||||||
req: codersdk.UpdateTemplateMeta{Icon: ptr.Ref("")},
|
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},
|
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},
|
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},
|
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) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
defer func() {
|
defer func() {
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
// Restore reference after each test case
|
// Restore reference after each test case. The restore
|
||||||
_, err := client.UpdateTemplateMeta(ctx, reference.ID, restoreReq)
|
// itself can be a no-op (and return an error) when the
|
||||||
require.NoError(t, err)
|
// 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)
|
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)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, tc.expected.displayName, updated.DisplayName)
|
assert.Equal(t, tc.expected.displayName, updated.DisplayName)
|
||||||
assert.Equal(t, tc.expected.description, updated.Description)
|
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) {
|
func TestDeleteTemplate(t *testing.T) {
|
||||||
|
|||||||
@@ -667,7 +667,7 @@ func TestWorkspaceAgentAppStatus_ActivityBump(t *testing.T) {
|
|||||||
|
|
||||||
// Configure template with activity_bump to enable deadline bumping.
|
// Configure template with activity_bump to enable deadline bumping.
|
||||||
_, err := client.UpdateTemplateMeta(ctx, r.Template.ID, codersdk.UpdateTemplateMeta{
|
_, err := client.UpdateTemplateMeta(ctx, r.Template.ID, codersdk.UpdateTemplateMeta{
|
||||||
ActivityBumpMillis: time.Hour.Milliseconds(),
|
ActivityBumpMillis: ptr.Ref(time.Hour.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -4756,7 +4756,7 @@ func TestWorkspaceUsageTracking(t *testing.T) {
|
|||||||
DefaultTTL: int64(8 * time.Hour),
|
DefaultTTL: int64(8 * time.Hour),
|
||||||
})
|
})
|
||||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
ActivityBumpMillis: 8 * time.Hour.Milliseconds(),
|
ActivityBumpMillis: ptr.Ref(8 * time.Hour.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||||
|
|||||||
+19
-19
@@ -215,40 +215,43 @@ type ACLAvailable struct {
|
|||||||
Groups []Group `json:"groups"`
|
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 {
|
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"`
|
DisplayName *string `json:"display_name,omitempty" validate:"omitempty,template_display_name"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Icon *string `json:"icon,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
|
// ActivityBumpMillis allows optionally specifying the activity bump
|
||||||
// duration for all workspaces created from this template. Defaults to 1h
|
// duration for all workspaces created from this template. Defaults to 1h
|
||||||
// but can be set to 0 to disable activity bumping.
|
// 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
|
// AutostopRequirement and AutostartRequirement can only be set if your license
|
||||||
// includes the advanced template scheduling feature. If you attempt to set this
|
// includes the advanced template scheduling feature. If you attempt to set this
|
||||||
// value while unlicensed, it will be ignored.
|
// value while unlicensed, it will be ignored.
|
||||||
AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"`
|
AutostopRequirement *TemplateAutostopRequirement `json:"autostop_requirement,omitempty"`
|
||||||
AutostartRequirement *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"`
|
AutostartRequirement *TemplateAutostartRequirement `json:"autostart_requirement,omitempty"`
|
||||||
AllowUserAutostart bool `json:"allow_user_autostart,omitempty"`
|
AllowUserAutostart *bool `json:"allow_user_autostart,omitempty"`
|
||||||
AllowUserAutostop bool `json:"allow_user_autostop,omitempty"`
|
AllowUserAutostop *bool `json:"allow_user_autostop,omitempty"`
|
||||||
AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
|
AllowUserCancelWorkspaceJobs *bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
|
||||||
FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"`
|
FailureTTLMillis *int64 `json:"failure_ttl_ms,omitempty"`
|
||||||
TimeTilDormantMillis int64 `json:"time_til_dormant_ms,omitempty"`
|
TimeTilDormantMillis *int64 `json:"time_til_dormant_ms,omitempty"`
|
||||||
TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms,omitempty"`
|
TimeTilDormantAutoDeleteMillis *int64 `json:"time_til_dormant_autodelete_ms,omitempty"`
|
||||||
// UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces
|
// UpdateWorkspaceLastUsedAt updates the last_used_at field of workspaces
|
||||||
// spawned from the template. This is useful for preventing workspaces being
|
// spawned from the template. This is useful for preventing workspaces being
|
||||||
// immediately locked when updating the inactivity_ttl field to a new, shorter
|
// immediately locked when updating the inactivity_ttl field to a new, shorter
|
||||||
// value.
|
// value.
|
||||||
UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"`
|
UpdateWorkspaceLastUsedAt *bool `json:"update_workspace_last_used_at,omitempty"`
|
||||||
// UpdateWorkspaceDormant updates the dormant_at field of workspaces spawned
|
// UpdateWorkspaceDormantAt updates the dormant_at field of workspaces spawned
|
||||||
// from the template. This is useful for preventing dormant workspaces being immediately
|
// from the template. This is useful for preventing dormant workspaces being
|
||||||
// deleted when updating the dormant_ttl field to a new, shorter value.
|
// immediately deleted when updating the dormant_ttl field to a new, shorter
|
||||||
UpdateWorkspaceDormantAt bool `json:"update_workspace_dormant_at"`
|
// value.
|
||||||
|
UpdateWorkspaceDormantAt *bool `json:"update_workspace_dormant_at,omitempty"`
|
||||||
// RequireActiveVersion mandates workspaces built using this template
|
// RequireActiveVersion mandates workspaces built using this template
|
||||||
// use the active version of the template. This option has no
|
// use the active version of the template. This option has no
|
||||||
// effect on template admins.
|
// 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
|
// DeprecationMessage if set, will mark the template as deprecated and block
|
||||||
// any new workspaces from using this template.
|
// any new workspaces from using this template.
|
||||||
// If passed an empty string, will remove the deprecated message, making
|
// 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,
|
// 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
|
// and must be explicitly granted to users or groups in the permissions settings
|
||||||
// of the template.
|
// 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"`
|
MaxPortShareLevel *WorkspaceAgentPortShareLevel `json:"max_port_share_level,omitempty"`
|
||||||
CORSBehavior *CORSBehavior `json:"cors_behavior,omitempty"`
|
CORSBehavior *CORSBehavior `json:"cors_behavior,omitempty"`
|
||||||
// UseClassicParameterFlow is a flag that switches the default behavior to use the classic
|
// 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
|
return Template{}, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode == http.StatusNotModified {
|
|
||||||
return Template{}, xerrors.New("template metadata not modified")
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
if res.StatusCode != http.StatusOK {
|
||||||
return Template{}, ReadBodyAsError(res)
|
return Template{}, ReadBodyAsError(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/cli/clitest"
|
"github.com/coder/coder/v2/cli/clitest"
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"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/codersdk"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||||
@@ -46,7 +47,7 @@ func TestStart(t *testing.T) {
|
|||||||
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, oldVersion.ID)
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, oldVersion.ID)
|
||||||
require.Equal(t, oldVersion.ID, template.ActiveVersionID)
|
require.Equal(t, oldVersion.ID, template.ActiveVersionID)
|
||||||
template = coderdtest.UpdateTemplateMeta(t, templateAdminClient, template.ID, codersdk.UpdateTemplateMeta{
|
template = coderdtest.UpdateTemplateMeta(t, templateAdminClient, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
RequireActiveVersion: true,
|
RequireActiveVersion: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.True(t, template.RequireActiveVersion)
|
require.True(t, template.RequireActiveVersion)
|
||||||
|
|
||||||
|
|||||||
@@ -218,20 +218,20 @@ func TestTemplateEdit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
template, err := ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{
|
template, err := ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: expectedName,
|
Name: ptr.Ref(expectedName),
|
||||||
DisplayName: &expectedDisplayName,
|
DisplayName: &expectedDisplayName,
|
||||||
Description: &expectedDescription,
|
Description: &expectedDescription,
|
||||||
Icon: &expectedIcon,
|
Icon: &expectedIcon,
|
||||||
DefaultTTLMillis: expectedDefaultTTLMillis,
|
DefaultTTLMillis: ptr.Ref(expectedDefaultTTLMillis),
|
||||||
AllowUserAutostop: expectedAllowAutostop,
|
AllowUserAutostop: ptr.Ref(expectedAllowAutostop),
|
||||||
AllowUserAutostart: expectedAllowAutostart,
|
AllowUserAutostart: ptr.Ref(expectedAllowAutostart),
|
||||||
FailureTTLMillis: expectedFailureTTLMillis,
|
FailureTTLMillis: ptr.Ref(expectedFailureTTLMillis),
|
||||||
TimeTilDormantMillis: expectedDormancyMillis,
|
TimeTilDormantMillis: ptr.Ref(expectedDormancyMillis),
|
||||||
TimeTilDormantAutoDeleteMillis: expectedAutoDeleteMillis,
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(expectedAutoDeleteMillis),
|
||||||
RequireActiveVersion: expectedRequireActiveVersion,
|
RequireActiveVersion: ptr.Ref(expectedRequireActiveVersion),
|
||||||
DeprecationMessage: ptr.Ref(deprecationMessage),
|
DeprecationMessage: ptr.Ref(deprecationMessage),
|
||||||
DisableEveryoneGroupAccess: expectedDisableEveryone,
|
DisableEveryoneGroupAccess: ptr.Ref(expectedDisableEveryone),
|
||||||
AllowUserCancelWorkspaceJobs: expectedAllowCancelJobs,
|
AllowUserCancelWorkspaceJobs: ptr.Ref(expectedAllowCancelJobs),
|
||||||
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
||||||
DaysOfWeek: expectedAutostartDaysOfWeek,
|
DaysOfWeek: expectedAutostartDaysOfWeek,
|
||||||
},
|
},
|
||||||
@@ -266,20 +266,20 @@ func TestTemplateEdit(t *testing.T) {
|
|||||||
expectedAutoStopWeeks = 2
|
expectedAutoStopWeeks = 2
|
||||||
|
|
||||||
template, err = ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{
|
template, err = ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: expectedName,
|
Name: ptr.Ref(expectedName),
|
||||||
DisplayName: &expectedDisplayName,
|
DisplayName: &expectedDisplayName,
|
||||||
Description: &expectedDescription,
|
Description: &expectedDescription,
|
||||||
Icon: &expectedIcon,
|
Icon: &expectedIcon,
|
||||||
DefaultTTLMillis: expectedDefaultTTLMillis,
|
DefaultTTLMillis: ptr.Ref(expectedDefaultTTLMillis),
|
||||||
AllowUserAutostop: expectedAllowAutostop,
|
AllowUserAutostop: ptr.Ref(expectedAllowAutostop),
|
||||||
AllowUserAutostart: expectedAllowAutostart,
|
AllowUserAutostart: ptr.Ref(expectedAllowAutostart),
|
||||||
FailureTTLMillis: expectedFailureTTLMillis,
|
FailureTTLMillis: ptr.Ref(expectedFailureTTLMillis),
|
||||||
TimeTilDormantMillis: expectedDormancyMillis,
|
TimeTilDormantMillis: ptr.Ref(expectedDormancyMillis),
|
||||||
TimeTilDormantAutoDeleteMillis: expectedAutoDeleteMillis,
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(expectedAutoDeleteMillis),
|
||||||
RequireActiveVersion: expectedRequireActiveVersion,
|
RequireActiveVersion: ptr.Ref(expectedRequireActiveVersion),
|
||||||
DeprecationMessage: ptr.Ref(deprecationMessage),
|
DeprecationMessage: ptr.Ref(deprecationMessage),
|
||||||
DisableEveryoneGroupAccess: expectedDisableEveryone,
|
DisableEveryoneGroupAccess: ptr.Ref(expectedDisableEveryone),
|
||||||
AllowUserCancelWorkspaceJobs: expectedAllowCancelJobs,
|
AllowUserCancelWorkspaceJobs: ptr.Ref(expectedAllowCancelJobs),
|
||||||
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
||||||
DaysOfWeek: expectedAutostartDaysOfWeek,
|
DaysOfWeek: expectedAutostartDaysOfWeek,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -186,14 +186,16 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
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
|
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,
|
MaxPortShareLevel: &level,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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
|
// Invalid level
|
||||||
level = "invalid"
|
level = "invalid"
|
||||||
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
@@ -258,7 +260,7 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: ptr.Ref(template.Name),
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
@@ -275,7 +277,7 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
// Ensure a missing field is a noop
|
// Ensure a missing field is a noop
|
||||||
updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: ptr.Ref(template.Name),
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: ptr.Ref(template.Icon + "something"),
|
Icon: ptr.Ref(template.Icon + "something"),
|
||||||
@@ -312,7 +314,7 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
_, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: ptr.Ref(template.Name),
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
@@ -348,12 +350,12 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: ptr.Ref(template.Name),
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
|
||||||
DefaultTTLMillis: time.Hour.Milliseconds(),
|
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
|
||||||
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
|
||||||
DaysOfWeek: []string{"monday", "saturday"},
|
DaysOfWeek: []string{"monday", "saturday"},
|
||||||
Weeks: 3,
|
Weeks: 3,
|
||||||
@@ -402,14 +404,14 @@ func TestTemplates(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: ptr.Ref(template.Name),
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
|
||||||
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
|
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
|
||||||
FailureTTLMillis: failureTTL.Milliseconds(),
|
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
|
||||||
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, failureTTL.Milliseconds(), updated.FailureTTLMillis)
|
require.Equal(t, failureTTL.Milliseconds(), updated.FailureTTLMillis)
|
||||||
@@ -471,14 +473,14 @@ func TestTemplates(t *testing.T) {
|
|||||||
// nolint: paralleltest // context is from parent t.Run
|
// nolint: paralleltest // context is from parent t.Run
|
||||||
t.Run(c.Name, func(t *testing.T) {
|
t.Run(c.Name, func(t *testing.T) {
|
||||||
_, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: ptr.Ref(template.Name),
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
|
||||||
TimeTilDormantMillis: c.TimeTilDormantMS,
|
TimeTilDormantMillis: ptr.Ref(c.TimeTilDormantMS),
|
||||||
FailureTTLMillis: c.FailureTTLMS,
|
FailureTTLMillis: ptr.Ref(c.FailureTTLMS),
|
||||||
TimeTilDormantAutoDeleteMillis: c.DormantAutoDeleteMS,
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(c.DormantAutoDeleteMS),
|
||||||
})
|
})
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
cerr, ok := codersdk.AsError(err)
|
cerr, ok := codersdk.AsError(err)
|
||||||
@@ -529,7 +531,7 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
dormantTTL := time.Minute
|
dormantTTL := time.Minute
|
||||||
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
|
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
|
// Disable the time_til_dormant_auto_delete on the template, then we can assert that the workspaces
|
||||||
// no longer have a deleting_at field.
|
// no longer have a deleting_at field.
|
||||||
updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
TimeTilDormantAutoDeleteMillis: 0,
|
TimeTilDormantAutoDeleteMillis: ptr.Ref[int64](0),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, 0, updated.TimeTilDormantAutoDeleteMillis)
|
require.EqualValues(t, 0, updated.TimeTilDormantAutoDeleteMillis)
|
||||||
@@ -604,8 +606,8 @@ func TestTemplates(t *testing.T) {
|
|||||||
dormantTTL := time.Minute
|
dormantTTL := time.Minute
|
||||||
//nolint:gocritic // non-template-admin cannot update template meta
|
//nolint:gocritic // non-template-admin cannot update template meta
|
||||||
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
|
||||||
UpdateWorkspaceDormantAt: true,
|
UpdateWorkspaceDormantAt: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
|
require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
|
||||||
@@ -661,8 +663,8 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
inactivityTTL := time.Minute
|
inactivityTTL := time.Minute
|
||||||
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
|
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
|
||||||
UpdateWorkspaceLastUsedAt: true,
|
UpdateWorkspaceLastUsedAt: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis)
|
require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis)
|
||||||
@@ -706,14 +708,14 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
// Update the field and assert it persists.
|
// Update the field and assert it persists.
|
||||||
updatedTemplate, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updatedTemplate, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
RequireActiveVersion: false,
|
RequireActiveVersion: ptr.Ref(false),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, updatedTemplate.RequireActiveVersion)
|
require.False(t, updatedTemplate.RequireActiveVersion)
|
||||||
|
|
||||||
// Flip it back to ensure we aren't hardcoding to a default value.
|
// Flip it back to ensure we aren't hardcoding to a default value.
|
||||||
updatedTemplate, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
updatedTemplate, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
RequireActiveVersion: true,
|
RequireActiveVersion: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, updatedTemplate.RequireActiveVersion)
|
require.True(t, updatedTemplate.RequireActiveVersion)
|
||||||
@@ -1003,12 +1005,12 @@ func TestTemplateACL(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 1, len(acl.Groups))
|
require.Equal(t, 1, len(acl.Groups))
|
||||||
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
Name: template.Name,
|
Name: ptr.Ref(template.Name),
|
||||||
DisplayName: &template.DisplayName,
|
DisplayName: &template.DisplayName,
|
||||||
Description: &template.Description,
|
Description: &template.Description,
|
||||||
Icon: &template.Icon,
|
Icon: &template.Icon,
|
||||||
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
|
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
|
||||||
DisableEveryoneGroupAccess: true,
|
DisableEveryoneGroupAccess: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"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/codersdk"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||||
@@ -43,7 +44,7 @@ func TestWorkspaceBuild(t *testing.T) {
|
|||||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, tplAv1.ID)
|
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, tplAv1.ID)
|
||||||
require.Equal(t, tplAv1.ID, tplA.ActiveVersionID)
|
require.Equal(t, tplAv1.ID, tplA.ActiveVersionID)
|
||||||
tplA = coderdtest.UpdateTemplateMeta(t, ownerClient, tplA.ID, codersdk.UpdateTemplateMeta{
|
tplA = coderdtest.UpdateTemplateMeta(t, ownerClient, tplA.ID, codersdk.UpdateTemplateMeta{
|
||||||
RequireActiveVersion: true,
|
RequireActiveVersion: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.True(t, tplA.RequireActiveVersion)
|
require.True(t, tplA.RequireActiveVersion)
|
||||||
tplAv2 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
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)
|
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, tplBv1.ID)
|
||||||
require.Equal(t, tplBv1.ID, tplB.ActiveVersionID)
|
require.Equal(t, tplBv1.ID, tplB.ActiveVersionID)
|
||||||
tplB = coderdtest.UpdateTemplateMeta(t, ownerClient, tplB.ID, codersdk.UpdateTemplateMeta{
|
tplB = coderdtest.UpdateTemplateMeta(t, ownerClient, tplB.ID, codersdk.UpdateTemplateMeta{
|
||||||
RequireActiveVersion: true,
|
RequireActiveVersion: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.True(t, tplB.RequireActiveVersion)
|
require.True(t, tplB.RequireActiveVersion)
|
||||||
|
|
||||||
|
|||||||
@@ -784,7 +784,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
|||||||
}).Do().Template
|
}).Do().Template
|
||||||
|
|
||||||
template := coderdtest.UpdateTemplateMeta(t, client, tpl.ID, codersdk.UpdateTemplateMeta{
|
template := coderdtest.UpdateTemplateMeta(t, client, tpl.ID, codersdk.UpdateTemplateMeta{
|
||||||
TimeTilDormantMillis: inactiveTTL.Milliseconds(),
|
TimeTilDormantMillis: ptr.Ref(inactiveTTL.Milliseconds()),
|
||||||
})
|
})
|
||||||
|
|
||||||
resp := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
resp := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||||
@@ -1260,7 +1260,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
|||||||
require.Len(t, stats.Transitions, 0)
|
require.Len(t, stats.Transitions, 0)
|
||||||
|
|
||||||
_, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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
|
// Now that we've validated that the workspace is eligible for autostart
|
||||||
// lets cause it to become dormant.
|
// lets cause it to become dormant.
|
||||||
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
TimeTilDormantMillis: inactiveTTL.Milliseconds(),
|
TimeTilDormantMillis: ptr.Ref(inactiveTTL.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1433,7 +1433,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
|||||||
|
|
||||||
// Enable auto-deletion for the template.
|
// Enable auto-deletion for the template.
|
||||||
_, err = templateAdmin.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err = templateAdmin.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
TimeTilDormantAutoDeleteMillis: transitionTTL.Milliseconds(),
|
TimeTilDormantAutoDeleteMillis: ptr.Ref(transitionTTL.Milliseconds()),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1538,8 +1538,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
|||||||
|
|
||||||
// Update the template to require the promoted version.
|
// Update the template to require the promoted version.
|
||||||
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
RequireActiveVersion: true,
|
RequireActiveVersion: ptr.Ref(true),
|
||||||
AllowUserAutostart: true,
|
AllowUserAutostart: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -1832,7 +1832,7 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
|
|||||||
templateTTL = 72 * time.Hour.Milliseconds()
|
templateTTL = 72 * time.Hour.Milliseconds()
|
||||||
ctx := testutil.Context(t, testutil.WaitShort)
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
template = coderdtest.UpdateTemplateMeta(t, client, template.ID, codersdk.UpdateTemplateMeta{
|
template = coderdtest.UpdateTemplateMeta(t, client, template.ID, codersdk.UpdateTemplateMeta{
|
||||||
DefaultTTLMillis: templateTTL,
|
DefaultTTLMillis: ptr.Ref(templateTTL),
|
||||||
})
|
})
|
||||||
workspace, err := client.Workspace(ctx, workspace.ID)
|
workspace, err := client.Workspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -4068,7 +4068,7 @@ func TestResolveAutostart(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := ownerClient.UpdateTemplateMeta(ctx, version1.Template.ID, codersdk.UpdateTemplateMeta{
|
_, err := ownerClient.UpdateTemplateMeta(ctx, version1.Template.ID, codersdk.UpdateTemplateMeta{
|
||||||
RequireActiveVersion: true,
|
RequireActiveVersion: ptr.Ref(true),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
Generated
+11
-6
@@ -8259,6 +8259,10 @@ export interface UpdateTemplateACL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// From codersdk/templates.go
|
// 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 {
|
export interface UpdateTemplateMeta {
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
readonly display_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
|
* immediately locked when updating the inactivity_ttl field to a new, shorter
|
||||||
* value.
|
* value.
|
||||||
*/
|
*/
|
||||||
readonly update_workspace_last_used_at: boolean;
|
readonly update_workspace_last_used_at?: boolean;
|
||||||
/**
|
/**
|
||||||
* UpdateWorkspaceDormant updates the dormant_at field of workspaces spawned
|
* UpdateWorkspaceDormantAt updates the dormant_at field of workspaces spawned
|
||||||
* from the template. This is useful for preventing dormant workspaces being immediately
|
* from the template. This is useful for preventing dormant workspaces being
|
||||||
* deleted when updating the dormant_ttl field to a new, shorter value.
|
* 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
|
* RequireActiveVersion mandates workspaces built using this template
|
||||||
* use the active version of the template. This option has no
|
* 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
|
* and must be explicitly granted to users or groups in the permissions settings
|
||||||
* of the template.
|
* of the template.
|
||||||
*/
|
*/
|
||||||
readonly disable_everyone_group_access: boolean;
|
readonly disable_everyone_group_access?: boolean;
|
||||||
readonly max_port_share_level?: WorkspaceAgentPortShareLevel;
|
readonly max_port_share_level?: WorkspaceAgentPortShareLevel;
|
||||||
readonly cors_behavior?: CORSBehavior;
|
readonly cors_behavior?: CORSBehavior;
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user