From 0ba8f70c31df0ee035bae10f55eb599bb674f588 Mon Sep 17 00:00:00 2001 From: Gustavo Lira e Silva Date: Mon, 9 Nov 2026 12:07:45 -0300 Subject: [PATCH 1/3] test(rhdh): assert mirrored images and StorageClass in rendered manifests Constrained clusters (mirrored registry only, no default StorageClass) depend on the chart applying global.imageRegistry to every image and on the dynamic plugins volume honouring the StorageClass settings. Nothing asserted either: CI only checked that helm template succeeds. Add hack/assert-rendered-values.sh, run by the test-charts action next to the helm template sanity step, before the kind cluster is created: - with global.imageRegistry set, every image (containers, init containers, helm test pod, SonataFlow CR images, CATALOG_INDEX_IMAGE and EXTRA_CATALOG_INDEX_IMAGES) is under the mirror, for the default values, every ci/*-values.yaml and a scenario enabling the optional images (OKP, orchestrator data index/job service, extra indexes); - the helm test pod image follows test.image.* (RHDHBUGS-559); - the ephemeral volumeClaimTemplate carries ephemeral.storageClassName or global.defaultStorageClass, and no storageClassName when neither is set (RHDHBUGS-781); - statefulSetPVC honours its storageClassName; - type=emptyDir renders no PVC or ephemeral claim. Add ci/with-custom-storage-class-values.yaml so ct install exercises the dynamic plugins volume on the custom-sc StorageClass the action already creates on kind. Bump the chart version for the new ci file. Refs: RHIDP-16230 Co-Authored-By: Claude Opus 5.5 --- .github/actions/test-charts/action.yml | 10 ++ CONTRIBUTING.md | 1 + charts/rhdh/Chart.yaml | 2 +- charts/rhdh/README.md | 4 +- .../ci/with-custom-storage-class-values.yaml | 17 ++ hack/assert-rendered-values.sh | 146 ++++++++++++++++++ 6 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 charts/rhdh/ci/with-custom-storage-class-values.yaml create mode 100755 hack/assert-rendered-values.sh diff --git a/.github/actions/test-charts/action.yml b/.github/actions/test-charts/action.yml index f11f015be..2965bb9d1 100644 --- a/.github/actions/test-charts/action.yml +++ b/.github/actions/test-charts/action.yml @@ -121,6 +121,16 @@ runs: --values "$VALUES_FILE" 2>&1) || { echo "$output"; exit 1; } done + # Mirrored-registry and StorageClass assertions on the rendered manifests (no cluster needed). + - name: Rendered values assertions + if: steps.list-changed.outputs.changed == 'true' && inputs.chart == 'charts/rhdh' + shell: bash + env: + INPUT_CHART: ${{ inputs.chart }} + run: | + helm dependency build "$INPUT_CHART" + ./hack/assert-rendered-values.sh "$INPUT_CHART" + - name: Generate KinD Config if: steps.list-changed.outputs.changed == 'true' shell: bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91eb4bd0a..86aa7b738 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,7 @@ Before making a contribution to the charts in this repository, you will need to - Run `helm template` on the changes you're making to ensure they are correctly rendered into Kubernetes manifests. - Lint tests have been run for the Chart using the [Chart Testing](https://github.com/helm/chart-testing) tool and the `ct lint` command. +- For the [rhdh](./charts/rhdh) chart, run `./hack/assert-rendered-values.sh` (after `helm dependency build charts/rhdh`) to check that every rendered image honours `global.imageRegistry` and that the dynamic plugins volume honours the StorageClass settings. CI runs it too. - For each Chart updated, version bumped in the corresponding `Chart.yaml` according to [Semantic Versioning](http://semver.org/). - For each Chart updated, ensure variables are documented in the corresponding `values.yaml` file and the [pre-commit](https://pre-commit.com/) hook has been run with `pre-commit run --all-files` to generate the corresponding `README.md` documentation. The [pre-commit Workflow](./.github/workflows/pre-commit.yaml) will enforce this and warn you if needed. - JSON Schema template updated and re-generated the raw schema via the `pre-commit` hook. diff --git a/charts/rhdh/Chart.yaml b/charts/rhdh/Chart.yaml index 18f646d7a..34ed9cb02 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.5.17 +version: 3.5.18 appVersion: 2.2.0 annotations: artifacthub.io/category: integration-delivery diff --git a/charts/rhdh/README.md b/charts/rhdh/README.md index c7f4a5f71..7765f2111 100644 --- a/charts/rhdh/README.md +++ b/charts/rhdh/README.md @@ -1,7 +1,7 @@ # RHDH Helm Chart for OpenShift and Kubernetes -![Version: 3.5.17](https://img.shields.io/badge/Version-3.5.17-informational?style=flat-square) +![Version: 3.5.18](https://img.shields.io/badge/Version-3.5.18-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 3.5.17 +helm install my-rhdh redhat-developer/redhat-developer-hub --version 3.5.18 ``` ## Introduction diff --git a/charts/rhdh/ci/with-custom-storage-class-values.yaml b/charts/rhdh/ci/with-custom-storage-class-values.yaml new file mode 100644 index 000000000..88bd4dac2 --- /dev/null +++ b/charts/rhdh/ci/with-custom-storage-class-values.yaml @@ -0,0 +1,17 @@ +# CI: provision the dynamic plugins ephemeral volume through an explicit StorageClass +# instead of the cluster default. The test action creates `custom-sc` before `ct install`. +dynamicPlugins: + includes: [] + volume: + type: ephemeral + ephemeral: + storageClassName: custom-sc +intelligentAssistant: + plugins: [] + okp: + route: + enabled: false + ingress: + enabled: false +orchestrator: + plugins: [] diff --git a/hack/assert-rendered-values.sh b/hack/assert-rendered-values.sh new file mode 100755 index 000000000..3bbf81b9f --- /dev/null +++ b/hack/assert-rendered-values.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# +# Renders the rhdh chart with `helm template` and asserts the values that a +# constrained cluster (mirrored registry only, no default StorageClass) relies on: +# - global.imageRegistry is applied to every rendered image reference, +# - the helm test pod image follows test.image.*, +# - the dynamic plugins volume honours the StorageClass settings. +# +# Usage: hack/assert-rendered-values.sh [chart-dir] (default: charts/rhdh) +# Requires helm and yq (mikefarah v4); run `helm dependency build ` first. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" +CHART="${1:-${REPO_ROOT}/charts/rhdh}" +MIRROR="mirror.local" + +failures=0 +checks=0 + +pass() { + checks=$((checks + 1)) + echo " ok: $1" +} + +fail() { + checks=$((checks + 1)) + failures=$((failures + 1)) + echo " FAIL: $1" >&2 +} + +render() { + helm template test-release "${CHART}" "$@" +} + +# Prints every image reference in the rendered manifests: `image` string fields +# (containers, initContainers, test pod, CRs) and *_IMAGE / *_IMAGES env values. +list_images() { + yq -N ' + (.. | select(tag == "!!map" and has("image")) | .image | select(tag == "!!str")), + (.. | select(tag == "!!map" and has("name") and has("value")) + | select(.name | test("_IMAGES?$")) | .value | select(tag == "!!str") + | split(",") | .[] | sub("^[^=/]+=", "")) + ' | grep -v '^$' | sort -u +} + +assert_images_mirrored() { + local label=$1 + shift + local images + images=$(render --set global.imageRegistry="${MIRROR}" "$@" | list_images) + if [[ -z "${images}" ]]; then + fail "${label}: no image references found in the rendered output" + return + fi + local bad + bad=$(grep -v "^${MIRROR}/" <<<"${images}" || true) + if [[ -n "${bad}" ]]; then + fail "${label}: images not under ${MIRROR}/: $(tr '\n' ' ' <<<"${bad}")" + else + pass "${label}: $(wc -l <<<"${images}" | tr -d ' ') images under ${MIRROR}/" + fi +} + +assert_equals() { + local label=$1 expected=$2 actual=$3 + if [[ "${actual}" == "${expected}" ]]; then + pass "${label}" + else + fail "${label}: expected '${expected}', got '${actual}'" + fi +} + +plugins_root_volume() { + yq -N 'select(.kind == "Deployment") | .spec.template.spec.volumes[] | select(.name == "dynamic-plugins-root")' +} + +echo "==> global.imageRegistry=${MIRROR} applies to every rendered image" +assert_images_mirrored "default values" +for ci_values in "${CHART}"/ci/*-values.yaml; do + [[ -f "${ci_values}" ]] || continue + assert_images_mirrored "$(basename "${ci_values}")" --values "${ci_values}" +done +# Optional images that no CI scenario enables. +assert_images_mirrored "optional images enabled" \ + --set intelligentAssistant.okp.enabled=true \ + --set orchestrator.enabled=true \ + --set orchestrator.sonataflowPlatform.dataIndex.image.registry=quay.io \ + --set orchestrator.sonataflowPlatform.dataIndex.image.repository=ci/data-index \ + --set orchestrator.sonataflowPlatform.dataIndex.image.tag=1.0 \ + --set orchestrator.sonataflowPlatform.jobService.image.registry=quay.io \ + --set orchestrator.sonataflowPlatform.jobService.image.repository=ci/jobs-service \ + --set orchestrator.sonataflowPlatform.jobService.image.tag=1.0 \ + --set 'catalogIndex.extraImages[0].name=community' \ + --set 'catalogIndex.extraImages[0].registry=ghcr.io' \ + --set 'catalogIndex.extraImages[0].repository=ci/community-index' \ + --set 'catalogIndex.extraImages[0].tag=1.0' \ + --set 'catalogIndex.extraImages[1].registry=ghcr.io' \ + --set 'catalogIndex.extraImages[1].repository=ci/other-index' \ + --set 'catalogIndex.extraImages[1].tag=1.0' + +echo "==> helm test pod image follows test.image.*" +test_image=$(render \ + --set test.image.registry=registry.example.test \ + --set test.image.repository=ci/curl \ + --set test.image.tag=9.9.9 | + yq -N 'select(.metadata.annotations["helm.sh/hook"] == "test") | .spec.containers[].image') +assert_equals "test pod image" "registry.example.test/ci/curl:9.9.9" "${test_image}" + +echo "==> dynamic plugins volume StorageClass" +sc=$(render | plugins_root_volume | yq -N '.ephemeral.volumeClaimTemplate.spec | has("storageClassName")') +assert_equals "ephemeral, nothing set: no storageClassName (cluster default)" "false" "${sc}" + +sc=$(render --set dynamicPlugins.volume.ephemeral.storageClassName=custom-sc | + plugins_root_volume | yq -N '.ephemeral.volumeClaimTemplate.spec.storageClassName') +assert_equals "ephemeral.storageClassName" "custom-sc" "${sc}" + +sc=$(render --set global.defaultStorageClass=global-sc | + plugins_root_volume | yq -N '.ephemeral.volumeClaimTemplate.spec.storageClassName') +assert_equals "global.defaultStorageClass" "global-sc" "${sc}" + +sc=$(render \ + --set workload.kind=StatefulSet \ + --set dynamicPlugins.volume.type=statefulSetPVC \ + --set dynamicPlugins.volume.statefulSetPVC.storageClassName=custom-sc | + yq -N 'select(.kind == "StatefulSet") | .spec.volumeClaimTemplates[] + | select(.metadata.name == "dynamic-plugins-root") | .spec.storageClassName') +assert_equals "statefulSetPVC.storageClassName" "custom-sc" "${sc}" + +# emptyDir must not create any claim, even when a StorageClass is configured. +out=$(render --set dynamicPlugins.volume.type=emptyDir \ + --set dynamicPlugins.volume.ephemeral.storageClassName=custom-sc) +vol=$(plugins_root_volume <<<"${out}" | yq -N -o=json -I=0 'keys') +assert_equals "type=emptyDir: plugins volume is an emptyDir" '["name","emptyDir"]' "${vol}" +claims=$(yq ea -N '[select(.kind == "PersistentVolumeClaim"), + (.. | select(tag == "!!map" and has("volumeClaimTemplate")))] + | length' <<<"${out}") +assert_equals "type=emptyDir: no PVC or ephemeral claim rendered" "0" "${claims}" + +echo +if ((failures > 0)); then + echo "${failures} of ${checks} rendered-values assertions failed" >&2 + exit 1 +fi +echo "All ${checks} rendered-values assertions passed" From 5b5a8378287bedfde702f993204548faf32967dd Mon Sep 17 00:00:00 2001 From: Gustavo Lira e Silva Date: Mon, 9 Nov 2026 12:45:58 -0300 Subject: [PATCH 2/3] test(rhdh): tighten mirror prefix and emptyDir volume checks The mirror check matched the registry prefix as a regex, so a lookalike host such as mirrorXlocal/ passed; escape the dots. Compare the emptyDir volume's sorted keys so the check does not depend on the order the template renders fields in. Co-Authored-By: Claude Opus 5.5 --- hack/assert-rendered-values.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hack/assert-rendered-values.sh b/hack/assert-rendered-values.sh index 3bbf81b9f..9743482af 100755 --- a/hack/assert-rendered-values.sh +++ b/hack/assert-rendered-values.sh @@ -55,7 +55,7 @@ assert_images_mirrored() { return fi local bad - bad=$(grep -v "^${MIRROR}/" <<<"${images}" || true) + bad=$(grep -v "^${MIRROR//./\\.}/" <<<"${images}" || true) if [[ -n "${bad}" ]]; then fail "${label}: images not under ${MIRROR}/: $(tr '\n' ' ' <<<"${bad}")" else @@ -131,8 +131,8 @@ assert_equals "statefulSetPVC.storageClassName" "custom-sc" "${sc}" # emptyDir must not create any claim, even when a StorageClass is configured. out=$(render --set dynamicPlugins.volume.type=emptyDir \ --set dynamicPlugins.volume.ephemeral.storageClassName=custom-sc) -vol=$(plugins_root_volume <<<"${out}" | yq -N -o=json -I=0 'keys') -assert_equals "type=emptyDir: plugins volume is an emptyDir" '["name","emptyDir"]' "${vol}" +vol=$(plugins_root_volume <<<"${out}" | yq -N -o=json -I=0 'keys | sort') +assert_equals "type=emptyDir: plugins volume is an emptyDir" '["emptyDir","name"]' "${vol}" claims=$(yq ea -N '[select(.kind == "PersistentVolumeClaim"), (.. | select(tag == "!!map" and has("volumeClaimTemplate")))] | length' <<<"${out}") From 49b8cc3b603725dabc1a35a1698c14e8c4890625 Mon Sep 17 00:00:00 2001 From: Gustavo Lira e Silva Date: Mon, 9 Nov 2026 12:48:46 -0300 Subject: [PATCH 3/3] test(rhdh): close false passes in the rendered-values assertions - The "nothing set" StorageClass check used has() on the claim spec, which yq answers false for a missing spec too, so it passed when the plugins volume was no longer ephemeral. Compare the spec's keys instead; a missing spec now fails the check. - An empty image list made grep exit 1 under pipefail and aborted the script before the "no image references" failure could be reported. - Assert that ephemeral.storageClassName takes precedence over global.defaultStorageClass when both are set. Co-Authored-By: Claude Opus 5.5 --- hack/assert-rendered-values.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/hack/assert-rendered-values.sh b/hack/assert-rendered-values.sh index 9743482af..9fd1d6456 100755 --- a/hack/assert-rendered-values.sh +++ b/hack/assert-rendered-values.sh @@ -42,7 +42,7 @@ list_images() { (.. | select(tag == "!!map" and has("name") and has("value")) | select(.name | test("_IMAGES?$")) | .value | select(tag == "!!str") | split(",") | .[] | sub("^[^=/]+=", "")) - ' | grep -v '^$' | sort -u + ' | { grep -v '^$' || true; } | sort -u } assert_images_mirrored() { @@ -109,8 +109,9 @@ test_image=$(render \ assert_equals "test pod image" "registry.example.test/ci/curl:9.9.9" "${test_image}" echo "==> dynamic plugins volume StorageClass" -sc=$(render | plugins_root_volume | yq -N '.ephemeral.volumeClaimTemplate.spec | has("storageClassName")') -assert_equals "ephemeral, nothing set: no storageClassName (cluster default)" "false" "${sc}" +# Default is an ephemeral claim with no storageClassName, so the cluster default applies. +spec_keys=$(render | plugins_root_volume | yq -N '.ephemeral.volumeClaimTemplate.spec // {} | keys | sort | join(",")') +assert_equals "ephemeral, nothing set: no storageClassName (cluster default)" "accessModes,resources" "${spec_keys}" sc=$(render --set dynamicPlugins.volume.ephemeral.storageClassName=custom-sc | plugins_root_volume | yq -N '.ephemeral.volumeClaimTemplate.spec.storageClassName') @@ -120,6 +121,11 @@ sc=$(render --set global.defaultStorageClass=global-sc | plugins_root_volume | yq -N '.ephemeral.volumeClaimTemplate.spec.storageClassName') assert_equals "global.defaultStorageClass" "global-sc" "${sc}" +sc=$(render --set global.defaultStorageClass=global-sc \ + --set dynamicPlugins.volume.ephemeral.storageClassName=custom-sc | + plugins_root_volume | yq -N '.ephemeral.volumeClaimTemplate.spec.storageClassName') +assert_equals "ephemeral.storageClassName overrides global.defaultStorageClass" "custom-sc" "${sc}" + sc=$(render \ --set workload.kind=StatefulSet \ --set dynamicPlugins.volume.type=statefulSetPVC \