mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
<!-- Generated by Coder Agents on behalf of @Emyrk. --> <img width="762" height="239" alt="Screenshot From 2026-09-11 17-09-41" src="https://github.com/user-attachments/assets/9f31fd1e-4cb2-47ab-8d37-4073df9f0a7c" /> Closes #22834. Adds `CODER_PROVISIONER_DISABLE_MODULE_CACHE` so an admin can disable Terraform module caching for every template at once instead of toggling ~100 templates by hand. - The deployment setting wins. Templates cannot opt back in, and the per-template toggle becomes read-only while the deployment setting is on. - Modules stay cached at template import, so dynamic parameter rendering still resolves module-defined parameters. Only the workspace build is denied the cached files. - Templates report `module_cache_disabled_by_deployment`, and template settings shows why the checkbox is locked. <details><summary>Agent context</summary> Decisions taken with @Emyrk before implementing: 1. **Precedence.** `deployment || template`, no per-template override, so no migration and no nullable column. The rule lives in one place, `codersdk.ModuleCacheDisabled`. 2. **Import-time caching untouched.** `dynamicparameters/render.go` overlays `cached_module_files` at `.terraform/modules` to render module-defined parameters. Skipping the cache there would reproduce the `Module not loaded. Did you run terraform init?` failure reported by a customer, so the deployment setting only affects what `AcquireJob` hands the provisioner. 3. **Write path.** A PATCH carrying `disable_module_cache` is discarded while the deployment setting is on, rather than persisted. Persisting it would silently keep templates opted out after the deployment setting is removed. 4. **API shape.** `disable_module_cache` keeps its meaning (this template's stored value) and the new read-only `module_cache_disabled_by_deployment` reports the deployment state. Reading the effective value from `/deployment/config` was rejected because template admins may lack deployment-config read permission. 5. **Naming.** `CODER_PROVISIONER_DISABLE_MODULE_CACHE` over the issue's `CODER_DISABLE_TERRAFORM_MODULE_CACHE`, to match the provisioning option group. Note: `InsertTemplateParams` has no `disable_module_cache` binding, so new templates always store `false`. With deployment precedence they still behave as opted out, so the create path needed no change. Out of scope and tracked elsewhere: the `coderd_template` Terraform provider attribute (separate repo) and backporting the per-template toggle to `release/2.30`. </details> --- <sub>Coder Agents on behalf of @Emyrk.</sub>
568 lines
19 KiB
Go
568 lines
19 KiB
Go
package coderd
|
|
|
|
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/codersdk"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
// 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,
|
|
AgentsAllowed: true,
|
|
RequireActiveVersion: true,
|
|
DefaultTTL: int64(60 * 60 * 1000 * 1000 * 1000), // 1 hour in ns
|
|
ActivityBump: int64(30 * 60 * 1000 * 1000 * 1000), // 30 minutes in ns
|
|
TimeTilAutostopNotify: int64(10 * 60 * 1000 * 1000 * 1000), // 10 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,
|
|
AllowWorkspaceRenames: 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,
|
|
timeTilAutostopNotifyMillis: tpl.TimeTilAutostopNotify / 1e6,
|
|
failureTTLMillis: tpl.FailureTTL / 1e6,
|
|
timeTilDormantMillis: tpl.TimeTilDormant / 1e6,
|
|
timeTilDormantAutoDeleteMillis: tpl.TimeTilDormantAutoDelete / 1e6,
|
|
allowUserAutostart: tpl.AllowUserAutostart,
|
|
allowUserAutostop: tpl.AllowUserAutostop,
|
|
allowUserCancelWorkspaceJobs: tpl.AllowUserCancelWorkspaceJobs,
|
|
agentsAllowed: tpl.AgentsAllowed,
|
|
requireActiveVersion: tpl.RequireActiveVersion,
|
|
deprecationMessage: tpl.Deprecated,
|
|
useClassicTemplateFlow: tpl.UseClassicParameterFlow,
|
|
disableModuleCache: tpl.DisableModuleCache,
|
|
allowWorkspaceRenames: tpl.AllowWorkspaceRenames,
|
|
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: new("renamed")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.name = "renamed"
|
|
}},
|
|
},
|
|
{
|
|
name: "NameEmptyStringFallsBackToCurrent",
|
|
req: codersdk.UpdateTemplateMeta{Name: new("")},
|
|
// 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: new("Renamed")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.displayName = "Renamed"
|
|
}},
|
|
},
|
|
{
|
|
name: "Description",
|
|
req: codersdk.UpdateTemplateMeta{Description: new("New description")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.description = "New description"
|
|
}},
|
|
},
|
|
{
|
|
name: "Icon",
|
|
req: codersdk.UpdateTemplateMeta{Icon: new("/new.svg")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.icon = "/new.svg"
|
|
}},
|
|
},
|
|
{
|
|
name: "DefaultTTLMillis",
|
|
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: new(int64(7200_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.defaultTTLMillis = 7200_000
|
|
}},
|
|
},
|
|
{
|
|
name: "DefaultTTLMillisZeroExplicit",
|
|
req: codersdk.UpdateTemplateMeta{DefaultTTLMillis: new(int64(0))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.defaultTTLMillis = 0
|
|
}},
|
|
},
|
|
{
|
|
name: "ActivityBumpMillis",
|
|
req: codersdk.UpdateTemplateMeta{ActivityBumpMillis: new(int64(900_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.activityBumpMillis = 900_000
|
|
}},
|
|
},
|
|
{
|
|
name: "TimeTilAutostopNotifyMillis",
|
|
req: codersdk.UpdateTemplateMeta{TimeTilAutostopNotifyMillis: new(int64(300_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.timeTilAutostopNotifyMillis = 300_000
|
|
}},
|
|
},
|
|
{
|
|
name: "TimeTilAutostopNotifyMillisZeroExplicit",
|
|
req: codersdk.UpdateTemplateMeta{TimeTilAutostopNotifyMillis: new(int64(0))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.timeTilAutostopNotifyMillis = 0
|
|
}},
|
|
},
|
|
{
|
|
name: "AllowUserAutostart",
|
|
req: codersdk.UpdateTemplateMeta{AllowUserAutostart: new(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.allowUserAutostart = true
|
|
}},
|
|
},
|
|
{
|
|
name: "AllowUserAutostop",
|
|
req: codersdk.UpdateTemplateMeta{AllowUserAutostop: new(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.allowUserAutostop = true
|
|
}},
|
|
},
|
|
{
|
|
name: "AllowUserAutostop/true",
|
|
req: codersdk.UpdateTemplateMeta{AllowUserAutostop: new(false)},
|
|
expected: expected{
|
|
base: func(update *database.Template) {
|
|
update.AllowUserAutostop = true
|
|
},
|
|
override: func(r *templateMetaUpdate) {
|
|
r.allowUserAutostop = false
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "AllowUserCancelWorkspaceJobs",
|
|
req: codersdk.UpdateTemplateMeta{AllowUserCancelWorkspaceJobs: new(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.allowUserCancelWorkspaceJobs = true
|
|
}},
|
|
},
|
|
{
|
|
name: "AgentsAllowed",
|
|
req: codersdk.UpdateTemplateMeta{AgentsAllowed: new(false)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.agentsAllowed = false
|
|
}},
|
|
},
|
|
{
|
|
name: "FailureTTLMillis",
|
|
req: codersdk.UpdateTemplateMeta{FailureTTLMillis: new(int64(3_600_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.failureTTLMillis = 3_600_000
|
|
}},
|
|
},
|
|
{
|
|
name: "TimeTilDormantMillis",
|
|
req: codersdk.UpdateTemplateMeta{TimeTilDormantMillis: new(int64(7_200_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.timeTilDormantMillis = 7_200_000
|
|
}},
|
|
},
|
|
{
|
|
name: "TimeTilDormantAutoDeleteMillis",
|
|
req: codersdk.UpdateTemplateMeta{TimeTilDormantAutoDeleteMillis: new(int64(14_400_000))},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.timeTilDormantAutoDeleteMillis = 14_400_000
|
|
}},
|
|
},
|
|
{
|
|
name: "RequireActiveVersion",
|
|
req: codersdk.UpdateTemplateMeta{RequireActiveVersion: new(false)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.requireActiveVersion = false
|
|
}},
|
|
},
|
|
{
|
|
name: "DeprecationMessage",
|
|
req: codersdk.UpdateTemplateMeta{DeprecationMessage: new("now deprecated")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.deprecationMessage = "now deprecated"
|
|
}},
|
|
},
|
|
{
|
|
name: "DeprecationMessageEmptyStringClears",
|
|
req: codersdk.UpdateTemplateMeta{DeprecationMessage: new("")},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.deprecationMessage = ""
|
|
}},
|
|
},
|
|
{
|
|
name: "UseClassicParameterFlow",
|
|
req: codersdk.UpdateTemplateMeta{UseClassicParameterFlow: new(false)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.useClassicTemplateFlow = false
|
|
}},
|
|
},
|
|
{
|
|
name: "DisableModuleCache",
|
|
req: codersdk.UpdateTemplateMeta{DisableModuleCache: new(false)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.disableModuleCache = false
|
|
}},
|
|
},
|
|
{
|
|
name: "AllowWorkspaceRenames",
|
|
req: codersdk.UpdateTemplateMeta{AllowWorkspaceRenames: new(false)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.allowWorkspaceRenames = false
|
|
}},
|
|
},
|
|
|
|
// CORS behavior.
|
|
{
|
|
name: "CORSBehaviorChange",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
CORSBehavior: new(codersdk.CORSBehavior(database.CorsBehaviorSimple)),
|
|
},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.corsBehavior = database.CorsBehaviorSimple
|
|
}},
|
|
},
|
|
{
|
|
name: "CORSBehaviorEmptyStringPreserves",
|
|
req: codersdk.UpdateTemplateMeta{
|
|
CORSBehavior: new(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: new(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: new(false)},
|
|
expected: expected{override: func(*templateMetaUpdate) {
|
|
// disableEveryoneIntent stays false.
|
|
}},
|
|
},
|
|
{
|
|
name: "DisableEveryoneGroupAccessTrueWithMembership",
|
|
req: codersdk.UpdateTemplateMeta{DisableEveryoneGroupAccess: new(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.groupACL = database.TemplateACL{}
|
|
}},
|
|
},
|
|
{
|
|
name: "UpdateWorkspaceLastUsedAtFalseIsNoop",
|
|
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceLastUsedAt: new(false)},
|
|
expected: expected{override: func(*templateMetaUpdate) {}},
|
|
},
|
|
{
|
|
name: "UpdateWorkspaceLastUsedAtTrue",
|
|
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceLastUsedAt: new(true)},
|
|
expected: expected{override: func(r *templateMetaUpdate) {
|
|
r.updateWorkspaceLastUsedAtIntent = true
|
|
}},
|
|
},
|
|
{
|
|
name: "UpdateWorkspaceDormantAtFalseIsNoop",
|
|
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceDormantAt: new(false)},
|
|
expected: expected{override: func(*templateMetaUpdate) {}},
|
|
},
|
|
{
|
|
name: "UpdateWorkspaceDormantAtTrue",
|
|
req: codersdk.UpdateTemplateMeta{UpdateWorkspaceDormantAt: new(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, nil)
|
|
|
|
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: new(""),
|
|
}, nil)
|
|
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{}, nil)
|
|
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)
|
|
}
|
|
}
|
|
|
|
// TestResolveTemplateMetaUpdate_ModuleCacheDisabledByDeployment verifies that the
|
|
// per-template toggle is read-only while the deployment disables the module
|
|
// cache: the request value is discarded in both directions.
|
|
func TestResolveTemplateMetaUpdate_ModuleCacheDisabledByDeployment(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
stored bool
|
|
request *bool
|
|
expected bool
|
|
}{
|
|
{name: "IgnoresOptIn", stored: true, request: new(false), expected: true},
|
|
{name: "IgnoresOptOut", stored: false, request: new(true), expected: false},
|
|
{name: "PreservesStored", stored: true, request: nil, expected: true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tpl := baselineTemplate()
|
|
tpl.DisableModuleCache = tc.stored
|
|
dv := &codersdk.DeploymentValues{}
|
|
dv.Provisioner.DisableModuleCache = serpent.Bool(true)
|
|
got, validErrs := resolveTemplateMetaUpdate(tpl, baselineScheduleOpts(), codersdk.UpdateTemplateMeta{
|
|
DisableModuleCache: tc.request,
|
|
}, dv)
|
|
if len(validErrs) != 0 {
|
|
t.Fatalf("unexpected validation errors: %+v", validErrs)
|
|
}
|
|
if got.disableModuleCache != tc.expected {
|
|
t.Errorf("disableModuleCache = %t, want %t", got.disableModuleCache, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|