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>
186 lines
8.3 KiB
Go
186 lines
8.3 KiB
Go
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
|
|
timeTilAutostopNotifyMillis int64
|
|
failureTTLMillis int64
|
|
timeTilDormantMillis int64
|
|
timeTilDormantAutoDeleteMillis int64
|
|
allowUserAutostart bool
|
|
allowUserAutostop bool
|
|
allowUserCancelWorkspaceJobs bool
|
|
agentsAllowed bool
|
|
requireActiveVersion bool
|
|
deprecationMessage string
|
|
useClassicTemplateFlow bool
|
|
disableModuleCache bool
|
|
allowWorkspaceRenames 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.
|
|
//
|
|
// The deployment values make disableModuleCache read-only when the deployment
|
|
// disables the Terraform module cache for every template: the request value is
|
|
// discarded rather than persisted.
|
|
func resolveTemplateMetaUpdate(
|
|
template database.Template,
|
|
scheduleOpts schedule.TemplateScheduleOptions,
|
|
req codersdk.UpdateTemplateMeta,
|
|
deploymentValues *codersdk.DeploymentValues,
|
|
) (templateMetaUpdate, []codersdk.ValidationError) {
|
|
var validErrs []codersdk.ValidationError
|
|
|
|
disableModuleCache := ptr.NilToDefault(req.DisableModuleCache, template.DisableModuleCache)
|
|
if codersdk.ModuleCacheDisabledByDeployment(deploymentValues) {
|
|
disableModuleCache = template.DisableModuleCache
|
|
}
|
|
|
|
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()),
|
|
timeTilAutostopNotifyMillis: ptr.NilToDefault(req.TimeTilAutostopNotifyMillis, time.Duration(template.TimeTilAutostopNotify).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),
|
|
agentsAllowed: ptr.NilToDefault(req.AgentsAllowed, template.AgentsAllowed),
|
|
requireActiveVersion: ptr.NilToDefault(req.RequireActiveVersion, template.RequireActiveVersion),
|
|
deprecationMessage: ptr.NilToDefault(req.DeprecationMessage, template.Deprecated),
|
|
useClassicTemplateFlow: ptr.NilToDefault(req.UseClassicParameterFlow, template.UseClassicParameterFlow),
|
|
disableModuleCache: disableModuleCache,
|
|
allowWorkspaceRenames: ptr.NilToDefault(req.AllowWorkspaceRenames, template.AllowWorkspaceRenames),
|
|
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
|
|
}
|