From cc5ad9173bc9b04eeb8d8caca4780cda054def63 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 14 Sep 2026 12:47:17 +0100 Subject: [PATCH 01/23] Creating workload templates Signed-off-by: Leanne Ahern --- charts/rhdh/templates/deployment.yaml | 447 +---------------- .../workload/_backstage-pod-template.tpl | 449 ++++++++++++++++++ 2 files changed, 450 insertions(+), 446 deletions(-) create mode 100644 charts/rhdh/templates/workload/_backstage-pod-template.tpl diff --git a/charts/rhdh/templates/deployment.yaml b/charts/rhdh/templates/deployment.yaml index b24c9373..0c797ab8 100644 --- a/charts/rhdh/templates/deployment.yaml +++ b/charts/rhdh/templates/deployment.yaml @@ -1,6 +1,3 @@ -{{- $installDir := "/opt/app-root/src" -}} -{{- $intelligentAssistant := include "rhdh.intelligentAssistant" . | fromYaml -}} -{{- $extraCatalogImages := include "rhdh.catalogIndex.extraImagesEnvValue" . | trim -}} apiVersion: apps/v1 kind: Deployment metadata: @@ -29,446 +26,4 @@ spec: matchLabels: {{- include "rhdh.selectorLabels" . | nindent 6 }} template: - metadata: - labels: - {{- include "rhdh.labels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - annotations: - checksum/app-config: {{ include "common.tplvalues.render" (dict "value" .Values.appConfig "context" $) | sha256sum }} - checksum/dynamic-plugins: {{ include "common.tplvalues.render" (dict "value" (dict "dynamicPlugins" .Values.dynamicPlugins "intelligentAssistant" (dict "enabled" $intelligentAssistant.enabled "plugins" $intelligentAssistant.plugins)) "context" $) | sha256sum }} - {{- if $intelligentAssistant.enabled }} - checksum/lightspeed-config: {{ toJson $intelligentAssistant.config | sha256sum }} - {{- end }} - {{- with .Values.podAnnotations }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - spec: - serviceAccountName: {{ include "rhdh.serviceAccountName" . }} - {{- include "rhdh.imagePullSecrets" . | nindent 6 }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - {{- with .Values.topologySpreadConstraints }} - topologySpreadConstraints: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - {{- with .Values.hostAliases }} - hostAliases: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - volumes: - # --- System volumes (hardcoded, never replaced) --- - - name: dynamic-plugins-root - {{- if eq .Values.dynamicPlugins.volume.type "emptyDir" }} - emptyDir: - {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.volume.emptyDir "context" $) | nindent 12 }} - {{- else if eq .Values.dynamicPlugins.volume.type "pvc" }} - persistentVolumeClaim: - {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.volume.pvc "context" $) | nindent 12 }} - {{- else }} - ephemeral: - volumeClaimTemplate: - spec: - {{- $persistence := dict "storageClass" (.Values.dynamicPlugins.volume.ephemeral.storageClassName | default "") }} - {{- $sc := include "common.storage.class" (dict "persistence" $persistence "global" .Values.global) }} - {{- if $sc }} - {{ $sc }} - {{- end }} - {{- with .Values.dynamicPlugins.volume.ephemeral.accessModes }} - accessModes: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 18 }} - {{- end }} - {{- with .Values.dynamicPlugins.volume.ephemeral.resources }} - resources: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 18 }} - {{- end }} - {{- end }} - - name: dynamic-plugins - configMap: - defaultMode: 420 - name: {{ printf "%s-dynamic-plugins" (include "rhdh.fullname" .) }} - optional: true - - name: dynamic-plugins-npmrc - secret: - defaultMode: 420 - optional: true - secretName: {{ printf "%s-dynamic-plugins-npmrc" (include "rhdh.fullname" .) }} - - name: dynamic-plugins-registry-auth - secret: - defaultMode: 416 - optional: true - secretName: {{ printf "%s-dynamic-plugins-registry-auth" (include "rhdh.fullname" .) }} - - name: npmcacache - emptyDir: {} - - name: extensions-catalog - emptyDir: {} - - name: temp - emptyDir: {} - {{- if .Values.appConfig }} - - name: backstage-app-config - configMap: - name: {{ include "rhdh.fullname" . }}-app-config - {{- end }} - {{- range .Values.extraAppConfig }} - - name: {{ .configMapRef }} - configMap: - name: {{ .configMapRef }} - {{- end }} - {{- if $intelligentAssistant.enabled }} - - name: lightspeed-data - {{- if eq $intelligentAssistant.runtimeVolume.type "persistentVolumeClaim" }} - persistentVolumeClaim: - {{- include "common.tplvalues.render" (dict "value" $intelligentAssistant.runtimeVolume.persistentVolumeClaim "context" $) | nindent 12 }} - {{- else }} - emptyDir: - {{- include "common.tplvalues.render" (dict "value" $intelligentAssistant.runtimeVolume.emptyDir "context" $) | nindent 12 }} - {{- end }} - {{- range $key := list "stack" "profile" }} - {{- $entry := index $intelligentAssistant.config $key }} - {{- $cmKey := include "rhdh.intelligentAssistant.configMapKey" (dict "key" $key "entry" $entry) }} - - name: {{ printf "lightspeed-config-%s" $key }} - configMap: - name: {{ include "rhdh.intelligentAssistant.configMapName" (dict "root" $ "key" $key "entry" $entry) }} - items: - - key: {{ $cmKey | quote }} - path: {{ $cmKey | quote }} - {{- end }} - {{- end }} - # --- User-additional volumes (appended) --- - {{- with .Values.extraVolumes }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - initContainers: - # --- User pre-init containers (run before system init containers) --- - {{- with .Values.preInitContainers }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - # --- System init containers (hardcoded) --- - - name: install-dynamic-plugins - image: {{ include "rhdh.image" . }} - imagePullPolicy: {{ .Values.image.pullPolicy | quote }} - {{- with (.Values.dynamicPlugins.initContainer.securityContext | default .Values.containerSecurityContext) }} - securityContext: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- if .Values.dynamicPlugins.initContainer.commandOverride }} - command: - {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.initContainer.commandOverride "context" $) | nindent 12 }} - {{- else }} - command: - - ./install-dynamic-plugins.sh - - /dynamic-plugins-root - {{- end }} - {{- if .Values.dynamicPlugins.initContainer.argsOverride }} - args: - {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.initContainer.argsOverride "context" $) | nindent 12 }} - {{- else if .Values.dynamicPlugins.initContainer.extraArgs }} - args: - {{- range .Values.dynamicPlugins.initContainer.extraArgs }} - - {{ . | quote }} - {{- end }} - {{- end }} - env: - - name: NPM_CONFIG_USERCONFIG - value: /opt/app-root/src/.npmrc.dynamic-plugins - - name: MAX_ENTRY_SIZE - value: {{ .Values.dynamicPlugins.maxEntrySize | int | quote }} - - name: CATALOG_INDEX_IMAGE - value: {{ include "rhdh.image.render" (dict "image" .Values.catalogIndex.image "global" .Values.global) | quote }} - - name: CATALOG_ENTITIES_EXTRACT_DIR - value: /extensions - {{- if $extraCatalogImages }} - - name: EXTRA_CATALOG_INDEX_IMAGES - value: {{ $extraCatalogImages | quote }} - {{- end }} - {{- with .Values.dynamicPlugins.initContainer.extraEnv }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- with .Values.dynamicPlugins.initContainer.resources }} - resources: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - volumeMounts: - - mountPath: /dynamic-plugins-root - name: dynamic-plugins-root - - mountPath: /opt/app-root/src/dynamic-plugins.yaml - name: dynamic-plugins - readOnly: true - subPath: dynamic-plugins.yaml - - mountPath: /opt/app-root/src/.npmrc.dynamic-plugins - name: dynamic-plugins-npmrc - readOnly: true - subPath: .npmrc - - mountPath: /opt/app-root/src/.config/containers - name: dynamic-plugins-registry-auth - readOnly: true - - mountPath: /opt/app-root/src/.npm/_cacache - name: npmcacache - - name: extensions-catalog - mountPath: /extensions - - name: temp - mountPath: /tmp - {{- with .Values.dynamicPlugins.initContainer.extraVolumeMounts }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - workingDir: /opt/app-root/src - {{- if or .Values.postgresql.enabled .Values.externalDatabase.host }} - - name: wait-for-db - image: {{ include "rhdh.image.render" (dict "image" .Values.postgresql.image "global" .Values.global) | quote }} - imagePullPolicy: {{ .Values.postgresql.image.pullPolicy | default "IfNotPresent" | quote }} - securityContext: - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - runAsNonRoot: true - capabilities: - drop: - - ALL - resources: - limits: - cpu: "100m" - memory: "64Mi" - requests: - cpu: "50m" - memory: "32Mi" - command: - - bash - - -c - - | - dbHost={{ include "rhdh.postgresql.host" . | quote }} - dbPort={{ .Values.externalDatabase.port | default 5432 | quote }} - echo "Waiting for DB at $dbHost:$dbPort..." - until timeout 2 bash -c ">/dev/tcp/$dbHost/$dbPort" 2>/dev/null; do - sleep 2 - done - echo "DB is reachable!" - {{- end }} - # --- User-additional init containers (appended) --- - {{- with .Values.extraInitContainers }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} - containers: - - name: backstage-backend - image: {{ include "rhdh.image" . }} - imagePullPolicy: {{ .Values.image.pullPolicy | quote }} - {{- with .Values.containerSecurityContext }} - securityContext: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- if .Values.commandOverride }} - command: - {{- include "common.tplvalues.render" (dict "value" .Values.commandOverride "context" $) | nindent 12 }} - {{- end }} - args: - {{- if .Values.argsOverride }} - {{- range .Values.argsOverride }} - - {{ . | quote }} - {{- end }} - {{- else }} - - "--config" - - "{{ $installDir }}/dynamic-plugins-root/app-config.dynamic-plugins.yaml" - {{- if .Values.appConfig }} - - "--config" - - "{{ $installDir }}/app-config-from-configmap.yaml" - {{- end }} - {{- range .Values.extraAppConfig }} - - "--config" - - "{{ $installDir }}/{{ .filename }}" - {{- end }} - {{- range .Values.extraArgs }} - - {{ . | quote }} - {{- end }} - {{- end }} - {{- with .Values.resources }} - resources: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- with .Values.startupProbe }} - startupProbe: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- with .Values.readinessProbe }} - readinessProbe: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- with .Values.livenessProbe }} - livenessProbe: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- if or .Values.envFromOverride .Values.extraEnvFrom }} - envFrom: - {{- if .Values.envFromOverride }} - {{- include "common.tplvalues.render" (dict "value" .Values.envFromOverride "context" $) | nindent 12 }} - {{- else }} - {{- include "common.tplvalues.render" (dict "value" .Values.extraEnvFrom "context" $) | nindent 12 }} - {{- end }} - {{- end }} - env: - {{- if .Values.envOverride }} - {{- include "common.tplvalues.render" (dict "value" .Values.envOverride "context" $) | nindent 12 }} - {{- else }} - # --- System env vars (hardcoded) --- - - name: APP_CONFIG_backend_listen_port - value: {{ .Values.service.port | quote }} - {{- if .Values.auth.backend.enabled }} - - name: BACKEND_SECRET - valueFrom: - secretKeyRef: - name: {{ include "rhdh.backend-secret-name" . }} - key: {{ include "rhdh.backend-secret-key" . }} - {{- end }} - {{- if .Values.postgresql.enabled }} - - name: POSTGRES_HOST - value: {{ include "rhdh.postgresql.host" . }} - - name: POSTGRES_PORT - value: "5432" - - name: POSTGRES_USER - value: {{ .Values.postgresql.auth.username | default "postgres" }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ include "rhdh.postgresql.secretName" . }} - key: {{ include "rhdh.postgresql.adminPasswordKey" . }} - {{- else if .Values.externalDatabase.host }} - - name: POSTGRES_HOST - value: {{ .Values.externalDatabase.host | quote }} - - name: POSTGRES_PORT - value: {{ .Values.externalDatabase.port | quote }} - - name: POSTGRES_USER - value: {{ .Values.externalDatabase.user | default "postgres" | quote }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ required "externalDatabase.existingSecretRef.name is required when externalDatabase.host is set" .Values.externalDatabase.existingSecretRef.name }} - key: {{ .Values.externalDatabase.existingSecretRef.key | default "password" }} - {{- end }} - # --- User-additional env vars (appended) --- - {{- with .Values.extraEnv }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- end }} - ports: - - name: backend - containerPort: {{ .Values.service.port }} - protocol: TCP - volumeMounts: - # --- System volume mounts (hardcoded) --- - - mountPath: {{ $installDir }}/dynamic-plugins-root - name: dynamic-plugins-root - - name: extensions-catalog - mountPath: /extensions - - name: temp - mountPath: /tmp - {{- if .Values.appConfig }} - - name: backstage-app-config - mountPath: "{{ $installDir }}/app-config-from-configmap.yaml" - subPath: app-config.yaml - {{- end }} - {{- range .Values.extraAppConfig }} - - name: {{ .configMapRef }} - mountPath: "{{ $installDir }}/{{ .filename }}" - subPath: {{ .filename }} - {{- end }} - # --- User-additional volume mounts (appended) --- - {{- with .Values.extraVolumeMounts }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- if $intelligentAssistant.enabled }} - - name: lightspeed-core - image: {{ include "rhdh.image.render" (dict "image" $intelligentAssistant.core.image "global" .Values.global) | quote }} - imagePullPolicy: {{ $intelligentAssistant.core.imagePullPolicy | quote }} - {{- with $intelligentAssistant.core.securityContext }} - securityContext: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- if $intelligentAssistant.core.commandOverride }} - command: - {{- include "common.tplvalues.render" (dict "value" $intelligentAssistant.core.commandOverride "context" $) | nindent 12 }} - {{- else if and (include "rhdh.intelligentAssistant.okp.active" $) (not $intelligentAssistant.core.argsOverride) }} - command: ["/bin/sh", "-c"] - {{- end }} - {{- if $intelligentAssistant.core.argsOverride }} - args: - {{- include "common.tplvalues.render" (dict "value" $intelligentAssistant.core.argsOverride "context" $) | nindent 12 }} - {{- else if and (include "rhdh.intelligentAssistant.okp.active" $) (not $intelligentAssistant.core.commandOverride) }} - args: - - | - if [ -f /var/run/secrets/kubernetes.io/serviceaccount/ca.crt ]; then - cat /etc/pki/tls/certs/ca-bundle.crt /var/run/secrets/kubernetes.io/serviceaccount/ca.crt > /tmp/combined-ca-bundle.crt - export SSL_CERT_FILE=/tmp/combined-ca-bundle.crt - export REQUESTS_CA_BUNDLE=/tmp/combined-ca-bundle.crt - fi - exec /app-root/entrypoint.sh --synthesized-config-output /tmp/.generated/run.yaml{{ range $intelligentAssistant.core.extraArgs }} {{ . | quote }}{{ end }} - {{- else }} - args: - - "--synthesized-config-output" - - "/tmp/.generated/run.yaml" - {{- range $intelligentAssistant.core.extraArgs }} - - {{ . | quote }} - {{- end }} - {{- end }} - ports: - - name: http-lightspeed - containerPort: 8080 - protocol: TCP - {{- if $intelligentAssistant.existingSecret }} - envFrom: - - secretRef: - name: {{ $intelligentAssistant.existingSecret }} - {{- end }} - env: - {{- if include "rhdh.intelligentAssistant.okp.active" $ }} - - name: OKP_SERVICE_URL - value: {{ include "rhdh.intelligentAssistant.okp.serviceUrl" $ | quote }} - {{- end }} - - name: KV_STORE_PATH - value: "/tmp/kvstore.db" - - name: SQL_STORE_PATH - value: "/tmp/sql_store.db" - - name: SQLITE_STORE_DIR - value: "/tmp/llama-stack-files" - - name: HF_HOME - value: "/tmp/hf_cache" - - name: OTEL_SDK_DISABLED - value: "true" - {{- with $intelligentAssistant.core.extraEnv }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- with $intelligentAssistant.core.resources }} - resources: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - volumeMounts: - - name: lightspeed-data - mountPath: "/tmp" - {{- range $key := list "stack" "profile" }} - {{- $entry := index $intelligentAssistant.config $key }} - {{- $file := include "rhdh.intelligentAssistant.configFile" $key }} - {{- $cmKey := include "rhdh.intelligentAssistant.configMapKey" (dict "key" $key "entry" $entry) }} - - name: {{ printf "lightspeed-config-%s" $key }} - mountPath: {{ printf "/app-root/%s" $file | quote }} - subPath: {{ $cmKey | quote }} - readOnly: true - {{- end }} - {{- with $intelligentAssistant.core.extraVolumeMounts }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 12 }} - {{- end }} - {{- end }} - # --- User-additional sidecar containers (appended) --- - {{- with .Values.extraContainers }} - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} - {{- end }} + {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} \ No newline at end of file diff --git a/charts/rhdh/templates/workload/_backstage-pod-template.tpl b/charts/rhdh/templates/workload/_backstage-pod-template.tpl new file mode 100644 index 00000000..97631e2f --- /dev/null +++ b/charts/rhdh/templates/workload/_backstage-pod-template.tpl @@ -0,0 +1,449 @@ +{{- define "rhdh.backstagePodTemplate" -}} +{{- $root := . -}} +{{- $installDir := "/opt/app-root/src" -}} +{{- $intelligentAssistant := include "rhdh.intelligentAssistant" $root | fromYaml -}} +{{- $extraCatalogImages := include "rhdh.catalogIndex.extraImagesEnvValue" $root | trim -}} +metadata: + labels: + {{- include "rhdh.labels" . | nindent 4 }} + {{- with .Values.podLabels }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + annotations: + checksum/app-config: {{ include "common.tplvalues.render" (dict "value" .Values.appConfig "context" $) | sha256sum }} + checksum/dynamic-plugins: {{ include "common.tplvalues.render" (dict "value" (dict "dynamicPlugins" .Values.dynamicPlugins "intelligentAssistant" (dict "enabled" $intelligentAssistant.enabled "plugins" $intelligentAssistant.plugins)) "context" $) | sha256sum }} + {{- if $intelligentAssistant.enabled }} + checksum/lightspeed-config: {{ toJson $intelligentAssistant.config | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} +spec: + serviceAccountName: {{ include "rhdh.serviceAccountName" . }} + {{- include "rhdh.imagePullSecrets" . | nindent 2 }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- with .Values.hostAliases }} + hostAliases: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + volumes: + # --- System volumes (hardcoded, never replaced) --- + - name: dynamic-plugins-root + {{- if eq .Values.dynamicPlugins.volume.type "emptyDir" }} + emptyDir: + {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.volume.emptyDir "context" $) | nindent 8 }} + {{- else if eq .Values.dynamicPlugins.volume.type "pvc" }} + persistentVolumeClaim: + {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.volume.pvc "context" $) | nindent 8 }} + {{- else }} + ephemeral: + volumeClaimTemplate: + spec: + {{- $persistence := dict "storageClass" (.Values.dynamicPlugins.volume.ephemeral.storageClassName | default "") }} + {{- $sc := include "common.storage.class" (dict "persistence" $persistence "global" .Values.global) }} + {{- if $sc }} + {{ $sc }} + {{- end }} + {{- with .Values.dynamicPlugins.volume.ephemeral.accessModes }} + accessModes: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 14 }} + {{- end }} + {{- with .Values.dynamicPlugins.volume.ephemeral.resources }} + resources: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 14 }} + {{- end }} + {{- end }} + - name: dynamic-plugins + configMap: + defaultMode: 420 + name: {{ printf "%s-dynamic-plugins" (include "rhdh.fullname" .) }} + optional: true + - name: dynamic-plugins-npmrc + secret: + defaultMode: 420 + optional: true + secretName: {{ printf "%s-dynamic-plugins-npmrc" (include "rhdh.fullname" .) }} + - name: dynamic-plugins-registry-auth + secret: + defaultMode: 416 + optional: true + secretName: {{ printf "%s-dynamic-plugins-registry-auth" (include "rhdh.fullname" .) }} + - name: npmcacache + emptyDir: {} + - name: extensions-catalog + emptyDir: {} + - name: temp + emptyDir: {} + {{- if .Values.appConfig }} + - name: backstage-app-config + configMap: + name: {{ include "rhdh.fullname" . }}-app-config + {{- end }} + {{- range .Values.extraAppConfig }} + - name: {{ .configMapRef }} + configMap: + name: {{ .configMapRef }} + {{- end }} + {{- if $intelligentAssistant.enabled }} + - name: lightspeed-data + {{- if eq $intelligentAssistant.runtimeVolume.type "persistentVolumeClaim" }} + persistentVolumeClaim: + {{- include "common.tplvalues.render" (dict "value" $intelligentAssistant.runtimeVolume.persistentVolumeClaim "context" $) | nindent 8 }} + {{- else }} + emptyDir: + {{- include "common.tplvalues.render" (dict "value" $intelligentAssistant.runtimeVolume.emptyDir "context" $) | nindent 8 }} + {{- end }} + {{- range $key := list "stack" "profile" }} + {{- $entry := index $intelligentAssistant.config $key }} + {{- $cmKey := include "rhdh.intelligentAssistant.configMapKey" (dict "key" $key "entry" $entry) }} + - name: {{ printf "lightspeed-config-%s" $key }} + configMap: + name: {{ include "rhdh.intelligentAssistant.configMapName" (dict "root" $ "key" $key "entry" $entry) }} + items: + - key: {{ $cmKey | quote }} + path: {{ $cmKey | quote }} + {{- end }} + {{- end }} + # --- User-additional volumes (appended) --- + {{- with .Values.extraVolumes }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + initContainers: + # --- User pre-init containers (run before system init containers) --- + {{- with .Values.preInitContainers }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + # --- System init containers (hardcoded) --- + - name: install-dynamic-plugins + image: {{ include "rhdh.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + {{- with (.Values.dynamicPlugins.initContainer.securityContext | default .Values.containerSecurityContext) }} + securityContext: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.dynamicPlugins.initContainer.commandOverride }} + command: + {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.initContainer.commandOverride "context" $) | nindent 8 }} + {{- else }} + command: + - ./install-dynamic-plugins.sh + - /dynamic-plugins-root + {{- end }} + {{- if .Values.dynamicPlugins.initContainer.argsOverride }} + args: + {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.initContainer.argsOverride "context" $) | nindent 8 }} + {{- else if .Values.dynamicPlugins.initContainer.extraArgs }} + args: + {{- range .Values.dynamicPlugins.initContainer.extraArgs }} + - {{ . | quote }} + {{- end }} + {{- end }} + env: + - name: NPM_CONFIG_USERCONFIG + value: /opt/app-root/src/.npmrc.dynamic-plugins + - name: MAX_ENTRY_SIZE + value: {{ .Values.dynamicPlugins.maxEntrySize | int | quote }} + - name: CATALOG_INDEX_IMAGE + value: {{ include "rhdh.image.render" (dict "image" .Values.catalogIndex.image "global" .Values.global) | quote }} + - name: CATALOG_ENTITIES_EXTRACT_DIR + value: /extensions + {{- if $extraCatalogImages }} + - name: EXTRA_CATALOG_INDEX_IMAGES + value: {{ $extraCatalogImages | quote }} + {{- end }} + {{- with .Values.dynamicPlugins.initContainer.extraEnv }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- with .Values.dynamicPlugins.initContainer.resources }} + resources: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + volumeMounts: + - mountPath: /dynamic-plugins-root + name: dynamic-plugins-root + - mountPath: /opt/app-root/src/dynamic-plugins.yaml + name: dynamic-plugins + readOnly: true + subPath: dynamic-plugins.yaml + - mountPath: /opt/app-root/src/.npmrc.dynamic-plugins + name: dynamic-plugins-npmrc + readOnly: true + subPath: .npmrc + - mountPath: /opt/app-root/src/.config/containers + name: dynamic-plugins-registry-auth + readOnly: true + - mountPath: /opt/app-root/src/.npm/_cacache + name: npmcacache + - name: extensions-catalog + mountPath: /extensions + - name: temp + mountPath: /tmp + {{- with .Values.dynamicPlugins.initContainer.extraVolumeMounts }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + workingDir: /opt/app-root/src + {{- if or .Values.postgresql.enabled .Values.externalDatabase.host }} + - name: wait-for-db + image: {{ include "rhdh.image.render" (dict "image" .Values.postgresql.image "global" .Values.global) | quote }} + imagePullPolicy: {{ .Values.postgresql.image.pullPolicy | default "IfNotPresent" | quote }} + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + resources: + limits: + cpu: "100m" + memory: "64Mi" + requests: + cpu: "50m" + memory: "32Mi" + command: + - bash + - -c + - | + dbHost={{ include "rhdh.postgresql.host" . | quote }} + dbPort={{ .Values.externalDatabase.port | default 5432 | quote }} + echo "Waiting for DB at $dbHost:$dbPort..." + until timeout 2 bash -c ">/dev/tcp/$dbHost/$dbPort" 2>/dev/null; do + sleep 2 + done + echo "DB is reachable!" + {{- end }} + # --- User-additional init containers (appended) --- + {{- with .Values.extraInitContainers }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + containers: + - name: backstage-backend + image: {{ include "rhdh.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.commandOverride }} + command: + {{- include "common.tplvalues.render" (dict "value" .Values.commandOverride "context" $) | nindent 8 }} + {{- end }} + args: + {{- if .Values.argsOverride }} + {{- range .Values.argsOverride }} + - {{ . | quote }} + {{- end }} + {{- else }} + - "--config" + - "{{ $installDir }}/dynamic-plugins-root/app-config.dynamic-plugins.yaml" + {{- if .Values.appConfig }} + - "--config" + - "{{ $installDir }}/app-config-from-configmap.yaml" + {{- end }} + {{- range .Values.extraAppConfig }} + - "--config" + - "{{ $installDir }}/{{ .filename }}" + {{- end }} + {{- range .Values.extraArgs }} + - {{ . | quote }} + {{- end }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- with .Values.startupProbe }} + startupProbe: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- with .Values.livenessProbe }} + livenessProbe: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- if or .Values.envFromOverride .Values.extraEnvFrom }} + envFrom: + {{- if .Values.envFromOverride }} + {{- include "common.tplvalues.render" (dict "value" .Values.envFromOverride "context" $) | nindent 8 }} + {{- else }} + {{- include "common.tplvalues.render" (dict "value" .Values.extraEnvFrom "context" $) | nindent 8 }} + {{- end }} + {{- end }} + env: + {{- if .Values.envOverride }} + {{- include "common.tplvalues.render" (dict "value" .Values.envOverride "context" $) | nindent 8 }} + {{- else }} + # --- System env vars (hardcoded) --- + - name: APP_CONFIG_backend_listen_port + value: {{ .Values.service.port | quote }} + {{- if .Values.auth.backend.enabled }} + - name: BACKEND_SECRET + valueFrom: + secretKeyRef: + name: {{ include "rhdh.backend-secret-name" . }} + key: {{ include "rhdh.backend-secret-key" . }} + {{- end }} + {{- if .Values.postgresql.enabled }} + - name: POSTGRES_HOST + value: {{ include "rhdh.postgresql.host" . }} + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: {{ .Values.postgresql.auth.username | default "postgres" }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "rhdh.postgresql.secretName" . }} + key: {{ include "rhdh.postgresql.adminPasswordKey" . }} + {{- else if .Values.externalDatabase.host }} + - name: POSTGRES_HOST + value: {{ .Values.externalDatabase.host | quote }} + - name: POSTGRES_PORT + value: {{ .Values.externalDatabase.port | quote }} + - name: POSTGRES_USER + value: {{ .Values.externalDatabase.user | default "postgres" | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ required "externalDatabase.existingSecretRef.name is required when externalDatabase.host is set" .Values.externalDatabase.existingSecretRef.name }} + key: {{ .Values.externalDatabase.existingSecretRef.key | default "password" }} + {{- end }} + # --- User-additional env vars (appended) --- + {{- with .Values.extraEnv }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- end }} + ports: + - name: backend + containerPort: {{ .Values.service.port }} + protocol: TCP + volumeMounts: + # --- System volume mounts (hardcoded) --- + - mountPath: {{ $installDir }}/dynamic-plugins-root + name: dynamic-plugins-root + - name: extensions-catalog + mountPath: /extensions + - name: temp + mountPath: /tmp + {{- if .Values.appConfig }} + - name: backstage-app-config + mountPath: "{{ $installDir }}/app-config-from-configmap.yaml" + subPath: app-config.yaml + {{- end }} + {{- range .Values.extraAppConfig }} + - name: {{ .configMapRef }} + mountPath: "{{ $installDir }}/{{ .filename }}" + subPath: {{ .filename }} + {{- end }} + # --- User-additional volume mounts (appended) --- + {{- with .Values.extraVolumeMounts }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- if $intelligentAssistant.enabled }} + - name: lightspeed-core + image: {{ include "rhdh.image.render" (dict "image" $intelligentAssistant.core.image "global" .Values.global) | quote }} + imagePullPolicy: {{ $intelligentAssistant.core.imagePullPolicy | quote }} + {{- with $intelligentAssistant.core.securityContext }} + securityContext: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- if $intelligentAssistant.core.commandOverride }} + command: + {{- include "common.tplvalues.render" (dict "value" $intelligentAssistant.core.commandOverride "context" $) | nindent 8 }} + {{- else if and (include "rhdh.intelligentAssistant.okp.active" $) (not $intelligentAssistant.core.argsOverride) }} + command: ["/bin/sh", "-c"] + {{- end }} + {{- if $intelligentAssistant.core.argsOverride }} + args: + {{- include "common.tplvalues.render" (dict "value" $intelligentAssistant.core.argsOverride "context" $) | nindent 8 }} + {{- else if and (include "rhdh.intelligentAssistant.okp.active" $) (not $intelligentAssistant.core.commandOverride) }} + args: + - | + if [ -f /var/run/secrets/kubernetes.io/serviceaccount/ca.crt ]; then + cat /etc/pki/tls/certs/ca-bundle.crt /var/run/secrets/kubernetes.io/serviceaccount/ca.crt > /tmp/combined-ca-bundle.crt + export SSL_CERT_FILE=/tmp/combined-ca-bundle.crt + export REQUESTS_CA_BUNDLE=/tmp/combined-ca-bundle.crt + fi + exec /app-root/entrypoint.sh --synthesized-config-output /tmp/.generated/run.yaml{{ range $intelligentAssistant.core.extraArgs }} {{ . | quote }}{{ end }} + {{- else }} + args: + - "--synthesized-config-output" + - "/tmp/.generated/run.yaml" + {{- range $intelligentAssistant.core.extraArgs }} + - {{ . | quote }} + {{- end }} + {{- end }} + ports: + - name: http-lightspeed + containerPort: 8080 + protocol: TCP + {{- if $intelligentAssistant.existingSecret }} + envFrom: + - secretRef: + name: {{ $intelligentAssistant.existingSecret }} + {{- end }} + env: + {{- if include "rhdh.intelligentAssistant.okp.active" $ }} + - name: OKP_SERVICE_URL + value: {{ include "rhdh.intelligentAssistant.okp.serviceUrl" $ | quote }} + {{- end }} + - name: KV_STORE_PATH + value: "/tmp/kvstore.db" + - name: SQL_STORE_PATH + value: "/tmp/sql_store.db" + - name: SQLITE_STORE_DIR + value: "/tmp/llama-stack-files" + - name: HF_HOME + value: "/tmp/hf_cache" + - name: OTEL_SDK_DISABLED + value: "true" + {{- with $intelligentAssistant.core.extraEnv }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- with $intelligentAssistant.core.resources }} + resources: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + volumeMounts: + - name: lightspeed-data + mountPath: "/tmp" + {{- range $key := list "stack" "profile" }} + {{- $entry := index $intelligentAssistant.config $key }} + {{- $file := include "rhdh.intelligentAssistant.configFile" $key }} + {{- $cmKey := include "rhdh.intelligentAssistant.configMapKey" (dict "key" $key "entry" $entry) }} + - name: {{ printf "lightspeed-config-%s" $key }} + mountPath: {{ printf "/app-root/%s" $file | quote }} + subPath: {{ $cmKey | quote }} + readOnly: true + {{- end }} + {{- with $intelligentAssistant.core.extraVolumeMounts }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 8 }} + {{- end }} + {{- end }} + # --- User-additional sidecar containers (appended) --- + {{- with .Values.extraContainers }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} +{{- end -}} From b4f757b740afa986253ff99f2ca2415e37ce3754 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 14 Sep 2026 12:55:31 +0100 Subject: [PATCH 02/23] Bumping chart version Signed-off-by: Leanne Ahern --- charts/rhdh/Chart.yaml | 2 +- charts/rhdh/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/rhdh/Chart.yaml b/charts/rhdh/Chart.yaml index 3d444056..3c510c70 100644 --- a/charts/rhdh/Chart.yaml +++ b/charts/rhdh/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: redhat-developer-hub type: application -version: 2.2.0 +version: 2.2.1 appVersion: 2.2.0 annotations: artifacthub.io/category: integration-delivery diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index 850783a9..c0aff6ee 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -1,7 +1,7 @@ # RHDH Helm Chart for OpenShift and Kubernetes -![Version: 2.2.0](https://img.shields.io/badge/Version-2.2.0-informational?style=flat-square) +![Version: 2.2.1](https://img.shields.io/badge/Version-2.2.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) A Helm chart for deploying Red Hat Developer Hub, which is a Red Hat supported version of Backstage. @@ -36,7 +36,7 @@ For the **Generally Available** version of this chart, see: helm repo add bitnami https://charts.bitnami.com/bitnami helm repo add redhat-developer https://redhat-developer.github.io/rhdh-chart -helm install my-rhdh redhat-developer/redhat-developer-hub --version 2.2.0 +helm install my-rhdh redhat-developer/redhat-developer-hub --version 2.2.1 ``` ## Introduction From 01474e084ddd4c6c525fea73f6257d9c0bc3dc25 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Thu, 17 Sep 2026 08:31:26 +0100 Subject: [PATCH 03/23] Enabling opt-in statefulset Signed-off-by: Leanne Ahern --- charts/rhdh/Chart.yaml | 2 +- charts/rhdh/README.md | 24 ++++++++-- charts/rhdh/README.md.gotmpl | 14 +++++- charts/rhdh/templates/_helpers.tpl | 15 ++++++ charts/rhdh/templates/deployment.yaml | 4 +- charts/rhdh/templates/hpa.yaml | 2 +- .../rhdh/templates/workload/statefulset.yaml | 38 +++++++++++++++ charts/rhdh/values.schema.json | 48 +++++++++++++++++++ charts/rhdh/values.schema.tmpl.json | 46 ++++++++++++++++++ charts/rhdh/values.yaml | 22 +++++++++ 10 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 charts/rhdh/templates/workload/statefulset.yaml diff --git a/charts/rhdh/Chart.yaml b/charts/rhdh/Chart.yaml index 3c510c70..ddbd44fe 100644 --- a/charts/rhdh/Chart.yaml +++ b/charts/rhdh/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: redhat-developer-hub type: application -version: 2.2.1 +version: 2.3.0 appVersion: 2.2.0 annotations: artifacthub.io/category: integration-delivery diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index c0aff6ee..e3d52ef6 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -1,7 +1,7 @@ # RHDH Helm Chart for OpenShift and Kubernetes -![Version: 2.2.1](https://img.shields.io/badge/Version-2.2.1-informational?style=flat-square) +![Version: 2.3.0](https://img.shields.io/badge/Version-2.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) A Helm chart for deploying Red Hat Developer Hub, which is a Red Hat supported version of Backstage. @@ -36,14 +36,14 @@ For the **Generally Available** version of this chart, see: helm repo add bitnami https://charts.bitnami.com/bitnami helm repo add redhat-developer https://redhat-developer.github.io/rhdh-chart -helm install my-rhdh redhat-developer/redhat-developer-hub --version 2.2.1 +helm install my-rhdh redhat-developer/redhat-developer-hub --version 2.3.0 ``` ## Introduction This chart bootstraps a [Red Hat Developer Hub](https://developers.redhat.com/rhdh) deployment on a [Kubernetes](https://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. -Unlike the legacy `backstage` chart, this chart owns all Kubernetes templates directly (Deployment, Service, ConfigMap, etc.) without depending on an upstream Backstage subchart. It uses an **"add, don't replace"** pattern: system-required volumes, volume mounts, environment variables, and init containers are hardcoded in the Deployment template, while user-provided values (`extraVolumes`, `extraVolumeMounts`, `extraEnv`, `extraInitContainers`, `extraContainers`) are always appended — never replacing the defaults. +Unlike the legacy `backstage` chart, this chart owns all Kubernetes templates directly (Deployment or StatefulSet, Service, ConfigMap, etc.) without depending on an upstream Backstage subchart. The Backstage pod specification is shared across workload kinds via a common template. It uses an **"add, don't replace"** pattern: system-required volumes, volume mounts, environment variables, and init containers are hardcoded in the pod template, while user-provided values (`extraVolumes`, `extraVolumeMounts`, `extraEnv`, `extraInitContainers`, `extraContainers`) are always appended — never replacing the defaults. ## Prerequisites @@ -312,6 +312,12 @@ Kubernetes: `>= 1.31.0-0` | test | Test pod configuration for `helm test`. | object | `{"enabled":true,"image":{"digest":"","pullPolicy":"IfNotPresent","registry":"quay.io","repository":"curl/curl","tag":"8.21.0"},"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true}}` | | tolerations | Tolerations for pod assignment. | list | `[]` | | topologySpreadConstraints | Topology spread constraints for pod scheduling. | list | `[]` | +| workload | Kubernetes workload controller for Backstage pod. | object | `{"kind":"Deployment","statefulSet":{"annotations":{},"persistentVolumeClaimRetentionPolicy":{},"podManagementPolicy":"","serviceName":"","updateStrategy":{}}}` | +| workload.kind | Workload kind: Deployment (default) or StatefulSet. | string | `"Deployment"` | +| workload.statefulSet.annotations | Annotations on the StatefulSet resource. | object | `{}` | +| workload.statefulSet.podManagementPolicy | Pod management policy for the StatefulSet. | string | `""` | +| workload.statefulSet.serviceName | Required for StatefulSet and must match an existing service. | string | `""` | +| workload.statefulSet.updateStrategy | StatefulSet update strategy. | object | `{}` | ## Opinionated RHDH deployment @@ -350,7 +356,7 @@ quay.io/rhdh-community/rhdh:next ### "Add, don't replace" pattern -System-required volumes, volume mounts, environment variables, init containers, and sidecar containers are hardcoded in the Deployment template. User-provided `extra*` values are always **appended** after the system defaults: +System-required volumes, volume mounts, environment variables, init containers, and sidecar containers are hardcoded in the Backstage pod template (used by both Deployment and StatefulSet). User-provided `extra*` values are always **appended** after the system defaults: - `extraVolumes` — appended after dynamic-plugins-root, temp, npmcacache, extensions-catalog, etc. - `extraVolumeMounts` — appended after dynamic-plugins-root, extensions, temp mounts @@ -362,6 +368,16 @@ This means you never need to copy system defaults to add your own entries. If you need full control, the corresponding `*Override` fields (`envOverride`, `envFromOverride`, `commandOverride`, `argsOverride`) **replace** the system defaults entirely — nothing is auto-injected when an override is set. +### Workload kind (Deployment or StatefulSet) + +By default, the chart creates a Kubernetes **Deployment** (`workload.kind: Deployment`). You can change the workload kind to **StatefulSet** (`workload.kind: StatefulSet`) by setting `workload.kind` to `StatefulSet` in your values.yaml. + +```yaml +# values.yaml +workload: + kind: StatefulSet +``` + ### OpenShift Routes This chart offers an OpenShift `Route` resource enabled by default. In order to use the chart without it, please set `openshift.route.enabled` to `false` and switch to the `Ingress` resource via `ingress` values. diff --git a/charts/rhdh/README.md.gotmpl b/charts/rhdh/README.md.gotmpl index 96a24bb2..b2e60d67 100644 --- a/charts/rhdh/README.md.gotmpl +++ b/charts/rhdh/README.md.gotmpl @@ -34,7 +34,7 @@ helm install my-rhdh redhat-developer/redhat-developer-hub --version {{ template This chart bootstraps a [Red Hat Developer Hub](https://developers.redhat.com/rhdh) deployment on a [Kubernetes](https://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. -Unlike the legacy `backstage` chart, this chart owns all Kubernetes templates directly (Deployment, Service, ConfigMap, etc.) without depending on an upstream Backstage subchart. It uses an **"add, don't replace"** pattern: system-required volumes, volume mounts, environment variables, and init containers are hardcoded in the Deployment template, while user-provided values (`extraVolumes`, `extraVolumeMounts`, `extraEnv`, `extraInitContainers`, `extraContainers`) are always appended — never replacing the defaults. +Unlike the legacy `backstage` chart, this chart owns all Kubernetes templates directly (Deployment or StatefulSet, Service, ConfigMap, etc.) without depending on an upstream Backstage subchart. The Backstage pod specification is shared across workload kinds via a common template. It uses an **"add, don't replace"** pattern: system-required volumes, volume mounts, environment variables, and init containers are hardcoded in the pod template, while user-provided values (`extraVolumes`, `extraVolumeMounts`, `extraEnv`, `extraInitContainers`, `extraContainers`) are always appended — never replacing the defaults. ## Prerequisites @@ -195,7 +195,7 @@ quay.io/rhdh-community/rhdh:next ### "Add, don't replace" pattern -System-required volumes, volume mounts, environment variables, init containers, and sidecar containers are hardcoded in the Deployment template. User-provided `extra*` values are always **appended** after the system defaults: +System-required volumes, volume mounts, environment variables, init containers, and sidecar containers are hardcoded in the Backstage pod template (used by both Deployment and StatefulSet). User-provided `extra*` values are always **appended** after the system defaults: - `extraVolumes` — appended after dynamic-plugins-root, temp, npmcacache, extensions-catalog, etc. - `extraVolumeMounts` — appended after dynamic-plugins-root, extensions, temp mounts @@ -207,6 +207,16 @@ This means you never need to copy system defaults to add your own entries. If you need full control, the corresponding `*Override` fields (`envOverride`, `envFromOverride`, `commandOverride`, `argsOverride`) **replace** the system defaults entirely — nothing is auto-injected when an override is set. +### Workload kind (Deployment or StatefulSet) + +By default, the chart creates a Kubernetes **Deployment** (`workload.kind: Deployment`). You can change the workload kind to **StatefulSet** (`workload.kind: StatefulSet`) by setting `workload.kind` to `StatefulSet` in your values.yaml. + +```yaml +# values.yaml +workload: + kind: StatefulSet +``` + ### OpenShift Routes This chart offers an OpenShift `Route` resource enabled by default. In order to use the chart without it, please set `openshift.route.enabled` to `false` and switch to the `Ingress` resource via `ingress` values. diff --git a/charts/rhdh/templates/_helpers.tpl b/charts/rhdh/templates/_helpers.tpl index 74ffdc2c..1cef54f3 100644 --- a/charts/rhdh/templates/_helpers.tpl +++ b/charts/rhdh/templates/_helpers.tpl @@ -54,6 +54,21 @@ app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/component: backstage {{- end }} +{{/* +Workload kind helpers +*/}} +{{- define "rhdh.workloadKind" -}} +{{- default "Deployment" .Values.workload.kind -}} +{{- end -}} + +{{- define "rhdh.isStatefulSet" -}} +{{- eq (include "rhdh.workloadKind" .) "StatefulSet" -}} +{{- end -}} + +{{- define "rhdh.statefulSetServiceName" -}} +{{- default (include "rhdh.fullname" .) .Values.workload.statefulSet.serviceName -}} +{{- end -}} + {{/* Create the name of the service account to use. */}} diff --git a/charts/rhdh/templates/deployment.yaml b/charts/rhdh/templates/deployment.yaml index 0c797ab8..f8d00d20 100644 --- a/charts/rhdh/templates/deployment.yaml +++ b/charts/rhdh/templates/deployment.yaml @@ -1,3 +1,4 @@ +{{- if not (eq (include "rhdh.workloadKind" .) "StatefulSet") }} apiVersion: apps/v1 kind: Deployment metadata: @@ -26,4 +27,5 @@ spec: matchLabels: {{- include "rhdh.selectorLabels" . | nindent 6 }} template: - {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} \ No newline at end of file + {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} + {{- end }} \ No newline at end of file diff --git a/charts/rhdh/templates/hpa.yaml b/charts/rhdh/templates/hpa.yaml index 7917d5ba..3e47cd49 100644 --- a/charts/rhdh/templates/hpa.yaml +++ b/charts/rhdh/templates/hpa.yaml @@ -12,7 +12,7 @@ metadata: spec: scaleTargetRef: apiVersion: apps/v1 - kind: Deployment + kind: {{ include "rhdh.workloadKind" . }} name: {{ include "rhdh.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} diff --git a/charts/rhdh/templates/workload/statefulset.yaml b/charts/rhdh/templates/workload/statefulset.yaml new file mode 100644 index 00000000..b513087a --- /dev/null +++ b/charts/rhdh/templates/workload/statefulset.yaml @@ -0,0 +1,38 @@ +{{- if eq (include "rhdh.workloadKind" .) "StatefulSet" }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "rhdh.fullname" . }} + labels: + {{- include "rhdh.labels" . | nindent 4 }} + {{- if or .Values.commonAnnotations .Values.workload.statefulSet.annotations }} + annotations: + {{- with .Values.commonAnnotations }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- with .Values.workload.statefulSet.annotations }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- end }} +spec: + serviceName: {{ include "rhdh.statefulSetServiceName" . }} + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + {{- with .Values.workload.statefulSet.podManagementPolicy }} + podManagementPolicy: {{ . }} + {{- end }} + {{- with .Values.workload.statefulSet.updateStrategy }} + updateStrategy: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + {{- with .Values.workload.statefulSet.persistentVolumeClaimRetentionPolicy }} + persistentVolumeClaimRetentionPolicy: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "rhdh.selectorLabels" . | nindent 6 }} + template: + {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/rhdh/values.schema.json b/charts/rhdh/values.schema.json index 59cc5fd6..8dd9f8cd 100644 --- a/charts/rhdh/values.schema.json +++ b/charts/rhdh/values.schema.json @@ -2537,6 +2537,54 @@ "default": [], "title": "Topology spread constraints for pod scheduling.", "type": "array" + }, + "workload": { + "additionalProperties": false, + "properties": { + "kind": { + "default": "Deployment", + "enum": [ + "Deployment", + "StatefulSet" + ], + "title": "Workload kind: Deployment (default) or StatefulSet.", + "type": "string" + }, + "statefulSet": { + "additionalProperties": false, + "properties": { + "annotations": { + "default": {}, + "title": "Annotations on the StatefulSet resource (not the pod).", + "type": "object" + }, + "persistentVolumeClaimRetentionPolicy": { + "default": {}, + "title": "Optional PVC retention policy for the StatefulSet.", + "type": "object" + }, + "podManagementPolicy": { + "default": "", + "title": "Pod management policy for the StatefulSet.", + "type": "string" + }, + "serviceName": { + "default": "", + "title": "Service name for the StatefulSet. Defaults to the main service name.", + "type": "string" + }, + "updateStrategy": { + "default": {}, + "title": "StatefulSet update strategy.", + "type": "object" + } + }, + "title": "StatefulSet specific settings (ignored when kind is Deployment).", + "type": "object" + } + }, + "title": "Kubernetes workload controller for Backstage pod.", + "type": "object" } }, "title": "Red Hat Developer Hub Helm Chart Values", diff --git a/charts/rhdh/values.schema.tmpl.json b/charts/rhdh/values.schema.tmpl.json index 121f839b..c13fd5d5 100644 --- a/charts/rhdh/values.schema.tmpl.json +++ b/charts/rhdh/values.schema.tmpl.json @@ -408,6 +408,52 @@ "type": "object", "default": {} }, + + "workload": { + "title": "Kubernetes workload controller for Backstage pod.", + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "title": "Workload kind: Deployment (default) or StatefulSet.", + "type": "string", + "default": "Deployment", + "enum": ["Deployment", "StatefulSet"] + }, + "statefulSet": { + "title": "StatefulSet specific settings (ignored when kind is Deployment).", + "type": "object", + "additionalProperties": false, + "properties": { + "serviceName": { + "title": "Service name for the StatefulSet. Defaults to the main service name.", + "type": "string", + "default": "" + }, + "podManagementPolicy": { + "title": "Pod management policy for the StatefulSet.", + "type": "string", + "default": "" + }, + "updateStrategy": { + "title": "StatefulSet update strategy.", + "type": "object", + "default": {} + }, + "persistentVolumeClaimRetentionPolicy": { + "title": "Optional PVC retention policy for the StatefulSet.", + "type": "object", + "default": {} + }, + "annotations": { + "title": "Annotations on the StatefulSet resource (not the pod).", + "type": "object", + "default": {} + } + } + } + } + }, "commandOverride": { "title": "Override the container command.", "type": "array", diff --git a/charts/rhdh/values.yaml b/charts/rhdh/values.yaml index 0d871893..2ee9c572 100644 --- a/charts/rhdh/values.yaml +++ b/charts/rhdh/values.yaml @@ -204,6 +204,28 @@ revisionHistoryLimit: 10 # -- Deployment update strategy. strategy: {} +# -- Kubernetes workload controller for Backstage pod. +workload: + # -- Workload kind: Deployment (default) or StatefulSet. + kind: "Deployment" + + statefulSet: + # -- service name for the StatefulSet. Defaults to the main service name. + # -- Required for StatefulSet and must match an existing service. + serviceName: "" + + # -- Pod management policy for the StatefulSet. + podManagementPolicy: "" + + # -- StatefulSet update strategy. + updateStrategy: {} + + # -- Optional PVC retention policy for the StatefulSet. + persistentVolumeClaimRetentionPolicy: {} + + # -- Annotations on the StatefulSet resource. + annotations: {} + # -- ServiceAccount configuration. serviceAccount: create: false From d658633814dae33f71568edface41b4dfc6f2abb Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Thu, 17 Sep 2026 16:32:37 +0100 Subject: [PATCH 04/23] Adding headless service Signed-off-by: Leanne Ahern --- charts/rhdh/templates/_helpers.tpl | 6 ++++- .../templates/workload/headless-service.yaml | 24 +++++++++++++++++++ charts/rhdh/values.schema.json | 2 +- charts/rhdh/values.schema.tmpl.json | 2 +- charts/rhdh/values.yaml | 4 ++-- 5 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 charts/rhdh/templates/workload/headless-service.yaml diff --git a/charts/rhdh/templates/_helpers.tpl b/charts/rhdh/templates/_helpers.tpl index 1cef54f3..603b6ca2 100644 --- a/charts/rhdh/templates/_helpers.tpl +++ b/charts/rhdh/templates/_helpers.tpl @@ -65,8 +65,12 @@ Workload kind helpers {{- eq (include "rhdh.workloadKind" .) "StatefulSet" -}} {{- end -}} +{{- define "rhdh.statefulSetHeadlessServiceName" -}} +{{- printf "%s-headless" (include "rhdh.fullname" .) -}} +{{- end -}} + {{- define "rhdh.statefulSetServiceName" -}} -{{- default (include "rhdh.fullname" .) .Values.workload.statefulSet.serviceName -}} +{{- default (include "rhdh.statefulSetHeadlessServiceName" .) .Values.workload.statefulSet.serviceName -}} {{- end -}} {{/* diff --git a/charts/rhdh/templates/workload/headless-service.yaml b/charts/rhdh/templates/workload/headless-service.yaml new file mode 100644 index 00000000..bfdbdf92 --- /dev/null +++ b/charts/rhdh/templates/workload/headless-service.yaml @@ -0,0 +1,24 @@ +{{- if eq (include "rhdh.workloadKind" .) "StatefulSet" }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "rhdh.statefulSetHeadlessServiceName" . }} + labels: + {{- include "rhdh.labels" . | nindent 4 }} + {{- with .Values.commonAnnotations }} + annotations: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} +spec: + clusterIP: None + ports: + - port: {{ .Values.service.port }} + targetPort: backend + protocol: TCP + name: http-backend + {{- with .Values.service.extraPorts }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} + selector: + {{- include "rhdh.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/charts/rhdh/values.schema.json b/charts/rhdh/values.schema.json index 30ae45ba..d9ddc05c 100644 --- a/charts/rhdh/values.schema.json +++ b/charts/rhdh/values.schema.json @@ -2570,7 +2570,7 @@ }, "serviceName": { "default": "", - "title": "Service name for the StatefulSet. Defaults to the main service name.", + "title": "serviceName for the StatefulSet spec. Defaults to the headless Service ({fullname}-headless).", "type": "string" }, "updateStrategy": { diff --git a/charts/rhdh/values.schema.tmpl.json b/charts/rhdh/values.schema.tmpl.json index c13fd5d5..89b829e0 100644 --- a/charts/rhdh/values.schema.tmpl.json +++ b/charts/rhdh/values.schema.tmpl.json @@ -426,7 +426,7 @@ "additionalProperties": false, "properties": { "serviceName": { - "title": "Service name for the StatefulSet. Defaults to the main service name.", + "title": "serviceName for the StatefulSet spec. Defaults to the headless Service ({fullname}-headless).", "type": "string", "default": "" }, diff --git a/charts/rhdh/values.yaml b/charts/rhdh/values.yaml index 87b7435b..4dba6639 100644 --- a/charts/rhdh/values.yaml +++ b/charts/rhdh/values.yaml @@ -210,8 +210,8 @@ workload: kind: "Deployment" statefulSet: - # -- service name for the StatefulSet. Defaults to the main service name. - # -- Required for StatefulSet and must match an existing service. + # -- service name for the StatefulSet. Defaults to headless when empty. + # -- Service ({fullname}-headless) must match an existing service. serviceName: "" # -- Pod management policy for the StatefulSet. From 8077df1c1f1f38dcabb9a56023249851194f59d8 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Thu, 17 Sep 2026 17:11:58 +0100 Subject: [PATCH 05/23] Adding ci values for stateful set & updating ReadMe Signed-off-by: Leanne Ahern --- charts/rhdh/README.md | 11 +++++++---- charts/rhdh/README.md.gotmpl | 9 ++++++--- charts/rhdh/ci/with-statefulset-values | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 charts/rhdh/ci/with-statefulset-values diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index 9e81e41d..b04cc19f 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -316,7 +316,7 @@ Kubernetes: `>= 1.31.0-0` | workload.kind | Workload kind: Deployment (default) or StatefulSet. | string | `"Deployment"` | | workload.statefulSet.annotations | Annotations on the StatefulSet resource. | object | `{}` | | workload.statefulSet.podManagementPolicy | Pod management policy for the StatefulSet. | string | `""` | -| workload.statefulSet.serviceName | Required for StatefulSet and must match an existing service. | string | `""` | +| workload.statefulSet.serviceName | Service ({fullname}-headless) must match an existing service. | string | `""` | | workload.statefulSet.updateStrategy | StatefulSet update strategy. | object | `{}` | ## Opinionated RHDH deployment @@ -385,7 +385,7 @@ Both kinds render the **same** Backstage pod (containers, volumes, probes, dynam - You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, or optional PVC retention policy). - You want parity with the [RHDH Operator](https://github.com/redhat-developer/rhdh-operator), which supports `spec.deployment.kind: StatefulSet` ([operator documentation](https://github.com/redhat-developer/rhdh-operator/blob/main/docs/configuration.md#deployment-kind)). -Most installs should keep the default **Deployment**. +Most installs should keep the default **Deployment** (no app StatefulSet and no headless Service). **Values that behave the same for both kinds** @@ -402,14 +402,17 @@ Most installs should keep the default **Deployment**. **StatefulSet-only settings** -`serviceName` is required on a StatefulSet. The chart defaults it to the main Service for this release (see `workload.statefulSet.serviceName` in the parameters table). Example with common options: +When `workload.kind` is `StatefulSet`, the chart also renders a **headless Service** (`{fullname}-headless`, `clusterIP: None`). The StatefulSet `serviceName` points at that Service (stable pod network identity). +The existing **ClusterIP Service** (`templates/service.yaml`) is unchanged — OpenShift Routes, Ingress, and `helm test` still use it for application traffic. + +Override `workload.statefulSet.serviceName` only if you create your own governing Service. ```yaml # values.yaml workload: kind: StatefulSet statefulSet: - serviceName: "" # default: main chart Service + serviceName: "" # default: {fullname}-headless podManagementPolicy: OrderedReady # or Parallel; omit when empty updateStrategy: type: RollingUpdate diff --git a/charts/rhdh/README.md.gotmpl b/charts/rhdh/README.md.gotmpl index 51e0fd55..ae0c2f85 100644 --- a/charts/rhdh/README.md.gotmpl +++ b/charts/rhdh/README.md.gotmpl @@ -224,7 +224,7 @@ Both kinds render the **same** Backstage pod (containers, volumes, probes, dynam - You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, or optional PVC retention policy). - You want parity with the [RHDH Operator](https://github.com/redhat-developer/rhdh-operator), which supports `spec.deployment.kind: StatefulSet` ([operator documentation](https://github.com/redhat-developer/rhdh-operator/blob/main/docs/configuration.md#deployment-kind)). -Most installs should keep the default **Deployment**. +Most installs should keep the default **Deployment** (no app StatefulSet and no headless Service). **Values that behave the same for both kinds** @@ -241,14 +241,17 @@ Most installs should keep the default **Deployment**. **StatefulSet-only settings** -`serviceName` is required on a StatefulSet. The chart defaults it to the main Service for this release (see `workload.statefulSet.serviceName` in the parameters table). Example with common options: +When `workload.kind` is `StatefulSet`, the chart also renders a **headless Service** (`{fullname}-headless`, `clusterIP: None`). The StatefulSet `serviceName` points at that Service (stable pod network identity). +The existing **ClusterIP Service** (`templates/service.yaml`) is unchanged — OpenShift Routes, Ingress, and `helm test` still use it for application traffic. + +Override `workload.statefulSet.serviceName` only if you create your own governing Service. ```yaml # values.yaml workload: kind: StatefulSet statefulSet: - serviceName: "" # default: main chart Service + serviceName: "" # default: {fullname}-headless podManagementPolicy: OrderedReady # or Parallel; omit when empty updateStrategy: type: RollingUpdate diff --git a/charts/rhdh/ci/with-statefulset-values b/charts/rhdh/ci/with-statefulset-values new file mode 100644 index 00000000..16b2399a --- /dev/null +++ b/charts/rhdh/ci/with-statefulset-values @@ -0,0 +1,14 @@ +# CI: render/install with workload.kind=StatefulSet (expects headless Service + StatefulSet). +dynamicPlugins: + includes: [] +workload: + kind: StatefulSet +intelligentAssistant: + plugins: [] + okp: + route: + enabled: false + ingress: + enabled: false +orchestrator: + plugins: [] \ No newline at end of file From 50dcb5879cfd3f060b01fede384e732383010172 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Thu, 17 Sep 2026 17:45:15 +0100 Subject: [PATCH 06/23] Making fixes based on testing and review Signed-off-by: Leanne Ahern --- charts/rhdh/README.md | 2 +- charts/rhdh/README.md.gotmpl | 2 +- .../{with-statefulset-values => with-statefulset-values.yaml} | 0 charts/rhdh/templates/deployment.yaml | 3 ++- 4 files changed, 4 insertions(+), 3 deletions(-) rename charts/rhdh/ci/{with-statefulset-values => with-statefulset-values.yaml} (100%) diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index b04cc19f..61a88bdc 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -421,7 +421,7 @@ workload: **Changing `workload.kind` on an existing release** -Upgrading with a different `workload.kind` renders the new controller but does **not** remove the previous Deployment or StatefulSet. After switching kinds, delete the obsolete workload object once (or reinstall cleanly) so two controllers do not manage pods with the same selector. +For a normal Helm-managed release, changing `workload.kind` and running `helm upgrade` replaces the rendered manifest: Helm removes the old workload kind (Deployment or StatefulSet) and creates the new one. Expect a brief cutover while pods are recreated. If you applied manifests outside Helm (for example GitOps that keeps both objects) or an old controller was left behind, delete the obsolete Deployment or StatefulSet so two controllers do not target the same selector. **Example install** diff --git a/charts/rhdh/README.md.gotmpl b/charts/rhdh/README.md.gotmpl index ae0c2f85..3ed5f311 100644 --- a/charts/rhdh/README.md.gotmpl +++ b/charts/rhdh/README.md.gotmpl @@ -260,7 +260,7 @@ workload: **Changing `workload.kind` on an existing release** -Upgrading with a different `workload.kind` renders the new controller but does **not** remove the previous Deployment or StatefulSet. After switching kinds, delete the obsolete workload object once (or reinstall cleanly) so two controllers do not manage pods with the same selector. +For a normal Helm-managed release, changing `workload.kind` and running `helm upgrade` replaces the rendered manifest: Helm removes the old workload kind (Deployment or StatefulSet) and creates the new one. Expect a brief cutover while pods are recreated. If you applied manifests outside Helm (for example GitOps that keeps both objects) or an old controller was left behind, delete the obsolete Deployment or StatefulSet so two controllers do not target the same selector. **Example install** diff --git a/charts/rhdh/ci/with-statefulset-values b/charts/rhdh/ci/with-statefulset-values.yaml similarity index 100% rename from charts/rhdh/ci/with-statefulset-values rename to charts/rhdh/ci/with-statefulset-values.yaml diff --git a/charts/rhdh/templates/deployment.yaml b/charts/rhdh/templates/deployment.yaml index f8d00d20..dffed5fd 100644 --- a/charts/rhdh/templates/deployment.yaml +++ b/charts/rhdh/templates/deployment.yaml @@ -28,4 +28,5 @@ spec: {{- include "rhdh.selectorLabels" . | nindent 6 }} template: {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} - {{- end }} \ No newline at end of file + {{- end }} +{{- end }} \ No newline at end of file From 09b7e6f590b35a1e43910919d5c480e174152c49 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Thu, 17 Sep 2026 17:49:28 +0100 Subject: [PATCH 07/23] Ci test fixes Signed-off-by: Leanne Ahern --- charts/rhdh/ci/with-statefulset-values.yaml | 2 +- charts/rhdh/templates/deployment.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/charts/rhdh/ci/with-statefulset-values.yaml b/charts/rhdh/ci/with-statefulset-values.yaml index 16b2399a..2a312e3f 100644 --- a/charts/rhdh/ci/with-statefulset-values.yaml +++ b/charts/rhdh/ci/with-statefulset-values.yaml @@ -11,4 +11,4 @@ intelligentAssistant: ingress: enabled: false orchestrator: - plugins: [] \ No newline at end of file + plugins: [] diff --git a/charts/rhdh/templates/deployment.yaml b/charts/rhdh/templates/deployment.yaml index dffed5fd..d38abe6b 100644 --- a/charts/rhdh/templates/deployment.yaml +++ b/charts/rhdh/templates/deployment.yaml @@ -28,5 +28,4 @@ spec: {{- include "rhdh.selectorLabels" . | nindent 6 }} template: {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} - {{- end }} {{- end }} \ No newline at end of file From cf7a07f80473fe5152d85ecc49a32e0192a7dd0d Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Fri, 18 Sep 2026 14:08:32 +0100 Subject: [PATCH 08/23] Removing workload Signed-off-by: Leanne Ahern --- charts/rhdh/templates/{workload => }/_backstage-pod-template.tpl | 0 charts/rhdh/templates/{workload => }/headless-service.yaml | 0 charts/rhdh/templates/{workload => }/statefulset.yaml | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename charts/rhdh/templates/{workload => }/_backstage-pod-template.tpl (100%) rename charts/rhdh/templates/{workload => }/headless-service.yaml (100%) rename charts/rhdh/templates/{workload => }/statefulset.yaml (100%) diff --git a/charts/rhdh/templates/workload/_backstage-pod-template.tpl b/charts/rhdh/templates/_backstage-pod-template.tpl similarity index 100% rename from charts/rhdh/templates/workload/_backstage-pod-template.tpl rename to charts/rhdh/templates/_backstage-pod-template.tpl diff --git a/charts/rhdh/templates/workload/headless-service.yaml b/charts/rhdh/templates/headless-service.yaml similarity index 100% rename from charts/rhdh/templates/workload/headless-service.yaml rename to charts/rhdh/templates/headless-service.yaml diff --git a/charts/rhdh/templates/workload/statefulset.yaml b/charts/rhdh/templates/statefulset.yaml similarity index 100% rename from charts/rhdh/templates/workload/statefulset.yaml rename to charts/rhdh/templates/statefulset.yaml From 443b27de1e206dda0fa81184107bf5e9f7637c5d Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Fri, 18 Sep 2026 14:09:53 +0100 Subject: [PATCH 09/23] Fixing appVersion Signed-off-by: Leanne Ahern --- charts/rhdh/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/rhdh/Chart.yaml b/charts/rhdh/Chart.yaml index 0ffc378f..8b407fa0 100644 --- a/charts/rhdh/Chart.yaml +++ b/charts/rhdh/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: redhat-developer-hub type: application version: 2.5.0 -appVersion: 2.2.0 +appVersion: 2.1.0 annotations: artifacthub.io/category: integration-delivery artifacthub.io/license: Apache-2.0 From 01a5a2f8bbd7c58da04a42cdf03eb755b5d5cff5 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Sun, 20 Sep 2026 23:25:47 +0100 Subject: [PATCH 10/23] Fixing values formatting Signed-off-by: Leanne Ahern --- charts/rhdh/README.md | 1 + charts/rhdh/values.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index 61a88bdc..5dbb2609 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -315,6 +315,7 @@ Kubernetes: `>= 1.31.0-0` | workload | Kubernetes workload controller for Backstage pod. | object | `{"kind":"Deployment","statefulSet":{"annotations":{},"persistentVolumeClaimRetentionPolicy":{},"podManagementPolicy":"","serviceName":"","updateStrategy":{}}}` | | workload.kind | Workload kind: Deployment (default) or StatefulSet. | string | `"Deployment"` | | workload.statefulSet.annotations | Annotations on the StatefulSet resource. | object | `{}` | +| workload.statefulSet.persistentVolumeClaimRetentionPolicy | Optional PVC retention policy for the StatefulSet. | object | `{}` | | workload.statefulSet.podManagementPolicy | Pod management policy for the StatefulSet. | string | `""` | | workload.statefulSet.serviceName | Service ({fullname}-headless) must match an existing service. | string | `""` | | workload.statefulSet.updateStrategy | StatefulSet update strategy. | object | `{}` | diff --git a/charts/rhdh/values.yaml b/charts/rhdh/values.yaml index 4dba6639..80bd8289 100644 --- a/charts/rhdh/values.yaml +++ b/charts/rhdh/values.yaml @@ -220,7 +220,7 @@ workload: # -- StatefulSet update strategy. updateStrategy: {} - # -- Optional PVC retention policy for the StatefulSet. + # -- Optional PVC retention policy for the StatefulSet. persistentVolumeClaimRetentionPolicy: {} # -- Annotations on the StatefulSet resource. From 5b2798d5cb5f261a336e9cbf3debd25f06611751 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 03:37:39 +0100 Subject: [PATCH 11/23] Adding volumeClaimTemplates Signed-off-by: Leanne Ahern --- charts/rhdh/README.md | 9 +++++++-- charts/rhdh/README.md.gotmpl | 6 +++++- charts/rhdh/templates/statefulset.yaml | 4 ++++ charts/rhdh/values.schema.json | 8 ++++++++ charts/rhdh/values.schema.tmpl.json | 8 ++++++++ charts/rhdh/values.yaml | 3 +++ 6 files changed, 35 insertions(+), 3 deletions(-) diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index 5dbb2609..2696c40a 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -312,13 +312,14 @@ Kubernetes: `>= 1.31.0-0` | test | Test pod configuration for `helm test`. | object | `{"enabled":true,"image":{"digest":"","pullPolicy":"IfNotPresent","registry":"quay.io","repository":"curl/curl","tag":"8.22.0"},"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true}}` | | tolerations | Tolerations for pod assignment. | list | `[]` | | topologySpreadConstraints | Topology spread constraints for pod scheduling. | list | `[]` | -| workload | Kubernetes workload controller for Backstage pod. | object | `{"kind":"Deployment","statefulSet":{"annotations":{},"persistentVolumeClaimRetentionPolicy":{},"podManagementPolicy":"","serviceName":"","updateStrategy":{}}}` | +| workload | Kubernetes workload controller for Backstage pod. | object | `{"kind":"Deployment","statefulSet":{"annotations":{},"persistentVolumeClaimRetentionPolicy":{},"podManagementPolicy":"","serviceName":"","updateStrategy":{},"volumeClaimTemplates":[]}}` | | workload.kind | Workload kind: Deployment (default) or StatefulSet. | string | `"Deployment"` | | workload.statefulSet.annotations | Annotations on the StatefulSet resource. | object | `{}` | | workload.statefulSet.persistentVolumeClaimRetentionPolicy | Optional PVC retention policy for the StatefulSet. | object | `{}` | | workload.statefulSet.podManagementPolicy | Pod management policy for the StatefulSet. | string | `""` | | workload.statefulSet.serviceName | Service ({fullname}-headless) must match an existing service. | string | `""` | | workload.statefulSet.updateStrategy | StatefulSet update strategy. | object | `{}` | +| workload.statefulSet.volumeClaimTemplates | StatefulSet volumeClaimTemplates. PVCs created for each pod. | list | `[]` | ## Opinionated RHDH deployment @@ -383,7 +384,7 @@ Both kinds render the **same** Backstage pod (containers, volumes, probes, dynam **When to use StatefulSet** -- You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, or optional PVC retention policy). +- You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, `volumeClaimTemplates`, or optional PVC retention policy). - You want parity with the [RHDH Operator](https://github.com/redhat-developer/rhdh-operator), which supports `spec.deployment.kind: StatefulSet` ([operator documentation](https://github.com/redhat-developer/rhdh-operator/blob/main/docs/configuration.md#deployment-kind)). Most installs should keep the default **Deployment** (no app StatefulSet and no headless Service). @@ -400,6 +401,8 @@ Most installs should keep the default **Deployment** (no app StatefulSet and no | ReplicaSet revision history | `revisionHistoryLimit` | Not used | | Controller annotations (not the pod) | `deploymentAnnotations` | `workload.statefulSet.annotations` | | Shared controller annotations | `commonAnnotations` | `commonAnnotations` | +| PVC claim templates | Not used | `workload.statefulSet.volumeClaimTemplates` | +| PVC retention policy | Not used | `workload.statefulSet.persistentVolumeClaimRetentionPolicy` | **StatefulSet-only settings** @@ -417,6 +420,8 @@ workload: podManagementPolicy: OrderedReady # or Parallel; omit when empty updateStrategy: type: RollingUpdate + volumeClaimTemplates: [] + persistentVolumeClaimRetentionPolicy: {} annotations: {} ``` diff --git a/charts/rhdh/README.md.gotmpl b/charts/rhdh/README.md.gotmpl index 3ed5f311..4599ec14 100644 --- a/charts/rhdh/README.md.gotmpl +++ b/charts/rhdh/README.md.gotmpl @@ -221,7 +221,7 @@ Both kinds render the **same** Backstage pod (containers, volumes, probes, dynam **When to use StatefulSet** -- You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, or optional PVC retention policy). +- You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, `volumeClaimTemplates`, or optional PVC retention policy). - You want parity with the [RHDH Operator](https://github.com/redhat-developer/rhdh-operator), which supports `spec.deployment.kind: StatefulSet` ([operator documentation](https://github.com/redhat-developer/rhdh-operator/blob/main/docs/configuration.md#deployment-kind)). Most installs should keep the default **Deployment** (no app StatefulSet and no headless Service). @@ -238,6 +238,8 @@ Most installs should keep the default **Deployment** (no app StatefulSet and no | ReplicaSet revision history | `revisionHistoryLimit` | Not used | | Controller annotations (not the pod) | `deploymentAnnotations` | `workload.statefulSet.annotations` | | Shared controller annotations | `commonAnnotations` | `commonAnnotations` | +| PVC claim templates | Not used | `workload.statefulSet.volumeClaimTemplates` | +| PVC retention policy | Not used | `workload.statefulSet.persistentVolumeClaimRetentionPolicy` | **StatefulSet-only settings** @@ -255,6 +257,8 @@ workload: podManagementPolicy: OrderedReady # or Parallel; omit when empty updateStrategy: type: RollingUpdate + volumeClaimTemplates: [] + persistentVolumeClaimRetentionPolicy: {} annotations: {} ``` diff --git a/charts/rhdh/templates/statefulset.yaml b/charts/rhdh/templates/statefulset.yaml index b513087a..ebfac166 100644 --- a/charts/rhdh/templates/statefulset.yaml +++ b/charts/rhdh/templates/statefulset.yaml @@ -35,4 +35,8 @@ spec: {{- include "rhdh.selectorLabels" . | nindent 6 }} template: {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} + {{- with .Values.workload.statefulSet.volumeClaimTemplates }} + volumeClaimTemplates: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 6 }} + {{- end }} {{- end }} \ No newline at end of file diff --git a/charts/rhdh/values.schema.json b/charts/rhdh/values.schema.json index d9ddc05c..d4859264 100644 --- a/charts/rhdh/values.schema.json +++ b/charts/rhdh/values.schema.json @@ -2577,6 +2577,14 @@ "default": {}, "title": "StatefulSet update strategy.", "type": "object" + }, + "volumeClaimTemplates": { + "default": [], + "items": { + "type": "object" + }, + "title": "VolumeClaimTemplates for the StatefulSet.", + "type": "array" } }, "title": "StatefulSet specific settings (ignored when kind is Deployment).", diff --git a/charts/rhdh/values.schema.tmpl.json b/charts/rhdh/values.schema.tmpl.json index 89b829e0..d8d1b839 100644 --- a/charts/rhdh/values.schema.tmpl.json +++ b/charts/rhdh/values.schema.tmpl.json @@ -445,6 +445,14 @@ "type": "object", "default": {} }, + "volumeClaimTemplates": { + "title": "VolumeClaimTemplates for the StatefulSet.", + "type": "array", + "default": [], + "items": { + "type": "object" + } + }, "annotations": { "title": "Annotations on the StatefulSet resource (not the pod).", "type": "object", diff --git a/charts/rhdh/values.yaml b/charts/rhdh/values.yaml index 80bd8289..308c42b3 100644 --- a/charts/rhdh/values.yaml +++ b/charts/rhdh/values.yaml @@ -223,6 +223,9 @@ workload: # -- Optional PVC retention policy for the StatefulSet. persistentVolumeClaimRetentionPolicy: {} + # -- StatefulSet volumeClaimTemplates. PVCs created for each pod. + volumeClaimTemplates: [] + # -- Annotations on the StatefulSet resource. annotations: {} From 3816a1630f3b799ffbad34c5653e1e4d33e44d7a Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 03:43:52 +0100 Subject: [PATCH 12/23] Small formatting fixes Signed-off-by: Leanne Ahern --- charts/rhdh/templates/statefulset.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/rhdh/templates/statefulset.yaml b/charts/rhdh/templates/statefulset.yaml index ebfac166..5e803cb7 100644 --- a/charts/rhdh/templates/statefulset.yaml +++ b/charts/rhdh/templates/statefulset.yaml @@ -37,6 +37,6 @@ spec: {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} {{- with .Values.workload.statefulSet.volumeClaimTemplates }} volumeClaimTemplates: - {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 6 }} + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} {{- end }} {{- end }} \ No newline at end of file From dfadd6620e590d6224ab39f590d1aa8188322327 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 03:48:05 +0100 Subject: [PATCH 13/23] Truncating long name - qodo bug fix Signed-off-by: Leanne Ahern --- charts/rhdh/templates/_helpers.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/rhdh/templates/_helpers.tpl b/charts/rhdh/templates/_helpers.tpl index 603b6ca2..7713e285 100644 --- a/charts/rhdh/templates/_helpers.tpl +++ b/charts/rhdh/templates/_helpers.tpl @@ -66,7 +66,7 @@ Workload kind helpers {{- end -}} {{- define "rhdh.statefulSetHeadlessServiceName" -}} -{{- printf "%s-headless" (include "rhdh.fullname" .) -}} +{{- printf "%s-headless" (include "rhdh.fullname" .) | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- define "rhdh.statefulSetServiceName" -}} From ff59fa31cd65768b17af5a922a3badff6a5a65c4 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 11:15:16 +0100 Subject: [PATCH 14/23] Removing unused helpers Signed-off-by: Leanne Ahern --- charts/rhdh/templates/_helpers.tpl | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/charts/rhdh/templates/_helpers.tpl b/charts/rhdh/templates/_helpers.tpl index 7713e285..73d3ff20 100644 --- a/charts/rhdh/templates/_helpers.tpl +++ b/charts/rhdh/templates/_helpers.tpl @@ -54,19 +54,8 @@ app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/component: backstage {{- end }} -{{/* -Workload kind helpers -*/}} -{{- define "rhdh.workloadKind" -}} -{{- default "Deployment" .Values.workload.kind -}} -{{- end -}} - -{{- define "rhdh.isStatefulSet" -}} -{{- eq (include "rhdh.workloadKind" .) "StatefulSet" -}} -{{- end -}} - {{- define "rhdh.statefulSetHeadlessServiceName" -}} -{{- printf "%s-headless" (include "rhdh.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- printf "%s-headless" (include "rhdh.fullname" .) |-}} {{- end -}} {{- define "rhdh.statefulSetServiceName" -}} From 2dfee61412cbfc46adb6900d9c8b2fae4d735399 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 12:09:10 +0100 Subject: [PATCH 15/23] Updating to .Values.workload.kind Signed-off-by: Leanne Ahern --- charts/rhdh/templates/deployment.yaml | 2 +- charts/rhdh/templates/headless-service.yaml | 2 +- charts/rhdh/templates/hpa.yaml | 2 +- charts/rhdh/templates/statefulset.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/charts/rhdh/templates/deployment.yaml b/charts/rhdh/templates/deployment.yaml index d38abe6b..37c2befb 100644 --- a/charts/rhdh/templates/deployment.yaml +++ b/charts/rhdh/templates/deployment.yaml @@ -1,4 +1,4 @@ -{{- if not (eq (include "rhdh.workloadKind" .) "StatefulSet") }} +{{- if ne .Values.workload.kind "StatefulSet" }} apiVersion: apps/v1 kind: Deployment metadata: diff --git a/charts/rhdh/templates/headless-service.yaml b/charts/rhdh/templates/headless-service.yaml index bfdbdf92..8717016d 100644 --- a/charts/rhdh/templates/headless-service.yaml +++ b/charts/rhdh/templates/headless-service.yaml @@ -1,4 +1,4 @@ -{{- if eq (include "rhdh.workloadKind" .) "StatefulSet" }} +{{- if eq .Values.workload.kind "StatefulSet" }} apiVersion: v1 kind: Service metadata: diff --git a/charts/rhdh/templates/hpa.yaml b/charts/rhdh/templates/hpa.yaml index 3e47cd49..ecf9ff25 100644 --- a/charts/rhdh/templates/hpa.yaml +++ b/charts/rhdh/templates/hpa.yaml @@ -12,7 +12,7 @@ metadata: spec: scaleTargetRef: apiVersion: apps/v1 - kind: {{ include "rhdh.workloadKind" . }} + kind: {{ .Values.workload.kind }} name: {{ include "rhdh.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} diff --git a/charts/rhdh/templates/statefulset.yaml b/charts/rhdh/templates/statefulset.yaml index 5e803cb7..29f3a231 100644 --- a/charts/rhdh/templates/statefulset.yaml +++ b/charts/rhdh/templates/statefulset.yaml @@ -1,4 +1,4 @@ -{{- if eq (include "rhdh.workloadKind" .) "StatefulSet" }} +{{- if eq .Values.workload.kind "StatefulSet" }} apiVersion: apps/v1 kind: StatefulSet metadata: From 6881ba042110a1c4403c8e920c621af79c6aacb7 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 12:40:24 +0100 Subject: [PATCH 16/23] Fixing values.yaml & adding statefulSetPVC Signed-off-by: Leanne Ahern --- charts/rhdh/README.md | 16 ++++++++++------ charts/rhdh/values.yaml | 20 ++++++++++++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index 2696c40a..77f25cb8 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -193,7 +193,7 @@ Kubernetes: `>= 1.31.0-0` | commonLabels | Labels applied to ALL chart resources. | object | `{}` | | containerSecurityContext | Security context for the main RHDH container (not the Lightspeed Core sidecar or init containers). | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true,"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}` | | deploymentAnnotations | Annotations for the Deployment resource (not the pod). | object | `{}` | -| dynamicPlugins | Dynamic plugin system configuration. | object | `{"includes":["dynamic-plugins.default.yaml"],"initContainer":{"argsOverride":[],"commandOverride":[],"extraArgs":[],"extraEnv":[],"extraVolumeMounts":[],"resources":{"limits":{"cpu":"1000m","ephemeral-storage":"5Gi","memory":"2.5Gi"},"requests":{"cpu":"250m","memory":"256Mi"}},"securityContext":{}},"maxEntrySize":40000000,"plugins":[],"volume":{"emptyDir":{},"ephemeral":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"pvc":{"claimName":""},"type":"ephemeral"}}` | +| dynamicPlugins | Dynamic plugin system configuration. | object | `{"includes":["dynamic-plugins.default.yaml"],"initContainer":{"argsOverride":[],"commandOverride":[],"extraArgs":[],"extraEnv":[],"extraVolumeMounts":[],"resources":{"limits":{"cpu":"1000m","ephemeral-storage":"5Gi","memory":"2.5Gi"},"requests":{"cpu":"250m","memory":"256Mi"}},"securityContext":{}},"maxEntrySize":40000000,"plugins":[],"volume":{"emptyDir":{},"ephemeral":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"pvc":{"claimName":""},"statefulSetPVC":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"2Gi"}},"storageClassName":""},"type":"ephemeral"}}` | | dynamicPlugins.includes | Array of YAML files listing dynamic plugins to include. Relative paths are resolved from the working directory of the initContainer (`/opt/app-root/src`). | list | `["dynamic-plugins.default.yaml"]` | | dynamicPlugins.initContainer | Configuration for the install-dynamic-plugins init container. | object | `{"argsOverride":[],"commandOverride":[],"extraArgs":[],"extraEnv":[],"extraVolumeMounts":[],"resources":{"limits":{"cpu":"1000m","ephemeral-storage":"5Gi","memory":"2.5Gi"},"requests":{"cpu":"250m","memory":"256Mi"}},"securityContext":{}}` | | dynamicPlugins.initContainer.argsOverride | Override the default arguments. Leave empty to use the defaults. | list | `[]` | @@ -205,14 +205,18 @@ Kubernetes: `>= 1.31.0-0` | dynamicPlugins.initContainer.securityContext | Security context for the init container. | object | Same as containerSecurityContext | | dynamicPlugins.maxEntrySize | Maximum uncompressed size (in bytes) of a single dynamic plugin entry. | int | `40000000` | | dynamicPlugins.plugins | List of dynamic plugins. Every item defines the plugin `package` as a NPM package spec or OCI reference. | list | `[]` | -| dynamicPlugins.volume | Volume configuration for the dynamic plugins root directory. | object | `{"emptyDir":{},"ephemeral":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"pvc":{"claimName":""},"type":"ephemeral"}` | +| dynamicPlugins.volume | Volume configuration for the dynamic plugins root directory. | object | `{"emptyDir":{},"ephemeral":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"pvc":{"claimName":""},"statefulSetPVC":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"2Gi"}},"storageClassName":""},"type":"ephemeral"}` | | dynamicPlugins.volume.emptyDir | Raw Kubernetes emptyDir volume spec. Used when type is "emptyDir". | object | `{}` | | dynamicPlugins.volume.ephemeral | Ephemeral volume configuration. Used when type is "ephemeral". The chart builds the full ephemeral.volumeClaimTemplate.spec from these fields. | object | `{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""}` | | dynamicPlugins.volume.ephemeral.accessModes | Access modes for the ephemeral PVC. | list | `["ReadWriteOnce"]` | | dynamicPlugins.volume.ephemeral.resources | Resource requests for the ephemeral PVC. | object | `{"requests":{"storage":"5Gi"}}` | | dynamicPlugins.volume.ephemeral.storageClassName | StorageClass for the ephemeral volume. When empty, uses global.defaultStorageClass or the cluster default. | string | `""` | | dynamicPlugins.volume.pvc | Raw Kubernetes persistentVolumeClaim volume spec. Used when type is "pvc". | object | `{"claimName":""}` | -| dynamicPlugins.volume.type | Volume type: "ephemeral" (auto-provisioned PVC per pod), "emptyDir" (scratch space, lost on pod restart), or "pvc" (pre-existing PersistentVolumeClaim). | string | `"ephemeral"` | +| dynamicPlugins.volume.statefulSetPVC | StatefulSet-owned PVC volume spec. Used when type is "statefulSetPVC". | object | `{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"2Gi"}},"storageClassName":""}` | +| dynamicPlugins.volume.statefulSetPVC.accessModes | Access modes for the StatefulSet-owned PVC. | list | `["ReadWriteOnce"]` | +| dynamicPlugins.volume.statefulSetPVC.resources | Resource requests for the StatefulSet-owned PVC. | object | `{"requests":{"storage":"2Gi"}}` | +| dynamicPlugins.volume.statefulSetPVC.storageClassName | StorageClass. When empty, uses global.defaultStorageClass or the cluster default. | string | `""` | +| dynamicPlugins.volume.type | Volume type: "ephemeral" (auto-provisioned PVC per pod), "emptyDir" (scratch space, lost on pod restart), "pvc" (pre-existing PersistentVolumeClaim), or "statefulSetPVC" (StatefulSet volumeClaimTemplate; requires workload.kind=StatefulSet). | string | `"ephemeral"` | | envFromOverride | Override the container envFrom entirely. When set, extraEnvFrom is ignored. Accepts raw Kubernetes envFrom entries (configMapRef, secretRef, prefix). | list | `[]` | | envOverride | Override the container environment variables entirely. When set, system env vars (BACKEND_SECRET, DB credentials, etc.) are NOT added automatically. | list | `[]` | | externalDatabase | External database connection. Used when postgresql.enabled is false. See docs/external-db.md for TLS setup and privilege requirements. When both postgresql.enabled and externalDatabase.host are false/empty, the chart renders no database env vars (BYO configuration via extraEnv or appConfig). | object | `{"existingSecretRef":{"key":"password","name":""},"host":"","port":5432,"user":"postgres"}` | @@ -312,14 +316,14 @@ Kubernetes: `>= 1.31.0-0` | test | Test pod configuration for `helm test`. | object | `{"enabled":true,"image":{"digest":"","pullPolicy":"IfNotPresent","registry":"quay.io","repository":"curl/curl","tag":"8.22.0"},"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true}}` | | tolerations | Tolerations for pod assignment. | list | `[]` | | topologySpreadConstraints | Topology spread constraints for pod scheduling. | list | `[]` | -| workload | Kubernetes workload controller for Backstage pod. | object | `{"kind":"Deployment","statefulSet":{"annotations":{},"persistentVolumeClaimRetentionPolicy":{},"podManagementPolicy":"","serviceName":"","updateStrategy":{},"volumeClaimTemplates":[]}}` | +| workload | Kubernetes workload controller for Backstage pod. | object | `{"kind":"Deployment","statefulSet":{"annotations":{},"extraVolumeClaimTemplates":[],"persistentVolumeClaimRetentionPolicy":{},"podManagementPolicy":"","serviceName":"","updateStrategy":{}}}` | | workload.kind | Workload kind: Deployment (default) or StatefulSet. | string | `"Deployment"` | | workload.statefulSet.annotations | Annotations on the StatefulSet resource. | object | `{}` | +| workload.statefulSet.extraVolumeClaimTemplates | StatefulSet extraVolumeClaimTemplates. PVCs created for each pod. | list | `[]` | | workload.statefulSet.persistentVolumeClaimRetentionPolicy | Optional PVC retention policy for the StatefulSet. | object | `{}` | | workload.statefulSet.podManagementPolicy | Pod management policy for the StatefulSet. | string | `""` | -| workload.statefulSet.serviceName | Service ({fullname}-headless) must match an existing service. | string | `""` | +| workload.statefulSet.serviceName | service name for the StatefulSet. Defaults to headless when empty. Service ({fullname}-headless) must match an existing service. | string | `""` | | workload.statefulSet.updateStrategy | StatefulSet update strategy. | object | `{}` | -| workload.statefulSet.volumeClaimTemplates | StatefulSet volumeClaimTemplates. PVCs created for each pod. | list | `[]` | ## Opinionated RHDH deployment diff --git a/charts/rhdh/values.yaml b/charts/rhdh/values.yaml index 308c42b3..c06413d0 100644 --- a/charts/rhdh/values.yaml +++ b/charts/rhdh/values.yaml @@ -124,7 +124,7 @@ dynamicPlugins: # -- Volume configuration for the dynamic plugins root directory. volume: # -- Volume type: "ephemeral" (auto-provisioned PVC per pod), "emptyDir" (scratch space, lost on pod restart), - # or "pvc" (pre-existing PersistentVolumeClaim). + # "pvc" (pre-existing PersistentVolumeClaim), or "statefulSetPVC" (StatefulSet volumeClaimTemplate; requires workload.kind=StatefulSet). type: "ephemeral" # -- Ephemeral volume configuration. Used when type is "ephemeral". # The chart builds the full ephemeral.volumeClaimTemplate.spec from these fields. @@ -144,6 +144,18 @@ dynamicPlugins: # -- Raw Kubernetes persistentVolumeClaim volume spec. Used when type is "pvc". pvc: claimName: "" + # -- StatefulSet-owned PVC volume spec. Used when type is "statefulSetPVC". + statefulSetPVC: + # -- StorageClass. When empty, uses global.defaultStorageClass or the cluster default. + storageClassName: "" + # -- Access modes for the StatefulSet-owned PVC. + accessModes: + - "ReadWriteOnce" + # -- Resource requests for the StatefulSet-owned PVC. + resources: + requests: + storage: "2Gi" + # -- Configuration for the install-dynamic-plugins init container. initContainer: # -- Override the default command. Leave empty to use the default (./install-dynamic-plugins.sh /dynamic-plugins-root). @@ -211,7 +223,7 @@ workload: statefulSet: # -- service name for the StatefulSet. Defaults to headless when empty. - # -- Service ({fullname}-headless) must match an existing service. + # Service ({fullname}-headless) must match an existing service. serviceName: "" # -- Pod management policy for the StatefulSet. @@ -223,8 +235,8 @@ workload: # -- Optional PVC retention policy for the StatefulSet. persistentVolumeClaimRetentionPolicy: {} - # -- StatefulSet volumeClaimTemplates. PVCs created for each pod. - volumeClaimTemplates: [] + # -- StatefulSet extraVolumeClaimTemplates. PVCs created for each pod. + extraVolumeClaimTemplates: [] # -- Annotations on the StatefulSet resource. annotations: {} From b0d9f580efd13d5cd41f1cd1f62718752b8a894e Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 12:58:30 +0100 Subject: [PATCH 17/23] Updating _backstage-pod-template to mount statefulSetPVC Signed-off-by: Leanne Ahern --- charts/rhdh/templates/_backstage-pod-template.tpl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/charts/rhdh/templates/_backstage-pod-template.tpl b/charts/rhdh/templates/_backstage-pod-template.tpl index 97631e2f..9f14bcdc 100644 --- a/charts/rhdh/templates/_backstage-pod-template.tpl +++ b/charts/rhdh/templates/_backstage-pod-template.tpl @@ -54,7 +54,13 @@ spec: {{- else if eq .Values.dynamicPlugins.volume.type "pvc" }} persistentVolumeClaim: {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.volume.pvc "context" $) | nindent 8 }} - {{- else }} + {{- else if eq .Values.dynamicPlugins.volume.type "statefulSetPVC"}} + {{- if ne .Values.workload.kind "StatefulSet" }} + {{- fail "dynamicPlugins.volume.type=statefulSetPVC requires workload.kind=StatefulSet"}} + {{- end }} + persistentVolumeClaim: + claimName: dynamic-plugins-root + {{- else if eq .Values.dynamicPlugins.volume.type "ephemeral" }} ephemeral: volumeClaimTemplate: spec: @@ -71,6 +77,8 @@ spec: resources: {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 14 }} {{- end }} + {{- else }} + {{- fail (printf "dynamicPlugins.volume.type must be ephemeral, emptyDir, pvc, or statefulSetPVC (got %q)" .Values.dynamicPlugins.volume.type) }} {{- end }} - name: dynamic-plugins configMap: From a52049a19f5204aa543d019b32eba7e6f27e8c5a Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 17:11:56 +0100 Subject: [PATCH 18/23] Updating statefulset.yaml Signed-off-by: Leanne Ahern --- charts/rhdh/templates/_helpers.tpl | 2 +- charts/rhdh/templates/statefulset.yaml | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/charts/rhdh/templates/_helpers.tpl b/charts/rhdh/templates/_helpers.tpl index 92476c5e..9bb51bfc 100644 --- a/charts/rhdh/templates/_helpers.tpl +++ b/charts/rhdh/templates/_helpers.tpl @@ -55,7 +55,7 @@ app.kubernetes.io/component: backstage {{- end }} {{- define "rhdh.statefulSetHeadlessServiceName" -}} -{{- printf "%s-headless" (include "rhdh.fullname" .) |-}} +{{- printf "%s-headless" (include "rhdh.fullname" .) | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- define "rhdh.statefulSetServiceName" -}} diff --git a/charts/rhdh/templates/statefulset.yaml b/charts/rhdh/templates/statefulset.yaml index 29f3a231..540fc8fc 100644 --- a/charts/rhdh/templates/statefulset.yaml +++ b/charts/rhdh/templates/statefulset.yaml @@ -35,8 +35,30 @@ spec: {{- include "rhdh.selectorLabels" . | nindent 6 }} template: {{- include "rhdh.backstagePodTemplate" . | nindent 4 }} - {{- with .Values.workload.statefulSet.volumeClaimTemplates }} + {{- $injectPluginsPVC := eq .Values.dynamicPlugins.volume.type "statefulSetPVC" }} + {{- $extraVCTs := .Values.workload.statefulSet.extraVolumeClaimTemplates | default list }} + {{- if or $injectPluginsPVC $extraVCTs }} volumeClaimTemplates: + {{- if $injectPluginsPVC }} + - metadata: + name: dynamic-plugins-root + spec: + {{- $persistence := dict "storageClass" (.Values.dynamicPlugins.volume.statefulSetPVC.storageClassName | default "") }} + {{- $sc := include "common.storage.class" (dict "persistence" $persistence "global" .Values.global) }} + {{- if $sc }} + {{ $sc }} + {{- end }} + {{- with .Values.dynamicPlugins.volume.statefulSetPVC.accessModes }} + accessModes: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 10 }} + {{- end }} + {{- with .Values.dynamicPlugins.volume.statefulSetPVC.resources }} + resources: + {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 10 }} + {{- end }} + {{- end }} + {{- with $extraVCTs }} {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 4 }} + {{- end }} {{- end }} {{- end }} \ No newline at end of file From 3341e03d44d157e22afc318183b53dc29a18d1a5 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 22:37:12 +0100 Subject: [PATCH 19/23] Updating values schema and README Signed-off-by: Leanne Ahern --- charts/rhdh/README.md | 36 +++++++++++--- charts/rhdh/README.md.gotmpl | 28 +++++++++-- charts/rhdh/values.schema.json | 75 +++++++++++++++++++++++++---- charts/rhdh/values.schema.tmpl.json | 29 +++++++++-- charts/rhdh/values.yaml | 2 +- 5 files changed, 147 insertions(+), 23 deletions(-) diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index 964c6201..831a6759 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -193,7 +193,7 @@ Kubernetes: `>= 1.31.0-0` | commonLabels | Labels applied to ALL chart resources. | object | `{}` | | containerSecurityContext | Security context for the main RHDH container (not the Lightspeed Core sidecar or init containers). | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"readOnlyRootFilesystem":true,"runAsNonRoot":true,"seccompProfile":{"type":"RuntimeDefault"}}` | | deploymentAnnotations | Annotations for the Deployment resource (not the pod). | object | `{}` | -| dynamicPlugins | Dynamic plugin system configuration. | object | `{"includes":["dynamic-plugins.default.yaml"],"initContainer":{"argsOverride":[],"commandOverride":[],"extraArgs":[],"extraEnv":[],"extraVolumeMounts":[],"resources":{"limits":{"cpu":"1000m","ephemeral-storage":"5Gi","memory":"2.5Gi"},"requests":{"cpu":"250m","memory":"256Mi"}},"securityContext":{}},"maxEntrySize":40000000,"plugins":[],"volume":{"emptyDir":{},"ephemeral":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"pvc":{"claimName":""},"statefulSetPVC":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"2Gi"}},"storageClassName":""},"type":"ephemeral"}}` | +| dynamicPlugins | Dynamic plugin system configuration. | object | `{"includes":["dynamic-plugins.default.yaml"],"initContainer":{"argsOverride":[],"commandOverride":[],"extraArgs":[],"extraEnv":[],"extraVolumeMounts":[],"resources":{"limits":{"cpu":"1000m","ephemeral-storage":"5Gi","memory":"2.5Gi"},"requests":{"cpu":"250m","memory":"256Mi"}},"securityContext":{}},"maxEntrySize":40000000,"plugins":[],"volume":{"emptyDir":{},"ephemeral":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"pvc":{"claimName":""},"statefulSetPVC":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"type":"ephemeral"}}` | | dynamicPlugins.includes | Array of YAML files listing dynamic plugins to include. Relative paths are resolved from the working directory of the initContainer (`/opt/app-root/src`). | list | `["dynamic-plugins.default.yaml"]` | | dynamicPlugins.initContainer | Configuration for the install-dynamic-plugins init container. | object | `{"argsOverride":[],"commandOverride":[],"extraArgs":[],"extraEnv":[],"extraVolumeMounts":[],"resources":{"limits":{"cpu":"1000m","ephemeral-storage":"5Gi","memory":"2.5Gi"},"requests":{"cpu":"250m","memory":"256Mi"}},"securityContext":{}}` | | dynamicPlugins.initContainer.argsOverride | Override the default arguments. Leave empty to use the defaults. | list | `[]` | @@ -205,16 +205,16 @@ Kubernetes: `>= 1.31.0-0` | dynamicPlugins.initContainer.securityContext | Security context for the init container. | object | Same as containerSecurityContext | | dynamicPlugins.maxEntrySize | Maximum uncompressed size (in bytes) of a single dynamic plugin entry. | int | `40000000` | | dynamicPlugins.plugins | List of dynamic plugins. Every item defines the plugin `package` as a NPM package spec or OCI reference. | list | `[]` | -| dynamicPlugins.volume | Volume configuration for the dynamic plugins root directory. | object | `{"emptyDir":{},"ephemeral":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"pvc":{"claimName":""},"statefulSetPVC":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"2Gi"}},"storageClassName":""},"type":"ephemeral"}` | +| dynamicPlugins.volume | Volume configuration for the dynamic plugins root directory. | object | `{"emptyDir":{},"ephemeral":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"pvc":{"claimName":""},"statefulSetPVC":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""},"type":"ephemeral"}` | | dynamicPlugins.volume.emptyDir | Raw Kubernetes emptyDir volume spec. Used when type is "emptyDir". | object | `{}` | | dynamicPlugins.volume.ephemeral | Ephemeral volume configuration. Used when type is "ephemeral". The chart builds the full ephemeral.volumeClaimTemplate.spec from these fields. | object | `{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""}` | | dynamicPlugins.volume.ephemeral.accessModes | Access modes for the ephemeral PVC. | list | `["ReadWriteOnce"]` | | dynamicPlugins.volume.ephemeral.resources | Resource requests for the ephemeral PVC. | object | `{"requests":{"storage":"5Gi"}}` | | dynamicPlugins.volume.ephemeral.storageClassName | StorageClass for the ephemeral volume. When empty, uses global.defaultStorageClass or the cluster default. | string | `""` | | dynamicPlugins.volume.pvc | Raw Kubernetes persistentVolumeClaim volume spec. Used when type is "pvc". | object | `{"claimName":""}` | -| dynamicPlugins.volume.statefulSetPVC | StatefulSet-owned PVC volume spec. Used when type is "statefulSetPVC". | object | `{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"2Gi"}},"storageClassName":""}` | +| dynamicPlugins.volume.statefulSetPVC | StatefulSet-owned PVC volume spec. Used when type is "statefulSetPVC". | object | `{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"5Gi"}},"storageClassName":""}` | | dynamicPlugins.volume.statefulSetPVC.accessModes | Access modes for the StatefulSet-owned PVC. | list | `["ReadWriteOnce"]` | -| dynamicPlugins.volume.statefulSetPVC.resources | Resource requests for the StatefulSet-owned PVC. | object | `{"requests":{"storage":"2Gi"}}` | +| dynamicPlugins.volume.statefulSetPVC.resources | Resource requests for the StatefulSet-owned PVC. | object | `{"requests":{"storage":"5Gi"}}` | | dynamicPlugins.volume.statefulSetPVC.storageClassName | StorageClass. When empty, uses global.defaultStorageClass or the cluster default. | string | `""` | | dynamicPlugins.volume.type | Volume type: "ephemeral" (auto-provisioned PVC per pod), "emptyDir" (scratch space, lost on pod restart), "pvc" (pre-existing PersistentVolumeClaim), or "statefulSetPVC" (StatefulSet volumeClaimTemplate; requires workload.kind=StatefulSet). | string | `"ephemeral"` | | envFromOverride | Override the container envFrom entirely. When set, extraEnvFrom is ignored. Accepts raw Kubernetes envFrom entries (configMapRef, secretRef, prefix). | list | `[]` | @@ -389,7 +389,7 @@ Both kinds render the **same** Backstage pod (containers, volumes, probes, dynam **When to use StatefulSet** -- You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, `volumeClaimTemplates`, or optional PVC retention policy). +- You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, `extraVolumeClaimTemplates`, or optional PVC retention policy). - You want parity with the [RHDH Operator](https://github.com/redhat-developer/rhdh-operator), which supports `spec.deployment.kind: StatefulSet` ([operator documentation](https://github.com/redhat-developer/rhdh-operator/blob/main/docs/configuration.md#deployment-kind)). Most installs should keep the default **Deployment** (no app StatefulSet and no headless Service). @@ -406,7 +406,7 @@ Most installs should keep the default **Deployment** (no app StatefulSet and no | ReplicaSet revision history | `revisionHistoryLimit` | Not used | | Controller annotations (not the pod) | `deploymentAnnotations` | `workload.statefulSet.annotations` | | Shared controller annotations | `commonAnnotations` | `commonAnnotations` | -| PVC claim templates | Not used | `workload.statefulSet.volumeClaimTemplates` | +| PVC claim templates | Not used | `workload.statefulSet.extraVolumeClaimTemplates` | | PVC retention policy | Not used | `workload.statefulSet.persistentVolumeClaimRetentionPolicy` | **StatefulSet-only settings** @@ -425,11 +425,33 @@ workload: podManagementPolicy: OrderedReady # or Parallel; omit when empty updateStrategy: type: RollingUpdate - volumeClaimTemplates: [] + extraVolumeClaimTemplates: [] persistentVolumeClaimRetentionPolicy: {} annotations: {} ``` +**Stable dynamic-plugins storage** + +To give each pod a StatefulSet-owned PVC for `dynamic-plugins-root` (Operator parity): + +```yaml +workload: + kind: StatefulSet +dynamicPlugins: + volume: + type: statefulSetPVC + statefulSetPVC: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +``` + +The PVCs are created for each pod in the StatefulSet and are named `{fullname}-dynamic-plugins-root-{0..N-1}`. + +To add extra PVCs for other dynamic plugins, use `workload.statefulSet.extraVolumeClaimTemplates`. + **Changing `workload.kind` on an existing release** For a normal Helm-managed release, changing `workload.kind` and running `helm upgrade` replaces the rendered manifest: Helm removes the old workload kind (Deployment or StatefulSet) and creates the new one. Expect a brief cutover while pods are recreated. If you applied manifests outside Helm (for example GitOps that keeps both objects) or an old controller was left behind, delete the obsolete Deployment or StatefulSet so two controllers do not target the same selector. diff --git a/charts/rhdh/README.md.gotmpl b/charts/rhdh/README.md.gotmpl index e1757c18..f3e11740 100644 --- a/charts/rhdh/README.md.gotmpl +++ b/charts/rhdh/README.md.gotmpl @@ -221,7 +221,7 @@ Both kinds render the **same** Backstage pod (containers, volumes, probes, dynam **When to use StatefulSet** -- You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, `volumeClaimTemplates`, or optional PVC retention policy). +- You need StatefulSet-specific settings (`workload.statefulSet.updateStrategy`, `podManagementPolicy`, `extraVolumeClaimTemplates`, or optional PVC retention policy). - You want parity with the [RHDH Operator](https://github.com/redhat-developer/rhdh-operator), which supports `spec.deployment.kind: StatefulSet` ([operator documentation](https://github.com/redhat-developer/rhdh-operator/blob/main/docs/configuration.md#deployment-kind)). Most installs should keep the default **Deployment** (no app StatefulSet and no headless Service). @@ -238,7 +238,7 @@ Most installs should keep the default **Deployment** (no app StatefulSet and no | ReplicaSet revision history | `revisionHistoryLimit` | Not used | | Controller annotations (not the pod) | `deploymentAnnotations` | `workload.statefulSet.annotations` | | Shared controller annotations | `commonAnnotations` | `commonAnnotations` | -| PVC claim templates | Not used | `workload.statefulSet.volumeClaimTemplates` | +| PVC claim templates | Not used | `workload.statefulSet.extraVolumeClaimTemplates` | | PVC retention policy | Not used | `workload.statefulSet.persistentVolumeClaimRetentionPolicy` | **StatefulSet-only settings** @@ -257,11 +257,33 @@ workload: podManagementPolicy: OrderedReady # or Parallel; omit when empty updateStrategy: type: RollingUpdate - volumeClaimTemplates: [] + extraVolumeClaimTemplates: [] persistentVolumeClaimRetentionPolicy: {} annotations: {} ``` +**Stable dynamic-plugins storage** + +To give each pod a StatefulSet-owned PVC for `dynamic-plugins-root` (Operator parity): + +```yaml +workload: + kind: StatefulSet +dynamicPlugins: + volume: + type: statefulSetPVC + statefulSetPVC: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +``` + +The PVCs are created for each pod in the StatefulSet and are named `{fullname}-dynamic-plugins-root-{0..N-1}`. + +To add extra PVCs for other dynamic plugins, use `workload.statefulSet.extraVolumeClaimTemplates`. + **Changing `workload.kind` on an existing release** For a normal Helm-managed release, changing `workload.kind` and running `helm upgrade` replaces the rendered manifest: Helm removes the old workload kind (Deployment or StatefulSet) and creates the new one. Expect a brief cutover while pods are recreated. If you applied manifests outside Helm (for example GitOps that keeps both objects) or an old controller was left behind, delete the obsolete Deployment or StatefulSet so two controllers do not target the same selector. diff --git a/charts/rhdh/values.schema.json b/charts/rhdh/values.schema.json index 82ca3c6e..7cbe2a1e 100644 --- a/charts/rhdh/values.schema.json +++ b/charts/rhdh/values.schema.json @@ -411,12 +411,69 @@ "title": "Raw Kubernetes persistentVolumeClaim volume spec. Used when type is pvc.", "type": "object" }, + "statefulSetPVC": { + "additionalProperties": false, + "properties": { + "accessModes": { + "default": [ + "ReadWriteOnce" + ], + "items": { + "type": "string" + }, + "title": "Access modes for the StatefulSet PVC.", + "type": "array" + }, + "resources": { + "description": "VolumeResourceRequirements describes the storage resource requirements for a volume.", + "properties": { + "limits": { + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "description": "Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + }, + "requests": { + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "description": "Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + } + }, + "type": "object" + }, + "storageClassName": { + "default": "", + "title": "StorageClass for the StatefulSet PVC. When empty, uses global.defaultStorageClass or the cluster default.", + "type": "string" + } + }, + "title": "StatefulSet volumeClaimTemplate settings for dynamic-plugins-root. Used when type is statefulSetPVC. Requires workload.kind=StatefulSet.", + "type": "object" + }, "type": { "default": "ephemeral", "enum": [ "ephemeral", "emptyDir", - "pvc" + "pvc", + "statefulSetPVC" ], "title": "Volume type.", "type": "string" @@ -2564,6 +2621,14 @@ "title": "Annotations on the StatefulSet resource (not the pod).", "type": "object" }, + "extraVolumeClaimTemplates": { + "default": [], + "items": { + "type": "object" + }, + "title": "Extra VolumeClaimTemplates appended after the chart-managed dynamic-plugins-root claim.", + "type": "array" + }, "persistentVolumeClaimRetentionPolicy": { "default": {}, "title": "Optional PVC retention policy for the StatefulSet.", @@ -2583,14 +2648,6 @@ "default": {}, "title": "StatefulSet update strategy.", "type": "object" - }, - "volumeClaimTemplates": { - "default": [], - "items": { - "type": "object" - }, - "title": "VolumeClaimTemplates for the StatefulSet.", - "type": "array" } }, "title": "StatefulSet specific settings (ignored when kind is Deployment).", diff --git a/charts/rhdh/values.schema.tmpl.json b/charts/rhdh/values.schema.tmpl.json index c65b0051..7dd16a8d 100644 --- a/charts/rhdh/values.schema.tmpl.json +++ b/charts/rhdh/values.schema.tmpl.json @@ -445,8 +445,8 @@ "type": "object", "default": {} }, - "volumeClaimTemplates": { - "title": "VolumeClaimTemplates for the StatefulSet.", + "extraVolumeClaimTemplates": { + "title": "Extra VolumeClaimTemplates appended after the chart-managed dynamic-plugins-root claim.", "type": "array", "default": [], "items": { @@ -676,7 +676,7 @@ "type": { "title": "Volume type.", "type": "string", - "enum": ["ephemeral", "emptyDir", "pvc"], + "enum": ["ephemeral", "emptyDir", "pvc", "statefulSetPVC"], "default": "ephemeral" }, "ephemeral": { @@ -709,6 +709,29 @@ "pvc": { "title": "Raw Kubernetes persistentVolumeClaim volume spec. Used when type is pvc.", "type": "object" + }, + "statefulSetPVC": { + "title": "StatefulSet volumeClaimTemplate settings for dynamic-plugins-root. Used when type is statefulSetPVC. Requires workload.kind=StatefulSet.", + "type": "object", + "additionalProperties": false, + "properties": { + "storageClassName": { + "title": "StorageClass for the StatefulSet PVC. When empty, uses global.defaultStorageClass or the cluster default.", + "type": "string", + "default": "" + }, + "accessModes": { + "title": "Access modes for the StatefulSet PVC.", + "type": "array", + "items": { "type": "string" }, + "default": ["ReadWriteOnce"] + }, + "resources": { + "title": "Resource requests for the StatefulSet PVC.", + "$ref": "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.4/_definitions.json#/definitions/io.k8s.api.core.v1.VolumeResourceRequirements", + "default": { "requests": { "storage": "5Gi" } } + } + } } } }, diff --git a/charts/rhdh/values.yaml b/charts/rhdh/values.yaml index 1135818f..40b6c355 100644 --- a/charts/rhdh/values.yaml +++ b/charts/rhdh/values.yaml @@ -154,7 +154,7 @@ dynamicPlugins: # -- Resource requests for the StatefulSet-owned PVC. resources: requests: - storage: "2Gi" + storage: "5Gi" # -- Configuration for the install-dynamic-plugins init container. initContainer: From 7fd570a7ad81a41b671272ae27216f4316061c91 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 22:39:18 +0100 Subject: [PATCH 20/23] Updating ci to include plugins VCT Signed-off-by: Leanne Ahern --- charts/rhdh/ci/with-statefulset-values.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/charts/rhdh/ci/with-statefulset-values.yaml b/charts/rhdh/ci/with-statefulset-values.yaml index 2a312e3f..8bd0feea 100644 --- a/charts/rhdh/ci/with-statefulset-values.yaml +++ b/charts/rhdh/ci/with-statefulset-values.yaml @@ -1,6 +1,8 @@ -# CI: render/install with workload.kind=StatefulSet (expects headless Service + StatefulSet). +# CI: render/install with workload.kind=StatefulSet (expects headless Service + StatefulSet + plugins VCT). dynamicPlugins: includes: [] + volume: + type: statefulSetPVC workload: kind: StatefulSet intelligentAssistant: From f61240fe0f287b6817d548205e9dbc51b883fa9d Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 22:51:52 +0100 Subject: [PATCH 21/23] Making fixes after review Signed-off-by: Leanne Ahern --- charts/rhdh/README.md | 6 +++--- charts/rhdh/README.md.gotmpl | 6 +++--- charts/rhdh/templates/_backstage-pod-template.tpl | 4 ++-- charts/rhdh/values.yaml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index 831a6759..cdeeb854 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -432,7 +432,7 @@ workload: **Stable dynamic-plugins storage** -To give each pod a StatefulSet-owned PVC for `dynamic-plugins-root` (Operator parity): +To give each pod a StatefulSet-owned PVC for `dynamic-plugins-root`: ```yaml workload: @@ -448,9 +448,9 @@ dynamicPlugins: storage: 5Gi ``` -The PVCs are created for each pod in the StatefulSet and are named `{fullname}-dynamic-plugins-root-{0..N-1}`. +The chart injects a `volumeClaimTemplate` named `dynamic-plugins-root` and mounts it on the pod. Kubernetes creates one PVC per pod named `dynamic-plugins-root-{fullname}-{ordinal}` (for example `dynamic-plugins-root-my-rhdh-redhat-developer-hub-0`). This requires `workload.kind=StatefulSet`. -To add extra PVCs for other dynamic plugins, use `workload.statefulSet.extraVolumeClaimTemplates`. +To add additional StatefulSet-owned PVCs, use `workload.statefulSet.extraVolumeClaimTemplates`. **Changing `workload.kind` on an existing release** diff --git a/charts/rhdh/README.md.gotmpl b/charts/rhdh/README.md.gotmpl index f3e11740..a36ab40e 100644 --- a/charts/rhdh/README.md.gotmpl +++ b/charts/rhdh/README.md.gotmpl @@ -264,7 +264,7 @@ workload: **Stable dynamic-plugins storage** -To give each pod a StatefulSet-owned PVC for `dynamic-plugins-root` (Operator parity): +To give each pod a StatefulSet-owned PVC for `dynamic-plugins-root`: ```yaml workload: @@ -280,9 +280,9 @@ dynamicPlugins: storage: 5Gi ``` -The PVCs are created for each pod in the StatefulSet and are named `{fullname}-dynamic-plugins-root-{0..N-1}`. +The chart injects a `volumeClaimTemplate` named `dynamic-plugins-root` and mounts it on the pod. Kubernetes creates one PVC per pod named `dynamic-plugins-root-{fullname}-{ordinal}` (for example `dynamic-plugins-root-my-rhdh-redhat-developer-hub-0`). This requires `workload.kind=StatefulSet`. -To add extra PVCs for other dynamic plugins, use `workload.statefulSet.extraVolumeClaimTemplates`. +To add additional StatefulSet-owned PVCs, use `workload.statefulSet.extraVolumeClaimTemplates`. **Changing `workload.kind` on an existing release** diff --git a/charts/rhdh/templates/_backstage-pod-template.tpl b/charts/rhdh/templates/_backstage-pod-template.tpl index 9f14bcdc..a8dfb351 100644 --- a/charts/rhdh/templates/_backstage-pod-template.tpl +++ b/charts/rhdh/templates/_backstage-pod-template.tpl @@ -54,9 +54,9 @@ spec: {{- else if eq .Values.dynamicPlugins.volume.type "pvc" }} persistentVolumeClaim: {{- include "common.tplvalues.render" (dict "value" .Values.dynamicPlugins.volume.pvc "context" $) | nindent 8 }} - {{- else if eq .Values.dynamicPlugins.volume.type "statefulSetPVC"}} + {{- else if eq .Values.dynamicPlugins.volume.type "statefulSetPVC" }} {{- if ne .Values.workload.kind "StatefulSet" }} - {{- fail "dynamicPlugins.volume.type=statefulSetPVC requires workload.kind=StatefulSet"}} + {{- fail "dynamicPlugins.volume.type=statefulSetPVC requires workload.kind=StatefulSet" }} {{- end }} persistentVolumeClaim: claimName: dynamic-plugins-root diff --git a/charts/rhdh/values.yaml b/charts/rhdh/values.yaml index 40b6c355..fe10edca 100644 --- a/charts/rhdh/values.yaml +++ b/charts/rhdh/values.yaml @@ -155,7 +155,7 @@ dynamicPlugins: resources: requests: storage: "5Gi" - + # -- Configuration for the install-dynamic-plugins init container. initContainer: # -- Override the default command. Leave empty to use the default (./install-dynamic-plugins.sh /dynamic-plugins-root). From 4bb2dc5d37d0a91f47ef73145b8a4c6f5e0a8589 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Mon, 21 Sep 2026 23:27:29 +0100 Subject: [PATCH 22/23] Fixing comments Signed-off-by: Leanne Ahern --- charts/rhdh/README.md | 2 +- charts/rhdh/values.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index cdeeb854..7c11d1aa 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -320,7 +320,7 @@ Kubernetes: `>= 1.31.0-0` | workload | Kubernetes workload controller for Backstage pod. | object | `{"kind":"Deployment","statefulSet":{"annotations":{},"extraVolumeClaimTemplates":[],"persistentVolumeClaimRetentionPolicy":{},"podManagementPolicy":"","serviceName":"","updateStrategy":{}}}` | | workload.kind | Workload kind: Deployment (default) or StatefulSet. | string | `"Deployment"` | | workload.statefulSet.annotations | Annotations on the StatefulSet resource. | object | `{}` | -| workload.statefulSet.extraVolumeClaimTemplates | StatefulSet extraVolumeClaimTemplates. PVCs created for each pod. | list | `[]` | +| workload.statefulSet.extraVolumeClaimTemplates | Extra volumeClaimTemplates appended after the chart-managed dynamic-plugins-root claim (when type=statefulSetPVC). | list | `[]` | | workload.statefulSet.persistentVolumeClaimRetentionPolicy | Optional PVC retention policy for the StatefulSet. | object | `{}` | | workload.statefulSet.podManagementPolicy | Pod management policy for the StatefulSet. | string | `""` | | workload.statefulSet.serviceName | service name for the StatefulSet. Defaults to headless when empty. Service ({fullname}-headless) must match an existing service. | string | `""` | diff --git a/charts/rhdh/values.yaml b/charts/rhdh/values.yaml index fe10edca..df7c020c 100644 --- a/charts/rhdh/values.yaml +++ b/charts/rhdh/values.yaml @@ -235,7 +235,7 @@ workload: # -- Optional PVC retention policy for the StatefulSet. persistentVolumeClaimRetentionPolicy: {} - # -- StatefulSet extraVolumeClaimTemplates. PVCs created for each pod. + # -- Extra volumeClaimTemplates appended after the chart-managed dynamic-plugins-root claim (when type=statefulSetPVC). extraVolumeClaimTemplates: [] # -- Annotations on the StatefulSet resource. From 9d9363f9640481a62818170b2d1deb501d987898 Mon Sep 17 00:00:00 2001 From: Leanne Ahern Date: Tue, 22 Sep 2026 12:54:28 +0100 Subject: [PATCH 23/23] Applying suggestions Signed-off-by: Leanne Ahern --- charts/rhdh/templates/_backstage-pod-template.tpl | 2 -- charts/rhdh/templates/deployment.yaml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/charts/rhdh/templates/_backstage-pod-template.tpl b/charts/rhdh/templates/_backstage-pod-template.tpl index a8dfb351..ca8fbf1c 100644 --- a/charts/rhdh/templates/_backstage-pod-template.tpl +++ b/charts/rhdh/templates/_backstage-pod-template.tpl @@ -77,8 +77,6 @@ spec: resources: {{- include "common.tplvalues.render" (dict "value" . "context" $) | nindent 14 }} {{- end }} - {{- else }} - {{- fail (printf "dynamicPlugins.volume.type must be ephemeral, emptyDir, pvc, or statefulSetPVC (got %q)" .Values.dynamicPlugins.volume.type) }} {{- end }} - name: dynamic-plugins configMap: diff --git a/charts/rhdh/templates/deployment.yaml b/charts/rhdh/templates/deployment.yaml index 37c2befb..a107667a 100644 --- a/charts/rhdh/templates/deployment.yaml +++ b/charts/rhdh/templates/deployment.yaml @@ -1,4 +1,4 @@ -{{- if ne .Values.workload.kind "StatefulSet" }} +{{- if eq .Values.workload.kind "Deployment" }} apiVersion: apps/v1 kind: Deployment metadata: