From 92de37516b3f5acd3e92593375b2ec48f9aeaa20 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Thu, 24 Sep 2026 17:46:13 -0700 Subject: [PATCH 01/18] Accept ordered config files and deprecate the map form for Workbench --- charts/rstudio-workbench/Chart.yaml | 4 +- charts/rstudio-workbench/NEWS.md | 27 +++ charts/rstudio-workbench/README.md | 71 +++++-- charts/rstudio-workbench/README.md.gotmpl | 59 ++++-- charts/rstudio-workbench/templates/NOTES.txt | 51 ++++- .../templates/configmap-general.yaml | 22 ++- .../tests/configmap_test.yaml | 178 ++++++++++++++++++ .../rstudio-workbench/tests/notes_test.yaml | 96 ++++++++++ charts/rstudio-workbench/values.yaml | 7 +- 9 files changed, 475 insertions(+), 40 deletions(-) diff --git a/charts/rstudio-workbench/Chart.yaml b/charts/rstudio-workbench/Chart.yaml index e22a7ffa..3de079c5 100644 --- a/charts/rstudio-workbench/Chart.yaml +++ b/charts/rstudio-workbench/Chart.yaml @@ -1,6 +1,6 @@ name: rstudio-workbench description: Official Helm chart for Posit Workbench -version: 0.22.2 +version: 0.23.0 apiVersion: v2 appVersion: 2026.09.0 icon: https://raw.githubusercontent.com/rstudio/helm/main/images/posit-icon-fullcolor.svg @@ -13,7 +13,7 @@ maintainers: url: https://github.com/sol-eng dependencies: - name: rstudio-library - version: 0.1.35 + version: 0.1.38 repository: https://helm.rstudio.com annotations: artifacthub.io/images: | diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index c0249b87..82948936 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -1,5 +1,32 @@ # Changelog +## 0.23.0 + +- Config files whose behavior depends on the order of their sections or entries can now be written + as a list, putting `- ` in front of each section or entry, and are rendered in the order written. + This covers `config.server.profiles`, `config.server.launcher\.*\.resources\.conf`, + `config.profiles.launcher\.*\.profiles\.conf`, and `config.session.repos\.conf`: + + ```yaml + config: + server: + profiles: + - "*": + max-memory-mb: 1024 + - "@analysts": + max-memory-mb: 4096 + session: + repos.conf: + - Internal: https://pkgs.example.com/internal + - CRAN: https://packagemanager.posit.co/cran/latest + ``` + +- **DEPRECATED**: writing those files as a map. A map is rendered in alphabetical order, which + silently changes what these files do, and nothing in `values.yaml` shows it. The map form still + works and now prints a `WARNING` in `NOTES.txt` once a file holds more than one section or entry. + It will be removed in a future chart release. The raw string form (`profiles: |`) keeps the + written order and is unaffected. + ## 0.22.2 - Bump Workbench version to 2026.09.0 diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index ca399c43..c8cda6bd 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -1,6 +1,6 @@ # Posit Workbench -![Version: 0.22.2](https://img.shields.io/badge/Version-0.22.2-informational?style=flat-square) ![AppVersion: 2026.09.0](https://img.shields.io/badge/AppVersion-2026.09.0-informational?style=flat-square) +![Version: 0.23.0](https://img.shields.io/badge/Version-0.23.0-informational?style=flat-square) ![AppVersion: 2026.09.0](https://img.shields.io/badge/AppVersion-2026.09.0-informational?style=flat-square) #### _Official Helm chart for Posit Workbench_ @@ -24,11 +24,11 @@ To ensure a stable production deployment: ## Installing the chart -To install the chart with the release name `my-release` at version 0.22.2: +To install the chart with the release name `my-release` at version 0.23.0: ```{.bash} helm repo add rstudio https://helm.rstudio.com -helm upgrade --install my-release rstudio/rstudio-workbench --version=0.22.2 +helm upgrade --install my-release rstudio/rstudio-workbench --version=0.23.0 ``` To explore other chart versions, look at: @@ -340,15 +340,33 @@ pip can be configured with `config.session.pip.conf`: #### R repositories -R package repositories can be configured with `config.session.repos.conf`: +R package repositories can be configured with `config.session.repos.conf`. R reads the file in +order and uses that order to break ties when a package version is in more than one repository, so +write the repositories as a list, putting `- ` in front of each one: ```yaml config: session: repos.conf: - CRAN: https://packagemanager.posit.co/cran/__linux__/jammy/latest + - Internal: https://pkgs.example.com/internal + - CRAN: https://packagemanager.posit.co/cran/__linux__/jammy/latest ``` +Becomes: + +_/etc/rstudio/repos.conf_ + +```ini +Internal=https://pkgs.example.com/internal +CRAN=https://packagemanager.posit.co/cran/__linux__/jammy/latest +``` + +:::{.callout-warning} +Writing `repos.conf` as a map is deprecated and will be removed in a future chart release. A map +does not keep the order you wrote it in: the chart renders map keys alphabetically, so an internal +repository can't be put ahead of CRAN. +::: + For more information about configuring CRAN repositories in Workbench, see the [Posit Workbench Administrator Guide's - Package Installation > CRAN repositories](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/package_installation.html#cran-repositories) section. ## User provisioning @@ -410,6 +428,16 @@ Sections define whether a set of configurations is applied to a user's jobs base The product reads configuration from top to bottom and "last-in-wins" for a given configuration value. +Because these files are read in order, write their sections as a list, putting `- ` in front of +each section header. A map does not keep the order you wrote it in: the chart renders map keys +alphabetically, so, for example, a user named `12345` would lose their own settings to every group +they belong to. Writing an order-sensitive file as a map is deprecated and will be removed in a +future chart release. + +This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and +`launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and +pre-selects the first one). + ### `/etc/rstudio/profiles` The `/etc/rstudio/profiles` file enables you to tailor the behavior of sessions on a per-user or per-group basis. See the [Posit Workbench Administrator Guide - User and Group Profiles](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/user_and_group_profiles.html) page for more information. @@ -420,9 +448,11 @@ In the `values.yaml`, define the content of `/etc/rstudio/profiles` in `config.s config: server: profiles: - "*": - session-limit: 5 - session-timeout-minutes: 60 + - "*": + session-limit: 5 + session-timeout-minutes: 60 + - "@analysts": + session-limit: 10 ``` Becomes: @@ -433,6 +463,9 @@ _/etc/rstudio/profiles_ [*] session-limit=5 session-timeout-minutes=60 + +[@analysts] +session-limit=10 ``` ### `/etc/rstudio/launcher.kubernetes.profiles.conf` @@ -455,14 +488,14 @@ For example: config: profiles: launcher.kubernetes.profiles.conf: - "*": - some-key: - - value1 - - value2 - myuser: - some-key: - - value4 - - value5 + - "*": + some-key: + - value1 + - value2 + - myuser: + some-key: + - value4 + - value5 ``` Becomes: @@ -764,11 +797,11 @@ When combining `sealedSecret.enabled=true` with rootless mode (`pod.runAsRoot=fa | config.defaultMode.userProvisioning | int | 0600 | default mode for userProvisioning config | | config.existingSecrets | list | `[]` | a list of existing Kubernetes Secrets to project into `/mnt/secret-configmap/rstudio/`. Each item should have `name` (secret name) and `items` (list of keys to mount with their paths). Mounted with 0600 permissions by default. | | config.pam | object | `{}` | a map of pam config files. Will be mounted into the container directly / per file, in order to avoid overwriting system pam files | -| config.profiles | object | `{}` | a map of server-scoped config files (akin to `config.server`), but with specific behavior that supports profiles. See README for more information. | +| config.profiles | object | `{}` | a map of server-scoped config files (akin to `config.server`), but with specific behavior that supports profiles. `launcher.*.profiles.conf` is read in order, so write its sections as a list of single-entry maps. See README for more information. | | config.secret | string | `nil` | a map of secret, server-scoped config files (database.conf, databricks.conf, openid-client-secret). Mounted to `/mnt/secret-configmap/rstudio/` with 0600 permissions | -| config.server | object | [RStudio Workbench Configuration Reference](https://docs.rstudio.com/ide/server-pro/rstudio_server_configuration/rstudio_server_configuration.html). See defaults with `helm show values` | a map of server config files. Mounted to `/mnt/configmap/rstudio/` | +| config.server | object | [RStudio Workbench Configuration Reference](https://docs.rstudio.com/ide/server-pro/rstudio_server_configuration/rstudio_server_configuration.html). See defaults with `helm show values` | a map of server config files. Mounted to `/mnt/configmap/rstudio/`. Each file's contents may be a map, a raw string, or - for files read in order, such as `profiles` and `launcher.*.resources.conf` - a list of single-entry maps. See README for more information. | | config.serverDcf | object | `{"launcher-mounts":[]}` | a map of server-scoped config files (akin to `config.server`), but with .dcf file formatting (i.e. `launcher-mounts`, `launcher-env`, etc.) | -| config.session | object | `{"notifications.conf":{},"repos.conf":{"CRAN":"https://packagemanager.posit.co/cran/__linux__/jammy/latest"},"rsession.conf":{},"rstudio-prefs.json":"{}\n"}` | a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. | +| config.session | object | `{"notifications.conf":{},"repos.conf":{"CRAN":"https://packagemanager.posit.co/cran/__linux__/jammy/latest"},"rsession.conf":{},"rstudio-prefs.json":"{}\n"}` | a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. Each file's contents may be a map, a raw string, or - for files read in order, such as `repos.conf` - a list of single-entry maps. See README for more information. | | config.sessionSecret | object | `{}` | a map of secret, session-scoped config files (odbc.ini, etc.). Mounted to `/mnt/session-secret/` on both server and session, by default | | config.sssd | object | `{"conf":{},"enabled":true}` | Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. On by default; automatically skipped when the pod runs unprivileged (`pod.runAsRoot: false`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD. | | config.sssd.conf | object | `{}` | a map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Replaces the deprecated `config.userProvisioning`. | diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index 4eda4cab..8b18ae01 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -286,15 +286,33 @@ pip can be configured with `config.session.pip.conf`: #### R repositories -R package repositories can be configured with `config.session.repos.conf`: +R package repositories can be configured with `config.session.repos.conf`. R reads the file in +order and uses that order to break ties when a package version is in more than one repository, so +write the repositories as a list, putting `- ` in front of each one: ```yaml config: session: repos.conf: - CRAN: https://packagemanager.posit.co/cran/__linux__/jammy/latest + - Internal: https://pkgs.example.com/internal + - CRAN: https://packagemanager.posit.co/cran/__linux__/jammy/latest ``` +Becomes: + +_/etc/rstudio/repos.conf_ + +```ini +Internal=https://pkgs.example.com/internal +CRAN=https://packagemanager.posit.co/cran/__linux__/jammy/latest +``` + +:::{.callout-warning} +Writing `repos.conf` as a map is deprecated and will be removed in a future chart release. A map +does not keep the order you wrote it in: the chart renders map keys alphabetically, so an internal +repository can't be put ahead of CRAN. +::: + For more information about configuring CRAN repositories in Workbench, see the [Posit Workbench Administrator Guide's - Package Installation > CRAN repositories](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/package_installation.html#cran-repositories) section. ## User provisioning @@ -356,6 +374,16 @@ Sections define whether a set of configurations is applied to a user's jobs base The product reads configuration from top to bottom and "last-in-wins" for a given configuration value. +Because these files are read in order, write their sections as a list, putting `- ` in front of +each section header. A map does not keep the order you wrote it in: the chart renders map keys +alphabetically, so, for example, a user named `12345` would lose their own settings to every group +they belong to. Writing an order-sensitive file as a map is deprecated and will be removed in a +future chart release. + +This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and +`launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and +pre-selects the first one). + ### `/etc/rstudio/profiles` The `/etc/rstudio/profiles` file enables you to tailor the behavior of sessions on a per-user or per-group basis. See the [Posit Workbench Administrator Guide - User and Group Profiles](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/user_and_group_profiles.html) page for more information. @@ -366,9 +394,11 @@ In the `values.yaml`, define the content of `/etc/rstudio/profiles` in `config.s config: server: profiles: - "*": - session-limit: 5 - session-timeout-minutes: 60 + - "*": + session-limit: 5 + session-timeout-minutes: 60 + - "@analysts": + session-limit: 10 ``` Becomes: @@ -379,6 +409,9 @@ _/etc/rstudio/profiles_ [*] session-limit=5 session-timeout-minutes=60 + +[@analysts] +session-limit=10 ``` ### `/etc/rstudio/launcher.kubernetes.profiles.conf` @@ -401,14 +434,14 @@ For example: config: profiles: launcher.kubernetes.profiles.conf: - "*": - some-key: - - value1 - - value2 - myuser: - some-key: - - value4 - - value5 + - "*": + some-key: + - value1 + - value2 + - myuser: + some-key: + - value4 + - value5 ``` Becomes: diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index b26b72ca..e8bd94e9 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -22,6 +22,51 @@ kubectl -n {{ $.Release.Namespace }} get secret {{ include "rstudio-workbench.fu ``` {{- end }} +{{- /* Files whose behavior depends on the order of their sections or entries. Rendering these + from a map sorts them by name, silently changing what they do, so the map form is + deprecated in favor of the ordered list form. A raw string keeps the written order too, + so only the map form is flagged, and only once it holds more than one section or entry -- + below that there is no order to lose. */}} +{{- $unordered := list }} +{{- range $file, $contents := .Values.config.server }} + {{- if and (kindIs "map" $contents) (gt (len (keys $contents)) 1) (or (eq $file "profiles") (regexMatch "^launcher\\..+\\.resources\\.conf$" $file)) }} + {{- $unordered = append $unordered (printf "config.server.%s" ($file | replace "." "\\.")) }} + {{- end }} +{{- end }} +{{- range $file, $contents := .Values.config.profiles }} + {{- if and (kindIs "map" $contents) (gt (len (keys $contents)) 1) (regexMatch "^launcher\\..+\\.profiles\\.conf$" $file) }} + {{- $unordered = append $unordered (printf "config.profiles.%s" ($file | replace "." "\\.")) }} + {{- end }} +{{- end }} +{{- range $file, $contents := .Values.config.session }} + {{- if and (kindIs "map" $contents) (gt (len (keys $contents)) 1) (eq $file "repos.conf") }} + {{- $unordered = append $unordered (printf "config.session.%s" ($file | replace "." "\\.")) }} + {{- end }} +{{- end }} +{{- if $unordered }} + +WARNING: the following configuration files are written as maps, which does not keep the order you wrote them in +{{- range $unordered | sortAlpha }} + - `.Values.{{ . }}` +{{- end }} + Workbench reads these files in order, so sorting their sections by name changes how they behave. + Write them as a list instead, putting `- ` in front of each section or entry: + + config: + server: + profiles: + - "*": + max-memory-mb: 1024 + - "@analysts": + max-memory-mb: 4096 + session: + repos.conf: + - Internal: https://pkgs.example.com/internal + - CRAN: https://packagemanager.posit.co/cran/latest + + The map form still works for now, but will be removed in a future chart release. +{{- end }} + {{- if hasKey .Values.config.server "launcher.kubernetes.profiles.conf" }} WARNING: `.Values.config.server.launcher\.kubernetes\.profiles\.conf` is deprecated @@ -41,8 +86,10 @@ Please consider removing this configuration value. {{- if and .Values.launcher.useTemplates .Values.launcher.enabled }} {{- if hasKey .Values.config.profiles "launcher.kubernetes.profiles.conf" }} - {{- range $k,$v := (get .Values.config.profiles "launcher.kubernetes.profiles.conf") }} - {{- if hasKey $v "job-json-overrides" }} + {{- $normalized := dict }} + {{- include "rstudio-library.config.entries" (dict "data" (get .Values.config.profiles "launcher.kubernetes.profiles.conf") "result" $normalized) }} + {{- range $entry := $normalized.entries }} + {{- if hasKey $entry.config "job-json-overrides" }} {{- fail "\n\n`profiles` has `job-json-overrides` defined. This cannot be used with `launcher.useTemplates=true`.\n\nPlease move `job-json-overrides` to the corresponding `launcher.templateValues`, or set `launcher.useTemplates=false`.\n\nNote: `launcher.useTemplates=true` was made the default in chart version 0.9.0" }} {{- end }} {{- end }} diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml index bedde656..67eddee0 100644 --- a/charts/rstudio-workbench/templates/configmap-general.yaml +++ b/charts/rstudio-workbench/templates/configmap-general.yaml @@ -151,8 +151,26 @@ data: {{- end }} {{ include "rstudio-library.config.ini" $overrideDict | indent 2 }} {{/* helper variables to make things here a bit more sane */}} -{{- $profilesConfig := .Values.config.profiles }} -{{- $profilesConfig = mergeOverwrite (dict "launcher.kubernetes.profiles.conf" $defaultProfilesConfig) $profilesConfig }} +{{- $profilesConfig := .Values.config.profiles | deepCopy }} +{{- /* Apply the default [*] session image settings to launcher.kubernetes.profiles.conf. When it is + written as an ordered list, mergeOverwrite would drop the defaults along with the map, so + merge them into the [*] entry in place instead (prepending one if the admin wrote none). */}} +{{- $writtenProfiles := get $profilesConfig "launcher.kubernetes.profiles.conf" }} +{{- if kindIs "slice" $writtenProfiles }} + {{- $hasEveryone := false }} + {{- range $item := $writtenProfiles }} + {{- if hasKey $item "*" }} + {{- $hasEveryone = true }} + {{- $_ := set $item "*" (mergeOverwrite (deepCopy $defaultProfiles) (get $item "*")) }} + {{- end }} + {{- end }} + {{- if not $hasEveryone }} + {{- $writtenProfiles = prepend $writtenProfiles (dict "*" $defaultProfiles) }} + {{- end }} + {{- $_ := set $profilesConfig "launcher.kubernetes.profiles.conf" $writtenProfiles }} +{{- else }} + {{- $profilesConfig = mergeOverwrite (dict "launcher.kubernetes.profiles.conf" $defaultProfilesConfig) $profilesConfig }} +{{- end }} {{- $useNewerOverrides := and (not (hasKey .Values.config.server "launcher.kubernetes.profiles.conf")) (not .Values.launcher.useTemplates) }} {{- $jobJsonFilePath := "/mnt/job-json-overrides-new/" }} {{- /* $defaultOverrides should be empty from above if we are using templates */ -}} diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index f338e5e9..98ce0a5b 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -458,3 +458,181 @@ tests: - matchRegex: path: data["rserver.conf"] pattern: "(?m)^user-provisioning-enabled=0$" + + # -- Ordered (list form) config files. See https://github.com/rstudio/helm/issues/944 + - it: should render config.server order-sensitive files in the order written + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + profiles: + - jsmith: + max-memory-mb: 8192 + - "12345": + max-memory-mb: 8192 + - "@contractors": + max-memory-mb: 2048 + - "@analysts": + max-memory-mb: 4096 + - "*": + max-memory-mb: 1024 + launcher.kubernetes.resources.conf: + - small: + name: Small + cpus: 1 + - large: + name: Large + cpus: 8 + asserts: + - equal: + path: data["profiles"] + value: | + [jsmith] + max-memory-mb=8192 + + [12345] + max-memory-mb=8192 + + [@contractors] + max-memory-mb=2048 + + [@analysts] + max-memory-mb=4096 + + [*] + max-memory-mb=1024 + - equal: + path: data["launcher.kubernetes.resources.conf"] + value: | + [small] + cpus=1 + name=Small + + [large] + cpus=8 + name=Large + + - it: should sort config.server order-sensitive files by name when written as a map + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + profiles: + jsmith: + max-memory-mb: 8192 + "@analysts": + max-memory-mb: 4096 + "*": + max-memory-mb: 1024 + asserts: + - equal: + path: data["profiles"] + value: | + [*] + max-memory-mb=1024 + + [@analysts] + max-memory-mb=4096 + + [jsmith] + max-memory-mb=8192 + + - it: should render a raw string config.server file verbatim + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + profiles: | + [jsmith] + max-memory-mb=8192 + + [*] + max-memory-mb=1024 + asserts: + - equal: + path: data["profiles"] + value: | + [jsmith] + max-memory-mb=8192 + + [*] + max-memory-mb=1024 + + - it: should render config.session repos.conf in the order written + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + repos.conf: + - Internal: https://pkgs.example.com/internal + - CRAN: https://packagemanager.posit.co/cran/latest + asserts: + - equal: + path: data["repos.conf"] + value: | + Internal=https://pkgs.example.com/internal + CRAN=https://packagemanager.posit.co/cran/latest + + - it: should keep rendering r-versions records separated by blank lines + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + r-versions: + - Path: /opt/R/4.4.1 + Label: Latest + - Path: /opt/R/4.0.2 + Label: Old + asserts: + - equal: + path: data["r-versions"] + value: | + Label=Latest + Path=/opt/R/4.4.1 + + Label=Old + Path=/opt/R/4.0.2 + + - it: should render config.profiles sections in the order written, merging the default everyone section + template: configmap-general.yaml + documentIndex: 0 + set: + launcher: + useTemplates: false + config: + profiles: + launcher.kubernetes.profiles.conf: + - jsmith: + max-cpus: 8 + - "@analysts": + max-cpus: 4 + - "*": + max-cpus: 1 + asserts: + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "\\[jsmith\\]\\nmax-cpus=8\\n\\n\\[@analysts\\]\\nmax-cpus=4\\n\\n\\[\\*\\]\\n" + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "\\[\\*\\]\\nallow-unknown-images=1\\ncontainer-images=posit/workbench-session:.*\\ndefault-container-image=posit/workbench-session:.*\\njob-json-overrides=.*\\nmax-cpus=1" + + - it: should prepend an everyone section to ordered config.profiles that omits one + template: configmap-general.yaml + documentIndex: 0 + set: + launcher: + useTemplates: false + config: + profiles: + launcher.kubernetes.profiles.conf: + - jsmith: + max-cpus: 8 + asserts: + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "\\[\\*\\]\\nallow-unknown-images=1\\ncontainer-images=.*\\ndefault-container-image=.*\\njob-json-overrides=.*\\n\\n\\[jsmith\\]\\nmax-cpus=8" diff --git a/charts/rstudio-workbench/tests/notes_test.yaml b/charts/rstudio-workbench/tests/notes_test.yaml index 0175f9a5..6a176175 100644 --- a/charts/rstudio-workbench/tests/notes_test.yaml +++ b/charts/rstudio-workbench/tests/notes_test.yaml @@ -51,3 +51,99 @@ tests: asserts: - failedTemplate: errorPattern: "session.image.tagPrefix.*has been removed.*session.image.os" + + # -- Order-sensitive config files written as maps. See https://github.com/rstudio/helm/issues/944 + - it: should warn when order-sensitive config files are written as maps + set: + config: + server: + profiles: + "*": + max-memory-mb: 1024 + "@analysts": + max-memory-mb: 4096 + launcher.kubernetes.resources.conf: + small: + cpus: 1 + large: + cpus: 8 + profiles: + launcher.kubernetes.profiles.conf: + "*": + max-cpus: 1 + jsmith: + max-cpus: 8 + session: + repos.conf: + CRAN: https://packagemanager.posit.co/cran/latest + Internal: https://pkgs.example.com/internal + asserts: + - matchRegexRaw: + pattern: "WARNING: the following configuration files are written as maps" + - matchRegexRaw: + pattern: "`\\.Values\\.config\\.server\\.profiles`" + - matchRegexRaw: + pattern: "`\\.Values\\.config\\.server\\.launcher\\\\\\.kubernetes\\\\\\.resources\\\\\\.conf`" + - matchRegexRaw: + pattern: "`\\.Values\\.config\\.profiles\\.launcher\\\\\\.kubernetes\\\\\\.profiles\\\\\\.conf`" + - matchRegexRaw: + pattern: "`\\.Values\\.config\\.session\\.repos\\\\\\.conf`" + + - it: should not warn when order-sensitive config files are written as lists + set: + config: + server: + profiles: + - "*": + max-memory-mb: 1024 + launcher.kubernetes.resources.conf: + - small: + cpus: 1 + profiles: + launcher.kubernetes.profiles.conf: + - "*": + max-cpus: 1 + session: + repos.conf: + - CRAN: https://packagemanager.posit.co/cran/latest + asserts: + - notMatchRegexRaw: + pattern: "WARNING: the following configuration files are written as maps" + + - it: should not warn when an order-sensitive config file is written as a raw string + set: + config: + server: + profiles: | + [*] + max-memory-mb=1024 + asserts: + - notMatchRegexRaw: + pattern: "WARNING: the following configuration files are written as maps" + + - it: should not warn for config files whose order does not matter + set: + config: + server: + rserver.conf: + www-port: 8787 + session: + rsession.conf: + session-timeout-minutes: 60 + asserts: + - notMatchRegexRaw: + pattern: "WARNING: the following configuration files are written as maps" + + - it: should not warn for a single-entry map, which has no order to lose + set: + config: + server: + profiles: + "*": + max-memory-mb: 1024 + session: + repos.conf: + CRAN: https://packagemanager.posit.co/cran/latest + asserts: + - notMatchRegexRaw: + pattern: "WARNING: the following configuration files are written as maps" diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml index bd8e75e4..148da48a 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -579,6 +579,7 @@ config: value: "" # -- a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. + # Each file's contents may be a map, a raw string, or - for files read in order, such as `repos.conf` - a list of single-entry maps. See README for more information. session: repos.conf: CRAN: https://packagemanager.posit.co/cran/__linux__/jammy/latest @@ -608,7 +609,8 @@ config: enabled: true # -- a map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Replaces the deprecated `config.userProvisioning`. conf: {} - # -- a map of server config files. Mounted to `/mnt/configmap/rstudio/` + # -- a map of server config files. Mounted to `/mnt/configmap/rstudio/`. + # Each file's contents may be a map, a raw string, or - for files read in order, such as `profiles` and `launcher.*.resources.conf` - a list of single-entry maps. See README for more information. # @default -- [RStudio Workbench Configuration Reference](https://docs.rstudio.com/ide/server-pro/rstudio_server_configuration/rstudio_server_configuration.html). See defaults with `helm show values` server: rserver.conf: @@ -695,7 +697,8 @@ config: # HELP license_days_left the number of days left on the license # TYPE license_days gauge license_days_left #license-days-left# - # -- a map of server-scoped config files (akin to `config.server`), but with specific behavior that supports profiles. See README for more information. + # -- a map of server-scoped config files (akin to `config.server`), but with specific behavior that supports profiles. + # `launcher.*.profiles.conf` is read in order, so write its sections as a list of single-entry maps. See README for more information. profiles: {} # -- a map of server-scoped config files (akin to `config.server`), but with .dcf file formatting (i.e. `launcher-mounts`, `launcher-env`, etc.) serverDcf: From f403ce06ad3db1e2b1c90d9ab01f0b485517ab45 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Thu, 24 Sep 2026 18:30:13 -0700 Subject: [PATCH 02/18] Document that only sections are ordered, not options within them --- charts/rstudio-workbench/README.md | 5 +++++ charts/rstudio-workbench/README.md.gotmpl | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index c8cda6bd..56d3ab3d 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -438,6 +438,11 @@ This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and `launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and pre-selects the first one). +Only the sections are ordered. The options written inside a section are still rendered +alphabetically, which is what these files expect - they are resolved section by section, not +option by option. A file that depends on the order of options within a section needs the raw +string form. + ### `/etc/rstudio/profiles` The `/etc/rstudio/profiles` file enables you to tailor the behavior of sessions on a per-user or per-group basis. See the [Posit Workbench Administrator Guide - User and Group Profiles](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/user_and_group_profiles.html) page for more information. diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index 8b18ae01..513ecab2 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -384,6 +384,11 @@ This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and `launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and pre-selects the first one). +Only the sections are ordered. The options written inside a section are still rendered +alphabetically, which is what these files expect - they are resolved section by section, not +option by option. A file that depends on the order of options within a section needs the raw +string form. + ### `/etc/rstudio/profiles` The `/etc/rstudio/profiles` file enables you to tailor the behavior of sessions on a per-user or per-group basis. See the [Posit Workbench Administrator Guide - User and Group Profiles](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/user_and_group_profiles.html) page for more information. From 5a4b78558c81015fc563fc3702506eb6059151a7 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Mon, 28 Sep 2026 19:32:53 -0700 Subject: [PATCH 03/18] Write r-versions lint values as a DCF string --- charts/rstudio-workbench/NEWS.md | 8 +++++ .../lint/complex-values.yaml | 14 +++++--- .../lint/other-complex-values.yaml | 14 +++++--- .../tests/configmap_test.yaml | 35 +++++++++++++------ 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 82948936..8b488b36 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -21,6 +21,14 @@ - CRAN: https://packagemanager.posit.co/cran/latest ``` +- **BREAKING**: a file written as a list must give each section or entry its own `- `, holding a + single key. This rejects two shapes that previously rendered something unusable: several sections + crammed into one entry (a missing `- `), which rendered as `name=map[key:value]`; and a multi-field + record, which was only ever used for `config.session.r-versions` and emitted `Key=Value` where + Workbench parses that file as DCF (`Key: Value`) - see + https://github.com/rstudio/helm/issues/948. Write `r-versions` as a string (`r-versions: |`), + which is passed through unchanged. An option inside a section must also be a single value, since + ini files have no nesting. - **DEPRECATED**: writing those files as a map. A map is rendered in alphabetical order, which silently changes what these files do, and nothing in `values.yaml` shows it. The map form still works and now prints a `WARNING` in `NOTES.txt` once a file holds more than one section or entry. diff --git a/charts/rstudio-workbench/lint/complex-values.yaml b/charts/rstudio-workbench/lint/complex-values.yaml index dcf71fac..6f854791 100644 --- a/charts/rstudio-workbench/lint/complex-values.yaml +++ b/charts/rstudio-workbench/lint/complex-values.yaml @@ -150,11 +150,15 @@ config: CRAN: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest rsession.conf: {} notifications.conf: {} - r-versions: - - Label: test - Path: /opt/R/3.6.3 - - Label: other - Path: /opt/R/4.0.2 + # r-versions is a DCF file (Key: Value, records separated by a blank line), + # not an ini file, so it is written as a string and passed through unchanged. + # See https://github.com/rstudio/helm/issues/948 + r-versions: | + Label: test + Path: /opt/R/3.6.3 + + Label: other + Path: /opt/R/4.0.2 sessionSecret: odbc.ini: | [hello] diff --git a/charts/rstudio-workbench/lint/other-complex-values.yaml b/charts/rstudio-workbench/lint/other-complex-values.yaml index 65fc9225..4a8e767c 100644 --- a/charts/rstudio-workbench/lint/other-complex-values.yaml +++ b/charts/rstudio-workbench/lint/other-complex-values.yaml @@ -88,11 +88,15 @@ config: CRAN: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest rsession.conf: {} notifications.conf: {} - r-versions: - - Label: test - Path: /opt/R/3.6.3 - - Label: other - Path: /opt/R/4.0.2 + # r-versions is a DCF file (Key: Value, records separated by a blank line), + # not an ini file, so it is written as a string and passed through unchanged. + # See https://github.com/rstudio/helm/issues/948 + r-versions: | + Label: test + Path: /opt/R/3.6.3 + + Label: other + Path: /opt/R/4.0.2 secret: "database.conf": {} server: diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index 98ce0a5b..0c8f448a 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -577,26 +577,41 @@ tests: Internal=https://pkgs.example.com/internal CRAN=https://packagemanager.posit.co/cran/latest - - it: should keep rendering r-versions records separated by blank lines + # r-versions is DCF, not ini, so it has to be written as a string. See #948. + - it: should pass an r-versions string through unchanged template: configmap-session.yaml documentIndex: 0 set: config: session: - r-versions: - - Path: /opt/R/4.4.1 - Label: Latest - - Path: /opt/R/4.0.2 - Label: Old + r-versions: | + Path: /opt/R/4.4.1 + Label: Latest + + Path: /opt/R/4.0.2 + Label: Old asserts: - equal: path: data["r-versions"] value: | - Label=Latest - Path=/opt/R/4.4.1 + Path: /opt/R/4.4.1 + Label: Latest - Label=Old - Path=/opt/R/4.0.2 + Path: /opt/R/4.0.2 + Label: Old + + - it: should fail when r-versions is written as a list of records + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + r-versions: + - Path: /opt/R/4.4.1 + Label: Latest + asserts: + - failedTemplate: + errorPattern: "entry 1 of 'r-versions' holds more than one key" - it: should render config.profiles sections in the order written, merging the default everyone section template: configmap-general.yaml From 4d51ca8233d10788d256dd122505b84f1b7d938f Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Mon, 28 Sep 2026 19:56:13 -0700 Subject: [PATCH 04/18] Document r-versions as DCF and sort pam volume mounts --- charts/rstudio-workbench/NEWS.md | 3 +++ charts/rstudio-workbench/README.md | 27 +++++++++++++++++++ charts/rstudio-workbench/README.md.gotmpl | 27 +++++++++++++++++++ .../rstudio-workbench/templates/_helpers.tpl | 3 ++- .../tests/deployment_test.yaml | 25 +++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 8b488b36..b14dbc0e 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -29,6 +29,9 @@ https://github.com/rstudio/helm/issues/948. Write `r-versions` as a string (`r-versions: |`), which is passed through unchanged. An option inside a section must also be a single value, since ini files have no nesting. +- Fixed: with more than one `config.pam` file, the pam `volumeMounts` were emitted in Go map order, + so `helm template` was not reproducible and the Deployment's pod template changed between renders + with no configuration change. They are now sorted by file name. - **DEPRECATED**: writing those files as a map. A map is rendered in alphabetical order, which silently changes what these files do, and nothing in `values.yaml` shows it. The map form still works and now prints a `WARNING` in `NOTES.txt` once a file holds more than one section or entry. diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 56d3ab3d..221878b4 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -369,6 +369,33 @@ repository can't be put ahead of CRAN. For more information about configuring CRAN repositories in Workbench, see the [Posit Workbench Administrator Guide's - Package Installation > CRAN repositories](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/package_installation.html#cran-repositories) section. +#### R versions + +`/etc/rstudio/r-versions` is not an ini file. It is DCF: `Key: Value`, with a blank line between +each R version. Write it as a string, which the chart passes through unchanged: + +```yaml +config: + session: + r-versions: | + Path: /opt/R/4.1.3 + Label: Custom 4.1.3 + Repo: https://packagemanager.posit.co/cran/__linux__/jammy/latest + + Path: /opt/R/4.2.3 + Label: Custom 4.2.3 +``` + +:::{.callout-important} +Use `Key: Value`, not `Key=Value`. Writing this file as a map or a list makes the chart render it +with `=`, which Workbench cannot parse - it silently registers no R versions and logs +`does not point to a valid directory` for each line. See +[#948](https://github.com/rstudio/helm/issues/948). Writing it as a list is rejected by the chart; +writing it as a map is not, so take care with the separator. +::: + +See [Extended R version definitions](https://docs.posit.co/ide/server-pro/admin/r/using_multiple_versions_of_r.html#extended-r-version-definitions) in the Administrator Guide for the full list of fields. + ## User provisioning Provisioning users in Workbench containers is challenging. Session images create users automatically (with diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index 513ecab2..e7e3d338 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -315,6 +315,33 @@ repository can't be put ahead of CRAN. For more information about configuring CRAN repositories in Workbench, see the [Posit Workbench Administrator Guide's - Package Installation > CRAN repositories](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/package_installation.html#cran-repositories) section. +#### R versions + +`/etc/rstudio/r-versions` is not an ini file. It is DCF: `Key: Value`, with a blank line between +each R version. Write it as a string, which the chart passes through unchanged: + +```yaml +config: + session: + r-versions: | + Path: /opt/R/4.1.3 + Label: Custom 4.1.3 + Repo: https://packagemanager.posit.co/cran/__linux__/jammy/latest + + Path: /opt/R/4.2.3 + Label: Custom 4.2.3 +``` + +:::{.callout-important} +Use `Key: Value`, not `Key=Value`. Writing this file as a map or a list makes the chart render it +with `=`, which Workbench cannot parse - it silently registers no R versions and logs +`does not point to a valid directory` for each line. See +[#948](https://github.com/rstudio/helm/issues/948). Writing it as a list is rejected by the chart; +writing it as a map is not, so take care with the separator. +::: + +See [Extended R version definitions](https://docs.posit.co/ide/server-pro/admin/r/using_multiple_versions_of_r.html#extended-r-version-definitions) in the Administrator Guide for the full list of fields. + ## User provisioning Provisioning users in Workbench containers is challenging. Session images create users automatically (with diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index b7198c23..79a991ed 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -210,7 +210,8 @@ containers: mountPath: "/startup/custom" {{- end }} {{- if .Values.config.pam }} - {{- range $i, $pamFileName := keys .Values.config.pam }} + {{- /* sortAlpha: sprig keys returns Go map order, which varies between renders */}} + {{- range $i, $pamFileName := keys .Values.config.pam | sortAlpha }} - name: rstudio-pam mountPath: "/etc/pam.d/{{ $pamFileName }}" subPath: "{{ $pamFileName }}" diff --git a/charts/rstudio-workbench/tests/deployment_test.yaml b/charts/rstudio-workbench/tests/deployment_test.yaml index e354a099..806533dc 100644 --- a/charts/rstudio-workbench/tests/deployment_test.yaml +++ b/charts/rstudio-workbench/tests/deployment_test.yaml @@ -501,6 +501,31 @@ tests: - equal: path: 'spec.template.spec.volumes[?(@.name=="rstudio-pam")].configMap.defaultMode' value: 0600 + - it: should mount several pam files in a stable order + template: deployment.yaml + set: + config: + pam: + zzz.conf: + dsn: "test" + aaa.conf: + dsn: "test" + mmm.conf: + dsn: "test" + asserts: + # sprig `keys` returns Go map order, which varies between renders unless sorted. + # Asserted by index because these have to come out in a stable order; if the + # surrounding volumeMounts change, update the offsets. + - equal: + path: 'spec.template.spec.containers[0].volumeMounts[8].subPath' + value: "aaa.conf" + - equal: + path: 'spec.template.spec.containers[0].volumeMounts[9].subPath' + value: "mmm.conf" + - equal: + path: 'spec.template.spec.containers[0].volumeMounts[10].subPath' + value: "zzz.conf" + - it: should not specify a volumeMount and a volume for pam if config.pam is not defined template: deployment.yaml set: From b647123a33afcba3b3b80d13f7e73da46381e627 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Mon, 28 Sep 2026 20:30:09 -0700 Subject: [PATCH 05/18] Warn on any map form and guard repos.conf against a missing CRAN entry --- charts/rstudio-workbench/NEWS.md | 4 ++ charts/rstudio-workbench/README.md | 11 +++- charts/rstudio-workbench/README.md.gotmpl | 9 ++++ charts/rstudio-workbench/templates/NOTES.txt | 46 ++++++++++++++-- .../rstudio-workbench/tests/notes_test.yaml | 52 +++++++++++++++++-- charts/rstudio-workbench/values.yaml | 5 +- 6 files changed, 117 insertions(+), 10 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index b14dbc0e..ad7a4716 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -32,6 +32,10 @@ - Fixed: with more than one `config.pam` file, the pam `volumeMounts` were emitted in Go map order, so `helm template` was not reproducible and the Deployment's pod template changed between renders with no configuration change. They are now sorted by file name. +- **BREAKING**: the `config.session.repos\.conf` default is now a list, so supplying your own + replaces it instead of merging with it. Previously a map default merged with a map you supplied, + which quietly added the chart's CRAN entry to your repositories. Include a `CRAN` entry in your + own list - Workbench ignores the whole file without one. The chart now warns when it is missing. - **DEPRECATED**: writing those files as a map. A map is rendered in alphabetical order, which silently changes what these files do, and nothing in `values.yaml` shows it. The map form still works and now prints a `WARNING` in `NOTES.txt` once a file holds more than one section or entry. diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 221878b4..a0a52986 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -367,6 +367,15 @@ does not keep the order you wrote it in: the chart renders map keys alphabetical repository can't be put ahead of CRAN. ::: +:::{.callout-important} +Your `repos.conf` **replaces** the chart default rather than merging with it, and it must contain an +entry named `CRAN`. Workbench ignores the whole file when nothing is named `CRAN`, so the other +repositories are lost too - the only sign is `is missing CRAN entry` in the session log. The entry +does not have to be CRAN itself; point it at your own mirror if that is what sessions should use. +To configure repositories somewhere else entirely, set `config.session.repos\.conf: null` and the +chart renders no file. +::: + For more information about configuring CRAN repositories in Workbench, see the [Posit Workbench Administrator Guide's - Package Installation > CRAN repositories](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/package_installation.html#cran-repositories) section. #### R versions @@ -833,7 +842,7 @@ When combining `sealedSecret.enabled=true` with rootless mode (`pod.runAsRoot=fa | config.secret | string | `nil` | a map of secret, server-scoped config files (database.conf, databricks.conf, openid-client-secret). Mounted to `/mnt/secret-configmap/rstudio/` with 0600 permissions | | config.server | object | [RStudio Workbench Configuration Reference](https://docs.rstudio.com/ide/server-pro/rstudio_server_configuration/rstudio_server_configuration.html). See defaults with `helm show values` | a map of server config files. Mounted to `/mnt/configmap/rstudio/`. Each file's contents may be a map, a raw string, or - for files read in order, such as `profiles` and `launcher.*.resources.conf` - a list of single-entry maps. See README for more information. | | config.serverDcf | object | `{"launcher-mounts":[]}` | a map of server-scoped config files (akin to `config.server`), but with .dcf file formatting (i.e. `launcher-mounts`, `launcher-env`, etc.) | -| config.session | object | `{"notifications.conf":{},"repos.conf":{"CRAN":"https://packagemanager.posit.co/cran/__linux__/jammy/latest"},"rsession.conf":{},"rstudio-prefs.json":"{}\n"}` | a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. Each file's contents may be a map, a raw string, or - for files read in order, such as `repos.conf` - a list of single-entry maps. See README for more information. | +| config.session | object | `{"notifications.conf":{},"repos.conf":[{"CRAN":"https://packagemanager.posit.co/cran/__linux__/jammy/latest"}],"rsession.conf":{},"rstudio-prefs.json":"{}\n"}` | a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. Each file's contents may be a map, a raw string, or - for files read in order, such as `repos.conf` - a list of single-entry maps. See README for more information. | | config.sessionSecret | object | `{}` | a map of secret, session-scoped config files (odbc.ini, etc.). Mounted to `/mnt/session-secret/` on both server and session, by default | | config.sssd | object | `{"conf":{},"enabled":true}` | Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. On by default; automatically skipped when the pod runs unprivileged (`pod.runAsRoot: false`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD. | | config.sssd.conf | object | `{}` | a map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Replaces the deprecated `config.userProvisioning`. | diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index e7e3d338..12d59557 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -313,6 +313,15 @@ does not keep the order you wrote it in: the chart renders map keys alphabetical repository can't be put ahead of CRAN. ::: +:::{.callout-important} +Your `repos.conf` **replaces** the chart default rather than merging with it, and it must contain an +entry named `CRAN`. Workbench ignores the whole file when nothing is named `CRAN`, so the other +repositories are lost too - the only sign is `is missing CRAN entry` in the session log. The entry +does not have to be CRAN itself; point it at your own mirror if that is what sessions should use. +To configure repositories somewhere else entirely, set `config.session.repos\.conf: null` and the +chart renders no file. +::: + For more information about configuring CRAN repositories in Workbench, see the [Posit Workbench Administrator Guide's - Package Installation > CRAN repositories](https://docs.posit.co/ide/server-pro/rstudio_pro_sessions/package_installation.html#cran-repositories) section. #### R versions diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index e8bd94e9..4a83bd45 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -25,21 +25,20 @@ kubectl -n {{ $.Release.Namespace }} get secret {{ include "rstudio-workbench.fu {{- /* Files whose behavior depends on the order of their sections or entries. Rendering these from a map sorts them by name, silently changing what they do, so the map form is deprecated in favor of the ordered list form. A raw string keeps the written order too, - so only the map form is flagged, and only once it holds more than one section or entry -- - below that there is no order to lose. */}} + so only the map form is flagged. */}} {{- $unordered := list }} {{- range $file, $contents := .Values.config.server }} - {{- if and (kindIs "map" $contents) (gt (len (keys $contents)) 1) (or (eq $file "profiles") (regexMatch "^launcher\\..+\\.resources\\.conf$" $file)) }} + {{- if and (kindIs "map" $contents) (or (eq $file "profiles") (regexMatch "^launcher\\..+\\.resources\\.conf$" $file)) }} {{- $unordered = append $unordered (printf "config.server.%s" ($file | replace "." "\\.")) }} {{- end }} {{- end }} {{- range $file, $contents := .Values.config.profiles }} - {{- if and (kindIs "map" $contents) (gt (len (keys $contents)) 1) (regexMatch "^launcher\\..+\\.profiles\\.conf$" $file) }} + {{- if and (kindIs "map" $contents) (regexMatch "^launcher\\..+\\.profiles\\.conf$" $file) }} {{- $unordered = append $unordered (printf "config.profiles.%s" ($file | replace "." "\\.")) }} {{- end }} {{- end }} {{- range $file, $contents := .Values.config.session }} - {{- if and (kindIs "map" $contents) (gt (len (keys $contents)) 1) (eq $file "repos.conf") }} + {{- if and (kindIs "map" $contents) (eq $file "repos.conf") }} {{- $unordered = append $unordered (printf "config.session.%s" ($file | replace "." "\\.")) }} {{- end }} {{- end }} @@ -67,6 +66,43 @@ WARNING: the following configuration files are written as maps, which does not k The map form still works for now, but will be removed in a future chart release. {{- end }} +{{- /* Workbench discards repos.conf entirely when it has no entry named CRAN + (SessionOptions.cpp, parseReposConfig), taking the admin's own repositories with it. + Nothing in the rendered file shows this, so check for the key in whichever form + the file was written. */}} +{{- if hasKey .Values.config.session "repos.conf" }} + {{- $repos := get .Values.config.session "repos.conf" }} + {{- $hasCran := false }} + {{- if kindIs "string" $repos }} + {{- $hasCran = regexMatch "(?m)^[ \t]*CRAN[ \t]*=" $repos }} + {{- else if kindIs "slice" $repos }} + {{- range $item := $repos }} + {{- if and (kindIs "map" $item) (hasKey $item "CRAN") }} + {{- $hasCran = true }} + {{- end }} + {{- end }} + {{- else if kindIs "map" $repos }} + {{- $hasCran = hasKey $repos "CRAN" }} + {{- end }} + {{- if and $repos (not $hasCran) }} + +WARNING: `.Values.config.session.repos\.conf` has no `CRAN` entry + - Workbench ignores the whole file when no entry is named `CRAN`, so none of these repositories + will be used, and the session log will show "is missing CRAN entry". + - Name one of your repositories `CRAN`. It does not have to be CRAN itself - point it at your + own mirror if that is what you want sessions to use: + + config: + session: + repos.conf: + - Internal: https://pkgs.example.com/internal + - CRAN: https://mirror.example.com/cran + + - To configure repositories somewhere else instead, set `config.session.repos\.conf: null` and + the chart will not render the file at all. + {{- end }} +{{- end }} + {{- if hasKey .Values.config.server "launcher.kubernetes.profiles.conf" }} WARNING: `.Values.config.server.launcher\.kubernetes\.profiles\.conf` is deprecated diff --git a/charts/rstudio-workbench/tests/notes_test.yaml b/charts/rstudio-workbench/tests/notes_test.yaml index 6a176175..1473a242 100644 --- a/charts/rstudio-workbench/tests/notes_test.yaml +++ b/charts/rstudio-workbench/tests/notes_test.yaml @@ -134,16 +134,62 @@ tests: - notMatchRegexRaw: pattern: "WARNING: the following configuration files are written as maps" - - it: should not warn for a single-entry map, which has no order to lose + - it: should warn for a single-entry map too, now that the gate is gone set: config: server: profiles: "*": max-memory-mb: 1024 + asserts: + - matchRegexRaw: + pattern: "WARNING: the following configuration files are written as maps" + + # Workbench discards repos.conf entirely when nothing is named CRAN + - it: should warn when repos.conf has no CRAN entry + set: + config: session: repos.conf: - CRAN: https://packagemanager.posit.co/cran/latest + - Internal: https://pkgs.example.com/internal + asserts: + - matchRegexRaw: + pattern: "has no `CRAN` entry" + - matchRegexRaw: + pattern: "Workbench ignores the whole file" + + - it: should not warn when a repos.conf list includes CRAN + set: + config: + session: + repos.conf: + - Internal: https://pkgs.example.com/internal + - CRAN: https://mirror.example.com/cran asserts: - notMatchRegexRaw: - pattern: "WARNING: the following configuration files are written as maps" + pattern: "has no `CRAN` entry" + + - it: should not warn when a repos.conf string includes CRAN + set: + config: + session: + repos.conf: | + Internal=https://pkgs.example.com/internal + CRAN=https://mirror.example.com/cran + asserts: + - notMatchRegexRaw: + pattern: "has no `CRAN` entry" + + - it: should not warn about CRAN when repos.conf is suppressed + set: + config: + session: + repos.conf: null + asserts: + - notMatchRegexRaw: + pattern: "has no `CRAN` entry" + + - it: should not warn about CRAN on a default install + asserts: + - notMatchRegexRaw: + pattern: "has no `CRAN` entry" diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml index 148da48a..d02304b3 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -581,8 +581,11 @@ config: # -- a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. # Each file's contents may be a map, a raw string, or - for files read in order, such as `repos.conf` - a list of single-entry maps. See README for more information. session: + # Written as a list because repos.conf is read in order. Supplying your own + # replaces this default entirely, and it must include a CRAN entry - Workbench + # ignores the whole file without one. repos.conf: - CRAN: https://packagemanager.posit.co/cran/__linux__/jammy/latest + - CRAN: https://packagemanager.posit.co/cran/__linux__/jammy/latest rsession.conf: {} notifications.conf: {} rstudio-prefs.json: | From 7a57723dbc348d05253e4d9043ed75b8a48956f4 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Tue, 29 Sep 2026 15:59:13 -0700 Subject: [PATCH 06/18] Route session config files to a renderer by filename --- charts/rstudio-workbench/NEWS.md | 7 ++ charts/rstudio-workbench/README.md | 30 +++--- charts/rstudio-workbench/README.md.gotmpl | 30 +++--- .../lint/complex-values.yaml | 14 +-- .../lint/other-complex-values.yaml | 14 +-- .../rstudio-workbench/templates/_helpers.tpl | 38 ++++++++ .../templates/configmap-session.yaml | 4 +- .../tests/configmap_test.yaml | 96 ++++++++++++++++--- 8 files changed, 172 insertions(+), 61 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index ad7a4716..af7f4fa9 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -21,6 +21,13 @@ - CRAN: https://packagemanager.posit.co/cran/latest ``` +- `config.session` files are now rendered according to their format rather than the shape of the + value written. `r-versions` and `notifications.conf` are DCF (`Key: Value`, records separated by + a blank line), `*.json` files are JSON, and everything else stays ini. Previously all of them + were rendered as ini, so `r-versions` came out as `Key=Value` and Workbench discarded it, + logging `does not point to a valid directory` for each line. Resolves + https://github.com/rstudio/helm/issues/948. A file written as a raw string is still passed + through unchanged. - **BREAKING**: a file written as a list must give each section or entry its own `- `, holding a single key. This rejects two shapes that previously rendered something unusable: several sections crammed into one entry (a missing `- `), which rendered as `name=map[key:value]`; and a multi-field diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index a0a52986..a8b8a0c1 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -381,26 +381,28 @@ For more information about configuring CRAN repositories in Workbench, see the [ #### R versions `/etc/rstudio/r-versions` is not an ini file. It is DCF: `Key: Value`, with a blank line between -each R version. Write it as a string, which the chart passes through unchanged: +each R version. The chart renders it with a DCF renderer, so write each R version as one list +entry and its fields as that entry's keys: ```yaml config: session: - r-versions: | - Path: /opt/R/4.1.3 - Label: Custom 4.1.3 - Repo: https://packagemanager.posit.co/cran/__linux__/jammy/latest - - Path: /opt/R/4.2.3 - Label: Custom 4.2.3 + r-versions: + - Path: /opt/R/4.1.3 + Label: Custom 4.1.3 + Repo: https://packagemanager.posit.co/cran/__linux__/jammy/latest + - Path: /opt/R/4.2.3 + Label: Custom 4.2.3 ``` -:::{.callout-important} -Use `Key: Value`, not `Key=Value`. Writing this file as a map or a list makes the chart render it -with `=`, which Workbench cannot parse - it silently registers no R versions and logs -`does not point to a valid directory` for each line. See -[#948](https://github.com/rstudio/helm/issues/948). Writing it as a list is rejected by the chart; -writing it as a map is not, so take care with the separator. +A raw string also works, and is passed through unchanged. + +:::{.callout-note} +`config.session` files are rendered according to their format, not the shape of the value you +write. `r-versions` and `notifications.conf` are DCF, `*.json` files are JSON, and everything else +(`repos.conf`, `rsession.conf`, `pip.conf`, ...) is ini. Before chart 0.23.0 every file was +rendered as ini, so `r-versions` came out with `=` and Workbench ignored it entirely - see +[#948](https://github.com/rstudio/helm/issues/948). ::: See [Extended R version definitions](https://docs.posit.co/ide/server-pro/admin/r/using_multiple_versions_of_r.html#extended-r-version-definitions) in the Administrator Guide for the full list of fields. diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index 12d59557..5d87caff 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -327,26 +327,28 @@ For more information about configuring CRAN repositories in Workbench, see the [ #### R versions `/etc/rstudio/r-versions` is not an ini file. It is DCF: `Key: Value`, with a blank line between -each R version. Write it as a string, which the chart passes through unchanged: +each R version. The chart renders it with a DCF renderer, so write each R version as one list +entry and its fields as that entry's keys: ```yaml config: session: - r-versions: | - Path: /opt/R/4.1.3 - Label: Custom 4.1.3 - Repo: https://packagemanager.posit.co/cran/__linux__/jammy/latest - - Path: /opt/R/4.2.3 - Label: Custom 4.2.3 + r-versions: + - Path: /opt/R/4.1.3 + Label: Custom 4.1.3 + Repo: https://packagemanager.posit.co/cran/__linux__/jammy/latest + - Path: /opt/R/4.2.3 + Label: Custom 4.2.3 ``` -:::{.callout-important} -Use `Key: Value`, not `Key=Value`. Writing this file as a map or a list makes the chart render it -with `=`, which Workbench cannot parse - it silently registers no R versions and logs -`does not point to a valid directory` for each line. See -[#948](https://github.com/rstudio/helm/issues/948). Writing it as a list is rejected by the chart; -writing it as a map is not, so take care with the separator. +A raw string also works, and is passed through unchanged. + +:::{.callout-note} +`config.session` files are rendered according to their format, not the shape of the value you +write. `r-versions` and `notifications.conf` are DCF, `*.json` files are JSON, and everything else +(`repos.conf`, `rsession.conf`, `pip.conf`, ...) is ini. Before chart 0.23.0 every file was +rendered as ini, so `r-versions` came out with `=` and Workbench ignored it entirely - see +[#948](https://github.com/rstudio/helm/issues/948). ::: See [Extended R version definitions](https://docs.posit.co/ide/server-pro/admin/r/using_multiple_versions_of_r.html#extended-r-version-definitions) in the Administrator Guide for the full list of fields. diff --git a/charts/rstudio-workbench/lint/complex-values.yaml b/charts/rstudio-workbench/lint/complex-values.yaml index 6f854791..dcf71fac 100644 --- a/charts/rstudio-workbench/lint/complex-values.yaml +++ b/charts/rstudio-workbench/lint/complex-values.yaml @@ -150,15 +150,11 @@ config: CRAN: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest rsession.conf: {} notifications.conf: {} - # r-versions is a DCF file (Key: Value, records separated by a blank line), - # not an ini file, so it is written as a string and passed through unchanged. - # See https://github.com/rstudio/helm/issues/948 - r-versions: | - Label: test - Path: /opt/R/3.6.3 - - Label: other - Path: /opt/R/4.0.2 + r-versions: + - Label: test + Path: /opt/R/3.6.3 + - Label: other + Path: /opt/R/4.0.2 sessionSecret: odbc.ini: | [hello] diff --git a/charts/rstudio-workbench/lint/other-complex-values.yaml b/charts/rstudio-workbench/lint/other-complex-values.yaml index 4a8e767c..65fc9225 100644 --- a/charts/rstudio-workbench/lint/other-complex-values.yaml +++ b/charts/rstudio-workbench/lint/other-complex-values.yaml @@ -88,15 +88,11 @@ config: CRAN: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest rsession.conf: {} notifications.conf: {} - # r-versions is a DCF file (Key: Value, records separated by a blank line), - # not an ini file, so it is written as a string and passed through unchanged. - # See https://github.com/rstudio/helm/issues/948 - r-versions: | - Label: test - Path: /opt/R/3.6.3 - - Label: other - Path: /opt/R/4.0.2 + r-versions: + - Label: test + Path: /opt/R/3.6.3 + - Label: other + Path: /opt/R/4.0.2 secret: "database.conf": {} server: diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index 79a991ed..c7f8d5a2 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -716,3 +716,41 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- define "rstudio-workbench.xdg-config-dirs" -}} {{ trimSuffix ":" ( join ":" (list .Values.xdgConfigDirs (join ":" .Values.xdgConfigDirsExtra) ) ) }} {{- end -}} + +{{- /* + Renders `config.session`, choosing a renderer per file rather than treating every file as ini. + The files in that directory are not all the same format: + + - `r-versions` and `notifications.conf` are DCF: `Key: Value`, with records separated by a + blank line. Rendered as ini they came out as `Key=Value`, which Workbench cannot parse -- it + falls back to a legacy mode and logs "does not point to a valid directory" per line, so the + file is silently ignored. See https://github.com/rstudio/helm/issues/948 + - `*.json` files are JSON. + - everything else (`repos.conf`, `rsession.conf`, `pip.conf`, ...) is ini. + + A raw string is always passed through unchanged, so it stays with the ini renderer: the JSON + renderer would re-encode it with `toPrettyJson` and turn `{}` into the quoted string `"{}"`. +*/ -}} +{{- define "rstudio-workbench.config.sessionFiles" -}} +{{- $dcfNames := list "r-versions" "notifications.conf" }} +{{- $ini := dict }} +{{- $dcf := dict }} +{{- $json := dict }} +{{- range $file, $contents := . }} + {{- if kindIs "string" $contents }} + {{- $_ := set $ini $file $contents }} + {{- else if empty $contents }} + {{- /* nothing to render either way; keep it with ini so output is unchanged */ -}} + {{- $_ := set $ini $file $contents }} + {{- else if has $file $dcfNames }} + {{- $_ := set $dcf $file $contents }} + {{- else if hasSuffix ".json" $file }} + {{- $_ := set $json $file $contents }} + {{- else }} + {{- $_ := set $ini $file $contents }} + {{- end }} +{{- end }} +{{- if $ini }}{{- include "rstudio-library.config.ini" $ini }}{{- end }} +{{- if $dcf }}{{- include "rstudio-library.config.dcf" $dcf }}{{- end }} +{{- if $json }}{{- include "rstudio-library.config.json" $json }}{{- end }} +{{- end }} diff --git a/charts/rstudio-workbench/templates/configmap-session.yaml b/charts/rstudio-workbench/templates/configmap-session.yaml index f71a28d9..c0fb12c3 100644 --- a/charts/rstudio-workbench/templates/configmap-session.yaml +++ b/charts/rstudio-workbench/templates/configmap-session.yaml @@ -5,7 +5,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-session namespace: {{ $.Release.Namespace }} data: - {{- include "rstudio-library.config.ini" .Values.config.session | nindent 2 }} + {{- include "rstudio-workbench.config.sessionFiles" .Values.config.session | nindent 2 }} {{- if .Values.config.sessionSecret }} --- {{- if .Values.sealedSecret.enabled }} @@ -47,7 +47,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-session namespace: {{ $targetNamespace }} data: - {{- include "rstudio-library.config.ini" .Values.config.session | nindent 2 }} + {{- include "rstudio-workbench.config.sessionFiles" .Values.config.session | nindent 2 }} {{- if .Values.config.sessionSecret }} --- {{- if .Values.sealedSecret.enabled }} diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index 0c8f448a..9778dd53 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -600,19 +600,6 @@ tests: Path: /opt/R/4.0.2 Label: Old - - it: should fail when r-versions is written as a list of records - template: configmap-session.yaml - documentIndex: 0 - set: - config: - session: - r-versions: - - Path: /opt/R/4.4.1 - Label: Latest - asserts: - - failedTemplate: - errorPattern: "entry 1 of 'r-versions' holds more than one key" - - it: should render config.profiles sections in the order written, merging the default everyone section template: configmap-general.yaml documentIndex: 0 @@ -651,3 +638,86 @@ tests: - matchRegex: path: data["launcher.kubernetes.profiles.conf"] pattern: "\\[\\*\\]\\nallow-unknown-images=1\\ncontainer-images=.*\\ndefault-container-image=.*\\njob-json-overrides=.*\\n\\n\\[jsmith\\]\\nmax-cpus=8" + + # -- config.session files are routed by format, not by value shape. See #948 + - it: should render r-versions as DCF when written as records + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + r-versions: + - Path: /opt/R/4.4.1 + Label: Latest + - Path: /opt/R/4.0.2 + Label: Old + asserts: + - equal: + path: data["r-versions"] + value: | + Label: Latest + Path: /opt/R/4.4.1 + + Label: Old + Path: /opt/R/4.0.2 + + - it: should render notifications.conf as DCF + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + notifications.conf: + - StartTime: 2026-01-01 + EndTime: 2026-01-02 + Message: Maintenance window + asserts: + - matchRegex: + path: data["notifications.conf"] + pattern: "StartTime: 2026-01-01" + - notMatchRegex: + path: data["notifications.conf"] + pattern: "StartTime=" + + - it: should render a structured json session file as JSON + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + rstudio-prefs.json: + save_workspace: never + asserts: + - matchRegex: + path: data["rstudio-prefs.json"] + pattern: '"save_workspace": "never"' + + - it: should pass a json session file written as a string through unchanged + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + rstudio-prefs.json: | + {"save_workspace": "never"} + asserts: + - equal: + path: data["rstudio-prefs.json"] + value: | + {"save_workspace": "never"} + + - it: should still render repos.conf as ini + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + repos.conf: + - Internal: https://pkgs.example.com/internal + - CRAN: https://packagemanager.posit.co/cran/latest + asserts: + - equal: + path: data["repos.conf"] + value: | + Internal=https://pkgs.example.com/internal + CRAN=https://packagemanager.posit.co/cran/latest From 568b0a295349409a5ea274de0daed813c9de5b26 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Tue, 29 Sep 2026 16:18:03 -0700 Subject: [PATCH 07/18] Correct the profiles array-append example, which described behavior the chart never had --- charts/rstudio-workbench/README.md | 7 ++++--- charts/rstudio-workbench/README.md.gotmpl | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index a8b8a0c1..a4c0f359 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -523,7 +523,7 @@ The `/etc/rstudio/launcher.kubernetes.profiles.conf` contains the configuration - value2 ``` -- The `[*]` section has arrays "appended" to user and group sections, along with "defaults" defined by the chart. +- The `[*]` section receives the "defaults" defined by the chart (the session image settings), and its `job-json-overrides` are prepended to every other section that defines its own. Other keys are not merged across sections - a user or group section overrides `[*]` for that key, which is how the product resolves profiles. For example: @@ -547,9 +547,10 @@ _/etc/rstudio/launcher.kubernetes.profiles.conf_ ```ini [*] -some-key: value1,value2 +some-key=value1,value2 + [myuser] -some-key: value1,value2,value3,value4 +some-key=value4,value5 ``` :::{.callout-note} diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index 5d87caff..a776b752 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -469,7 +469,7 @@ The `/etc/rstudio/launcher.kubernetes.profiles.conf` contains the configuration - value2 ``` -- The `[*]` section has arrays "appended" to user and group sections, along with "defaults" defined by the chart. +- The `[*]` section receives the "defaults" defined by the chart (the session image settings), and its `job-json-overrides` are prepended to every other section that defines its own. Other keys are not merged across sections - a user or group section overrides `[*]` for that key, which is how the product resolves profiles. For example: @@ -493,9 +493,10 @@ _/etc/rstudio/launcher.kubernetes.profiles.conf_ ```ini [*] -some-key: value1,value2 +some-key=value1,value2 + [myuser] -some-key: value1,value2,value3,value4 +some-key=value4,value5 ``` :::{.callout-note} From 84e924f73df80b2a6bb289555fe86a61a1d999aa Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Tue, 29 Sep 2026 17:37:07 -0700 Subject: [PATCH 08/18] Follow the profiles helper rename --- charts/rstudio-workbench/templates/configmap-general.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml index 67eddee0..75f94b94 100644 --- a/charts/rstudio-workbench/templates/configmap-general.yaml +++ b/charts/rstudio-workbench/templates/configmap-general.yaml @@ -177,7 +177,7 @@ data: {{- $profilesDict := dict "data" ($profilesConfig | deepCopy) "filePath" ($jobJsonFilePath) "jobJsonDefaults" ($defaultOverrides) }} {{- if not (hasKey .Values.config.server "launcher.kubernetes.profiles.conf") }} {{/* generate the profiles configuration */}} - {{- include "rstudio-library.profiles.ini.advanced" $profilesDict | nindent 2 }} + {{- include "rstudio-library.profiles.ini" $profilesDict | nindent 2 }} {{- end }} {{- /* generate the server configuration (dcf files) minus launcher-mounts */}} {{- include "rstudio-library.config.dcf" ( omit .Values.config.serverDcf "launcher-mounts" ) | nindent 2 }} From e510d20c09d68d807b54fa148695d79599f055e2 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Tue, 29 Sep 2026 17:43:58 -0700 Subject: [PATCH 09/18] Note the profiles whitespace change in NEWS --- charts/rstudio-workbench/NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index af7f4fa9..d3203f55 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -43,6 +43,11 @@ replaces it instead of merging with it. Previously a map default merged with a map you supplied, which quietly added the chart's CRAN entry to your repositories. Include a `CRAN` entry in your own list - Workbench ignores the whole file without one. The chart now warns when it is missing. +- `launcher.*.profiles.conf` no longer starts with a blank line. Profiles files now render through + the same helper as every other ini file, which places the blank line between sections rather than + before the first one. Nothing reads it - the file is parsed with an ini parser that skips blank + lines - but it changes the rendered file, so the config checksum shifts and pods restart once on + upgrade. - **DEPRECATED**: writing those files as a map. A map is rendered in alphabetical order, which silently changes what these files do, and nothing in `values.yaml` shows it. The map form still works and now prints a `WARNING` in `NOTES.txt` once a file holds more than one section or entry. From 4fe6aa17f054cc522010699569e5f151c0c53516 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Tue, 29 Sep 2026 18:43:04 -0700 Subject: [PATCH 10/18] Guard that files documented as lists ship no chart default --- .../tests/configmap_test.yaml | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index 9778dd53..9287b105 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -721,3 +721,41 @@ tests: value: | Internal=https://pkgs.example.com/internal CRAN=https://packagemanager.posit.co/cran/latest + + # -- Guard: the README tells admins to write these files as a list, and Helm *replaces* a list + # rather than merging it. So none of them may gain a chart default without someone deciding what + # happens to admins who already use the list form. If this fails, that decision is now due. + - it: should ship no chart default for the files documented as lists + template: configmap-general.yaml + documentIndex: 0 + asserts: + - notExists: + path: data["profiles"] + - notExists: + path: data["launcher.kubernetes.resources.conf"] + - notExists: + path: data["launcher.local.resources.conf"] + - notExists: + path: data["launcher.slurm.resources.conf"] + + # config.session.repos.conf is the exception: it does ship a default, deliberately as a list so + # the map form warns. An admin's own value replaces it, which is why NOTES.txt guards for CRAN. + - it: should ship the repos.conf default as a list, not a map + template: configmap-session.yaml + documentIndex: 0 + asserts: + - equal: + path: data["repos.conf"] + value: | + CRAN=https://packagemanager.posit.co/cran/__linux__/jammy/latest + + # config.profiles does receive chart defaults, so the list form has to merge rather than replace. + # Covered by "should render config.profiles sections in the order written" above; this pins that + # the defaults exist, so that test cannot silently become vacuous. + - it: should ship chart defaults for config.profiles + template: configmap-general.yaml + documentIndex: 0 + asserts: + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "default-container-image=posit/workbench-session:" From bcf25812603ee05438e373bf2138f0e6eefa67c0 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Tue, 29 Sep 2026 18:46:49 -0700 Subject: [PATCH 11/18] Derive the map-default guard from the deprecation warning --- charts/rstudio-workbench/tests/configmap_test.yaml | 5 +++++ charts/rstudio-workbench/tests/notes_test.yaml | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index 9287b105..ebae050b 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -725,6 +725,11 @@ tests: # -- Guard: the README tells admins to write these files as a list, and Helm *replaces* a list # rather than merging it. So none of them may gain a chart default without someone deciding what # happens to admins who already use the list form. If this fails, that decision is now due. + # + # The file list below restates NOTES.txt's order-sensitive patterns, which is the source of truth + # — keep them in step. A *map* default would also be caught automatically by "should not warn + # about the map form on a default install" in notes_test.yaml; this catches a *list* default too, + # which warns about nothing and so would otherwise be silent. - it: should ship no chart default for the files documented as lists template: configmap-general.yaml documentIndex: 0 diff --git a/charts/rstudio-workbench/tests/notes_test.yaml b/charts/rstudio-workbench/tests/notes_test.yaml index 1473a242..68e2e056 100644 --- a/charts/rstudio-workbench/tests/notes_test.yaml +++ b/charts/rstudio-workbench/tests/notes_test.yaml @@ -193,3 +193,10 @@ tests: asserts: - notMatchRegexRaw: pattern: "has no `CRAN` entry" + + # Derives from NOTES.txt's own list rather than restating it: if a map default is ever added for + # an order-sensitive file, a default install starts warning about the chart's own values. + - it: should not warn about the map form on a default install + asserts: + - notMatchRegexRaw: + pattern: "WARNING: the following configuration files are written as maps" From 00c5327e77358ed46a089a7e7191c5d8161b6986 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Tue, 29 Sep 2026 18:57:31 -0700 Subject: [PATCH 12/18] Warn when an order-agnostic ini file is written as a list --- charts/rstudio-workbench/NEWS.md | 6 ++ charts/rstudio-workbench/README.md | 6 ++ charts/rstudio-workbench/README.md.gotmpl | 6 ++ charts/rstudio-workbench/templates/NOTES.txt | 61 +++++++++++-------- .../rstudio-workbench/templates/_helpers.tpl | 38 ++++++++++-- .../rstudio-workbench/tests/notes_test.yaml | 54 ++++++++++++++++ 6 files changed, 142 insertions(+), 29 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index d3203f55..2fb30154 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -48,6 +48,12 @@ before the first one. Nothing reads it - the file is parsed with an ini parser that skips blank lines - but it changes the rendered file, so the config checksum shifts and pods restart once on upgrade. +- The chart now warns when an order-agnostic ini file is written as a *list*. Helm merges a map + with the chart's defaults for a file but replaces them with a list, so the list form silently + drops any default the chart ships - `launcher\.conf` would lose its `[server]` section, which + the launcher needs. Write those files as a map unless you need to control section order. Files + that are not ini (`r-versions`, `notifications.conf`, `*.json`) are exempt, since a list is how + you legitimately write those. - **DEPRECATED**: writing those files as a map. A map is rendered in alphabetical order, which silently changes what these files do, and nothing in `values.yaml` shows it. The map form still works and now prints a `WARNING` in `NOTES.txt` once a file holds more than one section or entry. diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index a4c0f359..3d4129b6 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -476,6 +476,12 @@ This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and `launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and pre-selects the first one). +Which form to use is decided by the file, not by preference: **an order-sensitive file wants a +list, every other ini file wants a map.** Helm merges a map with the chart's defaults for a file +but replaces them with a list, so writing an order-agnostic file as a list silently drops whatever +the chart ships for it. The chart warns in both directions. Files that are not ini - `r-versions`, +`notifications.conf`, and `*.json` - are exempt, since a list is how you legitimately write those. + Only the sections are ordered. The options written inside a section are still rendered alphabetically, which is what these files expect - they are resolved section by section, not option by option. A file that depends on the order of options within a section needs the raw diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index a776b752..6d2b516b 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -422,6 +422,12 @@ This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and `launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and pre-selects the first one). +Which form to use is decided by the file, not by preference: **an order-sensitive file wants a +list, every other ini file wants a map.** Helm merges a map with the chart's defaults for a file +but replaces them with a list, so writing an order-agnostic file as a list silently drops whatever +the chart ships for it. The chart warns in both directions. Files that are not ini - `r-versions`, +`notifications.conf`, and `*.json` - are exempt, since a list is how you legitimately write those. + Only the sections are ordered. The options written inside a section are still rendered alphabetically, which is what these files expect - they are resolved section by section, not option by option. A file that depends on the order of options within a section needs the raw diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index 4a83bd45..c2451f7e 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -22,30 +22,34 @@ kubectl -n {{ $.Release.Namespace }} get secret {{ include "rstudio-workbench.fu ``` {{- end }} -{{- /* Files whose behavior depends on the order of their sections or entries. Rendering these - from a map sorts them by name, silently changing what they do, so the map form is - deprecated in favor of the ordered list form. A raw string keeps the written order too, - so only the map form is flagged. */}} -{{- $unordered := list }} -{{- range $file, $contents := .Values.config.server }} - {{- if and (kindIs "map" $contents) (or (eq $file "profiles") (regexMatch "^launcher\\..+\\.resources\\.conf$" $file)) }} - {{- $unordered = append $unordered (printf "config.server.%s" ($file | replace "." "\\.")) }} - {{- end }} -{{- end }} -{{- range $file, $contents := .Values.config.profiles }} - {{- if and (kindIs "map" $contents) (regexMatch "^launcher\\..+\\.profiles\\.conf$" $file) }} - {{- $unordered = append $unordered (printf "config.profiles.%s" ($file | replace "." "\\.")) }} - {{- end }} -{{- end }} -{{- range $file, $contents := .Values.config.session }} - {{- if and (kindIs "map" $contents) (eq $file "repos.conf") }} - {{- $unordered = append $unordered (printf "config.session.%s" ($file | replace "." "\\.")) }} +{{- /* Two form warnings, both keyed off the filename dispatch in _helpers.tpl. + + An order-sensitive file wants the list form: a map sorts its sections by name, silently + changing what the file does. An order-agnostic file wants the map form: Helm merges a map + with the chart's defaults for that file, but *replaces* them with a list. + + Only ini files are considered. r-versions and notifications.conf are DCF and .json files + are JSON; a list is how you legitimately write those. */}} +{{- $wantsList := list }} +{{- $wantsMap := list }} +{{- range $scope := (list "server" "session" "profiles") }} + {{- range $file, $contents := (get $.Values.config $scope) }} + {{- $path := printf "config.%s.%s" $scope ($file | replace "." "\\.") }} + {{- $ordered := include "rstudio-workbench.config.orderSensitive" (dict "file" $file "scope" $scope) }} + {{- $nonIni := (eq $scope "session") | ternary (include "rstudio-workbench.config.nonIni" (dict "file" $file)) "" }} + {{- if not $nonIni }} + {{- if and $ordered (kindIs "map" $contents) }} + {{- $wantsList = append $wantsList $path }} + {{- else if and (not $ordered) (kindIs "slice" $contents) }} + {{- $wantsMap = append $wantsMap $path }} + {{- end }} + {{- end }} {{- end }} {{- end }} -{{- if $unordered }} +{{- if $wantsList }} WARNING: the following configuration files are written as maps, which does not keep the order you wrote them in -{{- range $unordered | sortAlpha }} +{{- range $wantsList | sortAlpha }} - `.Values.{{ . }}` {{- end }} Workbench reads these files in order, so sorting their sections by name changes how they behave. @@ -58,13 +62,22 @@ WARNING: the following configuration files are written as maps, which does not k max-memory-mb: 1024 - "@analysts": max-memory-mb: 4096 - session: - repos.conf: - - Internal: https://pkgs.example.com/internal - - CRAN: https://packagemanager.posit.co/cran/latest The map form still works for now, but will be removed in a future chart release. {{- end }} +{{- if $wantsMap }} + +WARNING: the following configuration files are written as lists, which replaces the chart's defaults for them +{{- range $wantsMap | sortAlpha }} + - `.Values.{{ . }}` +{{- end }} + Nothing in these files depends on the order of their sections, and Helm merges a map with the + chart's defaults while a list replaces them. Written as a list, any default the chart ships for + these files is silently dropped - for example `launcher\.conf` loses its `[server]` section, + which the launcher needs. + + Write them as a map unless you specifically need to control the order of their sections. +{{- end }} {{- /* Workbench discards repos.conf entirely when it has no entry named CRAN (SessionOptions.cpp, parseReposConfig), taking the admin's own repositories with it. diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index c7f8d5a2..a1746983 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -732,7 +732,6 @@ app.kubernetes.io/instance: {{ .Release.Name }} renderer would re-encode it with `toPrettyJson` and turn `{}` into the quoted string `"{}"`. */ -}} {{- define "rstudio-workbench.config.sessionFiles" -}} -{{- $dcfNames := list "r-versions" "notifications.conf" }} {{- $ini := dict }} {{- $dcf := dict }} {{- $json := dict }} @@ -742,10 +741,12 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- else if empty $contents }} {{- /* nothing to render either way; keep it with ini so output is unchanged */ -}} {{- $_ := set $ini $file $contents }} - {{- else if has $file $dcfNames }} - {{- $_ := set $dcf $file $contents }} - {{- else if hasSuffix ".json" $file }} - {{- $_ := set $json $file $contents }} + {{- else if include "rstudio-workbench.config.nonIni" (dict "file" $file) }} + {{- if hasSuffix ".json" $file }} + {{- $_ := set $json $file $contents }} + {{- else }} + {{- $_ := set $dcf $file $contents }} + {{- end }} {{- else }} {{- $_ := set $ini $file $contents }} {{- end }} @@ -754,3 +755,30 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- if $dcf }}{{- include "rstudio-library.config.dcf" $dcf }}{{- end }} {{- if $json }}{{- include "rstudio-library.config.json" $json }}{{- end }} {{- end }} + +{{- /* + Filename dispatch, in one place. Three consumers: `configmap-session.yaml` picks a renderer, + and `NOTES.txt` raises both of the form warnings. Keeping the lists here is what stops them + drifting apart. +*/ -}} + +{{- /* Files whose behavior depends on the order of their sections or entries. These want the + list form; a map sorts them by name and silently changes what they do. */ -}} +{{- define "rstudio-workbench.config.orderSensitive" -}} +{{- $file := .file -}} +{{- $scope := .scope -}} +{{- if eq $scope "server" -}} + {{- or (eq $file "profiles") (regexMatch "^launcher\\..+\\.resources\\.conf$" $file) | ternary "yes" "" -}} +{{- else if eq $scope "profiles" -}} + {{- regexMatch "^launcher\\..+\\.profiles\\.conf$" $file | ternary "yes" "" -}} +{{- else if eq $scope "session" -}} + {{- eq $file "repos.conf" | ternary "yes" "" -}} +{{- end -}} +{{- end -}} + +{{- /* Session files that are not ini. `r-versions` and `notifications.conf` are DCF; `.json` is + JSON. These legitimately take a list, so the ini form warnings must not apply to them. */ -}} +{{- define "rstudio-workbench.config.nonIni" -}} +{{- $file := .file -}} +{{- or (has $file (list "r-versions" "notifications.conf")) (hasSuffix ".json" $file) | ternary "yes" "" -}} +{{- end -}} diff --git a/charts/rstudio-workbench/tests/notes_test.yaml b/charts/rstudio-workbench/tests/notes_test.yaml index 68e2e056..be3ef1a8 100644 --- a/charts/rstudio-workbench/tests/notes_test.yaml +++ b/charts/rstudio-workbench/tests/notes_test.yaml @@ -200,3 +200,57 @@ tests: asserts: - notMatchRegexRaw: pattern: "WARNING: the following configuration files are written as maps" + + # -- The inverse warning: an order-agnostic ini file written as a list replaces the chart's + # defaults for that file rather than merging with them. + - it: should warn when an order-agnostic ini file is written as a list + set: + config: + server: + launcher.conf: + - cluster: + name: Cluster1 + asserts: + - matchRegexRaw: + pattern: "WARNING: the following configuration files are written as lists" + - matchRegexRaw: + pattern: "`\\.Values\\.config\\.server\\.launcher\\\\\\.conf`" + + - it: should not warn when an order-agnostic ini file is written as a map + set: + config: + server: + logging.conf: + "*": + log-level: warn + asserts: + - notMatchRegexRaw: + pattern: "written as lists" + + # r-versions and notifications.conf are DCF, and .json files are JSON; a list is how you + # legitimately write those, so the ini form warnings must not apply to them. + - it: should not warn about the list form for a DCF session file + set: + config: + session: + r-versions: + - Path: /opt/R/4.4.1 + Label: Latest + asserts: + - notMatchRegexRaw: + pattern: "written as lists" + + - it: should not warn about the list form for a json session file + set: + config: + session: + rstudio-prefs.json: + save_workspace: never + asserts: + - notMatchRegexRaw: + pattern: "written as lists" + + - it: should not warn about the list form on a default install + asserts: + - notMatchRegexRaw: + pattern: "written as lists" From 33449c7ad3f7f0c9446de1e8b2afd2de67517726 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Tue, 29 Sep 2026 22:09:05 -0700 Subject: [PATCH 13/18] Route config files through one filename table and warn on unrecognized ones --- charts/rstudio-workbench/NEWS.md | 8 ++ charts/rstudio-workbench/README.md | 21 ++- charts/rstudio-workbench/README.md.gotmpl | 21 ++- charts/rstudio-workbench/templates/NOTES.txt | 26 +++- .../rstudio-workbench/templates/_helpers.tpl | 123 +++++++++++------- .../templates/configmap-general.yaml | 2 +- .../templates/configmap-session.yaml | 4 +- .../rstudio-workbench/tests/notes_test.yaml | 45 +++++++ 8 files changed, 185 insertions(+), 65 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 2fb30154..84f4b4e3 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -54,6 +54,14 @@ the launcher needs. Write those files as a map unless you need to control section order. Files that are not ini (`r-versions`, `notifications.conf`, `*.json`) are exempt, since a list is how you legitimately write those. +- `config.server` files are now rendered by format too, the same way `config.session` already is. + A single table in the chart gives each configuration file its format, and both ConfigMaps read + from it. `config.server.*.json` files written as a map are now rendered as JSON rather than ini. +- The chart now warns when a configuration file it does not recognize is written as a map or a + list. Such a file is still rendered as ini, which is what the chart has always done, but ini is + a guess for a file the chart knows nothing about, and a wrong guess renders a file that looks + fine and is ignored by the product. Give the file's contents as text to render it exactly as + written. `Renviron.site` is recognized as ini and does not warn. - **DEPRECATED**: writing those files as a map. A map is rendered in alphabetical order, which silently changes what these files do, and nothing in `values.yaml` shows it. The map form still works and now prints a `WARNING` in `NOTES.txt` once a file holds more than one section or entry. diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 3d4129b6..8c346c2f 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -398,11 +398,22 @@ config: A raw string also works, and is passed through unchanged. :::{.callout-note} -`config.session` files are rendered according to their format, not the shape of the value you -write. `r-versions` and `notifications.conf` are DCF, `*.json` files are JSON, and everything else -(`repos.conf`, `rsession.conf`, `pip.conf`, ...) is ini. Before chart 0.23.0 every file was -rendered as ini, so `r-versions` came out with `=` and Workbench ignored it entirely - see -[#948](https://github.com/rstudio/helm/issues/948). +Configuration files are rendered according to their format, not the shape of the value you write. +`r-versions` and `notifications.conf` are DCF, `*.json` files are JSON, and everything else the +chart recognizes (`rserver.conf`, `repos.conf`, `rsession.conf`, `pip.conf`, ...) is ini. Before +chart 0.23.0 every `config.session` file was rendered as ini, so `r-versions` came out with `=` +and Workbench ignored it entirely - see [#948](https://github.com/rstudio/helm/issues/948). + +A file the chart does not recognize is rendered as ini and prints a warning on install, since ini +is a guess for a file it knows nothing about. Give that file's contents as text to render it +exactly as written: + +```yaml +config: + session: + my-file: | + whatever the file needs to say +``` ::: See [Extended R version definitions](https://docs.posit.co/ide/server-pro/admin/r/using_multiple_versions_of_r.html#extended-r-version-definitions) in the Administrator Guide for the full list of fields. diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index 6d2b516b..f1588047 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -344,11 +344,22 @@ config: A raw string also works, and is passed through unchanged. :::{.callout-note} -`config.session` files are rendered according to their format, not the shape of the value you -write. `r-versions` and `notifications.conf` are DCF, `*.json` files are JSON, and everything else -(`repos.conf`, `rsession.conf`, `pip.conf`, ...) is ini. Before chart 0.23.0 every file was -rendered as ini, so `r-versions` came out with `=` and Workbench ignored it entirely - see -[#948](https://github.com/rstudio/helm/issues/948). +Configuration files are rendered according to their format, not the shape of the value you write. +`r-versions` and `notifications.conf` are DCF, `*.json` files are JSON, and everything else the +chart recognizes (`rserver.conf`, `repos.conf`, `rsession.conf`, `pip.conf`, ...) is ini. Before +chart 0.23.0 every `config.session` file was rendered as ini, so `r-versions` came out with `=` +and Workbench ignored it entirely - see [#948](https://github.com/rstudio/helm/issues/948). + +A file the chart does not recognize is rendered as ini and prints a warning on install, since ini +is a guess for a file it knows nothing about. Give that file's contents as text to render it +exactly as written: + +```yaml +config: + session: + my-file: | + whatever the file needs to say +``` ::: See [Extended R version definitions](https://docs.posit.co/ide/server-pro/admin/r/using_multiple_versions_of_r.html#extended-r-version-definitions) in the Administrator Guide for the full list of fields. diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index c2451f7e..abe3de6c 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -32,17 +32,20 @@ kubectl -n {{ $.Release.Namespace }} get secret {{ include "rstudio-workbench.fu are JSON; a list is how you legitimately write those. */}} {{- $wantsList := list }} {{- $wantsMap := list }} +{{- $assumedIni := list }} {{- range $scope := (list "server" "session" "profiles") }} {{- range $file, $contents := (get $.Values.config $scope) }} {{- $path := printf "config.%s.%s" $scope ($file | replace "." "\\.") }} - {{- $ordered := include "rstudio-workbench.config.orderSensitive" (dict "file" $file "scope" $scope) }} - {{- $nonIni := (eq $scope "session") | ternary (include "rstudio-workbench.config.nonIni" (dict "file" $file)) "" }} - {{- if not $nonIni }} + {{- $kind := include "rstudio-workbench.config.fileKind" (dict "scope" $scope "file" $file) }} + {{- $ordered := eq $kind "ordered_ini" }} + {{- if has $kind (list "ini" "ordered_ini") }} {{- if and $ordered (kindIs "map" $contents) }} {{- $wantsList = append $wantsList $path }} {{- else if and (not $ordered) (kindIs "slice" $contents) }} {{- $wantsMap = append $wantsMap $path }} {{- end }} + {{- else if and (eq $kind "unknown") (or (kindIs "map" $contents) (kindIs "slice" $contents)) }} + {{- $assumedIni = append $assumedIni $path }} {{- end }} {{- end }} {{- end }} @@ -79,6 +82,23 @@ WARNING: the following configuration files are written as lists, which replaces Write them as a map unless you specifically need to control the order of their sections. {{- end }} +{{- if $assumedIni }} + +WARNING: the chart does not recognize the following configuration files +{{- range $assumedIni | sortAlpha }} + - `.Values.{{ . }}` +{{- end }} + They will be rendered as ini: `Key=Value` under `[section]` headings. If that is not the right + format, give the file's contents as text instead and the chart will use them as they are: + + config: + session: + my-file: | + whatever the file needs to say + + Open an issue if this is a file the chart should know how to build. +{{- end }} + {{- /* Workbench discards repos.conf entirely when it has no entry named CRAN (SessionOptions.cpp, parseReposConfig), taking the admin's own repositories with it. Nothing in the rendered file shows this, so check for the key in whichever form diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index a1746983..2e015b81 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -718,35 +718,87 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} {{- /* - Renders `config.session`, choosing a renderer per file rather than treating every file as ini. - The files in that directory are not all the same format: + ========================================================================== + Config file table - the one place that knows anything about config filenames + ========================================================================== - - `r-versions` and `notifications.conf` are DCF: `Key: Value`, with records separated by a - blank line. Rendered as ini they came out as `Key=Value`, which Workbench cannot parse -- it - falls back to a legacy mode and logs "does not point to a valid directory" per line, so the - file is silently ignored. See https://github.com/rstudio/helm/issues/948 - - `*.json` files are JSON. - - everything else (`repos.conf`, `rsession.conf`, `pip.conf`, ...) is ini. + One row per file: scope | filename pattern | kind. The first matching row wins. - A raw string is always passed through unchanged, so it stays with the ini renderer: the JSON - renderer would re-encode it with `toPrettyJson` and turn `{}` into the quoted string `"{}"`. + scope config.server / config.session / config.profiles, or * for any + pattern regex matched against the filename + kind ini Key=Value under [section] headings. Nothing depends on + the order of the sections, so a map is the right form. + ordered_ini ini whose behavior depends on the order of its sections + or entries, so it wants the list form. + dcf Key: Value, records separated by a blank line + json JSON + + A file matching no row is "unknown". Its contents are best given as a string, + which is passed through untouched whatever the format. Written as a map or a + list it is still built as ini, because that is what the chart has always done, + but NOTES.txt says so: guessing ini for a file we do not recognize is how + `r-versions` came out as `Key=Value`, which Workbench silently ignores (#948). + + Consumers: configmap-general.yaml and configmap-session.yaml pick the renderer, + NOTES.txt raises the two form warnings. Add a file here and all of them follow. +*/ -}} +{{- define "rstudio-workbench.config.fileTable" -}} +server | ^profiles$ | ordered_ini +server | ^launcher\..+\.resources\.conf$ | ordered_ini +profiles | ^launcher\..+\.profiles\.conf$ | ordered_ini +* | ^repos\.conf$ | ordered_ini +* | ^r-versions$ | dcf +* | ^notifications\.conf$ | dcf +* | \.json$ | json +* | ^chronicle-local\.gcfg$ | ini +* | ^Renviron\.site$ | ini +* | \.conf$ | ini +{{- end -}} + +{{- /* + Looks a file up in the table. Takes `scope` and `file`; returns its kind, or + "unknown" when no row matches. +*/ -}} +{{- define "rstudio-workbench.config.fileKind" -}} +{{- $scope := .scope -}} +{{- $file := .file -}} +{{- $hit := "" -}} +{{- range $line := splitList "\n" (include "rstudio-workbench.config.fileTable" .) -}} + {{- if and (not $hit) (contains "|" $line) -}} + {{- $col := splitList "|" $line -}} + {{- if and (or (eq (trim (index $col 0)) "*") (eq (trim (index $col 0)) $scope)) (regexMatch (trim (index $col 1)) $file) -}} + {{- $hit = trim (index $col 2) -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{- $hit | default "unknown" -}} +{{- end -}} + +{{- /* + Renders one config scope, picking a renderer per file from the table above rather than + treating every file as ini. The files in these directories are not all the same format: + `r-versions` and `notifications.conf` are DCF, `*.json` files are JSON, and the rest of + what the chart recognizes is ini. + + Takes `scope` and `data`. A file the table does not list falls back to ini, which is + a guess; NOTES.txt warns about it so the guess is at least visible. */ -}} -{{- define "rstudio-workbench.config.sessionFiles" -}} +{{- define "rstudio-workbench.config.files" -}} +{{- $scope := .scope }} {{- $ini := dict }} {{- $dcf := dict }} {{- $json := dict }} -{{- range $file, $contents := . }} - {{- if kindIs "string" $contents }} +{{- range $file, $contents := .data }} + {{- $kind := include "rstudio-workbench.config.fileKind" (dict "scope" $scope "file" $file) }} + {{- if or (kindIs "string" $contents) (empty $contents) }} + {{- /* Already the finished file. Every renderer has to pass a string through untouched and + the ini one does; the JSON one would re-encode it and turn `{}` into `"{}"`. Empty + renders to nothing whichever bucket it lands in. */ -}} {{- $_ := set $ini $file $contents }} - {{- else if empty $contents }} - {{- /* nothing to render either way; keep it with ini so output is unchanged */ -}} - {{- $_ := set $ini $file $contents }} - {{- else if include "rstudio-workbench.config.nonIni" (dict "file" $file) }} - {{- if hasSuffix ".json" $file }} - {{- $_ := set $json $file $contents }} - {{- else }} - {{- $_ := set $dcf $file $contents }} - {{- end }} + {{- else if eq $kind "dcf" }} + {{- $_ := set $dcf $file $contents }} + {{- else if eq $kind "json" }} + {{- $_ := set $json $file $contents }} {{- else }} {{- $_ := set $ini $file $contents }} {{- end }} @@ -755,30 +807,3 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- if $dcf }}{{- include "rstudio-library.config.dcf" $dcf }}{{- end }} {{- if $json }}{{- include "rstudio-library.config.json" $json }}{{- end }} {{- end }} - -{{- /* - Filename dispatch, in one place. Three consumers: `configmap-session.yaml` picks a renderer, - and `NOTES.txt` raises both of the form warnings. Keeping the lists here is what stops them - drifting apart. -*/ -}} - -{{- /* Files whose behavior depends on the order of their sections or entries. These want the - list form; a map sorts them by name and silently changes what they do. */ -}} -{{- define "rstudio-workbench.config.orderSensitive" -}} -{{- $file := .file -}} -{{- $scope := .scope -}} -{{- if eq $scope "server" -}} - {{- or (eq $file "profiles") (regexMatch "^launcher\\..+\\.resources\\.conf$" $file) | ternary "yes" "" -}} -{{- else if eq $scope "profiles" -}} - {{- regexMatch "^launcher\\..+\\.profiles\\.conf$" $file | ternary "yes" "" -}} -{{- else if eq $scope "session" -}} - {{- eq $file "repos.conf" | ternary "yes" "" -}} -{{- end -}} -{{- end -}} - -{{- /* Session files that are not ini. `r-versions` and `notifications.conf` are DCF; `.json` is - JSON. These legitimately take a list, so the ini form warnings must not apply to them. */ -}} -{{- define "rstudio-workbench.config.nonIni" -}} -{{- $file := .file -}} -{{- or (has $file (list "r-versions" "notifications.conf")) (hasSuffix ".json" $file) | ternary "yes" "" -}} -{{- end -}} diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml index 75f94b94..cdb1dfa5 100644 --- a/charts/rstudio-workbench/templates/configmap-general.yaml +++ b/charts/rstudio-workbench/templates/configmap-general.yaml @@ -149,7 +149,7 @@ data: {{- with .Values.chronicle.localConfig }} {{- $overrideDict = mergeOverwrite $overrideDict (dict "chronicle-local.gcfg" .) }} {{- end }} -{{ include "rstudio-library.config.ini" $overrideDict | indent 2 }} +{{ include "rstudio-workbench.config.files" (dict "scope" "server" "data" $overrideDict) | indent 2 }} {{/* helper variables to make things here a bit more sane */}} {{- $profilesConfig := .Values.config.profiles | deepCopy }} {{- /* Apply the default [*] session image settings to launcher.kubernetes.profiles.conf. When it is diff --git a/charts/rstudio-workbench/templates/configmap-session.yaml b/charts/rstudio-workbench/templates/configmap-session.yaml index c0fb12c3..9ce261bd 100644 --- a/charts/rstudio-workbench/templates/configmap-session.yaml +++ b/charts/rstudio-workbench/templates/configmap-session.yaml @@ -5,7 +5,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-session namespace: {{ $.Release.Namespace }} data: - {{- include "rstudio-workbench.config.sessionFiles" .Values.config.session | nindent 2 }} + {{- include "rstudio-workbench.config.files" (dict "scope" "session" "data" .Values.config.session) | nindent 2 }} {{- if .Values.config.sessionSecret }} --- {{- if .Values.sealedSecret.enabled }} @@ -47,7 +47,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-session namespace: {{ $targetNamespace }} data: - {{- include "rstudio-workbench.config.sessionFiles" .Values.config.session | nindent 2 }} + {{- include "rstudio-workbench.config.files" (dict "scope" "session" "data" .Values.config.session) | nindent 2 }} {{- if .Values.config.sessionSecret }} --- {{- if .Values.sealedSecret.enabled }} diff --git a/charts/rstudio-workbench/tests/notes_test.yaml b/charts/rstudio-workbench/tests/notes_test.yaml index be3ef1a8..a5bf93aa 100644 --- a/charts/rstudio-workbench/tests/notes_test.yaml +++ b/charts/rstudio-workbench/tests/notes_test.yaml @@ -254,3 +254,48 @@ tests: asserts: - notMatchRegexRaw: pattern: "written as lists" + + # -- A file matching no row in rstudio-workbench.config.fileTable still renders as ini, which is + # a guess; the chart says so rather than guessing quietly. + - it: should warn when a file the chart does not recognize is written as a map + set: + config: + session: + my-thing: + FOO: bar + asserts: + - matchRegexRaw: + pattern: "WARNING: the chart does not recognize the following configuration files" + - matchRegexRaw: + pattern: "`\\.Values\\.config\\.session\\.my-thing`" + + - it: should not warn when an unrecognized file is given as text + set: + config: + session: + my-thing: | + FOO=bar + asserts: + - notMatchRegexRaw: + pattern: "does not recognize the following" + + - it: should not warn for files the table does list + set: + config: + server: + otel.conf: + exporter: otlp + session: + Renviron.site: + TZ: UTC + r-versions: + - Path: /opt/R/4.4.1 + Label: Latest + asserts: + - notMatchRegexRaw: + pattern: "does not recognize the following" + + - it: should not warn about unrecognized files on a default install + asserts: + - notMatchRegexRaw: + pattern: "does not recognize the following" From 7a5b7a3bd66a1ce897359b60b720b09204845b82 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Wed, 30 Sep 2026 11:19:52 -0700 Subject: [PATCH 14/18] Repeat keys for program_options files, keep CRAN in repos.conf maps, and fail list-form files the chart merges into --- charts/rstudio-workbench/templates/NOTES.txt | 56 ++++----- .../rstudio-workbench/templates/_helpers.tpl | 115 ++++++++++++------ .../templates/configmap-general.yaml | 16 ++- .../templates/configmap-session.yaml | 29 ++++- charts/rstudio-workbench/values.yaml | 8 +- 5 files changed, 149 insertions(+), 75 deletions(-) diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index abe3de6c..5ecb4326 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -36,15 +36,15 @@ kubectl -n {{ $.Release.Namespace }} get secret {{ include "rstudio-workbench.fu {{- range $scope := (list "server" "session" "profiles") }} {{- range $file, $contents := (get $.Values.config $scope) }} {{- $path := printf "config.%s.%s" $scope ($file | replace "." "\\.") }} - {{- $kind := include "rstudio-workbench.config.fileKind" (dict "scope" $scope "file" $file) }} - {{- $ordered := eq $kind "ordered_ini" }} - {{- if has $kind (list "ini" "ordered_ini") }} + {{- $format := include "rstudio-workbench.config.fileFormat" (dict "scope" $scope "file" $file) }} + {{- $ordered := include "rstudio-workbench.config.fileOrdered" (dict "scope" $scope "file" $file) }} + {{- if not (has $format (list "dcf" "json" "unknown")) }} {{- if and $ordered (kindIs "map" $contents) }} {{- $wantsList = append $wantsList $path }} {{- else if and (not $ordered) (kindIs "slice" $contents) }} {{- $wantsMap = append $wantsMap $path }} {{- end }} - {{- else if and (eq $kind "unknown") (or (kindIs "map" $contents) (kindIs "slice" $contents)) }} + {{- else if and (eq $format "unknown") (or (kindIs "map" $contents) (kindIs "slice" $contents)) }} {{- $assumedIni = append $assumedIni $path }} {{- end }} {{- end }} @@ -66,20 +66,24 @@ WARNING: the following configuration files are written as maps, which does not k - "@analysts": max-memory-mb: 4096 - The map form still works for now, but will be removed in a future chart release. + A map is fine for every other config file; these are the ones where the order matters. + {{- $repos := get .Values.config.session "repos.conf" }} + {{- if and (kindIs "map" $repos) (not (hasKey $repos "CRAN")) }} + + `repos\.conf` has no `CRAN` entry, so the chart added its default one. A list replaces the + chart's default, so name a `CRAN` entry yourself when you convert it. + {{- end }} {{- end }} {{- if $wantsMap }} -WARNING: the following configuration files are written as lists, which replaces the chart's defaults for them +WARNING: the following configuration files are written as lists, which replaces the chart's defaults for them instead of merging with them {{- range $wantsMap | sortAlpha }} - `.Values.{{ . }}` {{- end }} - Nothing in these files depends on the order of their sections, and Helm merges a map with the - chart's defaults while a list replaces them. Written as a list, any default the chart ships for - these files is silently dropped - for example `launcher\.conf` loses its `[server]` section, - which the launcher needs. - - Write them as a map unless you specifically need to control the order of their sections. + Helm merges a map with the chart's defaults for a file, but a list replaces them: any default the + chart ships for these files is dropped, and only what you wrote is rendered. Nothing in these + files depends on the order of their sections, so write them as a map unless you specifically + need to control that order - and then include any chart defaults you still want in your list. {{- end }} {{- if $assumedIni }} @@ -101,23 +105,11 @@ WARNING: the chart does not recognize the following configuration files {{- /* Workbench discards repos.conf entirely when it has no entry named CRAN (SessionOptions.cpp, parseReposConfig), taking the admin's own repositories with it. - Nothing in the rendered file shows this, so check for the key in whichever form - the file was written. */}} -{{- if hasKey .Values.config.session "repos.conf" }} - {{- $repos := get .Values.config.session "repos.conf" }} - {{- $hasCran := false }} - {{- if kindIs "string" $repos }} - {{- $hasCran = regexMatch "(?m)^[ \t]*CRAN[ \t]*=" $repos }} - {{- else if kindIs "slice" $repos }} - {{- range $item := $repos }} - {{- if and (kindIs "map" $item) (hasKey $item "CRAN") }} - {{- $hasCran = true }} - {{- end }} - {{- end }} - {{- else if kindIs "map" $repos }} - {{- $hasCran = hasKey $repos "CRAN" }} - {{- end }} - {{- if and $repos (not $hasCran) }} + configmap-session.yaml fails for the map and list forms; a string is passed through + untouched, so only warn for it. */}} +{{- $repos := get .Values.config.session "repos.conf" }} +{{- if kindIs "string" $repos }} + {{- if and $repos (not (regexMatch "(?m)^[ \t]*CRAN[ \t]*=" $repos)) }} WARNING: `.Values.config.session.repos\.conf` has no `CRAN` entry - Workbench ignores the whole file when no entry is named `CRAN`, so none of these repositories @@ -127,9 +119,9 @@ WARNING: `.Values.config.session.repos\.conf` has no `CRAN` entry config: session: - repos.conf: - - Internal: https://pkgs.example.com/internal - - CRAN: https://mirror.example.com/cran + repos.conf: | + CRAN=https://mirror.example.com/cran + Internal=https://pkgs.example.com/internal - To configure repositories somewhere else instead, set `config.session.repos\.conf: null` and the chart will not render the file at all. diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index 2e015b81..8bb65ae4 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -717,71 +717,106 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{ trimSuffix ":" ( join ":" (list .Values.xdgConfigDirs (join ":" .Values.xdgConfigDirsExtra) ) ) }} {{- end -}} +{{- /* + The CRAN entry of the chart's default repos.conf, which values.yaml writes as a list. A map + written by the admin replaces that list rather than merging with it, so configmap-session.yaml + adds this back to a map with no CRAN, as the chart did when the default was a map. Must match + values.yaml; tests/configmap_test.yaml asserts both render the same URL. +*/ -}} +{{- define "rstudio-workbench.config.defaultCran" -}} +https://packagemanager.posit.co/cran/__linux__/jammy/latest +{{- end -}} + {{- /* ========================================================================== Config file table - the one place that knows anything about config filenames ========================================================================== - One row per file: scope | filename pattern | kind. The first matching row wins. + One row per file: scope | filename pattern | format | ordered. The first matching row wins. scope config.server / config.session / config.profiles, or * for any pattern regex matched against the filename - kind ini Key=Value under [section] headings. Nothing depends on - the order of the sections, so a map is the right form. - ordered_ini ini whose behavior depends on the order of its sections - or entries, so it wants the list form. - dcf Key: Value, records separated by a blank line - json JSON + format which parser Workbench reads the file with, which decides both the renderer and + how an option with several values is written: + ini_ptree boost property_tree read_ini. [section] headings; a repeated key is an + error, so a list of values is comma-joined (a,b,c) + ini_popt boost program_options. A list of values repeats the key, one line per + value, and a comma is part of the value (www-allow-origin, + server-add-header) + gcfg Go gcfg, as ini with repeated keys + renviron R's Renviron, as ini with repeated keys + dcf Key: Value, records separated by a blank line + json JSON + ordered yes when the file's behavior depends on the order of its sections or entries, so it + wants the list form. Read only by NOTES.txt - A file matching no row is "unknown". Its contents are best given as a string, - which is passed through untouched whatever the format. Written as a map or a - list it is still built as ini, because that is what the chart has always done, - but NOTES.txt says so: guessing ini for a file we do not recognize is how - `r-versions` came out as `Key=Value`, which Workbench silently ignores (#948). + A file matching no row is "unknown". Its contents are best given as a string, which is passed + through untouched whatever the format. Written as a map or a list it is still built as ini with + repeated keys, because that is what the chart has always done, but NOTES.txt says so: guessing + ini for a file we do not recognize is how `r-versions` came out as `Key=Value`, which Workbench + silently ignores (#948). - Consumers: configmap-general.yaml and configmap-session.yaml pick the renderer, - NOTES.txt raises the two form warnings. Add a file here and all of them follow. + Consumers: configmap-general.yaml and configmap-session.yaml pick the renderer, NOTES.txt + raises the form warnings. Add a file here and all of them follow. Not every scope goes through + here: config.secret, config.sessionSecret, config.sssd.conf and config.startupCustom call + rstudio-library.config.ini directly, comma-joining lists and getting no NOTES warnings. */ -}} {{- define "rstudio-workbench.config.fileTable" -}} -server | ^profiles$ | ordered_ini -server | ^launcher\..+\.resources\.conf$ | ordered_ini -profiles | ^launcher\..+\.profiles\.conf$ | ordered_ini -* | ^repos\.conf$ | ordered_ini -* | ^r-versions$ | dcf -* | ^notifications\.conf$ | dcf -* | \.json$ | json -* | ^chronicle-local\.gcfg$ | ini -* | ^Renviron\.site$ | ini -* | \.conf$ | ini +server | ^profiles$ | ini_ptree | yes +server | ^launcher\..+\.resources\.conf$ | ini_ptree | yes +profiles | ^launcher\..+\.profiles\.conf$ | ini_ptree | yes +server | ^launcher\.kubernetes\.profiles\.conf$ | ini_ptree | yes +* | ^repos\.conf$ | ini_ptree | yes +* | ^launcher\.conf$ | ini_ptree | no +* | ^logging\.conf$ | ini_ptree | no +* | ^r-versions$ | dcf | no +* | ^notifications\.conf$ | dcf | no +* | \.json$ | json | no +* | ^chronicle-local\.gcfg$ | gcfg | no +* | ^Renviron\.site$ | renviron | no +* | \.conf$ | ini_popt | no {{- end -}} {{- /* - Looks a file up in the table. Takes `scope` and `file`; returns its kind, or - "unknown" when no row matches. + Looks a file up in the table. Takes `scope`, `file`, and `column` (2 for format, 3 for + ordered); returns that column of the first matching row, or "" when no row matches. */ -}} -{{- define "rstudio-workbench.config.fileKind" -}} +{{- define "rstudio-workbench.config.fileLookup" -}} {{- $scope := .scope -}} {{- $file := .file -}} +{{- $column := .column -}} {{- $hit := "" -}} +{{- $found := false -}} {{- range $line := splitList "\n" (include "rstudio-workbench.config.fileTable" .) -}} - {{- if and (not $hit) (contains "|" $line) -}} + {{- if and (not $found) (contains "|" $line) -}} {{- $col := splitList "|" $line -}} {{- if and (or (eq (trim (index $col 0)) "*") (eq (trim (index $col 0)) $scope)) (regexMatch (trim (index $col 1)) $file) -}} - {{- $hit = trim (index $col 2) -}} + {{- $found = true -}} + {{- $hit = trim (index $col $column) -}} {{- end -}} {{- end -}} {{- end -}} -{{- $hit | default "unknown" -}} +{{- $hit -}} +{{- end -}} + +{{- /* The file's format from the table, or "unknown". Takes `scope` and `file`. */ -}} +{{- define "rstudio-workbench.config.fileFormat" -}} +{{- include "rstudio-workbench.config.fileLookup" (dict "scope" .scope "file" .file "column" 2) | default "unknown" -}} +{{- end -}} + +{{- /* "yes" when the table marks the file as ordered, otherwise "". Takes `scope` and `file`. */ -}} +{{- define "rstudio-workbench.config.fileOrdered" -}} +{{- if eq (include "rstudio-workbench.config.fileLookup" (dict "scope" .scope "file" .file "column" 3)) "yes" }}yes{{ end -}} {{- end -}} {{- /* Renders one config scope, picking a renderer per file from the table above rather than treating every file as ini. The files in these directories are not all the same format: `r-versions` and `notifications.conf` are DCF, `*.json` files are JSON, and the rest of - what the chart recognizes is ini. + what the chart recognizes is ini of one flavor or another. - Takes `scope` and `data`. A file the table does not list falls back to ini, which is - a guess; NOTES.txt warns about it so the guess is at least visible. + Takes `scope` and `data`. A file the table does not list falls back to ini with repeated + keys, which is a guess; NOTES.txt warns about it so the guess is at least visible. */ -}} {{- define "rstudio-workbench.config.files" -}} {{- $scope := .scope }} @@ -789,21 +824,27 @@ profiles | ^launcher\..+\.profiles\.conf$ | ordered_ini {{- $dcf := dict }} {{- $json := dict }} {{- range $file, $contents := .data }} - {{- $kind := include "rstudio-workbench.config.fileKind" (dict "scope" $scope "file" $file) }} + {{- $format := include "rstudio-workbench.config.fileFormat" (dict "scope" $scope "file" $file) }} {{- if or (kindIs "string" $contents) (empty $contents) }} {{- /* Already the finished file. Every renderer has to pass a string through untouched and the ini one does; the JSON one would re-encode it and turn `{}` into `"{}"`. Empty renders to nothing whichever bucket it lands in. */ -}} {{- $_ := set $ini $file $contents }} - {{- else if eq $kind "dcf" }} + {{- else if eq $format "dcf" }} {{- $_ := set $dcf $file $contents }} - {{- else if eq $kind "json" }} + {{- else if eq $format "json" }} {{- $_ := set $json $file $contents }} {{- else }} {{- $_ := set $ini $file $contents }} {{- end }} {{- end }} -{{- if $ini }}{{- include "rstudio-library.config.ini" $ini }}{{- end }} +{{- /* One file at a time, in the sorted order rstudio-library.config.ini would use, so that each + gets its own way of writing several values. */ -}} +{{- range $file := keys $ini | sortAlpha }} + {{- $format := include "rstudio-workbench.config.fileFormat" (dict "scope" $scope "file" $file) }} + {{- $multi := eq $format "ini_ptree" | ternary "join" "repeat" }} + {{- include "rstudio-library.config.ini.files" (dict "files" (dict $file (get $ini $file)) "multi" $multi) }} +{{- end }} {{- if $dcf }}{{- include "rstudio-library.config.dcf" $dcf }}{{- end }} {{- if $json }}{{- include "rstudio-library.config.json" $json }}{{- end }} {{- end }} diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml index cdb1dfa5..7d4719fd 100644 --- a/charts/rstudio-workbench/templates/configmap-general.yaml +++ b/charts/rstudio-workbench/templates/configmap-general.yaml @@ -1,3 +1,16 @@ +{{- /* The chart adds its own settings to these files by merging a map over what was written. + rserver.conf must be a map for that merge to work at all. The rest may be a string, which + is taken as the whole file, but a list would silently drop the chart's settings - which + for launcher.conf includes the [server] section the launcher needs to start. Nothing in + them depends on order, so there is no reason to write them as a list. */}} +{{- range $file := list "rserver.conf" "launcher.conf" "launcher.kubernetes.conf" "positron.conf" }} + {{- $contents := get $.Values.config.server $file }} + {{- $allowed := eq $file "rserver.conf" | ternary (list "map") (list "map" "string") }} + {{- if and $contents (not (has (kindOf $contents) $allowed)) }} + {{- $orString := eq $file "rserver.conf" | ternary "" "\nA string is also accepted, and is used as the whole file.\n" }} + {{- fail (print "\n\nconfig.server." $file " is a " (kindOf $contents) ", but must be a map of options.\n\nThe chart merges its own settings into " $file ", which only works on a map.\nNothing in the file depends on order, so write it as a map:\n\n config:\n server:\n " $file ":\n option-name: value\n" $orString) }} + {{- end }} +{{- end }} {{- /* Define the default values that will be merged over */}} {{- $defaultVersion := .Values.versionOverride | default $.Chart.AppVersion }} {{- $sessionTag := .Values.session.image.tag | default (printf "R%s-python%s-%s" .Values.session.image.rVersion .Values.session.image.pythonVersion .Values.session.image.os ) }} @@ -159,7 +172,8 @@ data: {{- if kindIs "slice" $writtenProfiles }} {{- $hasEveryone := false }} {{- range $item := $writtenProfiles }} - {{- if hasKey $item "*" }} + {{- /* anything but a map is left for rstudio-library.config.entries to reject */}} + {{- if and (kindIs "map" $item) (hasKey $item "*") }} {{- $hasEveryone = true }} {{- $_ := set $item "*" (mergeOverwrite (deepCopy $defaultProfiles) (get $item "*")) }} {{- end }} diff --git a/charts/rstudio-workbench/templates/configmap-session.yaml b/charts/rstudio-workbench/templates/configmap-session.yaml index 9ce261bd..f9a4fa4a 100644 --- a/charts/rstudio-workbench/templates/configmap-session.yaml +++ b/charts/rstudio-workbench/templates/configmap-session.yaml @@ -1,3 +1,28 @@ +{{- /* Workbench discards repos.conf entirely when it has no entry named CRAN + (SessionOptions.cpp, parseReposConfig), taking the admin's own repositories with it, and + nothing in the rendered file shows it. + - A map is the form repos.conf had when the chart's default was a map, and Helm merged the + two, so the default CRAN entry was always there. The default is now a list, which a map + replaces, so add the entry back to keep that working. NOTES.txt still warns that the + map form sorts the entries. + - A list replaces the default by design, so it has to name CRAN itself. Fail rather than + render a file Workbench will ignore. + - A string is passed through untouched; NOTES.txt warns about it instead. */}} +{{- $sessionConfig := .Values.config.session | deepCopy }} +{{- $repos := get $sessionConfig "repos.conf" }} +{{- if and $repos (kindIs "map" $repos) (not (hasKey $repos "CRAN")) }} + {{- $_ := set $repos "CRAN" (include "rstudio-workbench.config.defaultCran" .) }} +{{- else if and $repos (kindIs "slice" $repos) }} + {{- $hasCran := false }} + {{- range $item := $repos }} + {{- if and (kindIs "map" $item) (hasKey $item "CRAN") }} + {{- $hasCran = true }} + {{- end }} + {{- end }} + {{- if not $hasCran }} + {{- fail "\n\nconfig.session.repos.conf has no entry named CRAN.\n\nWorkbench ignores the whole file without one, so none of these repositories would be\nused. Written as a list, repos.conf replaces the chart's default CRAN entry, so it must\nname its own. It does not have to be CRAN itself - name your own mirror CRAN if that\nis what sessions should use:\n\n config:\n session:\n repos.conf:\n - CRAN: https://mirror.example.com/cran\n - Internal: https://pkgs.example.com/internal\n\nTo configure repositories somewhere else instead, set config.session.repos.conf: null\nand the chart will not render the file at all.\n" }} + {{- end }} +{{- end }} --- apiVersion: v1 kind: ConfigMap @@ -5,7 +30,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-session namespace: {{ $.Release.Namespace }} data: - {{- include "rstudio-workbench.config.files" (dict "scope" "session" "data" .Values.config.session) | nindent 2 }} + {{- include "rstudio-workbench.config.files" (dict "scope" "session" "data" $sessionConfig) | nindent 2 }} {{- if .Values.config.sessionSecret }} --- {{- if .Values.sealedSecret.enabled }} @@ -47,7 +72,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-session namespace: {{ $targetNamespace }} data: - {{- include "rstudio-workbench.config.files" (dict "scope" "session" "data" .Values.config.session) | nindent 2 }} + {{- include "rstudio-workbench.config.files" (dict "scope" "session" "data" $sessionConfig) | nindent 2 }} {{- if .Values.config.sessionSecret }} --- {{- if .Values.sealedSecret.enabled }} diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml index d02304b3..9372be27 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -581,9 +581,11 @@ config: # -- a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. # Each file's contents may be a map, a raw string, or - for files read in order, such as `repos.conf` - a list of single-entry maps. See README for more information. session: - # Written as a list because repos.conf is read in order. Supplying your own - # replaces this default entirely, and it must include a CRAN entry - Workbench - # ignores the whole file without one. + # Written as a list because repos.conf is read in order. A list of your own + # replaces this default entirely, and must include a CRAN entry - Workbench + # ignores the whole file without one, so the chart fails. A map of your own gets + # this CRAN entry added when it has none. The URL is repeated in the chart's + # rstudio-workbench.config.defaultCran template; change both together. repos.conf: - CRAN: https://packagemanager.posit.co/cran/__linux__/jammy/latest rsession.conf: {} From f7000b467381141fb25c18159e4c1e5e772fd663 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Wed, 30 Sep 2026 11:19:52 -0700 Subject: [PATCH 15/18] Pin legacy config shapes to their main renders and cover the new guards --- .../tests/configmap_test.yaml | 166 ++++++++++++++ .../tests/legacy_config_test.yaml | 213 ++++++++++++++++++ .../rstudio-workbench/tests/notes_test.yaml | 74 +++++- 3 files changed, 445 insertions(+), 8 deletions(-) create mode 100644 charts/rstudio-workbench/tests/legacy_config_test.yaml diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index ebae050b..dfb2e1a9 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -764,3 +764,169 @@ tests: - matchRegex: path: data["launcher.kubernetes.profiles.conf"] pattern: "default-container-image=posit/workbench-session:" + + # -- Files the chart merges its own settings into must be maps + - it: should fail with a clear message when rserver.conf is a list + template: configmap-general.yaml + set: + config: + server: + rserver.conf: + - www-port: 8787 + asserts: + - failedTemplate: + errorPattern: "config.server.rserver.conf is a slice, but must be a map of options" + + - it: should fail with a clear message when rserver.conf is a string + template: configmap-general.yaml + set: + config: + server: + rserver.conf: | + www-port=8787 + asserts: + - failedTemplate: + errorPattern: "config.server.rserver.conf is a string, but must be a map of options" + + - it: should fail rather than drop the chart's [server] section when launcher.conf is a list + template: configmap-general.yaml + set: + config: + server: + launcher.conf: + - cluster: + name: Kubernetes + asserts: + - failedTemplate: + errorPattern: "config.server.launcher.conf is a slice(.|\\n)*A string is also accepted" + + - it: should fail rather than drop the chart's namespace when launcher.kubernetes.conf is a list + template: configmap-general.yaml + set: + config: + server: + launcher.kubernetes.conf: + - kubernetes-namespace: other + asserts: + - failedTemplate: + errorPattern: "config.server.launcher.kubernetes.conf is a slice" + + - it: should pass a launcher.conf string through + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + launcher.conf: | + [server] + address=127.0.0.1 + asserts: + - equal: + path: data["launcher.conf"] + value: | + [server] + address=127.0.0.1 + + - it: should report a non-map profiles list entry with the library's message + template: configmap-general.yaml + set: + config: + profiles: + launcher.kubernetes.profiles.conf: + - oops + asserts: + - failedTemplate: + errorPattern: "Every entry written as a list must be a map" + + # -- repos.conf must name CRAN; Workbench discards the whole file otherwise + - it: should fail when a repos.conf list has no CRAN entry + template: configmap-session.yaml + set: + config: + session: + repos.conf: + - Internal: https://pkgs.example.com/internal + asserts: + - failedTemplate: + errorPattern: "config.session.repos.conf has no entry named CRAN" + + - it: should render a repos.conf list with CRAN in the order written + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + repos.conf: + - Internal: https://pkgs.example.com/internal + - CRAN: https://mirror.example.com/cran + asserts: + - equal: + path: data["repos.conf"] + value: | + Internal=https://pkgs.example.com/internal + CRAN=https://mirror.example.com/cran + + - it: should not render repos.conf when it is null + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + repos.conf: null + asserts: + - notExists: + path: data["repos.conf"] + + # -- How several values are written follows the file's format in the table + - it: should comma-join a list of values in an ini_ptree file + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + logging.conf: + "*": + log-level: info + some-option: [a, b] + asserts: + - matchRegex: + path: data["logging.conf"] + pattern: "(?m)^some-option=a,b$" + + - it: should repeat the key for a list of values in an unrecognized file + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + custom.ini: + option: [a, b] + asserts: + - equal: + path: data["custom.ini"] + value: | + option=a + option=b + + - it: should render the deprecated server-scope launcher.kubernetes.profiles.conf in the order written + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + launcher.kubernetes.profiles.conf: + - "@analysts": + max-cpus: 4 + - "*": + max-cpus: 1 + container-images: [a, b] + asserts: + - equal: + path: data["launcher.kubernetes.profiles.conf"] + value: | + [@analysts] + max-cpus=4 + + [*] + container-images=a,b + max-cpus=1 diff --git a/charts/rstudio-workbench/tests/legacy_config_test.yaml b/charts/rstudio-workbench/tests/legacy_config_test.yaml new file mode 100644 index 00000000..541efa9b --- /dev/null +++ b/charts/rstudio-workbench/tests/legacy_config_test.yaml @@ -0,0 +1,213 @@ +suite: Workbench legacy config shapes +# Config written the way it was before the ordered list form existed (chart 0.22 and earlier) +# must render exactly as it did then. Each expected value below was rendered from `main` before +# #945/#953. The two known, intended differences are not pinned here: launcher.*.profiles.conf +# lost a leading blank line, and r-versions written as records became valid DCF (#948). +templates: + - configmap-general.yaml + - configmap-session.yaml +tests: + # rserver.conf is read by boost program_options, where www-allow-origin and server-add-header + # are multitoken: several values repeat the key, and a comma is part of the value. + - it: should repeat the key for a list of values in rserver.conf + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + rserver.conf: + www-enable-origin-check: 1 + www-allow-origin: [a.example.com, b.example.com] + server-add-header: ["X-Frame-Options: DENY", "X-Custom: 1"] + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^server-add-header=X-Frame-Options: DENY\\nserver-add-header=X-Custom: 1$" + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^www-allow-origin=a.example.com\\nwww-allow-origin=b.example.com\\nwww-enable-origin-check=1$" + + # The two tests below pin the same URL: the first as values.yaml ships it, the second as the + # template adds it back (rstudio-workbench.config.defaultCran). Update both together. + - it: should render the chart's default CRAN entry + template: configmap-session.yaml + documentIndex: 0 + asserts: + - equal: + path: data["repos.conf"] + value: | + CRAN=https://packagemanager.posit.co/cran/__linux__/jammy/latest + + - it: should add the chart's CRAN entry to a repos.conf map without one + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + repos.conf: + Internal: https://pkgs.example.com/internal + asserts: + - equal: + path: data["repos.conf"] + value: | + CRAN=https://packagemanager.posit.co/cran/__linux__/jammy/latest + Internal=https://pkgs.example.com/internal + + - it: should add the CRAN entry in the second namespace's session ConfigMap too + template: configmap-session.yaml + documentIndex: 1 + set: + launcher: + namespace: sessions + config: + session: + repos.conf: + Internal: https://pkgs.example.com/internal + asserts: + - equal: + path: metadata.namespace + value: sessions + - equal: + path: data["repos.conf"] + value: | + CRAN=https://packagemanager.posit.co/cran/__linux__/jammy/latest + Internal=https://pkgs.example.com/internal + + - it: should keep the admin's own CRAN entry in a repos.conf map + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + repos.conf: + RSPM: https://packagemanager.example.com/cran/latest + CRAN: https://mirror.example.com/cran + asserts: + - equal: + path: data["repos.conf"] + value: | + CRAN=https://mirror.example.com/cran + RSPM=https://packagemanager.example.com/cran/latest + + - it: should render a profiles map sorted, as before + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + profiles: + "*": + session-limit: 5 + session-timeout-minutes: 60 + "@analysts": + session-limit: 10 + asserts: + - equal: + path: data["profiles"] + value: | + [*] + session-limit=5 + session-timeout-minutes=60 + + [@analysts] + session-limit=10 + + - it: should repeat a section for a list of maps in launcher.conf, merged with the defaults + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + launcher.conf: + cluster: + - name: Kubernetes + type: Kubernetes + - name: Local + type: Local + asserts: + - equal: + path: data["launcher.conf"] + value: | + [cluster] + name=Kubernetes + type=Kubernetes + + [cluster] + name=Local + type=Local + + [server] + address=127.0.0.1 + admin-group=rstudio-server + authorization-enabled=1 + enable-debug-logging=0 + port=5559 + server-user=rstudio-server + thread-pool-size=4 + + - it: should render a pip.conf map as before + template: configmap-session.yaml + documentIndex: 0 + set: + config: + session: + pip.conf: + "global": + index-url: https://packagemanager.posit.co/pypi/latest/simple + trusted-host: packagemanager.posit.co + asserts: + - equal: + path: data["pip.conf"] + value: | + [global] + index-url=https://packagemanager.posit.co/pypi/latest/simple + trusted-host=packagemanager.posit.co + + - it: should merge map-form server files with their chart defaults, as before + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + logging.conf: + "*": + log-level: debug + "@rserver": + log-level: info + jupyter.conf: + jupyter-exe: /opt/python/bin/jupyter + vscode.conf: + enabled: 0 + launcher.kubernetes.resources.conf: + small: + name: Small + cpus: 1 + mem-mb: 512 + asserts: + - equal: + path: data["logging.conf"] + value: | + [*] + log-level=debug + logger-type=stderr + + [@rserver] + log-level=info + - equal: + path: data["jupyter.conf"] + value: | + default-session-cluster=Kubernetes + jupyter-exe=/opt/python/bin/jupyter + labs-enabled=1 + - equal: + path: data["vscode.conf"] + value: | + enabled=0 + session-timeout-kill-hours=12 + - equal: + path: data["launcher.kubernetes.resources.conf"] + value: | + [small] + cpus=1 + mem-mb=512 + name=Small diff --git a/charts/rstudio-workbench/tests/notes_test.yaml b/charts/rstudio-workbench/tests/notes_test.yaml index a5bf93aa..1eed1490 100644 --- a/charts/rstudio-workbench/tests/notes_test.yaml +++ b/charts/rstudio-workbench/tests/notes_test.yaml @@ -145,13 +145,14 @@ tests: - matchRegexRaw: pattern: "WARNING: the following configuration files are written as maps" - # Workbench discards repos.conf entirely when nothing is named CRAN - - it: should warn when repos.conf has no CRAN entry + # Workbench discards repos.conf entirely when nothing is named CRAN. A map gets the chart's + # CRAN entry and a list fails in configmap-session.yaml, so only a string reaches this warning. + - it: should warn when a repos.conf string has no CRAN entry set: config: session: - repos.conf: - - Internal: https://pkgs.example.com/internal + repos.conf: | + Internal=https://pkgs.example.com/internal asserts: - matchRegexRaw: pattern: "has no `CRAN` entry" @@ -189,6 +190,63 @@ tests: - notMatchRegexRaw: pattern: "has no `CRAN` entry" + - it: should note that the chart added CRAN to a repos.conf map without one + set: + config: + session: + repos.conf: + Internal: https://pkgs.example.com/internal + asserts: + - matchRegexRaw: + pattern: "written as maps" + - matchRegexRaw: + pattern: "has no `CRAN` entry, so the chart added its default one" + + - it: should not note an added CRAN when a repos.conf map has its own + set: + config: + session: + repos.conf: + Internal: https://pkgs.example.com/internal + CRAN: https://mirror.example.com/cran + asserts: + - notMatchRegexRaw: + pattern: "chart added its default one" + + # The deprecated server-scope location of launcher.kubernetes.profiles.conf is read in order too + - it: should warn when the server-scope launcher.kubernetes.profiles.conf is written as a map + set: + config: + server: + launcher.kubernetes.profiles.conf: + "*": + max-cpus: 1 + asserts: + - matchRegexRaw: + pattern: "written as maps(.|\\n)*config\\.server\\.launcher\\\\\\.kubernetes\\\\\\.profiles\\\\\\.conf" + + - it: should not say a list replaces defaults for the server-scope launcher.kubernetes.profiles.conf + set: + config: + server: + launcher.kubernetes.profiles.conf: + - "*": + max-cpus: 1 + asserts: + - notMatchRegexRaw: + pattern: "written as lists" + + - it: should not promise to remove the map form + set: + config: + server: + profiles: + "*": + max-memory-mb: 1024 + asserts: + - notMatchRegexRaw: + pattern: "will be removed" + - it: should not warn about CRAN on a default install asserts: - notMatchRegexRaw: @@ -207,14 +265,14 @@ tests: set: config: server: - launcher.conf: - - cluster: - name: Cluster1 + logging.conf: + - "*": + log-level: warn asserts: - matchRegexRaw: pattern: "WARNING: the following configuration files are written as lists" - matchRegexRaw: - pattern: "`\\.Values\\.config\\.server\\.launcher\\\\\\.conf`" + pattern: "`\\.Values\\.config\\.server\\.logging\\\\\\.conf`" - it: should not warn when an order-agnostic ini file is written as a map set: From c3c661577965aff629ef44808be1c3add8372d7c Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Wed, 30 Sep 2026 11:19:52 -0700 Subject: [PATCH 16/18] Bring Workbench NEWS and README in line with per-file multi-values and repos.conf --- charts/rstudio-workbench/NEWS.md | 81 ++++++++++++++++------- charts/rstudio-workbench/README.md | 29 +++++--- charts/rstudio-workbench/README.md.gotmpl | 29 +++++--- 3 files changed, 92 insertions(+), 47 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 84f4b4e3..f17cd684 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -5,7 +5,8 @@ - Config files whose behavior depends on the order of their sections or entries can now be written as a list, putting `- ` in front of each section or entry, and are rendered in the order written. This covers `config.server.profiles`, `config.server.launcher\.*\.resources\.conf`, - `config.profiles.launcher\.*\.profiles\.conf`, and `config.session.repos\.conf`: + `config.profiles.launcher\.*\.profiles\.conf` (and its deprecated `config.server` location), and + `config.session.repos\.conf`: ```yaml config: @@ -21,6 +22,27 @@ - CRAN: https://packagemanager.posit.co/cran/latest ``` + Written as a map, these files are still rendered as before - sorted by name - and the chart now + prints a `WARNING` in `NOTES.txt` saying so, since sorting silently changes what they do. The map + form is not going away; it is simply the wrong form for these four files. The raw string form + (`profiles: |`) keeps the written order and is unaffected. +- A list of values for one option is now written the way the file's parser expects. Files read by + boost `program_options` - `rserver.conf`, `rsession.conf`, `launcher.*.conf`, `jupyter.conf`, + `vscode.conf`, `positron.conf` - repeat the key, one line per value, as the chart always did at + the top level of a file: + + ```yaml + config: + server: + rserver.conf: + www-allow-origin: [a.example.com, b.example.com] # www-allow-origin=a.example.com + # www-allow-origin=b.example.com + ``` + + Files read by boost `property_tree`, which rejects a repeated key - `profiles`, `launcher.conf`, + `logging.conf`, `repos.conf`, and the `launcher.*.resources.conf` and `launcher.*.profiles.conf` + files - comma-join them (`container-images=a,b`), as `config.profiles` always did. Either way the + same applies inside a `[section]`, where a list previously rendered as `key=[a b]`. - `config.session` files are now rendered according to their format rather than the shape of the value written. `r-versions` and `notifications.conf` are DCF (`Key: Value`, records separated by a blank line), `*.json` files are JSON, and everything else stays ini. Previously all of them @@ -28,45 +50,54 @@ logging `does not point to a valid directory` for each line. Resolves https://github.com/rstudio/helm/issues/948. A file written as a raw string is still passed through unchanged. + + **Your R version list may change on upgrade.** If you wrote `r-versions` as a list of records, + Workbench has been ignoring it and scanning for R on its own; it now reads your entries. Entries + whose `Path` no longer exists are logged and skipped. The same goes for `*.json` session files + written as a map, which Workbench has been ignoring as invalid JSON. - **BREAKING**: a file written as a list must give each section or entry its own `- `, holding a - single key. This rejects two shapes that previously rendered something unusable: several sections + single key. This rejects shapes that previously rendered something unusable: several sections crammed into one entry (a missing `- `), which rendered as `name=map[key:value]`; and a multi-field record, which was only ever used for `config.session.r-versions` and emitted `Key=Value` where - Workbench parses that file as DCF (`Key: Value`) - see - https://github.com/rstudio/helm/issues/948. Write `r-versions` as a string (`r-versions: |`), - which is passed through unchanged. An option inside a section must also be a single value, since - ini files have no nesting. + Workbench parses that file as DCF (`Key: Value`) - see https://github.com/rstudio/helm/issues/948. + Write `r-versions` as a string (`r-versions: |`), which is passed through unchanged. An entry with + no value (`- "*":` with nothing under it) also fails; it rendered as `*=`. +- **BREAKING**: a list entry holding several options in a file without sections, such as + `rsession.conf: [{session-timeout-minutes: 60, session-save-action-default: none}]`, now fails + where it used to render one line per option. Write the file as a map instead. +- **BREAKING**: an option's value must be a single value or a list of single values. ini files have + no nesting, so a map there (`limits: {cpu: 1}`) rendered as `limits=map[cpu:1]`; it now fails. +- `config.session.repos\.conf` now defaults to a list, so that its order is kept. A `repos.conf` + you write as a map still gets the chart's `CRAN` entry when it has none, as it did when the + default was a map. A list replaces the default, so it must name a `CRAN` entry itself - Workbench + ignores the whole file without one - and the chart now fails if it does not. A string without a + `CRAN=` line prints a `WARNING` instead. +- `config.server.rserver\.conf`, `launcher\.conf`, `launcher\.kubernetes\.conf`, and + `positron\.conf` written as a list now fail with a message saying to write them as a map. The + chart merges its own settings into these files, which a list silently dropped - `launcher\.conf` + lost the `[server]` section the launcher needs. `rserver\.conf` written as a string now fails with + the same message instead of a template type error. - Fixed: with more than one `config.pam` file, the pam `volumeMounts` were emitted in Go map order, so `helm template` was not reproducible and the Deployment's pod template changed between renders with no configuration change. They are now sorted by file name. -- **BREAKING**: the `config.session.repos\.conf` default is now a list, so supplying your own - replaces it instead of merging with it. Previously a map default merged with a map you supplied, - which quietly added the chart's CRAN entry to your repositories. Include a `CRAN` entry in your - own list - Workbench ignores the whole file without one. The chart now warns when it is missing. - `launcher.*.profiles.conf` no longer starts with a blank line. Profiles files now render through the same helper as every other ini file, which places the blank line between sections rather than before the first one. Nothing reads it - the file is parsed with an ini parser that skips blank lines - but it changes the rendered file, so the config checksum shifts and pods restart once on upgrade. -- The chart now warns when an order-agnostic ini file is written as a *list*. Helm merges a map - with the chart's defaults for a file but replaces them with a list, so the list form silently - drops any default the chart ships - `launcher\.conf` would lose its `[server]` section, which - the launcher needs. Write those files as a map unless you need to control section order. Files - that are not ini (`r-versions`, `notifications.conf`, `*.json`) are exempt, since a list is how - you legitimately write those. +- The chart now warns when any other ini file is written as a *list*. Helm merges a map with the + chart's defaults for a file but replaces them with a list, so the list form drops any default the + chart ships for that file. Write those files as a map unless you need to control section order. + Files that are not ini (`r-versions`, `notifications.conf`, `*.json`) are exempt, since a list is + how you legitimately write those. - `config.server` files are now rendered by format too, the same way `config.session` already is. A single table in the chart gives each configuration file its format, and both ConfigMaps read from it. `config.server.*.json` files written as a map are now rendered as JSON rather than ini. - The chart now warns when a configuration file it does not recognize is written as a map or a - list. Such a file is still rendered as ini, which is what the chart has always done, but ini is - a guess for a file the chart knows nothing about, and a wrong guess renders a file that looks - fine and is ignored by the product. Give the file's contents as text to render it exactly as - written. `Renviron.site` is recognized as ini and does not warn. -- **DEPRECATED**: writing those files as a map. A map is rendered in alphabetical order, which - silently changes what these files do, and nothing in `values.yaml` shows it. The map form still - works and now prints a `WARNING` in `NOTES.txt` once a file holds more than one section or entry. - It will be removed in a future chart release. The raw string form (`profiles: |`) keeps the - written order and is unaffected. + list. Such a file is still rendered as ini with repeated keys, which is what the chart has always + done, but ini is a guess for a file the chart knows nothing about, and a wrong guess renders a + file that looks fine and is ignored by the product. Give the file's contents as text to render it + exactly as written. `Renviron.site` is recognized and does not warn. ## 0.22.2 diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 8c346c2f..6d43976c 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -362,16 +362,22 @@ CRAN=https://packagemanager.posit.co/cran/__linux__/jammy/latest ``` :::{.callout-warning} -Writing `repos.conf` as a map is deprecated and will be removed in a future chart release. A map -does not keep the order you wrote it in: the chart renders map keys alphabetically, so an internal -repository can't be put ahead of CRAN. +A map does not keep the order you wrote it in: the chart renders map keys alphabetically, so an +internal repository can't be put ahead of CRAN, and the chart warns when `repos.conf` is a map. ::: :::{.callout-important} -Your `repos.conf` **replaces** the chart default rather than merging with it, and it must contain an -entry named `CRAN`. Workbench ignores the whole file when nothing is named `CRAN`, so the other -repositories are lost too - the only sign is `is missing CRAN entry` in the session log. The entry -does not have to be CRAN itself; point it at your own mirror if that is what sessions should use. +`repos.conf` must contain an entry named `CRAN`. Workbench ignores the whole file when nothing is +named `CRAN`, so the other repositories are lost too - the only sign is `is missing CRAN entry` in +the session log. The entry does not have to be CRAN itself; point it at your own mirror if that is +what sessions should use. + +- Written as a **list**, your `repos.conf` **replaces** the chart default, so it must name `CRAN` + itself. The chart fails if it does not. +- Written as a **map** with no `CRAN` entry, it gets the chart's default `CRAN` entry added, as it + always has. +- Written as a **string**, it is used as-is, and the chart warns if it has no `CRAN=` line. + To configure repositories somewhere else entirely, set `config.session.repos\.conf: null` and the chart renders no file. ::: @@ -480,8 +486,7 @@ The product reads configuration from top to bottom and "last-in-wins" for a give Because these files are read in order, write their sections as a list, putting `- ` in front of each section header. A map does not keep the order you wrote it in: the chart renders map keys alphabetically, so, for example, a user named `12345` would lose their own settings to every group -they belong to. Writing an order-sensitive file as a map is deprecated and will be removed in a -future chart release. +they belong to. The map form still renders, sorted, and the chart warns about it. This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and `launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and @@ -489,8 +494,10 @@ pre-selects the first one). Which form to use is decided by the file, not by preference: **an order-sensitive file wants a list, every other ini file wants a map.** Helm merges a map with the chart's defaults for a file -but replaces them with a list, so writing an order-agnostic file as a list silently drops whatever -the chart ships for it. The chart warns in both directions. Files that are not ini - `r-versions`, +but replaces them with a list, so writing an order-agnostic file as a list drops whatever the +chart ships for it. The chart warns in both directions, and fails outright for `rserver.conf`, +`launcher.conf`, `launcher.kubernetes.conf`, and `positron.conf` written as a list, since the chart +merges settings of its own into those. Files that are not ini - `r-versions`, `notifications.conf`, and `*.json` - are exempt, since a list is how you legitimately write those. Only the sections are ordered. The options written inside a section are still rendered diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index f1588047..b2b5b381 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -308,16 +308,22 @@ CRAN=https://packagemanager.posit.co/cran/__linux__/jammy/latest ``` :::{.callout-warning} -Writing `repos.conf` as a map is deprecated and will be removed in a future chart release. A map -does not keep the order you wrote it in: the chart renders map keys alphabetically, so an internal -repository can't be put ahead of CRAN. +A map does not keep the order you wrote it in: the chart renders map keys alphabetically, so an +internal repository can't be put ahead of CRAN, and the chart warns when `repos.conf` is a map. ::: :::{.callout-important} -Your `repos.conf` **replaces** the chart default rather than merging with it, and it must contain an -entry named `CRAN`. Workbench ignores the whole file when nothing is named `CRAN`, so the other -repositories are lost too - the only sign is `is missing CRAN entry` in the session log. The entry -does not have to be CRAN itself; point it at your own mirror if that is what sessions should use. +`repos.conf` must contain an entry named `CRAN`. Workbench ignores the whole file when nothing is +named `CRAN`, so the other repositories are lost too - the only sign is `is missing CRAN entry` in +the session log. The entry does not have to be CRAN itself; point it at your own mirror if that is +what sessions should use. + +- Written as a **list**, your `repos.conf` **replaces** the chart default, so it must name `CRAN` + itself. The chart fails if it does not. +- Written as a **map** with no `CRAN` entry, it gets the chart's default `CRAN` entry added, as it + always has. +- Written as a **string**, it is used as-is, and the chart warns if it has no `CRAN=` line. + To configure repositories somewhere else entirely, set `config.session.repos\.conf: null` and the chart renders no file. ::: @@ -426,8 +432,7 @@ The product reads configuration from top to bottom and "last-in-wins" for a give Because these files are read in order, write their sections as a list, putting `- ` in front of each section header. A map does not keep the order you wrote it in: the chart renders map keys alphabetically, so, for example, a user named `12345` would lose their own settings to every group -they belong to. Writing an order-sensitive file as a map is deprecated and will be removed in a -future chart release. +they belong to. The map form still renders, sorted, and the chart warns about it. This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and `launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and @@ -435,8 +440,10 @@ pre-selects the first one). Which form to use is decided by the file, not by preference: **an order-sensitive file wants a list, every other ini file wants a map.** Helm merges a map with the chart's defaults for a file -but replaces them with a list, so writing an order-agnostic file as a list silently drops whatever -the chart ships for it. The chart warns in both directions. Files that are not ini - `r-versions`, +but replaces them with a list, so writing an order-agnostic file as a list drops whatever the +chart ships for it. The chart warns in both directions, and fails outright for `rserver.conf`, +`launcher.conf`, `launcher.kubernetes.conf`, and `positron.conf` written as a list, since the chart +merges settings of its own into those. Files that are not ini - `r-versions`, `notifications.conf`, and `*.json` - are exempt, since a list is how you legitimately write those. Only the sections are ordered. The options written inside a section are still rendered From 2d5ec7e8e8a8efaff033b68a99fce02b2a161d28 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Wed, 30 Sep 2026 12:49:51 -0700 Subject: [PATCH 17/18] Route every launcher profiles file and pip.conf through the table, say what a string replaces, and clean up the remaining error paths --- charts/rstudio-workbench/NEWS.md | 32 +++- charts/rstudio-workbench/README.md | 33 +++- charts/rstudio-workbench/README.md.gotmpl | 33 +++- charts/rstudio-workbench/templates/NOTES.txt | 4 +- .../rstudio-workbench/templates/_helpers.tpl | 18 +- .../templates/configmap-general.yaml | 20 ++- .../templates/configmap-secret.yaml | 4 +- .../templates/configmap-session.yaml | 16 +- .../templates/configmap-startup.yaml | 4 +- .../tests/configmap_test.yaml | 160 ++++++++++++++++++ .../rstudio-workbench/tests/notes_test.yaml | 24 +++ 11 files changed, 306 insertions(+), 42 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index f17cd684..e9e32905 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -5,8 +5,8 @@ - Config files whose behavior depends on the order of their sections or entries can now be written as a list, putting `- ` in front of each section or entry, and are rendered in the order written. This covers `config.server.profiles`, `config.server.launcher\.*\.resources\.conf`, - `config.profiles.launcher\.*\.profiles\.conf` (and its deprecated `config.server` location), and - `config.session.repos\.conf`: + `config.profiles.launcher\.*\.profiles\.conf` (and the deprecated `config.server` location of + those files), and `config.session.repos\.conf`: ```yaml config: @@ -27,9 +27,9 @@ form is not going away; it is simply the wrong form for these four files. The raw string form (`profiles: |`) keeps the written order and is unaffected. - A list of values for one option is now written the way the file's parser expects. Files read by - boost `program_options` - `rserver.conf`, `rsession.conf`, `launcher.*.conf`, `jupyter.conf`, - `vscode.conf`, `positron.conf` - repeat the key, one line per value, as the chart always did at - the top level of a file: + boost `program_options` - `rserver.conf`, `rsession.conf`, the `launcher..conf` plugin + files, `jupyter.conf`, `vscode.conf`, `positron.conf` - repeat the key, one line per value, as + the chart always did at the top level of a file: ```yaml config: @@ -42,7 +42,17 @@ Files read by boost `property_tree`, which rejects a repeated key - `profiles`, `launcher.conf`, `logging.conf`, `repos.conf`, and the `launcher.*.resources.conf` and `launcher.*.profiles.conf` files - comma-join them (`container-images=a,b`), as `config.profiles` always did. Either way the - same applies inside a `[section]`, where a list previously rendered as `key=[a b]`. + same applies inside a `[section]`, where a list previously rendered as `key=[a b]`. An empty list + (`container-images: []`) renders nothing rather than `container-images=[]`. + + `config.session.pip\.conf` is the exception: pip is not Workbench, and it writes several values + as one value continued over indented lines, which the chart does not produce. A repeated key + makes pip refuse the whole file, so a list of values in `pip.conf` now fails the render with a + message saying to write the file as a string. It previously rendered `extra-index-url=[a b]`. + + `config.secret`, `config.sessionSecret`, `config.startupCustom` and `config.startupUserProvisioning` + keep repeating the key at the top level of a file, as before. `config.sssd.conf` comma-joins + (`domains=a,b`), which is sssd's own syntax; it previously rendered `domains=[a b]`. - `config.session` files are now rendered according to their format rather than the shape of the value written. `r-versions` and `notifications.conf` are DCF (`Key: Value`, records separated by a blank line), `*.json` files are JSON, and everything else stays ini. Previously all of them @@ -71,12 +81,18 @@ you write as a map still gets the chart's `CRAN` entry when it has none, as it did when the default was a map. A list replaces the default, so it must name a `CRAN` entry itself - Workbench ignores the whole file without one - and the chart now fails if it does not. A string without a - `CRAN=` line prints a `WARNING` instead. + `CRAN=` line prints a `WARNING` instead. With a map, Helm itself also logs `destination for + rstudio-workbench.config.session.repos.conf is a table. Ignoring non-table value (...)` on every + command: that is the chart's default list being set aside in favor of your map, it is harmless, + and it goes away once the file is written as a list. - `config.server.rserver\.conf`, `launcher\.conf`, `launcher\.kubernetes\.conf`, and `positron\.conf` written as a list now fail with a message saying to write them as a map. The chart merges its own settings into these files, which a list silently dropped - `launcher\.conf` lost the `[server]` section the launcher needs. `rserver\.conf` written as a string now fails with - the same message instead of a template type error. + the same message instead of a template type error. The other three still accept a string, which + is used as the whole file and so replaces those settings too (`kubernetes-namespace` and + `use-templating`, the rootless `secure-cookie-key-file`, the Positron `exe`); the message and + README now say so, where before they only said a string was accepted. - Fixed: with more than one `config.pam` file, the pam `volumeMounts` were emitted in Go map order, so `helm template` was not reproducible and the Deployment's pod template changed between renders with no configuration change. They are now sorted by file name. diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 6d43976c..e9a1882a 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -259,10 +259,17 @@ The files are converted into configuration files in the necessary format via go- ```yaml config: server: - rserver.conf: | + logging.conf: | verbatim-file=format ``` +A string is used as the whole file, so it also replaces whatever the chart would have added to that +file. That matters for the files the chart merges its own settings into: `launcher.conf` (the +`[server]` `secure-cookie-key-file` entry when `pod.runAsRoot` is `false`), `launcher.kubernetes.conf` +(`kubernetes-namespace` and `use-templating`) and `positron.conf` (`exe` when the Positron init +container is enabled) accept a string but you must include those settings yourself; `rserver.conf` +must be a map. `config.profiles` files must be a map or a list, not a string. + The names of files are dynamically used, so you can add new files as needed. Beware that some files have default values, so moving them can have adverse effects. Also, if you use a different mounting paradigm, you need to change the `XDG_CONFIG_DIRS` environment variable. @@ -312,7 +319,7 @@ the `XDG_CONFIG_DIRS` environment variable. - `supervisord` service / unit definition `.conf` files. - Use the `.ini` file format by default. - Mounted at:
`/startup/custom` - - As with all configuration files above, you can override with a verbatim string if desired: + - As with all configuration files above (other than `config.profiles`), you can override with a verbatim string if desired: - Located at:
`config.startupCustom.<< name of file >>` Helm values: ```yaml config: @@ -338,6 +345,22 @@ pip can be configured with `config.session.pip.conf`: trusted-host: packagemanager.posit.co ``` +`pip.conf` is read by pip, not by Workbench, and pip writes an option with several values (such as +`extra-index-url`) as one value continued over indented lines. The chart does not produce that +form, and a repeated key makes pip refuse the whole file, so a list of values in `pip.conf` fails +the render. Write the file as a string instead: + + ```yaml + config: + session: + pip.conf: | + [global] + index-url = https://packagemanager.posit.co/pypi/latest/simple + extra-index-url = + https://pkgs.example.com/internal/simple + https://pkgs.example.com/other/simple + ``` + #### R repositories R package repositories can be configured with `config.session.repos.conf`. R reads the file in @@ -488,9 +511,9 @@ each section header. A map does not keep the order you wrote it in: the chart re alphabetically, so, for example, a user named `12345` would lose their own settings to every group they belong to. The map form still renders, sorted, and the chart warns about it. -This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and -`launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and -pre-selects the first one). +This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf` (under `config.profiles`, or +the deprecated `config.server` location), and `launcher.*.resources.conf` (where the session +launcher lists resource profiles in file order and pre-selects the first one). Which form to use is decided by the file, not by preference: **an order-sensitive file wants a list, every other ini file wants a map.** Helm merges a map with the chart's defaults for a file diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index b2b5b381..ef9b3481 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -205,10 +205,17 @@ The files are converted into configuration files in the necessary format via go- ```yaml config: server: - rserver.conf: | + logging.conf: | verbatim-file=format ``` +A string is used as the whole file, so it also replaces whatever the chart would have added to that +file. That matters for the files the chart merges its own settings into: `launcher.conf` (the +`[server]` `secure-cookie-key-file` entry when `pod.runAsRoot` is `false`), `launcher.kubernetes.conf` +(`kubernetes-namespace` and `use-templating`) and `positron.conf` (`exe` when the Positron init +container is enabled) accept a string but you must include those settings yourself; `rserver.conf` +must be a map. `config.profiles` files must be a map or a list, not a string. + The names of files are dynamically used, so you can add new files as needed. Beware that some files have default values, so moving them can have adverse effects. Also, if you use a different mounting paradigm, you need to change the `XDG_CONFIG_DIRS` environment variable. @@ -258,7 +265,7 @@ the `XDG_CONFIG_DIRS` environment variable. - `supervisord` service / unit definition `.conf` files. - Use the `.ini` file format by default. - Mounted at:
`/startup/custom` - - As with all configuration files above, you can override with a verbatim string if desired: + - As with all configuration files above (other than `config.profiles`), you can override with a verbatim string if desired: - Located at:
`config.startupCustom.<< name of file >>` Helm values: ```yaml config: @@ -284,6 +291,22 @@ pip can be configured with `config.session.pip.conf`: trusted-host: packagemanager.posit.co ``` +`pip.conf` is read by pip, not by Workbench, and pip writes an option with several values (such as +`extra-index-url`) as one value continued over indented lines. The chart does not produce that +form, and a repeated key makes pip refuse the whole file, so a list of values in `pip.conf` fails +the render. Write the file as a string instead: + + ```yaml + config: + session: + pip.conf: | + [global] + index-url = https://packagemanager.posit.co/pypi/latest/simple + extra-index-url = + https://pkgs.example.com/internal/simple + https://pkgs.example.com/other/simple + ``` + #### R repositories R package repositories can be configured with `config.session.repos.conf`. R reads the file in @@ -434,9 +457,9 @@ each section header. A map does not keep the order you wrote it in: the chart re alphabetically, so, for example, a user named `12345` would lose their own settings to every group they belong to. The map form still renders, sorted, and the chart warns about it. -This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf`, and -`launcher.*.resources.conf` (where the session launcher lists resource profiles in file order and -pre-selects the first one). +This applies to `/etc/rstudio/profiles`, `launcher.*.profiles.conf` (under `config.profiles`, or +the deprecated `config.server` location), and `launcher.*.resources.conf` (where the session +launcher lists resource profiles in file order and pre-selects the first one). Which form to use is decided by the file, not by preference: **an order-sensitive file wants a list, every other ini file wants a map.** Helm merges a map with the chart's defaults for a file diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index 5ecb4326..62adc34b 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -34,7 +34,7 @@ kubectl -n {{ $.Release.Namespace }} get secret {{ include "rstudio-workbench.fu {{- $wantsMap := list }} {{- $assumedIni := list }} {{- range $scope := (list "server" "session" "profiles") }} - {{- range $file, $contents := (get $.Values.config $scope) }} + {{- range $file, $contents := (get $.Values.config $scope | default dict) }} {{- $path := printf "config.%s.%s" $scope ($file | replace "." "\\.") }} {{- $format := include "rstudio-workbench.config.fileFormat" (dict "scope" $scope "file" $file) }} {{- $ordered := include "rstudio-workbench.config.fileOrdered" (dict "scope" $scope "file" $file) }} @@ -146,7 +146,7 @@ Please consider removing this configuration value. {{- end }} {{- if and .Values.launcher.useTemplates .Values.launcher.enabled }} - {{- if hasKey .Values.config.profiles "launcher.kubernetes.profiles.conf" }} + {{- if hasKey (default (dict) .Values.config.profiles) "launcher.kubernetes.profiles.conf" }} {{- $normalized := dict }} {{- include "rstudio-library.config.entries" (dict "data" (get .Values.config.profiles "launcher.kubernetes.profiles.conf") "result" $normalized) }} {{- range $entry := $normalized.entries }} diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index 8bb65ae4..e65a3d15 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -745,6 +745,10 @@ https://packagemanager.posit.co/cran/__linux__/jammy/latest server-add-header) gcfg Go gcfg, as ini with repeated keys renviron R's Renviron, as ini with repeated keys + pip Python configparser (pip.conf is not read by Workbench at all). Several + values are written as a newline-continued value, which this chart does + not produce, and a repeated key makes pip refuse the whole file, so a + list of values fails with "write the file as a string" dcf Key: Value, records separated by a blank line json JSON ordered yes when the file's behavior depends on the order of its sections or entries, so it @@ -758,14 +762,16 @@ https://packagemanager.posit.co/cran/__linux__/jammy/latest Consumers: configmap-general.yaml and configmap-session.yaml pick the renderer, NOTES.txt raises the form warnings. Add a file here and all of them follow. Not every scope goes through - here: config.secret, config.sessionSecret, config.sssd.conf and config.startupCustom call - rstudio-library.config.ini directly, comma-joining lists and getting no NOTES warnings. + here, and none of these get NOTES warnings: config.secret, config.sessionSecret, + config.startupCustom and config.startupUserProvisioning render as ini with repeated keys (what + the chart always did for them), and config.sssd.conf as ini with comma-joined lists (sssd's own + syntax for several values). */ -}} {{- define "rstudio-workbench.config.fileTable" -}} server | ^profiles$ | ini_ptree | yes server | ^launcher\..+\.resources\.conf$ | ini_ptree | yes profiles | ^launcher\..+\.profiles\.conf$ | ini_ptree | yes -server | ^launcher\.kubernetes\.profiles\.conf$ | ini_ptree | yes +server | ^launcher\..+\.profiles\.conf$ | ini_ptree | yes * | ^repos\.conf$ | ini_ptree | yes * | ^launcher\.conf$ | ini_ptree | no * | ^logging\.conf$ | ini_ptree | no @@ -774,6 +780,7 @@ server | ^launcher\.kubernetes\.profiles\.conf$ | ini_ptree | yes * | \.json$ | json | no * | ^chronicle-local\.gcfg$ | gcfg | no * | ^Renviron\.site$ | renviron | no +session | ^pip\.conf$ | pip | no * | \.conf$ | ini_popt | no {{- end -}} @@ -839,10 +846,11 @@ server | ^launcher\.kubernetes\.profiles\.conf$ | ini_ptree | yes {{- end }} {{- end }} {{- /* One file at a time, in the sorted order rstudio-library.config.ini would use, so that each - gets its own way of writing several values. */ -}} + gets its own way of writing several values: ini_ptree comma-joins, pip refuses, everything + else (ini_popt, gcfg, renviron, unknown) repeats the key. */ -}} {{- range $file := keys $ini | sortAlpha }} {{- $format := include "rstudio-workbench.config.fileFormat" (dict "scope" $scope "file" $file) }} - {{- $multi := eq $format "ini_ptree" | ternary "join" "repeat" }} + {{- $multi := get (dict "ini_ptree" "join" "pip" "reject") $format | default "repeat" }} {{- include "rstudio-library.config.ini.files" (dict "files" (dict $file (get $ini $file)) "multi" $multi) }} {{- end }} {{- if $dcf }}{{- include "rstudio-library.config.dcf" $dcf }}{{- end }} diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml index 7d4719fd..1e6a8b77 100644 --- a/charts/rstudio-workbench/templates/configmap-general.yaml +++ b/charts/rstudio-workbench/templates/configmap-general.yaml @@ -1,13 +1,18 @@ {{- /* The chart adds its own settings to these files by merging a map over what was written. rserver.conf must be a map for that merge to work at all. The rest may be a string, which - is taken as the whole file, but a list would silently drop the chart's settings - which - for launcher.conf includes the [server] section the launcher needs to start. Nothing in - them depends on order, so there is no reason to write them as a list. */}} + is taken as the whole file - and so replaces the chart's settings too, which the message + says. A list would silently drop them, which for launcher.conf includes the [server] + section the launcher needs to start. Nothing in them depends on order, so there is no + reason to write them as a list. */}} +{{- $stringReplaces := dict + "launcher.conf" "the [server] secure-cookie-key-file setting the chart adds when pod.runAsRoot is false" + "launcher.kubernetes.conf" "the kubernetes-namespace and use-templating settings the chart adds" + "positron.conf" "the exe setting the chart adds when the Positron init container is enabled" }} {{- range $file := list "rserver.conf" "launcher.conf" "launcher.kubernetes.conf" "positron.conf" }} {{- $contents := get $.Values.config.server $file }} {{- $allowed := eq $file "rserver.conf" | ternary (list "map") (list "map" "string") }} {{- if and $contents (not (has (kindOf $contents) $allowed)) }} - {{- $orString := eq $file "rserver.conf" | ternary "" "\nA string is also accepted, and is used as the whole file.\n" }} + {{- $orString := eq $file "rserver.conf" | ternary "" (print "\nA string is also accepted and is used as the whole file, so it replaces\n" (get $stringReplaces $file) "; include those yourself if you still want them.\n") }} {{- fail (print "\n\nconfig.server." $file " is a " (kindOf $contents) ", but must be a map of options.\n\nThe chart merges its own settings into " $file ", which only works on a map.\nNothing in the file depends on order, so write it as a map:\n\n config:\n server:\n " $file ":\n option-name: value\n" $orString) }} {{- end }} {{- end }} @@ -164,7 +169,7 @@ data: {{- end }} {{ include "rstudio-workbench.config.files" (dict "scope" "server" "data" $overrideDict) | indent 2 }} {{/* helper variables to make things here a bit more sane */}} -{{- $profilesConfig := .Values.config.profiles | deepCopy }} +{{- $profilesConfig := default (dict) .Values.config.profiles | deepCopy }} {{- /* Apply the default [*] session image settings to launcher.kubernetes.profiles.conf. When it is written as an ordered list, mergeOverwrite would drop the defaults along with the map, so merge them into the [*] entry in place instead (prepending one if the admin wrote none). */}} @@ -172,8 +177,9 @@ data: {{- if kindIs "slice" $writtenProfiles }} {{- $hasEveryone := false }} {{- range $item := $writtenProfiles }} - {{- /* anything but a map is left for rstudio-library.config.entries to reject */}} - {{- if and (kindIs "map" $item) (hasKey $item "*") }} + {{- /* anything but a map, and a [*] whose body is not a map, is left for the profiles helper + to reject with its own message */}} + {{- if and (kindIs "map" $item) (hasKey $item "*") (kindIs "map" (get $item "*")) }} {{- $hasEveryone = true }} {{- $_ := set $item "*" (mergeOverwrite (deepCopy $defaultProfiles) (get $item "*")) }} {{- end }} diff --git a/charts/rstudio-workbench/templates/configmap-secret.yaml b/charts/rstudio-workbench/templates/configmap-secret.yaml index bdadab08..30b7e6e9 100644 --- a/charts/rstudio-workbench/templates/configmap-secret.yaml +++ b/charts/rstudio-workbench/templates/configmap-secret.yaml @@ -14,7 +14,7 @@ metadata: namespace: {{ $.Release.Namespace }} spec: encryptedData: - {{- include "rstudio-library.config.ini" .Values.config.secret | nindent 4 }} + {{- include "rstudio-library.config.ini.files" (dict "files" .Values.config.secret "multi" "repeat") | nindent 4 }} {{- /* do not auto-generate value as the secret will not be encrypted */}} {{- if .Values.launcherPem.existingSecret }} launcher.pem: | @@ -53,7 +53,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-secret namespace: {{ $.Release.Namespace }} stringData: - {{- include "rstudio-library.config.ini" .Values.config.secret | nindent 2 }} + {{- include "rstudio-library.config.ini.files" (dict "files" .Values.config.secret "multi" "repeat") | nindent 2 }} {{- if not .Values.launcherPem.existingSecret }} launcher.pem: | {{- include "rstudio-workbench.launcherPem" . | nindent 4 }} diff --git a/charts/rstudio-workbench/templates/configmap-session.yaml b/charts/rstudio-workbench/templates/configmap-session.yaml index f9a4fa4a..ec503675 100644 --- a/charts/rstudio-workbench/templates/configmap-session.yaml +++ b/charts/rstudio-workbench/templates/configmap-session.yaml @@ -14,12 +14,16 @@ {{- $_ := set $repos "CRAN" (include "rstudio-workbench.config.defaultCran" .) }} {{- else if and $repos (kindIs "slice" $repos) }} {{- $hasCran := false }} + {{- $allMaps := true }} {{- range $item := $repos }} - {{- if and (kindIs "map" $item) (hasKey $item "CRAN") }} + {{- if not (kindIs "map" $item) }} + {{- $allMaps = false }} + {{- else if hasKey $item "CRAN" }} {{- $hasCran = true }} {{- end }} {{- end }} - {{- if not $hasCran }} + {{- /* an entry that is not a map is the renderer's error to report; its message is the useful one */}} + {{- if and $allMaps (not $hasCran) }} {{- fail "\n\nconfig.session.repos.conf has no entry named CRAN.\n\nWorkbench ignores the whole file without one, so none of these repositories would be\nused. Written as a list, repos.conf replaces the chart's default CRAN entry, so it must\nname its own. It does not have to be CRAN itself - name your own mirror CRAN if that\nis what sessions should use:\n\n config:\n session:\n repos.conf:\n - CRAN: https://mirror.example.com/cran\n - Internal: https://pkgs.example.com/internal\n\nTo configure repositories somewhere else instead, set config.session.repos.conf: null\nand the chart will not render the file at all.\n" }} {{- end }} {{- end }} @@ -44,7 +48,7 @@ metadata: spec: encryptedData: data: - {{- include "rstudio-library.config.ini" .Values.config.sessionSecret | nindent 6 }} + {{- include "rstudio-library.config.ini.files" (dict "files" .Values.config.sessionSecret "multi" "repeat") | nindent 6 }} {{- else }} apiVersion: v1 kind: Secret @@ -52,7 +56,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-session-secret namespace: {{ $.Release.Namespace }} stringData: - {{- include "rstudio-library.config.ini" .Values.config.sessionSecret | nindent 2 }} + {{- include "rstudio-library.config.ini.files" (dict "files" .Values.config.sessionSecret "multi" "repeat") | nindent 2 }} {{- end }} {{- end }} @@ -86,7 +90,7 @@ metadata: spec: encryptedData: data: - {{- include "rstudio-library.config.ini" .Values.config.sessionSecret | nindent 6 }} + {{- include "rstudio-library.config.ini.files" (dict "files" .Values.config.sessionSecret "multi" "repeat") | nindent 6 }} {{- else }} apiVersion: v1 kind: Secret @@ -94,7 +98,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-session-secret namespace: {{ $targetNamespace }} stringData: - {{- include "rstudio-library.config.ini" .Values.config.sessionSecret | nindent 2 }} + {{- include "rstudio-library.config.ini.files" (dict "files" .Values.config.sessionSecret "multi" "repeat") | nindent 2 }} {{- end }} {{- end }} {{- end }} diff --git a/charts/rstudio-workbench/templates/configmap-startup.yaml b/charts/rstudio-workbench/templates/configmap-startup.yaml index 46833595..9c87ad3f 100644 --- a/charts/rstudio-workbench/templates/configmap-startup.yaml +++ b/charts/rstudio-workbench/templates/configmap-startup.yaml @@ -43,7 +43,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-start-user namespace: {{ $.Release.Namespace }} data: - {{- include "rstudio-library.config.ini" $userStartup | nindent 2 }} + {{- include "rstudio-library.config.ini.files" (dict "files" $userStartup "multi" "repeat") | nindent 2 }} {{- end }} {{- if .Values.config.startupCustom }} --- @@ -53,5 +53,5 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-start-custom namespace: {{ $.Release.Namespace }} data: - {{- include "rstudio-library.config.ini" .Values.config.startupCustom | nindent 2}} + {{- include "rstudio-library.config.ini.files" (dict "files" .Values.config.startupCustom "multi" "repeat") | nindent 2}} {{- end }} diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index dfb2e1a9..29a9556b 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -930,3 +930,163 @@ tests: [*] container-images=a,b max-cpus=1 + + # -- Every launcher..profiles.conf is the same file format, in either scope + - it: should render a non-kubernetes launcher profiles file in config.server like config.profiles + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + launcher.slurm.profiles.conf: + "*": + max-cpus: 1 + some-list: [a, b] + asserts: + - equal: + path: data["launcher.slurm.profiles.conf"] + value: | + [*] + max-cpus=1 + some-list=a,b + + # -- A string for a file the chart merges settings into replaces those settings. Pinned so the + # message and README, which say so, stay true. + - it: should let a launcher.kubernetes.conf string replace the chart's namespace setting + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + launcher.kubernetes.conf: | + use-templating=0 + asserts: + - equal: + path: data["launcher.kubernetes.conf"] + value: | + use-templating=0 + - notMatchRegex: + path: data["launcher.kubernetes.conf"] + pattern: "kubernetes-namespace" + + - it: should say what a string replaces when launcher.kubernetes.conf is a list + template: configmap-general.yaml + set: + config: + server: + launcher.kubernetes.conf: + - kubernetes-namespace: other + asserts: + - failedTemplate: + errorPattern: "A string is also accepted and is used as the whole file, so it replaces" + - failedTemplate: + errorPattern: "kubernetes-namespace and use-templating" + + - it: should not offer a string for rserver.conf + template: configmap-general.yaml + set: + config: + server: + rserver.conf: + - www-port: 8787 + asserts: + - failedTemplate: + errorPattern: "config.server.rserver.conf is a slice, but must be a map of options" + + # -- pip.conf is not a Workbench file; a repeated key makes pip refuse it + - it: should fail a list of values in pip.conf + template: configmap-session.yaml + set: + config: + session: + pip.conf: + global: + extra-index-url: [https://b/simple, https://c/simple] + asserts: + - failedTemplate: + errorPattern: "'extra-index-url' in section \\[global\\] of 'pip.conf' is a list of values" + - failedTemplate: + errorPattern: "Write the whole file as a string \\(pip.conf: \\|\\)" + + - it: should report a non-map everyone section in an ordered profiles file with the profiles message + template: configmap-general.yaml + set: + launcher: + useTemplates: false + config: + profiles: + launcher.kubernetes.profiles.conf: + - "*": oops + asserts: + - failedTemplate: + errorPattern: "\\[\\*\\] section must be a 'map' of config values. Instead got 'string' : 'oops'" + + - it: should render the default profiles when config.profiles is null + template: configmap-general.yaml + documentIndex: 0 + set: + config: + profiles: null + asserts: + - matchRegex: + path: data["launcher.kubernetes.profiles.conf"] + pattern: "(?m)^\\[\\*\\]$" + + - it: should report a bare scalar in a repos.conf list as a shape error, not a missing CRAN + template: configmap-session.yaml + set: + config: + session: + repos.conf: + - https://packagemanager.posit.co/cran/latest + asserts: + - failedTemplate: + errorPattern: "entry 1 of 'repos.conf' must be a map of a name and its value" + + - it: should fail a DCF list entry that is not a map + template: configmap-session.yaml + set: + config: + session: + r-versions: + - /opt/R/4.4.1 + asserts: + - failedTemplate: + errorPattern: "entry 1 of 'r-versions' must be a map of fields" + + - it: should render nothing for an empty list of values + template: configmap-general.yaml + documentIndex: 0 + set: + config: + server: + profiles: + - "*": + container-images: [] + max-cpus: 1 + rserver.conf: + www-allow-origin: [] + asserts: + - equal: + path: data["profiles"] + value: | + [*] + max-cpus=1 + - notMatchRegex: + path: data["rserver.conf"] + pattern: "www-allow-origin" + + # -- The scopes that do not go through the file table keep main's repeated keys, except sssd + - it: should repeat the key for a list of values in config.secret, as before + template: configmap-secret.yaml + documentIndex: 0 + set: + config: + secret: + database.conf: + provider: postgresql + some-multi: [a, b] + asserts: + - matchRegex: + path: stringData["database.conf"] + pattern: "(?m)^some-multi=a\\nsome-multi=b$" diff --git a/charts/rstudio-workbench/tests/notes_test.yaml b/charts/rstudio-workbench/tests/notes_test.yaml index 1eed1490..80aa7a56 100644 --- a/charts/rstudio-workbench/tests/notes_test.yaml +++ b/charts/rstudio-workbench/tests/notes_test.yaml @@ -357,3 +357,27 @@ tests: asserts: - notMatchRegexRaw: pattern: "does not recognize the following" + + - it: should warn about the map form for a non-kubernetes launcher profiles file in config.server + set: + config: + server: + launcher.slurm.profiles.conf: + "*": + max-cpus: 1 + asserts: + - matchRegexRaw: + pattern: "written as maps(.|\\n)*config\\.server\\.launcher\\\\.slurm\\\\.profiles\\\\.conf" + - notMatchRegexRaw: + pattern: "written as lists" + + - it: should not warn about pip.conf written as a map + set: + config: + session: + pip.conf: + global: + index-url: https://a/simple + asserts: + - notMatchRegexRaw: + pattern: "WARNING: the (following configuration files|chart does not recognize)" From 0d8b644294f884cd3386d1736516315a3636a935 Mon Sep 17 00:00:00 2001 From: Kyle Husmann Date: Wed, 30 Sep 2026 12:58:35 -0700 Subject: [PATCH 18/18] Drop the pam mount ordering fix, which lands separately in #955 --- charts/rstudio-workbench/NEWS.md | 3 --- .../rstudio-workbench/templates/_helpers.tpl | 3 +-- .../tests/deployment_test.yaml | 25 ------------------- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index e9e32905..da34224a 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -93,9 +93,6 @@ is used as the whole file and so replaces those settings too (`kubernetes-namespace` and `use-templating`, the rootless `secure-cookie-key-file`, the Positron `exe`); the message and README now say so, where before they only said a string was accepted. -- Fixed: with more than one `config.pam` file, the pam `volumeMounts` were emitted in Go map order, - so `helm template` was not reproducible and the Deployment's pod template changed between renders - with no configuration change. They are now sorted by file name. - `launcher.*.profiles.conf` no longer starts with a blank line. Profiles files now render through the same helper as every other ini file, which places the blank line between sections rather than before the first one. Nothing reads it - the file is parsed with an ini parser that skips blank diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index e65a3d15..ed4905f7 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -210,8 +210,7 @@ containers: mountPath: "/startup/custom" {{- end }} {{- if .Values.config.pam }} - {{- /* sortAlpha: sprig keys returns Go map order, which varies between renders */}} - {{- range $i, $pamFileName := keys .Values.config.pam | sortAlpha }} + {{- range $i, $pamFileName := keys .Values.config.pam }} - name: rstudio-pam mountPath: "/etc/pam.d/{{ $pamFileName }}" subPath: "{{ $pamFileName }}" diff --git a/charts/rstudio-workbench/tests/deployment_test.yaml b/charts/rstudio-workbench/tests/deployment_test.yaml index 806533dc..e354a099 100644 --- a/charts/rstudio-workbench/tests/deployment_test.yaml +++ b/charts/rstudio-workbench/tests/deployment_test.yaml @@ -501,31 +501,6 @@ tests: - equal: path: 'spec.template.spec.volumes[?(@.name=="rstudio-pam")].configMap.defaultMode' value: 0600 - - it: should mount several pam files in a stable order - template: deployment.yaml - set: - config: - pam: - zzz.conf: - dsn: "test" - aaa.conf: - dsn: "test" - mmm.conf: - dsn: "test" - asserts: - # sprig `keys` returns Go map order, which varies between renders unless sorted. - # Asserted by index because these have to come out in a stable order; if the - # surrounding volumeMounts change, update the offsets. - - equal: - path: 'spec.template.spec.containers[0].volumeMounts[8].subPath' - value: "aaa.conf" - - equal: - path: 'spec.template.spec.containers[0].volumeMounts[9].subPath' - value: "mmm.conf" - - equal: - path: 'spec.template.spec.containers[0].volumeMounts[10].subPath' - value: "zzz.conf" - - it: should not specify a volumeMount and a volume for pam if config.pam is not defined template: deployment.yaml set: