diff --git a/charts/rhdh/Chart.yaml b/charts/rhdh/Chart.yaml index 14a69e6b..48b05a76 100644 --- a/charts/rhdh/Chart.yaml +++ b/charts/rhdh/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: redhat-developer-hub type: application -version: 3.0.0 +version: 3.1.0 appVersion: 2.1.0 annotations: artifacthub.io/category: integration-delivery diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index 226f8d7f..1f2edb81 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -1,7 +1,7 @@ # RHDH Helm Chart for OpenShift and Kubernetes -![Version: 3.0.0](https://img.shields.io/badge/Version-3.0.0-informational?style=flat-square) +![Version: 3.1.0](https://img.shields.io/badge/Version-3.1.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 3.0.0 +helm install my-rhdh redhat-developer/redhat-developer-hub --version 3.1.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 @@ -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":"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,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":"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.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":"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":"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 | `[]` | | 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"}` | @@ -313,6 +317,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":{},"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 | 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 | `""` | +| workload.statefulSet.updateStrategy | StatefulSet update strategy. | object | `{}` | ## Opinionated RHDH deployment @@ -351,7 +363,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 @@ -363,6 +375,94 @@ 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`). To use a **StatefulSet** instead: + +```yaml +# values.yaml +workload: + kind: StatefulSet +``` + +Both kinds render the **same** Backstage pod (containers, volumes, probes, dynamic plugins, Intelligent Assistant, init containers). Only the workload controller changes; the pod definition is shared. + +**When to use StatefulSet** + +- 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). + +**Values that behave the same for both kinds** + +`replicaCount`, `autoscaling`, probes, `resources`, `dynamicPlugins`, `extraVolumes`, `podLabels`, `podAnnotations`, and other pod-level settings apply regardless of `workload.kind`. When `autoscaling.enabled` is `true`, the HorizontalPodAutoscaler scales the selected kind (`Deployment` or `StatefulSet`). + +**Values that depend on the workload kind** + +| Helm value | Deployment | StatefulSet | +| ---------- | ---------- | ----------- | +| Update strategy | `strategy` | `workload.statefulSet.updateStrategy` | +| 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.extraVolumeClaimTemplates` | +| PVC retention policy | Not used | `workload.statefulSet.persistentVolumeClaimRetentionPolicy` | + +**StatefulSet-only settings** + +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: {fullname}-headless + podManagementPolicy: OrderedReady # or Parallel; omit when empty + updateStrategy: + type: RollingUpdate + extraVolumeClaimTemplates: [] + persistentVolumeClaimRetentionPolicy: {} + annotations: {} +``` + +**Stable dynamic-plugins storage** + +To give each pod a StatefulSet-owned PVC for `dynamic-plugins-root`: + +```yaml +workload: + kind: StatefulSet +dynamicPlugins: + volume: + type: statefulSetPVC + statefulSetPVC: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +``` + +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 additional StatefulSet-owned PVCs, 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. + +**Example install** + +```console +helm upgrade -i my-rhdh redhat-developer/redhat-developer-hub \ + --set 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. @@ -459,7 +559,7 @@ Use `intelligentAssistant.runtimeVolume` to change the writable `/tmp` runtime s When using the built-in Intelligent Assistant feature, do not also keep those plugin packages in `dynamicPlugins.plugins`. Existing installations that previously configured Lightspeed or Intelligent Assistant there should remove those entries if the built-in defaults are sufficient, or move their custom package definitions to `intelligentAssistant.plugins`; otherwise the rendered `dynamic-plugins.yaml` will contain duplicate plugin entries. -The Lightspeed Core sidecar loads `intelligentAssistant.existingSecret` as environment variables. If you update that Secret outside of Helm, Kubernetes does not guarantee that the Backstage Pod restarts automatically. Use a no-op `helm upgrade` or manually restart the Backstage deployment after changing the secret data. +The Lightspeed Core sidecar loads `intelligentAssistant.existingSecret` as environment variables. If you update that Secret outside of Helm, Kubernetes does not guarantee that the Backstage Pod restarts automatically. Use a no-op `helm upgrade` or manually restart the Backstage workload (Deployment or StatefulSet) after changing the secret data. RHDH documentation retrieval is disabled by default in Intelligent Assistant. To enable it, set `intelligentAssistant.okp.enabled=true`. This deploys the OKP workload and uses the OKP-enabled Lightspeed Stack configuration. The OKP container image is large, so enabling it can significantly increase the initial installation time while the image is downloaded. On vanilla Kubernetes, you must also configure the OKP Ingress and Red Hat registry credentials. See [Intelligent Assistant and OKP integration](../../docs/intelligent-assistant.md) for platform-specific configuration. diff --git a/charts/rhdh/README.md.gotmpl b/charts/rhdh/README.md.gotmpl index 9d05ac15..a36ab40e 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,94 @@ 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`). To use a **StatefulSet** instead: + +```yaml +# values.yaml +workload: + kind: StatefulSet +``` + +Both kinds render the **same** Backstage pod (containers, volumes, probes, dynamic plugins, Intelligent Assistant, init containers). Only the workload controller changes; the pod definition is shared. + +**When to use StatefulSet** + +- 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). + +**Values that behave the same for both kinds** + +`replicaCount`, `autoscaling`, probes, `resources`, `dynamicPlugins`, `extraVolumes`, `podLabels`, `podAnnotations`, and other pod-level settings apply regardless of `workload.kind`. When `autoscaling.enabled` is `true`, the HorizontalPodAutoscaler scales the selected kind (`Deployment` or `StatefulSet`). + +**Values that depend on the workload kind** + +| Helm value | Deployment | StatefulSet | +| ---------- | ---------- | ----------- | +| Update strategy | `strategy` | `workload.statefulSet.updateStrategy` | +| 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.extraVolumeClaimTemplates` | +| PVC retention policy | Not used | `workload.statefulSet.persistentVolumeClaimRetentionPolicy` | + +**StatefulSet-only settings** + +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: {fullname}-headless + podManagementPolicy: OrderedReady # or Parallel; omit when empty + updateStrategy: + type: RollingUpdate + extraVolumeClaimTemplates: [] + persistentVolumeClaimRetentionPolicy: {} + annotations: {} +``` + +**Stable dynamic-plugins storage** + +To give each pod a StatefulSet-owned PVC for `dynamic-plugins-root`: + +```yaml +workload: + kind: StatefulSet +dynamicPlugins: + volume: + type: statefulSetPVC + statefulSetPVC: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +``` + +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 additional StatefulSet-owned PVCs, 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. + +**Example install** + +```console +helm upgrade -i my-rhdh redhat-developer/redhat-developer-hub \ + --set 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. @@ -303,7 +391,7 @@ Use `intelligentAssistant.runtimeVolume` to change the writable `/tmp` runtime s When using the built-in Intelligent Assistant feature, do not also keep those plugin packages in `dynamicPlugins.plugins`. Existing installations that previously configured Lightspeed or Intelligent Assistant there should remove those entries if the built-in defaults are sufficient, or move their custom package definitions to `intelligentAssistant.plugins`; otherwise the rendered `dynamic-plugins.yaml` will contain duplicate plugin entries. -The Lightspeed Core sidecar loads `intelligentAssistant.existingSecret` as environment variables. If you update that Secret outside of Helm, Kubernetes does not guarantee that the Backstage Pod restarts automatically. Use a no-op `helm upgrade` or manually restart the Backstage deployment after changing the secret data. +The Lightspeed Core sidecar loads `intelligentAssistant.existingSecret` as environment variables. If you update that Secret outside of Helm, Kubernetes does not guarantee that the Backstage Pod restarts automatically. Use a no-op `helm upgrade` or manually restart the Backstage workload (Deployment or StatefulSet) after changing the secret data. RHDH documentation retrieval is disabled by default in Intelligent Assistant. To enable it, set `intelligentAssistant.okp.enabled=true`. This deploys the OKP workload and uses the OKP-enabled Lightspeed Stack configuration. The OKP container image is large, so enabling it can significantly increase the initial installation time while the image is downloaded. On vanilla Kubernetes, you must also configure the OKP Ingress and Red Hat registry credentials. See [Intelligent Assistant and OKP integration](../../docs/intelligent-assistant.md) for platform-specific configuration. diff --git a/charts/rhdh/ci/with-statefulset-values.yaml b/charts/rhdh/ci/with-statefulset-values.yaml new file mode 100644 index 00000000..8bd0feea --- /dev/null +++ b/charts/rhdh/ci/with-statefulset-values.yaml @@ -0,0 +1,16 @@ +# CI: render/install with workload.kind=StatefulSet (expects headless Service + StatefulSet + plugins VCT). +dynamicPlugins: + includes: [] + volume: + type: statefulSetPVC +workload: + kind: StatefulSet +intelligentAssistant: + plugins: [] + okp: + route: + enabled: false + ingress: + enabled: false +orchestrator: + plugins: [] diff --git a/charts/rhdh/templates/_backstage-pod-template.tpl b/charts/rhdh/templates/_backstage-pod-template.tpl new file mode 100644 index 00000000..ca8fbf1c --- /dev/null +++ b/charts/rhdh/templates/_backstage-pod-template.tpl @@ -0,0 +1,455 @@ +{{- 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 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: + {{- $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 -}} diff --git a/charts/rhdh/templates/_helpers.tpl b/charts/rhdh/templates/_helpers.tpl index de88be3d..9bb51bfc 100644 --- a/charts/rhdh/templates/_helpers.tpl +++ b/charts/rhdh/templates/_helpers.tpl @@ -54,6 +54,14 @@ app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/component: backstage {{- end }} +{{- define "rhdh.statefulSetHeadlessServiceName" -}} +{{- printf "%s-headless" (include "rhdh.fullname" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "rhdh.statefulSetServiceName" -}} +{{- default (include "rhdh.statefulSetHeadlessServiceName" .) .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 b24c9373..a107667a 100644 --- a/charts/rhdh/templates/deployment.yaml +++ b/charts/rhdh/templates/deployment.yaml @@ -1,6 +1,4 @@ -{{- $installDir := "/opt/app-root/src" -}} -{{- $intelligentAssistant := include "rhdh.intelligentAssistant" . | fromYaml -}} -{{- $extraCatalogImages := include "rhdh.catalogIndex.extraImagesEnvValue" . | trim -}} +{{- if eq .Values.workload.kind "Deployment" }} apiVersion: apps/v1 kind: Deployment metadata: @@ -29,446 +27,5 @@ 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 }} +{{- end }} \ No newline at end of file diff --git a/charts/rhdh/templates/headless-service.yaml b/charts/rhdh/templates/headless-service.yaml new file mode 100644 index 00000000..8717016d --- /dev/null +++ b/charts/rhdh/templates/headless-service.yaml @@ -0,0 +1,24 @@ +{{- if eq .Values.workload.kind "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/templates/hpa.yaml b/charts/rhdh/templates/hpa.yaml index 7917d5ba..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: Deployment + 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 new file mode 100644 index 00000000..540fc8fc --- /dev/null +++ b/charts/rhdh/templates/statefulset.yaml @@ -0,0 +1,64 @@ +{{- if eq .Values.workload.kind "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 }} + {{- $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 diff --git a/charts/rhdh/values.schema.json b/charts/rhdh/values.schema.json index ddaaab14..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" @@ -2543,6 +2600,62 @@ "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" + }, + "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.", + "type": "object" + }, + "podManagementPolicy": { + "default": "", + "title": "Pod management policy for the StatefulSet.", + "type": "string" + }, + "serviceName": { + "default": "", + "title": "serviceName for the StatefulSet spec. Defaults to the headless Service ({fullname}-headless).", + "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 fde40d69..7dd16a8d 100644 --- a/charts/rhdh/values.schema.tmpl.json +++ b/charts/rhdh/values.schema.tmpl.json @@ -408,6 +408,60 @@ "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": "serviceName for the StatefulSet spec. Defaults to the headless Service ({fullname}-headless).", + "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": {} + }, + "extraVolumeClaimTemplates": { + "title": "Extra VolumeClaimTemplates appended after the chart-managed dynamic-plugins-root claim.", + "type": "array", + "default": [], + "items": { + "type": "object" + } + }, + "annotations": { + "title": "Annotations on the StatefulSet resource (not the pod).", + "type": "object", + "default": {} + } + } + } + } + }, "commandOverride": { "title": "Override the container command.", "type": "array", @@ -622,7 +676,7 @@ "type": { "title": "Volume type.", "type": "string", - "enum": ["ephemeral", "emptyDir", "pvc"], + "enum": ["ephemeral", "emptyDir", "pvc", "statefulSetPVC"], "default": "ephemeral" }, "ephemeral": { @@ -655,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 e52e1502..df7c020c 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: "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). @@ -204,6 +216,31 @@ 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 headless when empty. + # Service ({fullname}-headless) 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: {} + + # -- Extra volumeClaimTemplates appended after the chart-managed dynamic-plugins-root claim (when type=statefulSetPVC). + extraVolumeClaimTemplates: [] + + # -- Annotations on the StatefulSet resource. + annotations: {} + # -- ServiceAccount configuration. serviceAccount: create: false