From e202340646445363e668508881efa80172a67c61 Mon Sep 17 00:00:00 2001 From: Lucas Rodriguez Date: Tue, 10 Mar 2026 17:04:51 -0500 Subject: [PATCH] Add declarative execution environment management to rstudio-connect chart --- charts/rstudio-connect/Chart.yaml | 2 +- charts/rstudio-connect/NEWS.md | 4 + charts/rstudio-connect/README.md | 42 ++- charts/rstudio-connect/README.md.gotmpl | 35 +++ .../lint/execution-environments-values.yaml | 12 + charts/rstudio-connect/templates/_helpers.tpl | 5 + .../configmap-execution-environments.yaml | 18 ++ .../rstudio-connect/templates/deployment.yaml | 10 + .../tests/test-execution-environments.yaml | 53 ++++ .../tests/execution_environments_test.yaml | 272 ++++++++++++++++++ charts/rstudio-connect/values.yaml | 29 ++ 11 files changed, 478 insertions(+), 4 deletions(-) create mode 100644 charts/rstudio-connect/lint/execution-environments-values.yaml create mode 100644 charts/rstudio-connect/templates/configmap-execution-environments.yaml create mode 100644 charts/rstudio-connect/templates/tests/test-execution-environments.yaml create mode 100644 charts/rstudio-connect/tests/execution_environments_test.yaml diff --git a/charts/rstudio-connect/Chart.yaml b/charts/rstudio-connect/Chart.yaml index 030b17df6..c7d0ff5b1 100644 --- a/charts/rstudio-connect/Chart.yaml +++ b/charts/rstudio-connect/Chart.yaml @@ -1,6 +1,6 @@ name: rstudio-connect description: Official Helm chart for Posit Connect -version: 0.8.30 +version: 0.8.31 apiVersion: v2 appVersion: 2026.02.0 icon: https://raw.githubusercontent.com/rstudio/helm/main/images/posit-icon-fullcolor.svg diff --git a/charts/rstudio-connect/NEWS.md b/charts/rstudio-connect/NEWS.md index e6b294469..6b4bf2021 100644 --- a/charts/rstudio-connect/NEWS.md +++ b/charts/rstudio-connect/NEWS.md @@ -1,5 +1,9 @@ # Changelog +## 0.8.31 + +- Add `executionEnvironments` value for declarative management of execution environments. Unlike `launcher.customRuntimeYaml`, changes take effect on every `helm upgrade` without requiring a pod restart or database reset. + ## 0.8.30 - Fix connect-version-check in CI diff --git a/charts/rstudio-connect/README.md b/charts/rstudio-connect/README.md index 3e59b379a..443476953 100644 --- a/charts/rstudio-connect/README.md +++ b/charts/rstudio-connect/README.md @@ -1,6 +1,6 @@ # Posit Connect -![Version: 0.8.30](https://img.shields.io/badge/Version-0.8.30-informational?style=flat-square) ![AppVersion: 2026.02.0](https://img.shields.io/badge/AppVersion-2026.02.0-informational?style=flat-square) +![Version: 0.8.31](https://img.shields.io/badge/Version-0.8.31-informational?style=flat-square) ![AppVersion: 2026.02.0](https://img.shields.io/badge/AppVersion-2026.02.0-informational?style=flat-square) #### _Official Helm chart for Posit Connect_ @@ -30,11 +30,11 @@ To ensure reproducibility in your environment and insulate yourself from future ## Installing the chart -To install the chart with the release name `my-release` at version 0.8.30: +To install the chart with the release name `my-release` at version 0.8.31: ```{.bash} helm repo add rstudio https://helm.rstudio.com -helm upgrade --install my-release rstudio/rstudio-connect --version=0.8.30 +helm upgrade --install my-release rstudio/rstudio-connect --version=0.8.31 ``` To explore other chart versions, look at: @@ -191,6 +191,41 @@ the API key unset for the Chronicle agent, deploy the chart, create an administr secret with the API key. Once the secret is created, the value of `chronicleAgent.connectApiKey.secretKeyRef` can be set and the release can be upgraded to include the new value. +## Execution Environments + +This chart supports declarative management of execution environments via +`ExecutionEnvironments.ConfigFilePath`. Unlike the legacy `launcher.customRuntimeYaml`, +changes to `executionEnvironments` take effect on every `helm upgrade` without requiring +a pod restart or database reset. + +When `executionEnvironments` is set, the chart: + +1. Renders the list into a dedicated ConfigMap. +2. Mounts the ConfigMap as a volume at `/etc/rstudio-connect/execution-environments/environments.yaml`. +3. Sets `ExecutionEnvironments.ConfigFilePath` in the Connect configuration. + +The ConfigMap is deliberately not included in the pod's checksum annotations, so changes +do not trigger a pod restart. The kubelet updates the mounted file automatically when the +ConfigMap changes (typically within 60-120 seconds), and Connect detects the update via +file polling. + +Example `values.yaml`: + +```yaml +executionEnvironments: + - name: ghcr.io/my-org/connect-runtime:ubuntu22 + title: "Default Runtime" + matching: any + python: + installations: + - version: "3.11.3" + path: /opt/python/3.11.3/bin/python3 + r: + installations: + - version: "4.4.0" + path: /opt/R/4.4.0/bin/R +``` + ## General principles - In most places, we opt to pass Helm values over configmaps. We translate these into the valid `.gcfg` file format @@ -234,6 +269,7 @@ The Helm `config` values are converted into the `rstudio-connect.gcfg` service c | chronicleAgent.volumeMounts | list | `[]` | Verbatim volumeMounts to attach to the Chronicle agent container | | command | list | `[]` | The pod's run command. By default, it uses the container's default | | config | object | [Posit Connect Configuration Reference](https://docs.posit.co/connect/admin/appendix/off-host/helm-reference/) | A nested map of maps that generates the rstudio-connect.gcfg file | +| executionEnvironments | list | `[]` (disabled) | Optional list of execution environments to manage declaratively. When set, the chart renders these into a ConfigMap, mounts it into the Connect pod, and sets ExecutionEnvironments.ConfigFilePath in the Connect configuration. Unlike launcher.customRuntimeYaml, changes take effect on every helm upgrade without requiring a pod restart or database reset. | | extraObjects | list | `[]` | Extra objects to deploy (value evaluated as a template) | | fullnameOverride | string | `""` | The full name of the release (can be overridden) | | image | object | `{"imagePullPolicy":"IfNotPresent","imagePullSecrets":[],"repository":"ghcr.io/rstudio/rstudio-connect","tag":"","tagPrefix":"ubuntu2204-"}` | Defines the Posit Connect image to deploy | diff --git a/charts/rstudio-connect/README.md.gotmpl b/charts/rstudio-connect/README.md.gotmpl index 7486a5db3..aa04dbae7 100644 --- a/charts/rstudio-connect/README.md.gotmpl +++ b/charts/rstudio-connect/README.md.gotmpl @@ -131,6 +131,41 @@ the API key unset for the Chronicle agent, deploy the chart, create an administr secret with the API key. Once the secret is created, the value of `chronicleAgent.connectApiKey.secretKeyRef` can be set and the release can be upgraded to include the new value. +## Execution Environments + +This chart supports declarative management of execution environments via +`ExecutionEnvironments.ConfigFilePath`. Unlike the legacy `launcher.customRuntimeYaml`, +changes to `executionEnvironments` take effect on every `helm upgrade` without requiring +a pod restart or database reset. + +When `executionEnvironments` is set, the chart: + +1. Renders the list into a dedicated ConfigMap. +2. Mounts the ConfigMap as a volume at `/etc/rstudio-connect/execution-environments/environments.yaml`. +3. Sets `ExecutionEnvironments.ConfigFilePath` in the Connect configuration. + +The ConfigMap is deliberately not included in the pod's checksum annotations, so changes +do not trigger a pod restart. The kubelet updates the mounted file automatically when the +ConfigMap changes (typically within 60-120 seconds), and Connect detects the update via +file polling. + +Example `values.yaml`: + +```yaml +executionEnvironments: + - name: ghcr.io/my-org/connect-runtime:ubuntu22 + title: "Default Runtime" + matching: any + python: + installations: + - version: "3.11.3" + path: /opt/python/3.11.3/bin/python3 + r: + installations: + - version: "4.4.0" + path: /opt/R/4.4.0/bin/R +``` + ## General principles - In most places, we opt to pass Helm values over configmaps. We translate these into the valid `.gcfg` file format diff --git a/charts/rstudio-connect/lint/execution-environments-values.yaml b/charts/rstudio-connect/lint/execution-environments-values.yaml new file mode 100644 index 000000000..f43e8257b --- /dev/null +++ b/charts/rstudio-connect/lint/execution-environments-values.yaml @@ -0,0 +1,12 @@ +executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + python: + installations: + - version: "3.11.3" + path: /opt/python/3.11.3/bin/python3 + r: + installations: + - version: "4.4.0" + path: /opt/R/4.4.0/bin/R diff --git a/charts/rstudio-connect/templates/_helpers.tpl b/charts/rstudio-connect/templates/_helpers.tpl index aadfa96ad..bf70b020a 100644 --- a/charts/rstudio-connect/templates/_helpers.tpl +++ b/charts/rstudio-connect/templates/_helpers.tpl @@ -76,6 +76,11 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{- $launcherDict := dict "Launcher" ( $launcherSettingsDict ) }} {{- $defaultConfig = merge $defaultConfig $launcherDict }} {{- end }} + {{- /* declarative execution environments configuration */}} + {{- if .Values.executionEnvironments }} + {{- $eeDict := dict "ExecutionEnvironments" (dict "ConfigFilePath" "/etc/rstudio-connect/execution-environments/environments.yaml") }} + {{- $defaultConfig = merge $defaultConfig $eeDict }} + {{- end }} {{- /* default licensing configuration */}} {{- if .Values.license.server }} {{- $licenseDict := dict "Licensing" ( dict "LicenseType" ("Remote") ) }} diff --git a/charts/rstudio-connect/templates/configmap-execution-environments.yaml b/charts/rstudio-connect/templates/configmap-execution-environments.yaml new file mode 100644 index 000000000..ef74d4efa --- /dev/null +++ b/charts/rstudio-connect/templates/configmap-execution-environments.yaml @@ -0,0 +1,18 @@ +--- +{{- if .Values.executionEnvironments }} +{{- range $i, $env := .Values.executionEnvironments }} +{{- if not $env.name }} +{{- fail (printf "executionEnvironments[%d].name is required" $i) }} +{{- end }} +{{- end }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "rstudio-connect.fullname" . }}-execution-environments + namespace: {{ $.Release.Namespace }} + labels: + {{- include "rstudio-connect.labels" . | nindent 4 }} +data: + environments.yaml: | + {{- toYaml .Values.executionEnvironments | nindent 4 }} +{{- end }} diff --git a/charts/rstudio-connect/templates/deployment.yaml b/charts/rstudio-connect/templates/deployment.yaml index b94acbdab..6d7a3ef91 100644 --- a/charts/rstudio-connect/templates/deployment.yaml +++ b/charts/rstudio-connect/templates/deployment.yaml @@ -225,6 +225,11 @@ spec: subPath: "libnss_connect.conf" readOnly: true {{- end }} + {{- if .Values.executionEnvironments }} + - name: execution-environments + mountPath: {{ dir (default "/etc/rstudio-connect/execution-environments/environments.yaml" (dig "ExecutionEnvironments" "ConfigFilePath" "" .Values.config)) | quote }} + readOnly: true + {{- end }} {{- if .Values.pod.volumeMounts }} {{- toYaml .Values.pod.volumeMounts | nindent 10 }} {{- end }} @@ -304,6 +309,11 @@ spec: - key: libnss_connect.conf path: libnss_connect.conf {{- end }} + {{- if .Values.executionEnvironments }} + - name: execution-environments + configMap: + name: {{ include "rstudio-connect.fullname" . }}-execution-environments + {{- end }} {{- if .Values.launcher.enabled }} {{- if .Values.launcher.useTemplates }} - name: rstudio-connect-templates diff --git a/charts/rstudio-connect/templates/tests/test-execution-environments.yaml b/charts/rstudio-connect/templates/tests/test-execution-environments.yaml new file mode 100644 index 000000000..54205625f --- /dev/null +++ b/charts/rstudio-connect/templates/tests/test-execution-environments.yaml @@ -0,0 +1,53 @@ +{{- if .Values.executionEnvironments }} +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "rstudio-connect.fullname" . }}-test-execution-environments + namespace: {{ $.Release.Namespace }} + labels: + {{- include "rstudio-connect.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test + "helm.sh/hook-delete-policy": before-hook-creation +spec: + restartPolicy: Never + activeDeadlineSeconds: 300 + containers: + - name: test-execution-environments + image: alpine:3 + command: ["sh", "-c"] + args: + - | + apk add --no-cache curl jq + + CONNECT_URL="http://{{ include "rstudio-connect.fullname" . }}:{{ .Values.service.port }}" + API_KEY=$(printf "admin" | md5sum | cut -d' ' -f1) + + # Wait for Connect to be ready + echo "Waiting for Connect to be ready..." + MAX_RETRIES=60 + RETRY=0 + until curl -sf "$CONNECT_URL/__ping__"; do + RETRY=$((RETRY + 1)) + if [ "$RETRY" -ge "$MAX_RETRIES" ]; then + echo "FAIL: Connect not ready after $MAX_RETRIES attempts" + exit 1 + fi + sleep 5 + done + echo "Connect is ready." + + # Check that at least one execution environment exists with managed_by: config + echo "Checking for managed execution environments..." + response=$(curl -sf -H "Authorization: Key $API_KEY" "$CONNECT_URL/v1/environments") + echo "$response" | jq . + + managed_count=$(echo "$response" | jq '[.results[] | select(.managed_by == "config")] | length') + if [ "$managed_count" -gt 0 ]; then + echo "PASS: Found $managed_count execution environment(s) with managed_by: config" + exit 0 + else + echo "FAIL: No execution environment found with managed_by: config" + exit 1 + fi +{{- end }} diff --git a/charts/rstudio-connect/tests/execution_environments_test.yaml b/charts/rstudio-connect/tests/execution_environments_test.yaml new file mode 100644 index 000000000..b32cd475c --- /dev/null +++ b/charts/rstudio-connect/tests/execution_environments_test.yaml @@ -0,0 +1,272 @@ +suite: Connect Execution Environments +templates: + - configmap-execution-environments.yaml + - deployment.yaml + - configmap.yaml + - configmap-prestart.yaml +tests: + # ============================================================================= + # configmap-execution-environments.yaml + # ============================================================================= + + - it: should not create ConfigMap when executionEnvironments is empty (default) + template: configmap-execution-environments.yaml + asserts: + - hasDocuments: + count: 0 + + - it: should create ConfigMap with correct content when executionEnvironments is set + template: configmap-execution-environments.yaml + set: + executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + python: + installations: + - version: "3.11.3" + path: /opt/python/3.11.3/bin/python3 + r: + installations: + - version: "4.4.0" + path: /opt/R/4.4.0/bin/R + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: RELEASE-NAME-rstudio-connect-execution-environments + - equal: + path: metadata.labels["app.kubernetes.io/name"] + value: rstudio-connect + - equal: + path: metadata.labels["app.kubernetes.io/instance"] + value: RELEASE-NAME + - exists: + path: data["environments.yaml"] + + # ============================================================================= + # deployment.yaml - volumeMount + # ============================================================================= + + - it: should not have execution-environments volumeMount when executionEnvironments is empty + template: deployment.yaml + set: + launcher: + enabled: true + sharedStorage: + create: true + mount: true + asserts: + - notContains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: execution-environments + mountPath: "/etc/rstudio-connect/execution-environments" + readOnly: true + + - it: should have execution-environments volumeMount when executionEnvironments is set + template: deployment.yaml + set: + launcher: + enabled: true + sharedStorage: + create: true + mount: true + executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: execution-environments + mountPath: "/etc/rstudio-connect/execution-environments" + readOnly: true + + # ============================================================================= + # deployment.yaml - volume + # ============================================================================= + + - it: should not have execution-environments volume when executionEnvironments is empty + template: deployment.yaml + set: + launcher: + enabled: true + sharedStorage: + create: true + mount: true + asserts: + - notContains: + path: spec.template.spec.volumes + content: + name: execution-environments + configMap: + name: RELEASE-NAME-rstudio-connect-execution-environments + + - it: should have execution-environments volume when executionEnvironments is set + template: deployment.yaml + set: + launcher: + enabled: true + sharedStorage: + create: true + mount: true + executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: execution-environments + configMap: + name: RELEASE-NAME-rstudio-connect-execution-environments + + # ============================================================================= + # configmap.yaml - gcfg config injection + # ============================================================================= + + - it: should not include ExecutionEnvironments in gcfg when executionEnvironments is empty + template: configmap.yaml + documentIndex: 0 + asserts: + - notMatchRegex: + path: data["rstudio-connect.gcfg"] + pattern: "ExecutionEnvironments" + + - it: should include ExecutionEnvironments.ConfigFilePath in gcfg when executionEnvironments is set + template: configmap.yaml + documentIndex: 0 + set: + launcher: + enabled: false + executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + asserts: + - matchRegex: + path: data["rstudio-connect.gcfg"] + pattern: "\\[ExecutionEnvironments\\]" + - matchRegex: + path: data["rstudio-connect.gcfg"] + pattern: "ConfigFilePath = /etc/rstudio-connect/execution-environments/environments.yaml" + + # ============================================================================= + # deployment.yaml - no checksum annotation for execution-environments ConfigMap + # ============================================================================= + + - it: should not have a checksum annotation for execution-environments ConfigMap + template: deployment.yaml + set: + launcher: + enabled: true + sharedStorage: + create: true + mount: true + executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + asserts: + - notExists: + path: spec.template.metadata.annotations["checksum/config-execution-environments"] + + # ============================================================================= + # gcfg injection works alongside launcher configuration + # ============================================================================= + + - it: should include ExecutionEnvironments.ConfigFilePath alongside Launcher config when both are enabled + template: configmap.yaml + documentIndex: 0 + set: + launcher: + enabled: true + customRuntimeYaml: "pro" + sharedStorage: + create: true + mount: true + executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + asserts: + - matchRegex: + path: data["rstudio-connect.gcfg"] + pattern: "\\[ExecutionEnvironments\\]" + - matchRegex: + path: data["rstudio-connect.gcfg"] + pattern: "ConfigFilePath = /etc/rstudio-connect/execution-environments/environments.yaml" + - matchRegex: + path: data["rstudio-connect.gcfg"] + pattern: "\\[Launcher\\]" + + # ============================================================================= + # User can override ExecutionEnvironments.ConfigFilePath via config + # ============================================================================= + + - it: should allow user to override ConfigFilePath via config + template: configmap.yaml + documentIndex: 0 + set: + launcher: + enabled: false + executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + config: + ExecutionEnvironments: + ConfigFilePath: /custom/path/environments.yaml + asserts: + - matchRegex: + path: data["rstudio-connect.gcfg"] + pattern: "ConfigFilePath = /custom/path/environments.yaml" + + - it: should mount at custom directory when ConfigFilePath is overridden + template: deployment.yaml + set: + launcher: + enabled: true + sharedStorage: + create: true + mount: true + executionEnvironments: + - name: ghcr.io/rstudio/content-base:r4.4.0-py3.11.3-jammy + title: "Test Runtime" + matching: any + config: + ExecutionEnvironments: + ConfigFilePath: /custom/path/environments.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: execution-environments + mountPath: "/custom/path" + readOnly: true + - notContains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: execution-environments + mountPath: "/etc/rstudio-connect/execution-environments" + readOnly: true + + # ============================================================================= + # Validation + # ============================================================================= + + - it: should fail when an execution environment is missing a name + template: configmap-execution-environments.yaml + set: + executionEnvironments: + - title: "Missing Name" + matching: any + asserts: + - failedTemplate: + errorMessage: "executionEnvironments[0].name is required" diff --git a/charts/rstudio-connect/values.yaml b/charts/rstudio-connect/values.yaml index 26846ed34..7d71fa971 100644 --- a/charts/rstudio-connect/values.yaml +++ b/charts/rstudio-connect/values.yaml @@ -404,6 +404,35 @@ launcher: # -- The securityContext for the default initContainer securityContext: {} +# -- Optional list of execution environments to manage declaratively. +# When set, the chart renders these into a ConfigMap, mounts it into the Connect pod, +# and sets ExecutionEnvironments.ConfigFilePath in the Connect configuration. +# Unlike launcher.customRuntimeYaml, changes take effect on every helm upgrade +# without requiring a pod restart or database reset. +# @default -- `[]` (disabled) +executionEnvironments: [] + # - name: ghcr.io/my-org/connect-runtime:ubuntu22 + # title: "Default Runtime" + # description: "Runtime with R and Python" + # matching: any + # supervisor: "" + # python: + # installations: + # - version: "3.11.3" + # path: /opt/python/3.11.3/bin/python3 + # r: + # installations: + # - version: "4.4.0" + # path: /opt/R/4.4.0/bin/R + # quarto: + # installations: + # - version: "1.4.557" + # path: /opt/quarto/1.4.557/bin/quarto + # tensorflow: + # installations: + # - version: "2.17.1" + # path: /usr/bin/tensorflow_model_server + # -- Nameservice configuration for current user execution (RunAsCurrentUser). # This can only be enabled if using an SSO authentication provider (OAuth2, SAML, or LDAP). nameservice: