This is an automated email from the ASF dual-hosted git repository.
hgruszecki pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 411a697eb ci: add Helm chart validation (#3019)
411a697eb is described below
commit 411a697eb184ebc33d7ce4b862f51db49ce46ea5
Author: Aviraj Khare <[email protected]>
AuthorDate: Fri Mar 27 16:42:41 2026 +0530
ci: add Helm chart validation (#3019)
---
.github/actions/utils/setup-helm-tools/action.yml | 67 ++++
.github/config/components.yml | 5 +
.github/workflows/_test.yml | 23 ++
helm/charts/iggy/README.md | 79 +++-
helm/charts/iggy/templates/hpa.yaml | 13 +-
helm/charts/iggy/templates/ingress.yaml | 12 +-
scripts/ci/setup-helm-smoke-cluster.sh | 235 +++++++++++
scripts/ci/setup-helm-tools.sh | 111 ++++++
scripts/ci/test-helm.sh | 452 ++++++++++++++++++++++
9 files changed, 981 insertions(+), 16 deletions(-)
diff --git a/.github/actions/utils/setup-helm-tools/action.yml
b/.github/actions/utils/setup-helm-tools/action.yml
new file mode 100644
index 000000000..971796613
--- /dev/null
+++ b/.github/actions/utils/setup-helm-tools/action.yml
@@ -0,0 +1,67 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: setup-helm-tools
+description: Install pinned Helm and optionally kind for Helm chart CI jobs
+inputs:
+ install-kind:
+ description: "Whether to install kind in addition to Helm"
+ required: false
+ default: "false"
+ helm-version:
+ description: "Helm release version"
+ required: false
+ default: "v4.1.3"
+ helm-checksum:
+ description: "SHA256 checksum for the Helm tarball"
+ required: false
+ default: "02ce9722d541238f81459938b84cf47df2fdf1187493b4bfb2346754d82a4700"
+ kind-version:
+ description: "kind release version"
+ required: false
+ default: "v0.31.0"
+ kind-checksum:
+ description: "SHA256 checksum for the kind binary"
+ required: false
+ default: "eb244cbafcc157dff60cf68693c14c9a75c4e6e6fedaf9cd71c58117cb93e3fa"
+
+runs:
+ using: "composite"
+ steps:
+ - name: Verify supported runner
+ shell: bash
+ run: |
+ set -euo pipefail
+ if [[ "${{ runner.os }}" != "Linux" || "${{ runner.arch }}" != "X64"
]]; then
+ echo "setup-helm-tools currently supports Linux X64 runners only" >&2
+ exit 1
+ fi
+
+ - name: Install Helm and optional kind
+ shell: bash
+ env:
+ HELM_VERSION: ${{ inputs.helm-version }}
+ HELM_CHECKSUM: ${{ inputs.helm-checksum }}
+ KIND_VERSION: ${{ inputs.kind-version }}
+ KIND_CHECKSUM: ${{ inputs.kind-checksum }}
+ run: |
+ set -euo pipefail
+ args=()
+ if [[ "${{ inputs.install-kind }}" == "true" ]]; then
+ args+=(--install-kind)
+ fi
+ scripts/ci/setup-helm-tools.sh "${args[@]}"
diff --git a/.github/config/components.yml b/.github/config/components.yml
index 29079bd8f..7585a91b9 100644
--- a/.github/config/components.yml
+++ b/.github/config/components.yml
@@ -373,6 +373,11 @@ components:
- "web/**"
tasks: ["lint", "build"]
+ helm:
+ paths:
+ - "helm/**"
+ tasks: ["validate", "smoke"]
+
rust-bench-dashboard:
paths:
- "core/bench/dashboard/**"
diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml
index ac3eb6ca7..75a7b7c1d 100644
--- a/.github/workflows/_test.yml
+++ b/.github/workflows/_test.yml
@@ -192,6 +192,29 @@ jobs:
npm run build
fi
+ # Helm chart testing
+ - name: Setup Helm test tools
+ if: inputs.component == 'helm' && (inputs.task == 'validate' ||
inputs.task == 'smoke')
+ uses: ./.github/actions/utils/setup-helm-tools
+ with:
+ install-kind: ${{ inputs.task == 'smoke' }}
+
+ - name: Setup Helm smoke cluster
+ if: inputs.component == 'helm' && inputs.task == 'smoke'
+ run: scripts/ci/setup-helm-smoke-cluster.sh
+
+ - name: Validate Helm chart
+ if: inputs.component == 'helm' && inputs.task == 'validate'
+ run: scripts/ci/test-helm.sh validate
+
+ - name: Smoke test Helm chart runtime
+ if: inputs.component == 'helm' && inputs.task == 'smoke'
+ run: scripts/ci/test-helm.sh smoke --cleanup
+
+ - name: Collect Helm smoke diagnostics
+ if: failure() && inputs.component == 'helm' && inputs.task == 'smoke'
+ run: scripts/ci/test-helm.sh collect-smoke-diagnostics
+
# CI workflow validation
- name: Validate CI workflows
if: inputs.component == 'ci-workflows' && inputs.task == 'validate'
diff --git a/helm/charts/iggy/README.md b/helm/charts/iggy/README.md
index 76c62b3e3..9082cb968 100644
--- a/helm/charts/iggy/README.md
+++ b/helm/charts/iggy/README.md
@@ -89,7 +89,7 @@ helm uninstall iggy
| `server.enabled` | bool | `true` | Enable the Iggy server deployment |
| `server.replicaCount` | int | `1` | Number of server replicas |
| `server.image.repository` | string | `"apache/iggy"` | Server image
repository |
-| `server.image.tag` | string | `""` | Server image tag (defaults to chart
appVersion) |
+| `server.image.tag` | string | `"0.7.0"` | Server image tag |
| `server.ports.http` | int | `3000` | HTTP API port |
| `server.ports.tcp` | int | `8090` | TCP protocol port |
| `server.ports.quic` | int | `8080` | QUIC protocol port |
@@ -135,6 +135,83 @@ helm uninstall iggy
| `ui.securityContext` | object | `{}` | UI container security context |
| `ui.podSecurityContext` | object | `{}` | UI pod security context |
+## Testing
+
+The chart CI paths are also available locally from the repository root.
+
+### Render Validation
+
+If `helm` is already installed locally:
+
+```bash
+scripts/ci/test-helm.sh validate
+```
+
+If you want the pinned Linux CI tool version instead:
+
+```bash
+scripts/ci/setup-helm-tools.sh
+scripts/ci/test-helm.sh validate
+```
+
+This runs `helm lint --strict` plus the CI render scenarios, including:
+
+* default chart output
+* all-features render
+* legacy Kubernetes 1.18 API coverage
+* server-only render
+* UI-only render
+* existing-secret render
+
+### Runtime Smoke Test
+
+The smoke path requires `helm`, `kind`, `kubectl`, and `curl`.
+
+Before running the local smoke path, keep these common gotchas in mind:
+
+* the Iggy server requires working `io_uring` support from the Kubernetes
node/kernel/runtime
+* the server also needs enough available memory and locked-memory headroom
during startup
+* `scripts/ci/test-helm.sh cleanup-smoke` removes the Helm release and smoke
namespace, but it does not delete the reusable kind cluster created by
`scripts/ci/setup-helm-smoke-cluster.sh`
+
+If `helm` and `kind` are already installed:
+
+```bash
+scripts/ci/setup-helm-smoke-cluster.sh
+scripts/ci/test-helm.sh smoke --cleanup
+```
+
+If you want the pinned Linux CI tool versions:
+
+```bash
+scripts/ci/setup-helm-tools.sh --install-kind
+scripts/ci/setup-helm-smoke-cluster.sh
+scripts/ci/test-helm.sh smoke --cleanup
+```
+
+If a previous local smoke install failed and left resources behind, reset the
smoke namespace with:
+
+```bash
+scripts/ci/test-helm.sh cleanup-smoke
+```
+
+On Apple Silicon hosts, the released `apache/iggy:0.7.0` `arm64` image may
still fail during the runtime smoke path in kind. If your Docker setup supports
amd64 emulation well enough, you can try recreating the dedicated smoke cluster
with:
+
+```bash
+HELM_SMOKE_KIND_PLATFORM=linux/amd64 scripts/ci/setup-helm-smoke-cluster.sh
+```
+
+The smoke script defaults `IGGY_SYSTEM_SHARDING_CPU_ALLOCATION=1` for the
server pod so the local kind path avoids the chart's `numa:auto` default and
keeps the local runtime to a single shard, which has been more reliable on
containerized local nodes. If you need a different local override, set
`HELM_SMOKE_SERVER_CPU_ALLOCATION` before running `scripts/ci/test-helm.sh
smoke`. Pass `--cleanup` to remove the smoke namespace after a successful run;
omit it if you want to inspect the deploy [...]
+
+On smoke-test failures you can collect the same diagnostics as CI with:
+
+```bash
+scripts/ci/test-helm.sh collect-smoke-diagnostics
+```
+
+> **Note:** `scripts/ci/setup-helm-tools.sh` currently supports Linux `x86_64`
only.
+> On other local platforms, install equivalent `helm` and `kind` binaries
yourself and then use the same scripts above.
+> The runtime smoke test may still fail on some local/containerized clusters
if the node/kernel does not provide the `io_uring` support required by the
server runtime even after the local sharding override, or if the local
environment does not provide enough memory for the server to initialize cleanly.
+
## Troubleshooting
### Pod CrashLoopBackOff with "Out of memory" error
diff --git a/helm/charts/iggy/templates/hpa.yaml
b/helm/charts/iggy/templates/hpa.yaml
index ea7b4cb54..5d9f635d9 100644
--- a/helm/charts/iggy/templates/hpa.yaml
+++ b/helm/charts/iggy/templates/hpa.yaml
@@ -14,9 +14,8 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-{{ if .Values.autoscaling.enabled }}
-{{- if .Capabilities.APIVersions.Has "autoscaling/v2" -}}
+{{ if .Values.autoscaling.enabled -}}
+{{- if semverCompare ">=1.23-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: autoscaling/v2
{{- else -}}
apiVersion: autoscaling/v2beta2
@@ -38,24 +37,24 @@ spec:
- type: Resource
resource:
name: cpu
- {{- if .Capabilities.APIVersions.Has "autoscaling/v2" }}
+ {{- if semverCompare ">=1.23-0" .Capabilities.KubeVersion.GitVersion }}
target:
type: Utilization
averageUtilization: {{
.Values.autoscaling.targetCPUUtilizationPercentage }}
{{- else }}
- targetAverageUtilization: {{
.Values.autoscaling.targetCPUUtilizationPercentage }}
+ targetAverageUtilization: {{
.Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
- {{- if .Capabilities.APIVersions.Has "autoscaling/v2" }}
+ {{- if semverCompare ">=1.23-0" .Capabilities.KubeVersion.GitVersion }}
target:
type: Utilization
averageUtilization: {{
.Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- else }}
- targetAverageUtilization: {{
.Values.autoscaling.targetMemoryUtilizationPercentage }}
+ targetAverageUtilization: {{
.Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
{{- end }}
diff --git a/helm/charts/iggy/templates/ingress.yaml
b/helm/charts/iggy/templates/ingress.yaml
index ddefbba40..b0339e574 100644
--- a/helm/charts/iggy/templates/ingress.yaml
+++ b/helm/charts/iggy/templates/ingress.yaml
@@ -14,8 +14,6 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-
{{ if .Values.server.ingress.enabled -}}
{{ $fullName := include "iggy.fullname" . -}}
{{ $svcPort := .Values.server.service.port -}}
@@ -24,11 +22,11 @@
{{- $_ := set .Values.server.ingress.annotations
"kubernetes.io/ingress.class" .Values.server.ingress.className}}
{{- end }}
{{- end }}
-{{ if .Capabilities.APIVersions.Has "networking.k8s.io/v1" }}
+{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
-{{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }}
+{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
-{{- else }}
+{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
@@ -77,8 +75,6 @@ spec:
{{- end }}
{{- end }}
{{- end }}
-
-
{{ if .Values.ui.ingress.enabled -}}
---
{{ $fullName := include "iggy.fullname" . -}}
@@ -97,7 +93,7 @@ apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
- name: {{ $fullName }}
+ name: {{ $fullName }}-ui
labels:
{{- include "iggy.labels" . | nindent 4 }}
{{- with .Values.ui.ingress.annotations }}
diff --git a/scripts/ci/setup-helm-smoke-cluster.sh
b/scripts/ci/setup-helm-smoke-cluster.sh
new file mode 100755
index 000000000..17921f4ed
--- /dev/null
+++ b/scripts/ci/setup-helm-smoke-cluster.sh
@@ -0,0 +1,235 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+set -euo pipefail
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/ci/setup-helm-smoke-cluster.sh
+
+Create or reuse the kind cluster used by the Helm smoke test and ensure
+ingress-nginx is installed and ready in that cluster.
+
+Environment:
+ HELM_SMOKE_KIND_NAME kind cluster name (default:
iggy-helm-smoke)
+ HELM_SMOKE_KIND_IMAGE kind node image (default:
kindest/node:v1.35.0)
+ HELM_SMOKE_KIND_PLATFORM docker platform for the kind node image
+ HELM_SMOKE_KIND_WAIT kind create wait timeout (default: 120s)
+ HELM_SMOKE_INGRESS_NGINX_VERSION ingress-nginx controller tag (default:
controller-v1.15.1)
+ HELM_SMOKE_INGRESS_NGINX_TIMEOUT rollout timeout (default: 5m)
+EOF
+}
+
+if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "help" ];
then
+ usage
+ exit 0
+fi
+
+if [ "$#" -ne 0 ]; then
+ usage
+ exit 1
+fi
+
+HELM_SMOKE_KIND_NAME="${HELM_SMOKE_KIND_NAME:-iggy-helm-smoke}"
+HELM_SMOKE_KIND_IMAGE="${HELM_SMOKE_KIND_IMAGE:-kindest/node:v1.35.0}"
+HELM_SMOKE_KIND_PLATFORM="${HELM_SMOKE_KIND_PLATFORM:-}"
+HELM_SMOKE_KIND_WAIT="${HELM_SMOKE_KIND_WAIT:-120s}"
+HELM_SMOKE_INGRESS_NGINX_VERSION="${HELM_SMOKE_INGRESS_NGINX_VERSION:-controller-v1.15.1}"
+HELM_SMOKE_INGRESS_NGINX_TIMEOUT="${HELM_SMOKE_INGRESS_NGINX_TIMEOUT:-5m}"
+
+require_command() {
+ if ! command -v "$1" >/dev/null 2>&1; then
+ echo "Error: required command '$1' not found" >&2
+ exit 1
+ fi
+}
+
+duration_to_seconds() {
+ local duration="$1"
+ local total=0
+ local value
+ local unit
+ local multiplier
+
+ if [[ "$duration" =~ ^[0-9]+$ ]]; then
+ echo "$duration"
+ return 0
+ fi
+
+ while [ -n "$duration" ]; do
+ if [[ "$duration" =~ ^([0-9]+)([smhd])(.*)$ ]]; then
+ value="${BASH_REMATCH[1]}"
+ unit="${BASH_REMATCH[2]}"
+ duration="${BASH_REMATCH[3]}"
+ case "$unit" in
+ s) multiplier=1 ;;
+ m) multiplier=60 ;;
+ h) multiplier=3600 ;;
+ d) multiplier=86400 ;;
+ esac
+ total=$((total + (value * multiplier)))
+ continue
+ fi
+
+ echo "Error: unsupported timeout format '$1'" >&2
+ echo "Use plain seconds or a combination of s, m, h, or d units such as
300, 5m, or 1h30m." >&2
+ return 1
+ done
+
+ echo "$total"
+}
+
+wait_for_completed_job() {
+ local job_name="$1"
+
+ if ! kubectl -n ingress-nginx get "job/${job_name}" >/dev/null 2>&1; then
+ return 0
+ fi
+
+ kubectl -n ingress-nginx wait \
+ --for=condition=complete \
+ "job/${job_name}" \
+ --timeout="$HELM_SMOKE_INGRESS_NGINX_TIMEOUT"
+}
+
+wait_for_ingress_validation() {
+ local sleep_seconds=4
+ local validation_timeout_seconds
+ local deadline
+ local probe_file
+
+ validation_timeout_seconds="$(duration_to_seconds
"$HELM_SMOKE_INGRESS_NGINX_TIMEOUT")"
+ deadline=$((SECONDS + validation_timeout_seconds))
+ probe_file="$(mktemp "${TMPDIR:-/tmp}/ingress-nginx-probe.XXXXXX.yaml")"
+
+ cat > "$probe_file" <<'EOF'
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-nginx-readiness-probe
+ namespace: ingress-nginx
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: readiness-probe.iggy.local
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: ingress-nginx-controller
+ port:
+ number: 80
+EOF
+
+ while (( SECONDS < deadline )); do
+ if kubectl apply --dry-run=server -f "$probe_file" >/dev/null 2>&1; then
+ rm -f "$probe_file"
+ return 0
+ fi
+ sleep "$sleep_seconds"
+ done
+
+ echo "Error: ingress-nginx admission webhook did not become ready within
${HELM_SMOKE_INGRESS_NGINX_TIMEOUT}" >&2
+ if ! kubectl apply --dry-run=server -f "$probe_file"; then
+ rm -f "$probe_file"
+ return 1
+ fi
+ rm -f "$probe_file"
+}
+
+require_command kind
+require_command kubectl
+
+if [ -n "$HELM_SMOKE_KIND_PLATFORM" ]; then
+ require_command docker
+fi
+
+kind_context="kind-${HELM_SMOKE_KIND_NAME}"
+kind_config="$(mktemp "${TMPDIR:-/tmp}/iggy-kind.XXXXXX.yaml")"
+trap 'rm -f "$kind_config"' EXIT
+
+cat > "$kind_config" <<'EOF'
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+nodes:
+ - role: control-plane
+ kubeadmConfigPatches:
+ - |
+ kind: InitConfiguration
+ nodeRegistration:
+ kubeletExtraArgs:
+ node-labels: "ingress-ready=true"
+ extraPortMappings:
+ - containerPort: 80
+ hostPort: 80
+ protocol: TCP
+ - containerPort: 443
+ hostPort: 443
+ protocol: TCP
+EOF
+
+cluster_exists=false
+if kind get clusters | grep -Fxq "$HELM_SMOKE_KIND_NAME"; then
+ cluster_exists=true
+fi
+
+if [ "$cluster_exists" = true ] && [ -n "$HELM_SMOKE_KIND_PLATFORM" ]; then
+ current_kind_image_id="$(
+ docker inspect "${HELM_SMOKE_KIND_NAME}-control-plane" \
+ --format '{{.Image}}' 2>/dev/null || true
+ )"
+ current_kind_platform="$(
+ docker image inspect "$current_kind_image_id" \
+ --format '{{.Os}}/{{.Architecture}}' 2>/dev/null || true
+ )"
+ if [ -n "$current_kind_platform" ] && [ "$current_kind_platform" !=
"$HELM_SMOKE_KIND_PLATFORM" ]; then
+ echo "Recreating kind cluster '$HELM_SMOKE_KIND_NAME' to switch from
${current_kind_platform} to ${HELM_SMOKE_KIND_PLATFORM}"
+ kind delete cluster --name "$HELM_SMOKE_KIND_NAME"
+ cluster_exists=false
+ fi
+fi
+
+if [ "$cluster_exists" = true ]; then
+ echo "Reusing existing kind cluster '$HELM_SMOKE_KIND_NAME'"
+else
+ if [ -n "$HELM_SMOKE_KIND_PLATFORM" ]; then
+ echo "Pulling ${HELM_SMOKE_KIND_IMAGE} for ${HELM_SMOKE_KIND_PLATFORM}"
+ docker pull --platform "$HELM_SMOKE_KIND_PLATFORM"
"$HELM_SMOKE_KIND_IMAGE" >/dev/null
+ fi
+ kind create cluster \
+ --name "$HELM_SMOKE_KIND_NAME" \
+ --wait "$HELM_SMOKE_KIND_WAIT" \
+ --config "$kind_config" \
+ --image "$HELM_SMOKE_KIND_IMAGE"
+fi
+
+kubectl config use-context "$kind_context" >/dev/null
+kubectl apply -f
"https://raw.githubusercontent.com/kubernetes/ingress-nginx/${HELM_SMOKE_INGRESS_NGINX_VERSION}/deploy/static/provider/kind/deploy.yaml"
+kubectl -n ingress-nginx rollout status deployment/ingress-nginx-controller
--timeout="$HELM_SMOKE_INGRESS_NGINX_TIMEOUT"
+kubectl -n ingress-nginx wait \
+ --for=condition=ready \
+ pod \
+ --selector=app.kubernetes.io/component=controller \
+ --timeout="$HELM_SMOKE_INGRESS_NGINX_TIMEOUT"
+wait_for_completed_job ingress-nginx-admission-create
+wait_for_completed_job ingress-nginx-admission-patch
+wait_for_ingress_validation
diff --git a/scripts/ci/setup-helm-tools.sh b/scripts/ci/setup-helm-tools.sh
new file mode 100755
index 000000000..5c4a2686f
--- /dev/null
+++ b/scripts/ci/setup-helm-tools.sh
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+set -euo pipefail
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/ci/setup-helm-tools.sh [--install-kind]
+
+Install the pinned Helm toolchain used by Helm chart CI jobs. When requested,
+also install the pinned kind binary used by the Helm smoke test.
+
+Environment:
+ HELM_VERSION Helm release version (default: v4.1.3)
+ HELM_CHECKSUM SHA256 checksum for the Helm tarball
+ KIND_VERSION kind release version (default: v0.31.0)
+ KIND_CHECKSUM SHA256 checksum for the kind binary
+ HELM_TOOLS_BIN_DIR Target binary directory (default: /usr/local/bin)
+EOF
+}
+
+INSTALL_KIND="false"
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --install-kind)
+ INSTALL_KIND="true"
+ ;;
+ --help|-h|help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Error: unknown option '$1'" >&2
+ usage
+ exit 1
+ ;;
+ esac
+ shift
+done
+
+HELM_VERSION="${HELM_VERSION:-v4.1.3}"
+HELM_CHECKSUM="${HELM_CHECKSUM:-02ce9722d541238f81459938b84cf47df2fdf1187493b4bfb2346754d82a4700}"
+KIND_VERSION="${KIND_VERSION:-v0.31.0}"
+KIND_CHECKSUM="${KIND_CHECKSUM:-eb244cbafcc157dff60cf68693c14c9a75c4e6e6fedaf9cd71c58117cb93e3fa}"
+HELM_TOOLS_BIN_DIR="${HELM_TOOLS_BIN_DIR:-/usr/local/bin}"
+
+require_command() {
+ if ! command -v "$1" >/dev/null 2>&1; then
+ echo "Error: required command '$1' not found" >&2
+ exit 1
+ fi
+}
+
+install_binary() {
+ local source="$1"
+ local destination="$2"
+
+ mkdir -p "$HELM_TOOLS_BIN_DIR"
+ if [ -w "$HELM_TOOLS_BIN_DIR" ]; then
+ install -m 0755 "$source" "$destination"
+ else
+ require_command sudo
+ sudo install -m 0755 "$source" "$destination"
+ fi
+}
+
+if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then
+ cat >&2 <<'EOF'
+Error: scripts/ci/setup-helm-tools.sh currently supports Linux x86_64 only.
+Install helm and kind manually on other platforms, then run the local Helm
test scripts.
+EOF
+ exit 1
+fi
+
+require_command wget
+require_command sha256sum
+require_command tar
+require_command install
+
+helm_archive="/tmp/helm.tar.gz"
+helm_dir="/tmp/linux-amd64"
+wget -qO "$helm_archive"
"https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
+echo "${HELM_CHECKSUM} ${helm_archive}" | sha256sum -c -
+rm -rf "$helm_dir"
+tar -zxf "$helm_archive" -C /tmp linux-amd64/helm
+install_binary "$helm_dir/helm" "${HELM_TOOLS_BIN_DIR}/helm"
+
+if [ "$INSTALL_KIND" = "true" ]; then
+ kind_binary="/tmp/kind"
+ wget -qO "$kind_binary"
"https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64"
+ echo "${KIND_CHECKSUM} ${kind_binary}" | sha256sum -c -
+ install_binary "$kind_binary" "${HELM_TOOLS_BIN_DIR}/kind"
+fi
diff --git a/scripts/ci/test-helm.sh b/scripts/ci/test-helm.sh
new file mode 100755
index 000000000..c8cd377f5
--- /dev/null
+++ b/scripts/ci/test-helm.sh
@@ -0,0 +1,452 @@
+#!/usr/bin/env bash
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+set -euo pipefail
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/ci/test-helm.sh
<validate|smoke|cleanup-smoke|collect-smoke-diagnostics> [--cleanup]
+
+Commands:
+ validate Run Helm lint and render validation scenarios.
+ smoke Run the Helm runtime smoke scenario against the
current Kubernetes context.
+ cleanup-smoke Remove the Helm smoke release namespace and any
failed-install leftovers.
+ collect-smoke-diagnostics Collect diagnostics for the Helm smoke namespace.
+
+Notes:
+ - validate requires helm.
+ - smoke requires helm, kubectl, and curl, plus an existing cluster and
ingress controller.
+ - pass --cleanup with smoke to remove the Helm smoke namespace after a
successful run.
+ - cleanup-smoke requires kubectl and optionally helm.
+ - collect-smoke-diagnostics is best-effort and does not fail on missing
resources.
+EOF
+}
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$REPO_ROOT"
+
+CHART_DIR="${CHART_DIR:-helm/charts/iggy}"
+HELM_RENDER_DIR="${HELM_RENDER_DIR:-/tmp/helm-render}"
+HELM_SMOKE_NAMESPACE="${HELM_SMOKE_NAMESPACE:-iggy-smoke}"
+HELM_SMOKE_RELEASE="${HELM_SMOKE_RELEASE:-iggy-smoke}"
+HELM_SMOKE_REPORT_DIR="${HELM_SMOKE_REPORT_DIR:-reports/helm-smoke}"
+HELM_SMOKE_SERVER_HOST="${HELM_SMOKE_SERVER_HOST:-server.iggy.local}"
+HELM_SMOKE_UI_HOST="${HELM_SMOKE_UI_HOST:-ui.iggy.local}"
+HELM_SMOKE_TIMEOUT="${HELM_SMOKE_TIMEOUT:-5m}"
+HELM_SMOKE_INGRESS_CLASS="${HELM_SMOKE_INGRESS_CLASS:-nginx}"
+HELM_SMOKE_KIND_NAME="${HELM_SMOKE_KIND_NAME:-iggy-helm-smoke}"
+HELM_SMOKE_SERVER_CPU_ALLOCATION="${HELM_SMOKE_SERVER_CPU_ALLOCATION:-1}"
+
+require_command() {
+ if ! command -v "$1" >/dev/null 2>&1; then
+ echo "Error: required command '$1' not found" >&2
+ exit 1
+ fi
+}
+
+extract_chart_field() {
+ local field="$1"
+ local value
+
+ value="$(
+ awk -v field="$field" '
+ $1 == field ":" {
+ gsub(/"/, "", $2)
+ print $2
+ exit
+ }
+ ' "$CHART_DIR/Chart.yaml"
+ )"
+
+ if [ -z "$value" ]; then
+ echo "Error: could not extract '$field' from $CHART_DIR/Chart.yaml" >&2
+ exit 1
+ fi
+
+ printf '%s\n' "$value"
+}
+
+extract_values_tag() {
+ local section="$1"
+ local value
+
+ value="$(
+ awk -v section="$section" '
+ $1 == section ":" {
+ in_section = 1
+ next
+ }
+ in_section && /^[^[:space:]]/ {
+ in_section = 0
+ }
+ in_section && $1 == "tag:" {
+ gsub(/"/, "", $2)
+ print $2
+ exit
+ }
+ ' "$CHART_DIR/values.yaml"
+ )"
+
+ if [ -z "$value" ]; then
+ echo "Error: could not extract '$section.image.tag' from
$CHART_DIR/values.yaml" >&2
+ exit 1
+ fi
+
+ printf '%s\n' "$value"
+}
+
+prepare_render_dir() {
+ if [ -z "$HELM_RENDER_DIR" ] || [ "$HELM_RENDER_DIR" = "/" ]; then
+ echo "Error: HELM_RENDER_DIR must not be empty or /" >&2
+ exit 1
+ fi
+
+ rm -rf "$HELM_RENDER_DIR"
+ mkdir -p "$HELM_RENDER_DIR"
+}
+
+extract_kind_names() {
+ local file="$1"
+ local kind="$2"
+
+ awk -v kind="$kind" '
+ /^kind: / {
+ current_kind = $2
+ in_metadata = 0
+ next
+ }
+ /^metadata:$/ {
+ in_metadata = 1
+ next
+ }
+ in_metadata && /^ name: / {
+ if (current_kind == kind) {
+ print $2
+ }
+ in_metadata = 0
+ }
+ ' "$file"
+}
+
+validate() {
+ require_command helm
+
+ local chart_version
+ local chart_app_version
+ local server_image_tag
+ local ui_image_tag
+
+ chart_version="$(extract_chart_field version)"
+ chart_app_version="$(extract_chart_field appVersion)"
+ server_image_tag="$(extract_values_tag server)"
+ ui_image_tag="$(extract_values_tag ui)"
+
+ prepare_render_dir
+
+ helm lint --strict "$CHART_DIR"
+
+ helm template iggy "$CHART_DIR" > "$HELM_RENDER_DIR/default.yaml"
+ test "$(grep -c '^kind: Deployment$' "$HELM_RENDER_DIR/default.yaml")" -eq 2
+ test "$(grep -c '^kind: Service$' "$HELM_RENDER_DIR/default.yaml")" -eq 2
+ test "$(grep -c '^kind: ServiceAccount$' "$HELM_RENDER_DIR/default.yaml")"
-eq 1
+ test "$(grep -c '^kind: Secret$' "$HELM_RENDER_DIR/default.yaml")" -eq 1
+ grep -q "helm.sh/chart: iggy-${chart_version}"
"$HELM_RENDER_DIR/default.yaml"
+ grep -q "helm.sh/chart: iggy-ui-${chart_version}"
"$HELM_RENDER_DIR/default.yaml"
+ grep -q "app.kubernetes.io/version: \"${chart_app_version}\""
"$HELM_RENDER_DIR/default.yaml"
+ grep -q "image: \"apache/iggy:${server_image_tag}\""
"$HELM_RENDER_DIR/default.yaml"
+ grep -q "image: \"apache/iggy-web-ui:${ui_image_tag}\""
"$HELM_RENDER_DIR/default.yaml"
+
+ helm template iggy "$CHART_DIR" \
+ --set server.persistence.enabled=true \
+ --set autoscaling.enabled=true \
+ --set autoscaling.targetCPUUtilizationPercentage=80 \
+ --set server.ingress.enabled=true \
+ --set ui.ingress.enabled=true \
+ --set server.serviceMonitor.enabled=true \
+ > "$HELM_RENDER_DIR/all-features.yaml"
+ grep -q '^kind: PersistentVolumeClaim$' "$HELM_RENDER_DIR/all-features.yaml"
+ grep -q '^kind: HorizontalPodAutoscaler$'
"$HELM_RENDER_DIR/all-features.yaml"
+ test "$(grep -c '^kind: Ingress$' "$HELM_RENDER_DIR/all-features.yaml")" -eq
2
+ test "$(extract_kind_names "$HELM_RENDER_DIR/all-features.yaml" Ingress |
sort -u | wc -l | tr -d ' ')" -eq 2
+ extract_kind_names "$HELM_RENDER_DIR/all-features.yaml" Ingress | grep -qx
'iggy'
+ extract_kind_names "$HELM_RENDER_DIR/all-features.yaml" Ingress | grep -qx
'iggy-ui'
+ grep -q '^kind: ServiceMonitor$' "$HELM_RENDER_DIR/all-features.yaml"
+
+ helm template iggy "$CHART_DIR" \
+ --kube-version 1.18.0 \
+ --api-versions networking.k8s.io/v1beta1 \
+ --api-versions autoscaling/v2beta2 \
+ --set autoscaling.enabled=true \
+ --set autoscaling.targetCPUUtilizationPercentage=80 \
+ --set server.ingress.enabled=true \
+ --set ui.ingress.enabled=true \
+ > "$HELM_RENDER_DIR/legacy-k8s-1.18.yaml"
+ test "$(grep -c '^apiVersion: networking.k8s.io/v1beta1$'
"$HELM_RENDER_DIR/legacy-k8s-1.18.yaml")" -eq 2
+ grep -q '^apiVersion: autoscaling/v2beta2$'
"$HELM_RENDER_DIR/legacy-k8s-1.18.yaml"
+
+ helm template iggy "$CHART_DIR" --set ui.enabled=false >
"$HELM_RENDER_DIR/server-only.yaml"
+ test "$(grep -c '^kind: Deployment$' "$HELM_RENDER_DIR/server-only.yaml")"
-eq 1
+ test "$(grep -c '^kind: Service$' "$HELM_RENDER_DIR/server-only.yaml")" -eq 1
+
+ helm template iggy "$CHART_DIR" --set server.enabled=false >
"$HELM_RENDER_DIR/ui-only.yaml"
+ test "$(grep -c '^kind: Deployment$' "$HELM_RENDER_DIR/ui-only.yaml")" -eq 1
+ test "$(grep -c '^kind: Service$' "$HELM_RENDER_DIR/ui-only.yaml")" -eq 1
+
+ helm template iggy "$CHART_DIR" \
+ --set server.users.root.createSecret=false \
+ --set server.users.root.existingSecret.name=supersecret \
+ > "$HELM_RENDER_DIR/existing-secret.yaml"
+ if grep -q 'root-credentials' "$HELM_RENDER_DIR/existing-secret.yaml"; then
+ echo "Error: existing-secret render should not include generated root
credentials" >&2
+ exit 1
+ fi
+ grep -q 'name: supersecret' "$HELM_RENDER_DIR/existing-secret.yaml"
+}
+
+smoke() {
+ local cleanup_after_success="$1"
+ require_command helm
+ require_command kubectl
+ require_command curl
+
+ local ui_image_tag
+ local server_ping_status
+ local ui_healthz_status
+ local leftover_resources
+ local helm_status
+ local smoke_values_file
+
+ mkdir -p "$HELM_SMOKE_REPORT_DIR"
+
+ if ! helm status "$HELM_SMOKE_RELEASE" -n "$HELM_SMOKE_NAMESPACE" >/dev/null
2>&1; then
+ leftover_resources="$(
+ kubectl -n "$HELM_SMOKE_NAMESPACE" get
deployment,service,ingress,secret,serviceaccount \
+ -l "app.kubernetes.io/instance=${HELM_SMOKE_RELEASE}" \
+ -o name 2>/dev/null || true
+ )"
+ if [ -n "$leftover_resources" ]; then
+ cat >&2 <<EOF
+Found leftover resources for a failed Helm smoke install in namespace
'${HELM_SMOKE_NAMESPACE}'.
+Run 'scripts/ci/test-helm.sh cleanup-smoke' once, then rerun the smoke test.
+EOF
+ printf '%s\n' "$leftover_resources" >&2
+ exit 1
+ fi
+ fi
+
+ ui_image_tag="$(extract_values_tag ui)"
+
+ echo "$ui_image_tag" > "$HELM_SMOKE_REPORT_DIR/ui-image-tag.txt"
+
+ smoke_values_file="$(mktemp
"${TMPDIR:-/tmp}/iggy-helm-smoke-values.XXXXXX.yaml")"
+
+ cat > "$smoke_values_file" <<EOF
+server:
+ ingress:
+ enabled: true
+ className: ${HELM_SMOKE_INGRESS_CLASS}
+ hosts:
+ - host: ${HELM_SMOKE_SERVER_HOST}
+ paths:
+ - path: /
+ pathType: Prefix
+ env:
+ - name: RUST_LOG
+ value: info
+ - name: IGGY_HTTP_ADDRESS
+ value: "0.0.0.0:3000"
+ - name: IGGY_TCP_ADDRESS
+ value: "0.0.0.0:8090"
+ - name: IGGY_QUIC_ADDRESS
+ value: "0.0.0.0:8080"
+ - name: IGGY_WEBSOCKET_ADDRESS
+ value: "0.0.0.0:8092"
+ - name: IGGY_SYSTEM_SHARDING_CPU_ALLOCATION
+ value: "${HELM_SMOKE_SERVER_CPU_ALLOCATION}"
+ui:
+ ingress:
+ enabled: true
+ className: ${HELM_SMOKE_INGRESS_CLASS}
+ hosts:
+ - host: ${HELM_SMOKE_UI_HOST}
+ paths:
+ - path: /
+ pathType: Prefix
+ image:
+ tag: "${ui_image_tag}"
+EOF
+
+ helm_status=0
+ if helm upgrade --install "$HELM_SMOKE_RELEASE" "$CHART_DIR" \
+ --atomic \
+ --namespace "$HELM_SMOKE_NAMESPACE" \
+ --create-namespace \
+ --wait \
+ --timeout "$HELM_SMOKE_TIMEOUT" \
+ -f "$smoke_values_file"; then
+ helm_status=0
+ else
+ helm_status=$?
+ fi
+
+ rm -f "$smoke_values_file"
+
+ if [ "$helm_status" -ne 0 ]; then
+ return "$helm_status"
+ fi
+
+ kubectl version --client=true > "$HELM_SMOKE_REPORT_DIR/kubectl-version.txt"
+ kubectl -n "$HELM_SMOKE_NAMESPACE" rollout status
"deployment/$HELM_SMOKE_RELEASE" --timeout="$HELM_SMOKE_TIMEOUT"
+ kubectl -n "$HELM_SMOKE_NAMESPACE" rollout status
"deployment/${HELM_SMOKE_RELEASE}-ui" --timeout="$HELM_SMOKE_TIMEOUT"
+ kubectl -n "$HELM_SMOKE_NAMESPACE" get pods,svc,ingress >
"$HELM_SMOKE_REPORT_DIR/resources.txt"
+
+ for _ in $(seq 1 30); do
+ server_ping_status="$(
+ curl -sS -o "$HELM_SMOKE_REPORT_DIR/ping.txt" -w '%{http_code}' \
+ -H "Host: ${HELM_SMOKE_SERVER_HOST}" \
+ http://127.0.0.1/ping || true
+ )"
+ if [ "$server_ping_status" = "200" ] && grep -qx 'pong'
"$HELM_SMOKE_REPORT_DIR/ping.txt"; then
+ break
+ fi
+ sleep 2
+ done
+
+ test "$server_ping_status" = "200"
+ grep -qx 'pong' "$HELM_SMOKE_REPORT_DIR/ping.txt"
+
+ for _ in $(seq 1 30); do
+ ui_healthz_status="$(
+ curl -sS -o "$HELM_SMOKE_REPORT_DIR/ui-healthz.txt" -w '%{http_code}' \
+ -H "Host: ${HELM_SMOKE_UI_HOST}" \
+ http://127.0.0.1/healthz || true
+ )"
+ if [ "$ui_healthz_status" = "200" ]; then
+ break
+ fi
+ sleep 2
+ done
+
+ test "$ui_healthz_status" = "200"
+
+ if [ "$cleanup_after_success" = true ]; then
+ cleanup_smoke
+ fi
+}
+
+cleanup_smoke() {
+ require_command kubectl
+
+ if command -v helm >/dev/null 2>&1; then
+ helm uninstall "$HELM_SMOKE_RELEASE" -n "$HELM_SMOKE_NAMESPACE" >/dev/null
2>&1 || true
+ fi
+
+ kubectl delete namespace "$HELM_SMOKE_NAMESPACE" --ignore-not-found=true
--wait=true >/dev/null 2>&1 || true
+}
+
+collect_smoke_diagnostics() {
+ set +e
+
+ mkdir -p "$HELM_SMOKE_REPORT_DIR"
+
+ helm list -A > "$HELM_SMOKE_REPORT_DIR/helm-list.txt" 2>&1 || true
+ kubectl get namespaces > "$HELM_SMOKE_REPORT_DIR/namespaces.txt" 2>&1 || true
+ kubectl -n ingress-nginx get all >
"$HELM_SMOKE_REPORT_DIR/ingress-nginx.txt" 2>&1 || true
+ kubectl -n "$HELM_SMOKE_NAMESPACE" get all >
"$HELM_SMOKE_REPORT_DIR/get-all.txt" 2>&1 || true
+ kubectl -n "$HELM_SMOKE_NAMESPACE" get ingress >
"$HELM_SMOKE_REPORT_DIR/ingresses.txt" 2>&1 || true
+ kubectl -n "$HELM_SMOKE_NAMESPACE" describe "deployment/$HELM_SMOKE_RELEASE"
> "$HELM_SMOKE_REPORT_DIR/describe-deployment.txt" 2>&1 || true
+ kubectl -n "$HELM_SMOKE_NAMESPACE" describe
"deployment/${HELM_SMOKE_RELEASE}-ui" >
"$HELM_SMOKE_REPORT_DIR/describe-ui-deployment.txt" 2>&1 || true
+ kubectl -n "$HELM_SMOKE_NAMESPACE" describe pods >
"$HELM_SMOKE_REPORT_DIR/describe-pods.txt" 2>&1 || true
+ kubectl -n "$HELM_SMOKE_NAMESPACE" logs "deployment/$HELM_SMOKE_RELEASE"
--all-containers=true > "$HELM_SMOKE_REPORT_DIR/server.log" 2>&1 || true
+ kubectl -n "$HELM_SMOKE_NAMESPACE" logs
"deployment/${HELM_SMOKE_RELEASE}-ui" --all-containers=true >
"$HELM_SMOKE_REPORT_DIR/ui.log" 2>&1 || true
+ kubectl -n ingress-nginx logs deployment/ingress-nginx-controller
--all-containers=true > "$HELM_SMOKE_REPORT_DIR/ingress-nginx-controller.log"
2>&1 || true
+
+ if command -v kind >/dev/null 2>&1; then
+ kind export logs "$HELM_SMOKE_REPORT_DIR/kind-logs" --name
"$HELM_SMOKE_KIND_NAME" >/dev/null 2>&1 || true
+ fi
+}
+
+main() {
+ local command
+ local smoke_cleanup=false
+
+ if [ $# -lt 1 ]; then
+ usage
+ exit 1
+ fi
+
+ command="$1"
+ shift
+
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --cleanup)
+ smoke_cleanup=true
+ ;;
+ --help|-h|help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Error: unknown option '$1'" >&2
+ usage
+ exit 1
+ ;;
+ esac
+ shift
+ done
+
+ case "$command" in
+ validate)
+ if [ "$smoke_cleanup" = true ]; then
+ echo "Error: --cleanup is only supported with the smoke command" >&2
+ exit 1
+ fi
+ validate
+ ;;
+ smoke)
+ smoke "$smoke_cleanup"
+ ;;
+ cleanup-smoke)
+ if [ "$smoke_cleanup" = true ]; then
+ echo "Error: --cleanup is only supported with the smoke command" >&2
+ exit 1
+ fi
+ cleanup_smoke
+ ;;
+ collect-smoke-diagnostics)
+ if [ "$smoke_cleanup" = true ]; then
+ echo "Error: --cleanup is only supported with the smoke command" >&2
+ exit 1
+ fi
+ collect_smoke_diagnostics
+ ;;
+ --help|-h|help)
+ usage
+ ;;
+ *)
+ echo "Error: unknown command '$command'" >&2
+ usage
+ exit 1
+ ;;
+ esac
+}
+
+main "$@"