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..9fd1d6456 --- /dev/null +++ b/hack/assert-rendered-values.sh @@ -0,0 +1,152 @@ +#!/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 '^$' || true; } | 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" +# 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') +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 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 \ + --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 | 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}") +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"