blob: 6bb1a41b7ce69301ec9cda049aa3bb9f9d4c730e [file]
#!/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_yamllint() {
if ! command -v yamllint >/dev/null 2>&1; then
echo "Warning: yamllint not found, skipping YAML lint check" >&2
return 0
fi
echo "Running yamllint on Helm chart..."
yamllint -c "$CHART_DIR/.yamllint.yml" "$CHART_DIR"
}
validate_helmfmt() {
if ! command -v helmfmt >/dev/null 2>&1; then
echo "Warning: helmfmt not found, skipping Helm template format check" >&2
return 0
fi
echo "Checking Helm template formatting..."
if ! helmfmt --check "$CHART_DIR"; then
echo "Error: Helm templates are not formatted. Run 'helmfmt $CHART_DIR' locally and commit the changes." >&2
exit 1
fi
}
validate_helm_docs() {
if ! command -v helm-docs >/dev/null 2>&1; then
echo "Warning: helm-docs not found, skipping README drift check" >&2
return 0
fi
local readme_before
local readme_after
readme_before="$(cat "$CHART_DIR/README.md")"
helm-docs --chart-search-root "$CHART_DIR" --template-files README.md.gotmpl
readme_after="$(cat "$CHART_DIR/README.md")"
if [ "$readme_before" != "$readme_after" ]; then
echo "Error: README.md is out of sync with values.yaml. Run 'helm-docs' locally and commit the updated README." >&2
exit 1
fi
}
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"
validate_yamllint
validate_helmfmt
validate_helm_docs
}
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 "$@"