From 940f988cfb4ee869f7f610a965fb03dd2440a872 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 11 Sep 2026 18:13:16 -0500 Subject: [PATCH] feat: add deployment-level option to disable Terraform module caching (#29187) Screenshot From 2026-09-11 17-09-41 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.
Agent context 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`.
--- Coder Agents on behalf of @Emyrk. --- cli/testdata/coder_server_--help.golden | 5 + cli/testdata/server-config.yaml.golden | 5 + coderd/apidoc/docs.go | 12 +- coderd/apidoc/swagger.json | 12 +- .../provisionerdserver/provisionerdserver.go | 6 +- .../provisionerdserver_test.go | 91 +++++++++++++++ coderd/templates.go | 23 ++-- coderd/templates_meta_update.go | 12 +- coderd/templates_meta_update_internal_test.go | 44 +++++++- coderd/templates_test.go | 28 +++++ codersdk/deployment.go | 14 +++ codersdk/templates.go | 24 +++- codersdk/templates_test.go | 62 +++++++++++ docs/admin/setup/configuration-reference.md | 9 ++ docs/reference/api/general.md | 1 + docs/reference/api/schemas.md | 105 ++++++++++-------- docs/reference/api/templatebuilder.md | 1 + docs/reference/api/templates.md | 12 +- docs/reference/cli/server.md | 11 ++ .../cli/testdata/coder_server_--help.golden | 5 + site/src/api/typesGenerated.ts | 18 ++- .../TemplateSettingsForm.test.tsx | 39 +++++++ .../TemplateSettingsForm.tsx | 51 +++++++-- .../TemplateSettingsPageView.stories.tsx | 9 ++ site/src/testHelpers/entities.ts | 1 + 25 files changed, 515 insertions(+), 85 deletions(-) create mode 100644 codersdk/templates_test.go diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 2103d10031..d3ce90b805 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -803,6 +803,11 @@ PROVISIONING OPTIONS: Tune the behavior of the provisioner, which is responsible for creating, updating, and deleting workspace resources. + --provisioner-disable-module-cache bool, $CODER_PROVISIONER_DISABLE_MODULE_CACHE (default: false) + Disable the reuse of Terraform modules cached at template import for + all templates. Modules are re-downloaded on every workspace build. + Individual templates cannot opt back in. + --provisioner-force-cancel-interval duration, $CODER_PROVISIONER_FORCE_CANCEL_INTERVAL (default: 10m0s) Time to force cancel provisioning tasks that are stuck. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index a8e96c2b60..b6e7ba7545 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -494,6 +494,11 @@ provisioning: # Time to force cancel provisioning tasks that are stuck. # (default: 10m0s, type: duration) forceCancelInterval: 10m0s + # Disable the reuse of Terraform modules cached at template import for all + # templates. Modules are re-downloaded on every workspace build. Individual + # templates cannot opt back in. + # (default: false, type: bool) + disableModuleCache: false # Enable one or more experiments. These are not ready for production. Separate # multiple experiments with commas, or enter '*' to opt-in to all available # experiments. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 72bf693deb..a576e285ec 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -26530,6 +26530,10 @@ const docTemplate = `{ "description": "Daemons is the number of built-in terraform provisioners.", "type": "integer" }, + "disable_module_cache": { + "description": "DisableModuleCache disables the reuse of Terraform modules cached at\ntemplate import for every template in the deployment. Templates cannot\nopt back in.", + "type": "boolean" + }, "force_cancel_interval": { "type": "integer" } @@ -27908,7 +27912,7 @@ const docTemplate = `{ "type": "string" }, "disable_module_cache": { - "description": "DisableModuleCache disables the use of cached Terraform modules during\nprovisioning.", + "description": "DisableModuleCache disables the use of cached Terraform modules during\nprovisioning for this template. It is read-only while\nModuleCacheDisabledByDeployment is true.", "type": "boolean" }, "display_name": { @@ -27928,6 +27932,10 @@ const docTemplate = `{ "max_port_share_level": { "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" }, + "module_cache_disabled_by_deployment": { + "description": "ModuleCacheDisabledByDeployment reports that the deployment disables the\nTerraform module cache for every template. Templates cannot opt back in,\nso the effective state is disabled regardless of DisableModuleCache.", + "type": "boolean" + }, "name": { "type": "string" }, @@ -29566,7 +29574,7 @@ const docTemplate = `{ "type": "boolean" }, "disable_module_cache": { - "description": "DisableModuleCache disables the using of cached Terraform modules during\nprovisioning. It is recommended not to disable this.", + "description": "DisableModuleCache disables the using of cached Terraform modules during\nprovisioning. It is ignored while the deployment disables the module\ncache for all templates. It is recommended not to disable this.", "type": "boolean" }, "display_name": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 58f7ca844a..06ed66cfc0 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -24317,6 +24317,10 @@ "description": "Daemons is the number of built-in terraform provisioners.", "type": "integer" }, + "disable_module_cache": { + "description": "DisableModuleCache disables the reuse of Terraform modules cached at\ntemplate import for every template in the deployment. Templates cannot\nopt back in.", + "type": "boolean" + }, "force_cancel_interval": { "type": "integer" } @@ -25632,7 +25636,7 @@ "type": "string" }, "disable_module_cache": { - "description": "DisableModuleCache disables the use of cached Terraform modules during\nprovisioning.", + "description": "DisableModuleCache disables the use of cached Terraform modules during\nprovisioning for this template. It is read-only while\nModuleCacheDisabledByDeployment is true.", "type": "boolean" }, "display_name": { @@ -25652,6 +25656,10 @@ "max_port_share_level": { "$ref": "#/definitions/codersdk.WorkspaceAgentPortShareLevel" }, + "module_cache_disabled_by_deployment": { + "description": "ModuleCacheDisabledByDeployment reports that the deployment disables the\nTerraform module cache for every template. Templates cannot opt back in,\nso the effective state is disabled regardless of DisableModuleCache.", + "type": "boolean" + }, "name": { "type": "string" }, @@ -27202,7 +27210,7 @@ "type": "boolean" }, "disable_module_cache": { - "description": "DisableModuleCache disables the using of cached Terraform modules during\nprovisioning. It is recommended not to disable this.", + "description": "DisableModuleCache disables the using of cached Terraform modules during\nprovisioning. It is ignored while the deployment disables the module\ncache for all templates. It is recommended not to disable this.", "type": "boolean" }, "display_name": { diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 771b121a6d..74688b6282 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -684,9 +684,11 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo return nil, failJob(fmt.Sprintf("get owner: %s", err)) } - // Fetch the file id of the cached module files if it exists. + // Fetch the file id of the cached module files if it exists. Modules + // stay cached for parameter rendering even when the cache is + // disabled; they are only withheld from the build. versionModulesFile := "" - if !template.DisableModuleCache { + if !codersdk.ModuleCacheDisabled(s.DeploymentValues, template.DisableModuleCache) { tfvals, err := s.Database.GetTemplateVersionTerraformValues(ctx, templateVersion.ID) if err != nil && !xerrors.Is(err, sql.ErrNoRows) { // Older templates (before dynamic parameters) will not have cached module files. diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 861231944b..ee7c125490 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -954,6 +954,97 @@ func TestAcquireJob(t *testing.T) { } } +func TestAcquireJob_ModuleCache(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + deploymentDisablesCache bool + expectModulesFile bool + }{ + {name: "Enabled", expectModulesFile: true}, + // The deployment setting wins even though the template does not opt out. + {name: "DeploymentDisabled", deploymentDisablesCache: true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Provisioner.DisableModuleCache = serpent.Bool(tc.deploymentDisablesCache) + srv, db, ps, pd := setup(t, false, &overrides{deploymentValues: dv}) + ctx := testutil.Context(t, testutil.WaitShort) + + user := dbgen.User(t, db, database.User{}) + template := dbgen.Template(t, db, database.Template{ + Provisioner: database.ProvisionerTypeEcho, + OrganizationID: pd.OrganizationID, + CreatedBy: user.ID, + }) + version := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + CreatedBy: user.ID, + OrganizationID: pd.OrganizationID, + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + JobID: uuid.New(), + }) + _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + OrganizationID: pd.OrganizationID, + ID: version.JobID, + InitiatorID: user.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + Type: database.ProvisionerJobTypeTemplateVersionImport, + Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{TemplateVersionID: version.ID})), + }) + moduleFile := dbgen.File(t, db, database.File{CreatedBy: user.ID, Hash: "modules"}) + err := db.InsertTemplateVersionTerraformValuesByJobID(ctx, database.InsertTemplateVersionTerraformValuesByJobIDParams{ + JobID: version.JobID, + CachedPlan: []byte("{}"), + CachedModuleFiles: uuid.NullUUID{UUID: moduleFile.ID, Valid: true}, + UpdatedAt: dbtime.Now(), + }) + require.NoError(t, err) + + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: template.ID, + OwnerID: user.ID, + OrganizationID: pd.OrganizationID, + }) + buildID := uuid.New() + buildJob := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + OrganizationID: pd.OrganizationID, + InitiatorID: user.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + FileID: dbgen.File(t, db, database.File{CreatedBy: user.ID, Hash: "build"}).ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{WorkspaceBuildID: buildID})), + Tags: pd.Tags, + }) + _ = dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + ID: buildID, + WorkspaceID: workspace.ID, + BuildNumber: 1, + JobID: buildJob.ID, + TemplateVersionID: version.ID, + Transition: database.WorkspaceTransitionStart, + Reason: database.BuildReasonInitiator, + InitiatorID: user.ID, + }) + + job, err := srv.AcquireJob(ctx, nil) + require.NoError(t, err) + + got := job.GetWorkspaceBuild().GetMetadata().GetTemplateVersionModulesFile() + if tc.expectModulesFile { + require.Equal(t, moduleFile.ID.String(), got) + return + } + require.Empty(t, got, "cached modules must not be shipped to the build") + }) + } +} + func TestUpdateJob(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/coderd/templates.go b/coderd/templates.go index fe84d26e82..d0593b2d66 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -692,7 +692,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { // values for any pointer field that is nil in the request, so that // omitted fields are preserved instead of being overwritten with // Go zero values. - resolved, validErrs := resolveTemplateMetaUpdate(template, scheduleOpts, req) + resolved, validErrs := resolveTemplateMetaUpdate(template, scheduleOpts, req, api.DeploymentValues) if resolved.defaultTTLMillis < 0 { validErrs = append(validErrs, codersdk.ValidationError{Field: "default_ttl_ms", Detail: "Must be a positive integer."}) @@ -1053,16 +1053,17 @@ func (api *API) convertTemplate( DaysOfWeek: codersdk.BitmapToWeekdays(template.AutostartAllowedDays()), }, // These values depend on entitlements and come from the templateAccessControl - RequireActiveVersion: templateAccessControl.RequireActiveVersion, - Deprecated: templateAccessControl.IsDeprecated(), - DeprecationMessage: templateAccessControl.Deprecated, - Deleted: template.Deleted, - MaxPortShareLevel: maxPortShareLevel, - UseClassicParameterFlow: template.UseClassicParameterFlow, - CORSBehavior: codersdk.CORSBehavior(template.CorsBehavior), - DisableModuleCache: template.DisableModuleCache, - AgentsAllowed: template.AgentsAllowed, - AllowWorkspaceRenames: template.AllowWorkspaceRenames, + RequireActiveVersion: templateAccessControl.RequireActiveVersion, + Deprecated: templateAccessControl.IsDeprecated(), + DeprecationMessage: templateAccessControl.Deprecated, + Deleted: template.Deleted, + MaxPortShareLevel: maxPortShareLevel, + UseClassicParameterFlow: template.UseClassicParameterFlow, + CORSBehavior: codersdk.CORSBehavior(template.CorsBehavior), + DisableModuleCache: template.DisableModuleCache, + ModuleCacheDisabledByDeployment: codersdk.ModuleCacheDisabledByDeployment(api.DeploymentValues), + AgentsAllowed: template.AgentsAllowed, + AllowWorkspaceRenames: template.AllowWorkspaceRenames, } } diff --git a/coderd/templates_meta_update.go b/coderd/templates_meta_update.go index b1d44878cd..26ea8d8209 100644 --- a/coderd/templates_meta_update.go +++ b/coderd/templates_meta_update.go @@ -62,13 +62,23 @@ type templateMetaUpdate struct { // 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), @@ -87,7 +97,7 @@ func resolveTemplateMetaUpdate( 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), + disableModuleCache: disableModuleCache, allowWorkspaceRenames: ptr.NilToDefault(req.AllowWorkspaceRenames, template.AllowWorkspaceRenames), groupACL: template.GroupACL, diff --git a/coderd/templates_meta_update_internal_test.go b/coderd/templates_meta_update_internal_test.go index 2ad71dd081..c23b7bfa45 100644 --- a/coderd/templates_meta_update_internal_test.go +++ b/coderd/templates_meta_update_internal_test.go @@ -9,6 +9,7 @@ import ( "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 @@ -451,7 +452,7 @@ func TestResolveTemplateMetaUpdate(t *testing.T) { tc.expected.base(&tpl) } schedOpts := baselineScheduleOpts() - got, validErrs := resolveTemplateMetaUpdate(tpl, schedOpts, tc.req) + got, validErrs := resolveTemplateMetaUpdate(tpl, schedOpts, tc.req, nil) want := baselineResolved() tc.expected.override(&want) @@ -485,7 +486,7 @@ func TestResolveTemplateMetaUpdate_NameClearedFallsBackToTemplateName(t *testing 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) } @@ -508,7 +509,7 @@ func TestResolveTemplateMetaUpdate_NilRequestUsesScheduleOptsForRequirements(t * }, } - got, validErrs := resolveTemplateMetaUpdate(tpl, schedOpts, codersdk.UpdateTemplateMeta{}) + got, validErrs := resolveTemplateMetaUpdate(tpl, schedOpts, codersdk.UpdateTemplateMeta{}, nil) if len(validErrs) != 0 { t.Fatalf("unexpected validation errors: %+v", validErrs) } @@ -527,3 +528,40 @@ func TestResolveTemplateMetaUpdate_NilRequestUsesScheduleOptsForRequirements(t * 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) + } + }) + } +} diff --git a/coderd/templates_test.go b/coderd/templates_test.go index f87ce0cc48..de3707cba2 100644 --- a/coderd/templates_test.go +++ b/coderd/templates_test.go @@ -31,6 +31,7 @@ import ( "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/provisioner/echo" "github.com/coder/coder/v2/testutil" + "github.com/coder/serpent" ) // TestTemplatesListSingleAuthorizePrepare guards against reintroducing the @@ -1843,6 +1844,33 @@ func TestPatchTemplateMeta(t *testing.T) { assert.False(t, updated.DisableModuleCache, "expected false") }) + t.Run("DisableModuleCacheDeploymentWide", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Provisioner.DisableModuleCache = serpent.Bool(true) + client := coderdtest.New(t, &coderdtest.Options{DeploymentValues: dv}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + require.True(t, template.ModuleCacheDisabledByDeployment, "the deployment disables the module cache") + require.False(t, template.DisableModuleCache, "the template itself does not opt out") + + ctx := testutil.Context(t, testutil.WaitLong) + + // The per-template toggle is read-only while the deployment disables the + // cache, so this request does not change anything. + _, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + DisableModuleCache: new(true), + }) + require.NoError(t, err) + + updated, err := client.Template(ctx, template.ID) + require.NoError(t, err) + assert.False(t, updated.DisableModuleCache, "expected the stored value to be untouched") + assert.True(t, updated.ModuleCacheDisabledByDeployment, "expected true") + }) + t.Run("AllowWorkspaceRenames", func(t *testing.T) { t.Parallel() diff --git a/codersdk/deployment.go b/codersdk/deployment.go index a5805de74c..620c65af29 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1239,6 +1239,10 @@ type ProvisionerConfig struct { DaemonPollJitter serpent.Duration `json:"daemon_poll_jitter" typescript:",notnull"` ForceCancelInterval serpent.Duration `json:"force_cancel_interval" typescript:",notnull"` DaemonPSK serpent.String `json:"daemon_psk" typescript:",notnull"` + // DisableModuleCache disables the reuse of Terraform modules cached at + // template import for every template in the deployment. Templates cannot + // opt back in. + DisableModuleCache serpent.Bool `json:"disable_module_cache" typescript:",notnull"` } type RateLimitConfig struct { @@ -3215,6 +3219,16 @@ communicating directly.`, YAML: "forceCancelInterval", Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"), }, + { + Name: "Disable Terraform Module Cache", + Description: "Disable the reuse of Terraform modules cached at template import for all templates. Modules are re-downloaded on every workspace build. Individual templates cannot opt back in.", + Flag: "provisioner-disable-module-cache", + Env: "CODER_PROVISIONER_DISABLE_MODULE_CACHE", + Default: "false", + Value: &c.Provisioner.DisableModuleCache, + Group: &deploymentGroupProvisioning, + YAML: "disableModuleCache", + }, { Name: "Provisioner Daemon Pre-shared Key (PSK)", Description: "Pre-shared key to authenticate external provisioner daemons to Coder server.", diff --git a/codersdk/templates.go b/codersdk/templates.go index 83bf570995..438aa5f8a4 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -71,9 +71,15 @@ type Template struct { AgentsAllowed bool `json:"agents_allowed"` // DisableModuleCache disables the use of cached Terraform modules during - // provisioning. + // provisioning for this template. It is read-only while + // ModuleCacheDisabledByDeployment is true. DisableModuleCache bool `json:"disable_module_cache"` + // ModuleCacheDisabledByDeployment reports that the deployment disables the + // Terraform module cache for every template. Templates cannot opt back in, + // so the effective state is disabled regardless of DisableModuleCache. + ModuleCacheDisabledByDeployment bool `json:"module_cache_disabled_by_deployment"` + // AllowWorkspaceRenames permits users to rename workspaces built from this // template. Renaming can be destructive for templates whose Terraform // references the workspace name. @@ -183,6 +189,19 @@ type ArchiveTemplateVersionsResponse struct { ArchivedIDs []uuid.UUID `json:"archived_ids"` } +// ModuleCacheDisabled reports whether cached Terraform modules must be withheld +// from provisioner jobs. The deployment-wide setting wins: a template cannot opt +// back into the cache once the deployment disables it. +func ModuleCacheDisabled(dv *DeploymentValues, templateDisableModuleCache bool) bool { + return templateDisableModuleCache || ModuleCacheDisabledByDeployment(dv) +} + +// ModuleCacheDisabledByDeployment reports whether the deployment disables the +// Terraform module cache for every template. +func ModuleCacheDisabledByDeployment(dv *DeploymentValues) bool { + return dv != nil && dv.Provisioner.DisableModuleCache.Value() +} + type TemplateRole string const ( @@ -286,7 +305,8 @@ type UpdateTemplateMeta struct { // An "opt-out" is present in case the new feature breaks some existing templates. UseClassicParameterFlow *bool `json:"use_classic_parameter_flow,omitempty"` // DisableModuleCache disables the using of cached Terraform modules during - // provisioning. It is recommended not to disable this. + // provisioning. It is ignored while the deployment disables the module + // cache for all templates. It is recommended not to disable this. DisableModuleCache *bool `json:"disable_module_cache,omitempty"` // AgentsAllowed controls whether Coder Agents can create workspaces using // this template. If omitted, the current value is preserved. diff --git a/codersdk/templates_test.go b/codersdk/templates_test.go new file mode 100644 index 0000000000..c0f1728d74 --- /dev/null +++ b/codersdk/templates_test.go @@ -0,0 +1,62 @@ +package codersdk_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" +) + +func TestModuleCacheDisabled(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + deploymentValues *codersdk.DeploymentValues + templateDisabled bool + expected bool + }{ + { + name: "NoDeploymentValues", + deploymentValues: nil, + expected: false, + }, + { + name: "NoDeploymentValuesTemplateDisabled", + deploymentValues: nil, + templateDisabled: true, + expected: true, + }, + { + name: "BothEnabled", + deploymentValues: deploymentValuesWithModuleCacheDisabled(false), + expected: false, + }, + { + name: "TemplateDisabled", + deploymentValues: deploymentValuesWithModuleCacheDisabled(false), + templateDisabled: true, + expected: true, + }, + { + // The deployment cannot be overridden by a template. + name: "DeploymentDisabled", + deploymentValues: deploymentValuesWithModuleCacheDisabled(true), + expected: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.expected, codersdk.ModuleCacheDisabled(tc.deploymentValues, tc.templateDisabled)) + }) + } +} + +func deploymentValuesWithModuleCacheDisabled(disabled bool) *codersdk.DeploymentValues { + dv := &codersdk.DeploymentValues{} + dv.Provisioner.DisableModuleCache = serpent.Bool(disabled) + return dv +} diff --git a/docs/admin/setup/configuration-reference.md b/docs/admin/setup/configuration-reference.md index 2f81cae418..f623ae9cb7 100644 --- a/docs/admin/setup/configuration-reference.md +++ b/docs/admin/setup/configuration-reference.md @@ -1618,6 +1618,15 @@ OIDC issuer urls must match in the request, the id_token 'iss' claim, and in the Tune the behavior of the provisioner, which is responsible for creating, updating, and deleting workspace resources. +### Disable Terraform module cache + +Disable the reuse of Terraform modules cached at template import for all templates. Modules are re-downloaded on every workspace build. Individual templates cannot opt back in. + +- Environment variable: `CODER_PROVISIONER_DISABLE_MODULE_CACHE` +- CLI flag: [`--provisioner-disable-module-cache`](../../reference/cli/server.md#--provisioner-disable-module-cache) +- YAML key: `provisioning.disableModuleCache` +- Default value: `false` + ### Force cancel interval Time to force cancel provisioning tasks that are stuck. diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 9ab85e12f1..f0b4eebfc7 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -533,6 +533,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "string" ], "daemons": 0, + "disable_module_cache": true, "force_cancel_interval": 0 }, "proxy_health_status_interval": 0, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index ce95802e3e..846a5e4853 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -7589,6 +7589,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "string" ], "daemons": 0, + "disable_module_cache": true, "force_cancel_interval": 0 }, "proxy_health_status_interval": 0, @@ -8195,6 +8196,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "string" ], "daemons": 0, + "disable_module_cache": true, "force_cancel_interval": 0 }, "proxy_health_status_interval": 0, @@ -12346,20 +12348,22 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "string" ], "daemons": 0, + "disable_module_cache": true, "force_cancel_interval": 0 } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------------------|-----------------|----------|--------------|-----------------------------------------------------------| -| `daemon_poll_interval` | integer | false | | | -| `daemon_poll_jitter` | integer | false | | | -| `daemon_psk` | string | false | | | -| `daemon_types` | array of string | false | | | -| `daemons` | integer | false | | Daemons is the number of built-in terraform provisioners. | -| `force_cancel_interval` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +|-------------------------|-----------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `daemon_poll_interval` | integer | false | | | +| `daemon_poll_jitter` | integer | false | | | +| `daemon_psk` | string | false | | | +| `daemon_types` | array of string | false | | | +| `daemons` | integer | false | | Daemons is the number of built-in terraform provisioners. | +| `disable_module_cache` | boolean | false | | Disable module cache disables the reuse of Terraform modules cached at template import for every template in the deployment. Templates cannot opt back in. | +| `force_cancel_interval` | integer | false | | | ## codersdk.ProvisionerDaemon @@ -13637,6 +13641,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", @@ -13654,46 +13659,47 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|------------------------------------|--------------------------------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `active_user_count` | integer | false | | Active user count is set to -1 when loading. | -| `active_version_id` | string | false | | | -| `activity_bump_ms` | integer | false | | | -| `agents_allowed` | boolean | false | | | -| `allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | -| `allow_user_autostop` | boolean | false | | | -| `allow_user_cancel_workspace_jobs` | boolean | false | | | -| `allow_workspace_renames` | boolean | false | | Allow workspace renames permits users to rename workspaces built from this template. Renaming can be destructive for templates whose Terraform references the workspace name. | -| `autostart_requirement` | [codersdk.TemplateAutostartRequirement](#codersdktemplateautostartrequirement) | false | | | -| `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement and AutostartRequirement are enterprise features. Its value is only used if your license is entitled to use the advanced template scheduling feature. | -| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | | -| `cors_behavior` | [codersdk.CORSBehavior](#codersdkcorsbehavior) | false | | | -| `created_at` | string | false | | | -| `created_by_id` | string | false | | | -| `created_by_name` | string | false | | | -| `default_ttl_ms` | integer | false | | | -| `deleted` | boolean | false | | | -| `deprecated` | boolean | false | | | -| `deprecation_message` | string | false | | | -| `description` | string | false | | | -| `disable_module_cache` | boolean | false | | Disable module cache disables the use of cached Terraform modules during provisioning. | -| `display_name` | string | false | | | -| `failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. | -| `icon` | string | false | | | -| `id` | string | false | | | -| `max_port_share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | | -| `name` | string | false | | | -| `organization_display_name` | string | false | | | -| `organization_icon` | string | false | | | -| `organization_id` | string | false | | | -| `organization_name` | string | false | | | -| `provisioner` | string | false | | | -| `require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. | -| `time_til_autostop_notify_ms` | integer | false | | Time til autostop notify ms is the duration before the workspace's autostop deadline at which a reminder notification is sent. 0 disables the notification. | -| `time_til_dormant_autodelete_ms` | integer | false | | | -| `time_til_dormant_ms` | integer | false | | | -| `updated_at` | string | false | | | -| `use_classic_parameter_flow` | boolean | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------------------|--------------------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `active_user_count` | integer | false | | Active user count is set to -1 when loading. | +| `active_version_id` | string | false | | | +| `activity_bump_ms` | integer | false | | | +| `agents_allowed` | boolean | false | | | +| `allow_user_autostart` | boolean | false | | Allow user autostart and AllowUserAutostop are enterprise-only. Their values are only used if your license is entitled to use the advanced template scheduling feature. | +| `allow_user_autostop` | boolean | false | | | +| `allow_user_cancel_workspace_jobs` | boolean | false | | | +| `allow_workspace_renames` | boolean | false | | Allow workspace renames permits users to rename workspaces built from this template. Renaming can be destructive for templates whose Terraform references the workspace name. | +| `autostart_requirement` | [codersdk.TemplateAutostartRequirement](#codersdktemplateautostartrequirement) | false | | | +| `autostop_requirement` | [codersdk.TemplateAutostopRequirement](#codersdktemplateautostoprequirement) | false | | Autostop requirement and AutostartRequirement are enterprise features. Its value is only used if your license is entitled to use the advanced template scheduling feature. | +| `build_time_stats` | [codersdk.TemplateBuildTimeStats](#codersdktemplatebuildtimestats) | false | | | +| `cors_behavior` | [codersdk.CORSBehavior](#codersdkcorsbehavior) | false | | | +| `created_at` | string | false | | | +| `created_by_id` | string | false | | | +| `created_by_name` | string | false | | | +| `default_ttl_ms` | integer | false | | | +| `deleted` | boolean | false | | | +| `deprecated` | boolean | false | | | +| `deprecation_message` | string | false | | | +| `description` | string | false | | | +| `disable_module_cache` | boolean | false | | Disable module cache disables the use of cached Terraform modules during provisioning for this template. It is read-only while ModuleCacheDisabledByDeployment is true. | +| `display_name` | string | false | | | +| `failure_ttl_ms` | integer | false | | Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature. | +| `icon` | string | false | | | +| `id` | string | false | | | +| `max_port_share_level` | [codersdk.WorkspaceAgentPortShareLevel](#codersdkworkspaceagentportsharelevel) | false | | | +| `module_cache_disabled_by_deployment` | boolean | false | | Module cache disabled by deployment reports that the deployment disables the Terraform module cache for every template. Templates cannot opt back in, so the effective state is disabled regardless of DisableModuleCache. | +| `name` | string | false | | | +| `organization_display_name` | string | false | | | +| `organization_icon` | string | false | | | +| `organization_id` | string | false | | | +| `organization_name` | string | false | | | +| `provisioner` | string | false | | | +| `require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. | +| `time_til_autostop_notify_ms` | integer | false | | Time til autostop notify ms is the duration before the workspace's autostop deadline at which a reminder notification is sent. 0 disables the notification. | +| `time_til_dormant_autodelete_ms` | integer | false | | | +| `time_til_dormant_ms` | integer | false | | | +| `updated_at` | string | false | | | +| `use_classic_parameter_flow` | boolean | false | | | #### Enumerated Values @@ -14144,6 +14150,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", @@ -15779,7 +15786,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W | `deprecation_message` | string | false | | Deprecation message if set, will mark the template as deprecated and block any new workspaces from using this template. If passed an empty string, will remove the deprecated message, making the template usable for new workspaces again. | | `description` | string | false | | | | `disable_everyone_group_access` | boolean | false | | Disable everyone group access allows optionally disabling the default behavior of granting the 'everyone' group access to use the template. If this is set to true, the template will not be available to all users, and must be explicitly granted to users or groups in the permissions settings of the template. | -| `disable_module_cache` | boolean | false | | Disable module cache disables the using of cached Terraform modules during provisioning. It is recommended not to disable this. | +| `disable_module_cache` | boolean | false | | Disable module cache disables the using of cached Terraform modules during provisioning. It is ignored while the deployment disables the module cache for all templates. It is recommended not to disable this. | | `display_name` | string | false | | | | `failure_ttl_ms` | integer | false | | | | `icon` | string | false | | | diff --git a/docs/reference/api/templatebuilder.md b/docs/reference/api/templatebuilder.md index 199f7abd46..5a494f10f1 100644 --- a/docs/reference/api/templatebuilder.md +++ b/docs/reference/api/templatebuilder.md @@ -216,6 +216,7 @@ curl -X POST http://coder-server:8080/api/v2/templatebuilder/compose/template \ "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index 5ce37ddcda..2adb098fa9 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -79,6 +79,7 @@ To include deprecated templates, specify `deprecated:true` in the search query. "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", @@ -135,12 +136,13 @@ Restarts will only happen on weekdays in this list on weeks which line up with W |`» deprecated`|boolean|false||| |`» deprecation_message`|string|false||| |`» description`|string|false||| -|`» disable_module_cache`|boolean|false||Disable module cache disables the use of cached Terraform modules during provisioning.| +|`» disable_module_cache`|boolean|false||Disable module cache disables the use of cached Terraform modules during provisioning for this template. It is read-only while ModuleCacheDisabledByDeployment is true.| |`» display_name`|string|false||| |`» failure_ttl_ms`|integer|false||Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature.| |`» icon`|string|false||| |`» id`|string(uuid)|false||| |`» max_port_share_level`|[codersdk.WorkspaceAgentPortShareLevel](schemas.md#codersdkworkspaceagentportsharelevel)|false||| +|`» module_cache_disabled_by_deployment`|boolean|false||Module cache disabled by deployment reports that the deployment disables the Terraform module cache for every template. Templates cannot opt back in, so the effective state is disabled regardless of DisableModuleCache.| |`» name`|string|false||| |`» organization_display_name`|string|false||| |`» organization_icon`|string|false||| @@ -274,6 +276,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", @@ -429,6 +432,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", @@ -854,6 +858,7 @@ To include deprecated templates, specify `deprecated:true` in the search query. "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", @@ -910,12 +915,13 @@ Restarts will only happen on weekdays in this list on weeks which line up with W |`» deprecated`|boolean|false||| |`» deprecation_message`|string|false||| |`» description`|string|false||| -|`» disable_module_cache`|boolean|false||Disable module cache disables the use of cached Terraform modules during provisioning.| +|`» disable_module_cache`|boolean|false||Disable module cache disables the use of cached Terraform modules during provisioning for this template. It is read-only while ModuleCacheDisabledByDeployment is true.| |`» display_name`|string|false||| |`» failure_ttl_ms`|integer|false||Failure ttl ms TimeTilDormantMillis, and TimeTilDormantAutoDeleteMillis are enterprise-only. Their values are used if your license is entitled to use the advanced template scheduling feature.| |`» icon`|string|false||| |`» id`|string(uuid)|false||| |`» max_port_share_level`|[codersdk.WorkspaceAgentPortShareLevel](schemas.md#codersdkworkspaceagentportsharelevel)|false||| +|`» module_cache_disabled_by_deployment`|boolean|false||Module cache disabled by deployment reports that the deployment disables the Terraform module cache for every template. Templates cannot opt back in, so the effective state is disabled regardless of DisableModuleCache.| |`» name`|string|false||| |`» organization_display_name`|string|false||| |`» organization_icon`|string|false||| @@ -1064,6 +1070,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \ "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", @@ -1244,6 +1251,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \ "icon": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "max_port_share_level": "owner", + "module_cache_disabled_by_deployment": true, "name": "string", "organization_display_name": "string", "organization_icon": "string", diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 09160f2cb5..d575858cb6 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -826,6 +826,17 @@ Deprecated and ignored. Time to force cancel provisioning tasks that are stuck. +### --provisioner-disable-module-cache + +| | | +|-------------|------------------------------------------------------| +| Type | bool | +| Environment | $CODER_PROVISIONER_DISABLE_MODULE_CACHE | +| YAML | provisioning.disableModuleCache | +| Default | false | + +Disable the reuse of Terraform modules cached at template import for all templates. Modules are re-downloaded on every workspace build. Individual templates cannot opt back in. + ### --provisioner-daemon-psk | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index c1bcdf8aab..c371f7b9d9 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -804,6 +804,11 @@ PROVISIONING OPTIONS: Tune the behavior of the provisioner, which is responsible for creating, updating, and deleting workspace resources. + --provisioner-disable-module-cache bool, $CODER_PROVISIONER_DISABLE_MODULE_CACHE (default: false) + Disable the reuse of Terraform modules cached at template import for + all templates. Modules are re-downloaded on every workspace build. + Individual templates cannot opt back in. + --provisioner-force-cancel-interval duration, $CODER_PROVISIONER_FORCE_CANCEL_INTERVAL (default: 10m0s) Time to force cancel provisioning tasks that are stuck. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 01c3c66503..01b880e234 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -7533,6 +7533,12 @@ export interface ProvisionerConfig { readonly daemon_poll_jitter: number; readonly force_cancel_interval: number; readonly daemon_psk: string; + /** + * DisableModuleCache disables the reuse of Terraform modules cached at + * template import for every template in the deployment. Templates cannot + * opt back in. + */ + readonly disable_module_cache: boolean; } // From codersdk/provisionerdaemons.go @@ -8936,9 +8942,16 @@ export interface Template { readonly agents_allowed: boolean; /** * DisableModuleCache disables the use of cached Terraform modules during - * provisioning. + * provisioning for this template. It is read-only while + * ModuleCacheDisabledByDeployment is true. */ readonly disable_module_cache: boolean; + /** + * ModuleCacheDisabledByDeployment reports that the deployment disables the + * Terraform module cache for every template. Templates cannot opt back in, + * so the effective state is disabled regardless of DisableModuleCache. + */ + readonly module_cache_disabled_by_deployment: boolean; /** * AllowWorkspaceRenames permits users to rename workspaces built from this * template. Renaming can be destructive for templates whose Terraform @@ -9945,7 +9958,8 @@ export interface UpdateTemplateMeta { readonly use_classic_parameter_flow?: boolean; /** * DisableModuleCache disables the using of cached Terraform modules during - * provisioning. It is recommended not to disable this. + * provisioning. It is ignored while the deployment disables the module + * cache for all templates. It is recommended not to disable this. */ readonly disable_module_cache?: boolean; /** diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.test.tsx index bb29d859a3..e983ac4cff 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.test.tsx @@ -9,6 +9,11 @@ const renameCheckbox = () => name: /allow users to rename their workspaces/i, }); +const moduleCacheCheckbox = () => + screen.getByRole("checkbox", { + name: /disable terraform module caching/i, + }); + const renderForm = ( overrides: Partial[0]> = {}, ) => { @@ -79,4 +84,38 @@ describe("TemplateSettingsForm", () => { }); }); }); + + describe("disable_module_cache", () => { + it("submits the new value when toggled on", async () => { + const user = userEvent.setup(); + const { onSubmit } = renderForm(); + + await user.click(moduleCacheCheckbox()); + await user.click(screen.getByRole("button", { name: /save/i })); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ disable_module_cache: true }), + expect.anything(), + ); + }); + }); + + it("is checked and read-only when the deployment disables caching", async () => { + const user = userEvent.setup(); + renderForm({ + template: { + ...MockTemplate, + module_cache_disabled_by_deployment: true, + }, + }); + + const checkbox = moduleCacheCheckbox(); + expect(checkbox).toBeChecked(); + expect(checkbox).toBeDisabled(); + + await user.click(checkbox); + expect(checkbox).toBeChecked(); + }); + }); }); diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 8a272d50fc..2862152190 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -1,5 +1,6 @@ import { cn } from "cn"; import { type FormikTouched, useFormik } from "formik"; +import { LockIcon, TriangleAlertIcon } from "lucide-react"; import type { FC } from "react"; import * as Yup from "yup"; import { @@ -8,6 +9,7 @@ import { type UpdateTemplateMeta, WorkspaceAppSharingLevels, } from "#/api/typesGenerated"; +import { Badge } from "#/components/Badge/Badge"; import { PremiumBadge } from "#/components/Badge/PresetBadges"; import { Button } from "#/components/Button/Button"; import { Checkbox } from "#/components/Checkbox/Checkbox"; @@ -124,6 +126,8 @@ export const TemplateSettingsForm: FC = ({ helperText: "Use Passthru to bypass Coder's built-in CORS protection.", }); const corsBehaviorHelperId = `${corsBehaviorField.id}-helper`; + const moduleCacheDisabledByDeployment = + template.module_cache_disabled_by_deployment; return ( = ({ { form.setFieldValue("disable_module_cache", checked === true); }} - disabled={isSubmitting} + disabled={isSubmitting || moduleCacheDisabledByDeployment} /> diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx index 6b07832f62..2b875e8dcb 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx @@ -30,6 +30,15 @@ export const AgentsNotAllowed: Story = { }, }; +export const ModuleCacheDisabledByDeployment: Story = { + args: { + template: { + ...MockTemplate, + module_cache_disabled_by_deployment: true, + }, + }, +}; + export const SaveTemplateSettingsError: Story = { args: { submitError: mockApiError({ diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index d452644dcf..003be5cf8f 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -976,6 +976,7 @@ export const MockTemplate: TypesGen.Template = { use_classic_parameter_flow: false, cors_behavior: "simple", disable_module_cache: false, + module_cache_disabled_by_deployment: false, allow_workspace_renames: false, };