initial commit
All checks were successful
Build and Publish TechDocs / build-and-publish (push) Successful in 1m15s

Change-Id: I2e2564a72b6be9af536235fc3795fd788fd9257b
This commit is contained in:
Scaffolder
2026-04-15 15:41:22 +00:00
commit b6460c4ea3
180 changed files with 12299 additions and 0 deletions

257
test/README.md Normal file
View File

@@ -0,0 +1,257 @@
# Helm Chart Testing
Three test scripts are provided:
1. **`local-test.sh`** — Offline lint + template validation (no cluster needed)
2. **`integration-test.sh`** — Deploy to a real Kind cluster and verify resources
3. **`ct-test.sh`** — Wrapper around [chart-testing](https://github.com/helm/chart-testing) (`ct`), matching what CircleCI runs
## 1. Offline Tests (`local-test.sh`)
Runs `helm lint` and `helm template` — no cluster required.
### Prerequisites
- [Helm](https://helm.sh/docs/intro/install/) v3.x
### Usage
```bash
# Test all charts
./test/local-test.sh
# Test a specific chart
./test/local-test.sh haproxy-unified-gateway
./test/local-test.sh kubernetes-ingress
```
### What It Tests
| Test | Description |
|------|-------------|
| **Chart.yaml metadata** | Verifies required fields (`name`, `version`, `appVersion`, `description`) |
| **helm lint (defaults)** | Runs `helm lint` with default values |
| **helm template (defaults)** | Renders templates and checks for errors |
| **Deployment vs DaemonSet** | Confirms charts that support `controller.kind` render the correct resource type |
| **HugConf cleanup hook** | Verifies cleanup hook renders/skips based on `hugconf.create` and targets correct name |
| **Metrics port** | Verifies metrics container port (31060) renders, `--metrics-auth` flag is correct for both `kube-rbac` (default) and `none`, metrics Service has both `stat` and `metrics` ports |
| **ServiceMonitor/PodMonitor** | Renders with `monitoring.coreos.com/v1` API, skipped without it, metrics Service only with ServiceMonitor |
| **CI values (lint + template)** | Lint and template for every `ci/*.yaml` file |
---
## 2. Integration Tests (`integration-test.sh`)
Installs charts into a real Kubernetes cluster, waits for pods to become Ready,
verifies key resources exist, then cleans up.
By default the script creates a [Kind](https://kind.sigs.k8s.io/) cluster named
`dev-helm-charts`, runs all tests, and deletes the cluster when done.
### Prerequisites
- [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
- [Helm](https://helm.sh/docs/intro/install/) v3.x
- `kubectl`
### Usage
```bash
# Test all charts (creates + destroys Kind cluster automatically)
./test/integration-test.sh
# Test a specific chart
./test/integration-test.sh haproxy-unified-gateway
./test/integration-test.sh kubernetes-ingress
```
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `TIMEOUT` | `120` | Max seconds to wait for pods per install |
| `KEEP_NS` | `0` | Set to `1` to keep test namespaces after run (for debugging) |
| `CI_FILTER` | _(empty)_ | Glob pattern to filter `ci/` values files (e.g. `deployment-*`) |
| `TEST_FILTER` | _(empty)_ | Run only a specific test: `defaults`, `daemonset`, `hpa`, `pdb`, `metrics-port`, `monitoring`, `hugconf-cleanup`, `ci` |
| `KIND_KEEP_CLUSTER` | `0` | Set to `1` to keep the Kind cluster after tests finish |
| `KIND_CLUSTER_NAME` | `dev-helm-charts` | Custom Kind cluster name |
| `SKIP_KIND` | `0` | Set to `1` to skip Kind management and use your existing kubeconfig |
### Examples
```bash
# Run tests and keep the Kind cluster for manual inspection afterwards
KIND_KEEP_CLUSTER=1 ./test/integration-test.sh haproxy-unified-gateway
# Re-run tests against the cluster you kept (reuses existing cluster)
./test/integration-test.sh haproxy-unified-gateway
# Delete the cluster manually when done
kind delete cluster --name dev-helm-charts
# Use your own cluster instead of Kind
SKIP_KIND=1 ./test/integration-test.sh haproxy-unified-gateway
# Longer timeout for slow clusters
TIMEOUT=300 ./test/integration-test.sh haproxy-unified-gateway
# Only run deployment-related ci/ test cases
CI_FILTER="deployment-*" ./test/integration-test.sh haproxy-unified-gateway
# Run only the monitoring test (ServiceMonitor/PodMonitor)
TEST_FILTER=monitoring ./test/integration-test.sh haproxy-unified-gateway
# Run only the metrics port test
TEST_FILTER=metrics-port ./test/integration-test.sh haproxy-unified-gateway
# Run only the HugConf cleanup test
TEST_FILTER=hugconf-cleanup ./test/integration-test.sh haproxy-unified-gateway
# Run only the defaults test
TEST_FILTER=defaults ./test/integration-test.sh haproxy-unified-gateway
# Keep namespaces after the run for manual inspection
KEEP_NS=1 ./test/integration-test.sh haproxy-unified-gateway
# Then clean up manually when done
kubectl get ns | grep '^it-' | awk '{print $1}' | xargs kubectl delete ns
```
### What It Tests
For each chart, the script runs these phases:
| Test | Description |
|------|-------------|
| **Install (defaults)** | `helm install` with default values, wait for pods, verify resources |
| **Install (DaemonSet)** | Same but with `controller.kind=DaemonSet` (if chart supports it) |
| **Install (HPA)** | Enables HPA, verifies HPA resource is created |
| **Install (PDB)** | Enables PDB, verifies PDB resource is created |
| **Metrics port** | Verifies controller pod has metrics container port 31060, `--metrics-auth=kube-rbac` arg, and main Service has stat port |
| **Install (monitoring)** | Installs Prometheus Operator CRDs, verifies ServiceMonitor/PodMonitor/metrics Service with both stat + metrics ports |
| **HugConf cleanup** | Verifies HugConf CR is deleted after `helm uninstall` |
| **Install ci/ values** | Installs with each `ci/*.yaml` file, waits for pods |
Each test:
1. Creates an isolated namespace (`it-<chart>-<suffix>-<timestamp>`)
2. Runs `helm install --wait`
3. Verifies pods reach Running/Completed state
4. On failure: dumps pod status, events, and container logs
5. Runs `helm uninstall` and deletes the namespace
### On Failure
When a test fails, the script automatically prints:
- Pod status (`kubectl get pods -o wide`)
- Recent events (`kubectl get events`)
- Container logs from unhealthy pods (last 20 lines)
---
## 3. Chart Testing (`ct-test.sh`)
Wrapper around [chart-testing](https://github.com/helm/chart-testing) (`ct`) — the same
tool CircleCI uses. Runs `ct lint` and `ct install` locally so you can validate before pushing.
### Prerequisites
- [ct](https://github.com/helm/chart-testing#installation) v3.x
- [yamale](https://github.com/23andMe/Yamale) (`pipx install yamale`)
- [yamllint](https://github.com/adrienverber/yamllint) (`pipx install yamllint`)
- [Helm](https://helm.sh/docs/intro/install/) v3.x
- [Kind](https://kind.sigs.k8s.io/) — only for `install` mode
- `kubectl` — only for `install` mode
### ct Usage
```bash
# Lint all charts
./test/ct-test.sh lint
# Lint a specific chart
./test/ct-test.sh lint haproxy-unified-gateway
# Install all charts on a Kind cluster (creates + destroys automatically)
./test/ct-test.sh install
# Install a specific chart
./test/ct-test.sh install haproxy-unified-gateway
# Lint + install in one go
./test/ct-test.sh all haproxy-unified-gateway
```
### ct Environment Variables
| Variable | Default | Description |
| ------------------- | --------- | ---------------------------------------------------------------- |
| `KIND_CLUSTER_NAME` | `ct-dev` | Kind cluster name |
| `KIND_KEEP_CLUSTER` | `0` | Set to `1` to keep the Kind cluster after tests |
| `SKIP_KIND` | `0` | Set to `1` to skip Kind management and use existing kubeconfig |
| `CT_ARGS` | _(empty)_ | Extra arguments passed to `ct` (e.g. `--debug`) |
### ct Examples
```bash
# Lint with debug output
CT_ARGS="--debug" ./test/ct-test.sh lint haproxy-unified-gateway
# Install and keep cluster for inspection
KIND_KEEP_CLUSTER=1 ./test/ct-test.sh install haproxy-unified-gateway
# Use an existing cluster
SKIP_KIND=1 ./test/ct-test.sh install haproxy-unified-gateway
# Delete the cluster manually when done
kind delete cluster --name ct-dev
```
### CircleCI Compatibility
The CircleCI pipeline (`.circleci/config.yml`) runs:
1. `ct lint --all` — validates Chart.yaml schema, runs `helm lint` with each `ci/*.yaml` file
2. `ct install --all` — installs each chart with each `ci/*.yaml` file on a Kind cluster
`ct-test.sh` runs the exact same commands locally using the config in `test/ct.yaml`,
so if it passes locally it will pass in CI.
---
## CI Values Files
Both scripts use the `ci/` values files in each chart directory. These are the same
files used by [chart-testing](https://github.com/helm/chart-testing) (`ct`) in CircleCI.
Naming convention:
```
<mode>-<feature>-values.yaml
```
Examples:
- `deployment-default-values.yaml` — Deployment with defaults
- `daemonset-hostport-values.yaml` — DaemonSet with host port mapping
- `deployment-hpa-values.yaml` — Deployment with HPA enabled
## Adding New Test Cases
1. Create a new values file in `<chart>/ci/` following the naming convention.
2. Validate offline:
```bash
./test/local-test.sh <chart-name>
```
3. Verify ct lint passes (matches CircleCI):
```bash
./test/ct-test.sh lint <chart-name>
```
4. Deploy to a cluster:
```bash
CI_FILTER="<your-new-file>" ./test/integration-test.sh <chart-name>
```
5. The file is automatically picked up by all three scripts and CircleCI's `ct lint`.

205
test/ct-test.sh Executable file
View File

@@ -0,0 +1,205 @@
#!/bin/bash
#
# Chart-testing (ct) wrapper for local development.
#
# Uses ct to lint and optionally install Helm charts, matching what
# CircleCI runs but locally against a Kind cluster.
#
# Prerequisites:
# - ct (https://github.com/helm/chart-testing)
# - kind (https://kind.sigs.k8s.io/) — only for install tests
# - kubectl, helm
#
# Usage:
# ./test/ct-test.sh # lint all charts
# ./test/ct-test.sh lint # lint all charts
# ./test/ct-test.sh lint haproxy-unified-gateway # lint one chart
# ./test/ct-test.sh install # install all charts (creates Kind cluster)
# ./test/ct-test.sh install haproxy-unified-gateway # install one chart
#
# Options (env vars):
# KIND_CLUSTER_NAME=name Custom cluster name (default: ct-dev)
# KIND_KEEP_CLUSTER=1 Don't delete the Kind cluster after tests
# SKIP_KIND=1 Skip Kind cluster management (use existing kubeconfig)
# CT_ARGS="..." Extra arguments passed to ct (e.g. "--debug")
#
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CT_CONFIG="${SCRIPT_DIR}/ct.yaml"
CT_VERSION="${CT_VERSION:-v3.10.0}"
KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-ct-dev}"
KIND_KEEP_CLUSTER="${KIND_KEEP_CLUSTER:-0}"
SKIP_KIND="${SKIP_KIND:-0}"
CT_ARGS="${CT_ARGS:-}"
KIND_CREATED=false
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BOLD='\033[1m'
NC='\033[0m'
# --- prerequisites ---
check_tool() {
if ! command -v "$1" &>/dev/null; then
echo -e "${RED}ERROR:${NC} '$1' not found in PATH" >&2
return 1
fi
}
# --- ct config files ---
# Download chart_schema.yaml and lintconf.yaml from the ct release matching CT_VERSION.
ensure_ct_configs() {
local base_url="https://raw.githubusercontent.com/helm/chart-testing/${CT_VERSION}/etc"
local files=(chart_schema.yaml lintconf.yaml)
for f in "${files[@]}"; do
if [ ! -f "${SCRIPT_DIR}/${f}" ]; then
echo -e "Downloading ${f} (ct ${CT_VERSION})..."
if ! curl -sSfL "${base_url}/${f}" -o "${SCRIPT_DIR}/${f}"; then
echo -e "${RED}ERROR:${NC} failed to download ${f}" >&2
exit 1
fi
fi
done
}
# --- Kind cluster management ---
create_kind_cluster() {
if [ "$SKIP_KIND" = "1" ]; then
echo -e "${YELLOW}SKIP_KIND=1, using existing kubeconfig${NC}"
return
fi
check_tool kind
if kind get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER_NAME}$"; then
echo -e "Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC} already exists, reusing"
kubectl cluster-info --context "kind-${KIND_CLUSTER_NAME}" &>/dev/null || {
echo -e "${RED}ERROR:${NC} cluster exists but is not reachable" >&2
exit 1
}
kubectl config use-context "kind-${KIND_CLUSTER_NAME}" &>/dev/null || true
return
fi
echo -e "Creating Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC}..."
kind create cluster --name "${KIND_CLUSTER_NAME}" --wait 120s
KIND_CREATED=true
echo -e "${GREEN}Kind cluster created${NC}"
}
delete_kind_cluster() {
if [ "$SKIP_KIND" = "1" ]; then
return
fi
if [ "$KIND_KEEP_CLUSTER" = "1" ]; then
echo -e "\n${YELLOW}KIND_KEEP_CLUSTER=1, keeping cluster '${KIND_CLUSTER_NAME}'${NC}"
echo -e " Delete: kind delete cluster --name ${KIND_CLUSTER_NAME}"
return
fi
if $KIND_CREATED || kind get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER_NAME}$"; then
echo -e "\nDeleting Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC}..."
kind delete cluster --name "${KIND_CLUSTER_NAME}"
echo -e "${GREEN}Kind cluster deleted${NC}"
fi
}
# --- ct commands ---
do_lint() {
local chart_arg="$1"
echo -e "\n${BOLD}=== ct lint ===${NC}"
local ct_cmd=(ct lint --config "$CT_CONFIG")
if [ -n "$chart_arg" ]; then
ct_cmd+=(--charts "$REPO_ROOT/$chart_arg")
else
ct_cmd+=(--all)
fi
# shellcheck disable=SC2086
"${ct_cmd[@]}" $CT_ARGS
echo -e "${GREEN}ct lint passed${NC}"
}
do_install() {
local chart_arg="$1"
echo -e "\n${BOLD}=== ct install ===${NC}"
check_tool kubectl
check_tool helm
create_kind_cluster
trap delete_kind_cluster EXIT
# Verify cluster is reachable
if ! kubectl cluster-info &>/dev/null; then
echo -e "${RED}ERROR:${NC} cannot connect to Kubernetes cluster" >&2
exit 1
fi
echo "Cluster: $(kubectl config current-context 2>/dev/null || echo 'unknown')"
local ct_cmd=(ct install --config "$CT_CONFIG")
if [ -n "$chart_arg" ]; then
ct_cmd+=(--charts "$REPO_ROOT/$chart_arg")
else
ct_cmd+=(--all)
fi
# shellcheck disable=SC2086
"${ct_cmd[@]}" $CT_ARGS
echo -e "${GREEN}ct install passed${NC}"
}
# --- main ---
main() {
local mode="${1:-lint}"
local chart="${2:-}"
check_tool ct
check_tool helm
ensure_ct_configs
echo -e "${BOLD}Chart Testing (ct) — local${NC}"
echo "Repo: $REPO_ROOT"
echo "Config: $CT_CONFIG"
echo "ct: $(ct version 2>&1 | head -1)"
echo "Helm: $(helm version --short 2>/dev/null)"
echo "Mode: $mode"
[ -n "$chart" ] && echo "Chart: $chart"
cd "$REPO_ROOT"
case "$mode" in
lint)
do_lint "$chart"
;;
install)
do_install "$chart"
;;
all)
do_lint "$chart"
do_install "$chart"
;;
*)
echo -e "${RED}Unknown mode:${NC} $mode"
echo "Usage: $0 [lint|install|all] [chart-name]"
exit 1
;;
esac
}
main "$@"

5
test/ct.yaml Normal file
View File

@@ -0,0 +1,5 @@
chart-dirs:
- .
check-version-increment: false
chart-yaml-schema: test/chart_schema.yaml
lint-conf: test/lintconf.yaml

784
test/integration-test.sh Executable file
View File

@@ -0,0 +1,784 @@
#!/bin/bash
#
# Integration test: deploy Helm chart(s) to a real Kubernetes cluster.
#
# By default, creates a Kind cluster named "dev-helm-charts", runs the tests,
# and deletes the cluster when done. Set KIND_KEEP_CLUSTER=1 to keep it.
#
# Prerequisites:
# - kind (https://kind.sigs.k8s.io/)
# - kubectl
# - Helm v3.x
#
# Usage:
# ./test/integration-test.sh # test all charts
# ./test/integration-test.sh haproxy-unified-gateway # test only HUG
# ./test/integration-test.sh kubernetes-ingress # test only ingress
#
# Options (env vars):
# TIMEOUT=120 Max seconds to wait for pods (default: 120)
# KEEP_NS=1 Don't delete test namespaces on success (for debugging)
# CI_FILTER=<glob> Only test ci/ files matching pattern (e.g. "deployment-*")
# TEST_FILTER=<name> Run only a specific test (defaults, daemonset, hpa, pdb, monitoring, ci)
# KIND_KEEP_CLUSTER=1 Don't delete the Kind cluster after tests
# KIND_CLUSTER_NAME=name Custom cluster name (default: dev-helm-charts)
# SKIP_KIND=1 Skip Kind cluster creation (use existing kubeconfig)
#
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TIMEOUT="${TIMEOUT:-120}"
KEEP_NS="${KEEP_NS:-0}"
CI_FILTER="${CI_FILTER:-}"
KIND_KEEP_CLUSTER="${KIND_KEEP_CLUSTER:-0}"
KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-dev-helm-charts}"
SKIP_KIND="${SKIP_KIND:-0}"
TEST_FILTER="${TEST_FILTER:-}"
PASSED=0
FAILED=0
SKIPPED=0
FAILURES=()
NAMESPACES=()
KIND_CREATED=false
KEDA_CRDS_INSTALLED=false
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BOLD='\033[1m'
NC='\033[0m'
log_pass() { echo -e " ${GREEN}PASS${NC} $1"; PASSED=$((PASSED + 1)); }
log_fail() { echo -e " ${RED}FAIL${NC} $1"; FAILED=$((FAILED + 1)); FAILURES+=("$1"); }
log_skip() { echo -e " ${YELLOW}SKIP${NC} $1"; SKIPPED=$((SKIPPED + 1)); }
log_section() { echo -e "\n${BOLD}=== $1 ===${NC}"; }
log_info() { echo -e " ${YELLOW}INFO${NC} $1"; }
# --- Kind cluster management ---
create_kind_cluster() {
if [ "$SKIP_KIND" = "1" ]; then
echo -e "${YELLOW}SKIP_KIND=1, using existing kubeconfig${NC}"
return
fi
if ! command -v kind &>/dev/null; then
echo -e "${RED}ERROR:${NC} 'kind' not found in PATH" >&2
echo "Install it: https://kind.sigs.k8s.io/docs/user/quick-start/#installation" >&2
exit 1
fi
# Check if cluster already exists
if kind get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER_NAME}$"; then
echo -e "Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC} already exists, reusing it"
kubectl cluster-info --context "kind-${KIND_CLUSTER_NAME}" &>/dev/null || {
echo -e "${RED}ERROR:${NC} cluster exists but is not reachable" >&2
exit 1
}
export KUBECONFIG
KUBECONFIG=$(kind get kubeconfig-path --name="${KIND_CLUSTER_NAME}" 2>/dev/null || echo "")
kubectl config use-context "kind-${KIND_CLUSTER_NAME}" &>/dev/null || true
return
fi
echo -e "Creating Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC}..."
kind create cluster \
--name "${KIND_CLUSTER_NAME}" \
--wait 120s
KIND_CREATED=true
echo -e "${GREEN}Kind cluster created${NC}"
}
delete_kind_cluster() {
if [ "$SKIP_KIND" = "1" ]; then
return
fi
if [ "$KIND_KEEP_CLUSTER" = "1" ]; then
echo -e "\n${YELLOW}KIND_KEEP_CLUSTER=1, keeping cluster '${KIND_CLUSTER_NAME}'${NC}"
echo -e " To use: kubectl --context kind-${KIND_CLUSTER_NAME} ..."
echo -e " Delete: kind delete cluster --name ${KIND_CLUSTER_NAME}"
return
fi
if $KIND_CREATED || kind get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER_NAME}$"; then
echo -e "\nDeleting Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC}..."
kind delete cluster --name "${KIND_CLUSTER_NAME}"
echo -e "${GREEN}Kind cluster deleted${NC}"
fi
}
# --- helpers ---
check_prerequisites() {
local ok=true
for cmd in kubectl helm; do
if ! command -v "$cmd" &>/dev/null; then
echo -e "${RED}ERROR:${NC} '$cmd' not found in PATH" >&2
ok=false
fi
done
if ! kubectl cluster-info &>/dev/null; then
echo -e "${RED}ERROR:${NC} cannot connect to Kubernetes cluster" >&2
ok=false
fi
if ! $ok; then
exit 1
fi
}
find_charts() {
local filter="${1:-}"
local charts=()
for dir in "$REPO_ROOT"/*/; do
[ -f "$dir/Chart.yaml" ] || continue
local name
name="$(basename "$dir")"
if [ -n "$filter" ] && [ "$name" != "$filter" ]; then
continue
fi
charts+=("$name")
done
if [ ${#charts[@]} -eq 0 ]; then
echo "No charts found${filter:+ matching '$filter'}." >&2
exit 1
fi
echo "${charts[@]}"
}
# Generate a unique namespace for a test run
make_ns() {
local chart="$1"
local suffix="$2"
# namespace max 63 chars; keep it short
local ns="it-${chart}-${suffix}-$(date +%s)"
ns="${ns:0:63}"
echo "$ns"
}
create_ns() {
local ns="$1"
kubectl create namespace "$ns" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
NAMESPACES+=("$ns")
}
delete_ns() {
local ns="$1"
if [ "$KEEP_NS" = "1" ]; then
log_info "KEEP_NS=1, not deleting namespace $ns"
return
fi
kubectl delete namespace "$ns" --wait=false --ignore-not-found >/dev/null 2>&1 || true
}
# Wait for all pods in a namespace to be Ready (or Completed for Jobs)
wait_for_pods() {
local ns="$1"
local deadline=$((SECONDS + TIMEOUT))
while [ $SECONDS -lt $deadline ]; do
local not_ready=0
local pod_lines
pod_lines=$(kubectl get pods -n "$ns" --no-headers 2>/dev/null || true)
if [ -z "$pod_lines" ]; then
# No pods yet, keep waiting
sleep 2
continue
fi
while IFS= read -r line; do
local status
status=$(echo "$line" | awk '{print $3}')
case "$status" in
Running|Completed|Succeeded)
# Check if Running pods have all containers ready
if [ "$status" = "Running" ]; then
local ready_col
ready_col=$(echo "$line" | awk '{print $2}')
local ready_count="${ready_col%/*}"
local total_count="${ready_col#*/}"
if [ "$ready_count" != "$total_count" ]; then
not_ready=$((not_ready + 1))
fi
fi
;;
Error|CrashLoopBackOff|ImagePullBackOff|ErrImagePull|CreateContainerConfigError|InvalidImageName)
# Terminal failure
return 1
;;
*)
not_ready=$((not_ready + 1))
;;
esac
done <<< "$pod_lines"
if [ "$not_ready" -eq 0 ]; then
return 0
fi
sleep 3
done
return 1 # timeout
}
# Collect debug info on failure
dump_debug() {
local ns="$1"
echo -e " ${YELLOW}--- debug info (ns: $ns) ---${NC}"
echo " Pods:"
kubectl get pods -n "$ns" -o wide 2>/dev/null | sed 's/^/ /'
echo " Events (last 10):"
kubectl get events -n "$ns" --sort-by='.lastTimestamp' 2>/dev/null | tail -10 | sed 's/^/ /'
# Show logs from non-ready pods
local pods
pods=$(kubectl get pods -n "$ns" --no-headers 2>/dev/null | awk '$3 !~ /Running|Completed|Succeeded/ {print $1}')
if [ -z "$pods" ]; then
# Also grab CrashLoopBackOff pods that show as "Running"
pods=$(kubectl get pods -n "$ns" --no-headers 2>/dev/null | awk '{split($2,a,"/"); if (a[1]!=a[2]) print $1}')
fi
for pod in $pods; do
echo " Logs ($pod):"
kubectl logs -n "$ns" "$pod" --all-containers --tail=20 2>/dev/null | sed 's/^/ /' || true
done
echo -e " ${YELLOW}--- end debug ---${NC}"
}
# --- test functions ---
# Return chart-specific helm args needed for test installs.
# Some charts need overrides to install cleanly on a fresh cluster.
chart_test_args() {
local chart="$1"
case "$chart" in
haproxy-unified-gateway)
# No special overrides needed: the HugConf CR is a post-install hook
# (weight 5) that runs after the CRD job (weight 0), avoiding the
# chicken-and-egg validation problem.
;;
esac
}
# Install chart with given values, wait for pods, verify resources, then clean up.
install_and_verify() {
local chart="$1"
local label="$2"
local ns="$3"
shift 3
# Remaining args are passed to helm install
local helm_args=("$@")
# Add chart-specific test overrides
local extra_args
extra_args=$(chart_test_args "$chart")
if [ -n "$extra_args" ]; then
# shellcheck disable=SC2206
helm_args+=($extra_args)
fi
create_ns "$ns"
# Install
if ! helm install "test-${chart}" "$REPO_ROOT/$chart" \
--namespace "$ns" \
--wait --timeout "${TIMEOUT}s" \
"${helm_args[@]}" 2>&1 | sed 's/^/ /'; then
log_fail "$label (helm install failed)"
dump_debug "$ns"
delete_ns "$ns"
return
fi
# Wait for pods to be ready (belt-and-suspenders on top of --wait)
if ! wait_for_pods "$ns"; then
log_fail "$label (pods not ready within ${TIMEOUT}s)"
dump_debug "$ns"
helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true
delete_ns "$ns"
return
fi
# Basic resource verification
local ok=true
# Check that we have at least one Running or Completed pod
local running
running=$(kubectl get pods -n "$ns" --no-headers 2>/dev/null | grep -cE 'Running|Completed|Succeeded' || true)
if [ "$running" -eq 0 ]; then
echo -e " ${RED}No running pods found${NC}"
ok=false
fi
# Verify ServiceAccount exists
if kubectl get serviceaccount -n "$ns" -l "app.kubernetes.io/instance=test-${chart}" --no-headers 2>/dev/null | grep -q .; then
: # ok
else
# Some charts may not label SA; check by name
if ! kubectl get serviceaccount -n "$ns" 2>/dev/null | grep -q "test-${chart}\|${chart}"; then
echo -e " ${YELLOW}ServiceAccount not found (may be expected)${NC}"
fi
fi
# Verify Service exists (if applicable)
local svc_count
svc_count=$(kubectl get svc -n "$ns" --no-headers 2>/dev/null | wc -l)
if [ "$svc_count" -gt 0 ]; then
: # ok
fi
if $ok; then
log_pass "$label"
else
log_fail "$label"
dump_debug "$ns"
fi
# Cleanup
helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true
delete_ns "$ns"
}
# Test: default values install
test_install_defaults() {
local chart="$1"
local label="$chart: install (defaults)"
local ns
ns=$(make_ns "$chart" "defaults")
install_and_verify "$chart" "$label" "$ns"
}
# Install KEDA CRDs if not already installed.
# Required for ci/ values files that enable KEDA ScaledObject.
ensure_keda_crds() {
if $KEDA_CRDS_INSTALLED; then
return 0
fi
local keda_crd_url="https://github.com/kedacore/keda/releases/download/v2.16.1/keda-2.16.1-crds.yaml"
if kubectl apply --server-side -f "$keda_crd_url" >/dev/null 2>&1; then
KEDA_CRDS_INSTALLED=true
log_info "Installed KEDA CRDs (ScaledObject, ScaledJob, TriggerAuthentication)"
return 0
else
return 1
fi
}
# Test: install with each ci/ values file
test_install_ci_values() {
local chart="$1"
local ci_dir="$REPO_ROOT/$chart/ci"
if [ ! -d "$ci_dir" ]; then
log_skip "$chart: no ci/ directory"
return
fi
local count=0
for values_file in "$ci_dir"/*.yaml "$ci_dir"/*.yml; do
[ -f "$values_file" ] || continue
local fname
fname="$(basename "$values_file")"
# Apply CI_FILTER if set
if [ -n "$CI_FILTER" ]; then
# shellcheck disable=SC2254
case "$fname" in
$CI_FILTER) ;;
*) continue ;;
esac
fi
count=$((count + 1))
# Install KEDA CRDs if this values file enables KEDA
if [[ "$fname" == *keda* ]]; then
if ! ensure_keda_crds; then
log_skip "$chart: install ci/$fname - could not install KEDA CRDs"
continue
fi
fi
local label="$chart: install ci/$fname"
local suffix="${fname%.yaml}"
suffix="${suffix%.yml}"
suffix="${suffix%-values}"
local ns
ns=$(make_ns "$chart" "$suffix")
install_and_verify "$chart" "$label" "$ns" -f "$values_file"
done
if [ "$count" -eq 0 ]; then
log_skip "$chart: ci/ directory is empty${CI_FILTER:+ (or no match for '$CI_FILTER')}"
fi
}
# Test: Deployment vs DaemonSet mode (if chart supports it)
test_install_daemonset() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
if ! grep -q 'kind: Deployment' "$values_yaml" 2>/dev/null; then
return
fi
if [ ! -f "$REPO_ROOT/$chart/templates/controller-daemonset.yaml" ]; then
return
fi
local label="$chart: install (DaemonSet mode)"
local ns
ns=$(make_ns "$chart" "daemonset")
install_and_verify "$chart" "$label" "$ns" --set controller.kind=DaemonSet
}
# Test: verify HPA creates when enabled (if chart supports it)
test_install_hpa() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
if ! grep -q 'autoscaling:' "$values_yaml" 2>/dev/null; then
return
fi
local label="$chart: install (HPA enabled)"
local ns
ns=$(make_ns "$chart" "hpa")
install_and_verify "$chart" "$label" "$ns" \
--set controller.autoscaling.enabled=true \
--set controller.autoscaling.minReplicas=1 \
--set controller.autoscaling.maxReplicas=3 \
--set controller.autoscaling.targetCPUUtilizationPercentage=80
}
# Test: verify PDB creates when enabled (if chart supports it)
test_install_pdb() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
if ! grep -q 'podDisruptionBudget:' "$values_yaml" 2>/dev/null; then
return
fi
local label="$chart: install (PDB enabled)"
local ns
ns=$(make_ns "$chart" "pdb")
install_and_verify "$chart" "$label" "$ns" \
--set controller.replicaCount=2 \
--set controller.podDisruptionBudget.enabled=true \
--set controller.podDisruptionBudget.minAvailable=1
}
# Test: verify metrics port is exposed and metricsAuth flag is passed
test_install_metrics_port() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
if ! grep -q 'metricsAuth:' "$values_yaml" 2>/dev/null; then
return
fi
local label="$chart: install (metrics port exposed)"
local ns
ns=$(make_ns "$chart" "metrics")
create_ns "$ns"
local extra_args
extra_args=$(chart_test_args "$chart")
# shellcheck disable=SC2086
if ! helm install "test-${chart}" "$REPO_ROOT/$chart" \
--namespace "$ns" \
--wait --timeout "${TIMEOUT}s" \
$extra_args 2>&1 | sed 's/^/ /'; then
log_fail "$label (helm install failed)"
dump_debug "$ns"
delete_ns "$ns"
return
fi
if ! wait_for_pods "$ns"; then
log_fail "$label (pods not ready within ${TIMEOUT}s)"
dump_debug "$ns"
helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true
delete_ns "$ns"
return
fi
# Verify the controller pod has the metrics container port (31060)
local ports_label="$chart: controller pod has metrics port 31060"
if kubectl get pods -n "$ns" -o jsonpath='{.items[*].spec.containers[*].ports[*].containerPort}' 2>/dev/null | tr ' ' '\n' | grep -q '31060'; then
log_pass "$ports_label"
else
log_fail "$ports_label"
fi
# Verify --metrics-auth=kube-rbac is in the container args
local args_label="$chart: controller pod has --metrics-auth=kube-rbac arg"
if kubectl get pods -n "$ns" -o jsonpath='{.items[*].spec.containers[*].args[*]}' 2>/dev/null | grep -q 'metrics-auth=kube-rbac'; then
log_pass "$args_label"
else
log_fail "$args_label"
fi
# Verify the main Service has the stat port (it should NOT have metrics — that's only on the metrics Service)
local svc_label="$chart: main Service has stat port"
if kubectl get svc -n "$ns" "test-${chart}" -o jsonpath='{.spec.ports[*].name}' 2>/dev/null | grep -q 'stat'; then
log_pass "$svc_label"
else
log_fail "$svc_label"
fi
helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true
delete_ns "$ns"
}
# Test: verify ServiceMonitor/PodMonitor install (if chart supports it)
# Installs Prometheus Operator CRDs so the monitoring.coreos.com/v1 API is available,
# then verifies the resources are created.
test_install_monitoring() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
if ! grep -q 'serviceMonitor:' "$values_yaml" 2>/dev/null; then
return
fi
# Install Prometheus Operator CRDs so the API is available
local prom_crds_installed=false
local prom_crd_url="https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/jsonnet/prometheus-operator/podmonitors-crd.json"
local sm_crd_url="https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/jsonnet/prometheus-operator/servicemonitors-crd.json"
# Try to apply the CRDs; if curl/URLs fail, skip
if kubectl apply -f "$sm_crd_url" >/dev/null 2>&1 && \
kubectl apply -f "$prom_crd_url" >/dev/null 2>&1; then
prom_crds_installed=true
log_info "Installed Prometheus Operator CRDs (ServiceMonitor, PodMonitor)"
else
log_skip "$chart: install (ServiceMonitor) - could not install Prometheus Operator CRDs"
log_skip "$chart: install (PodMonitor) - could not install Prometheus Operator CRDs"
return
fi
# ServiceMonitor test
local label="$chart: install (ServiceMonitor enabled)"
local ns
ns=$(make_ns "$chart" "svcmon")
create_ns "$ns"
local extra_args
extra_args=$(chart_test_args "$chart")
# shellcheck disable=SC2086
if helm install "test-${chart}" "$REPO_ROOT/$chart" \
--namespace "$ns" \
--wait --timeout "${TIMEOUT}s" \
--set controller.serviceMonitor.enabled=true \
$extra_args 2>&1 | sed 's/^/ /'; then
# Verify ServiceMonitor exists
if kubectl get servicemonitor -n "$ns" --no-headers 2>/dev/null | grep -q .; then
log_pass "$label"
else
log_fail "$label (ServiceMonitor resource not found)"
fi
# Verify metrics Service exists
local metrics_label="$chart: install (metrics Service created)"
if kubectl get svc -n "$ns" --no-headers 2>/dev/null | grep -q metrics; then
log_pass "$metrics_label"
else
log_fail "$metrics_label"
fi
# Verify metrics Service exposes both stat and metrics ports
local ports_label="$chart: install (metrics Service has stat + metrics ports)"
local svc_ports
svc_ports=$(kubectl get svc -n "$ns" -l "app.kubernetes.io/instance=test-${chart}" -o jsonpath='{.items[*].spec.ports[*].name}' 2>/dev/null || true)
if echo "$svc_ports" | grep -q 'stat' && echo "$svc_ports" | grep -q 'metrics'; then
log_pass "$ports_label"
else
log_fail "$ports_label"
fi
else
log_fail "$label (helm install failed)"
dump_debug "$ns"
fi
helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true
delete_ns "$ns"
# PodMonitor test
label="$chart: install (PodMonitor enabled)"
ns=$(make_ns "$chart" "podmon")
create_ns "$ns"
# shellcheck disable=SC2086
if helm install "test-${chart}" "$REPO_ROOT/$chart" \
--namespace "$ns" \
--wait --timeout "${TIMEOUT}s" \
--set controller.podMonitor.enabled=true \
$extra_args 2>&1 | sed 's/^/ /'; then
# Verify PodMonitor exists
if kubectl get podmonitor -n "$ns" --no-headers 2>/dev/null | grep -q .; then
log_pass "$label"
else
log_fail "$label (PodMonitor resource not found)"
fi
# Verify no metrics Service (PodMonitor shouldn't create one)
local no_metrics_label="$chart: install (no metrics Service with PodMonitor)"
if kubectl get svc -n "$ns" --no-headers 2>/dev/null | grep -q metrics; then
log_fail "$no_metrics_label"
else
log_pass "$no_metrics_label"
fi
else
log_fail "$label (helm install failed)"
dump_debug "$ns"
fi
helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true
delete_ns "$ns"
}
# Test: verify HugConf is deleted after helm uninstall
test_hugconf_cleanup() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
if ! grep -q 'hugconf:' "$values_yaml" 2>/dev/null; then
return
fi
local label="$chart: HugConf deleted on uninstall"
local ns
ns=$(make_ns "$chart" "hugconf-cleanup")
create_ns "$ns"
local extra_args
extra_args=$(chart_test_args "$chart")
# shellcheck disable=SC2086
if ! helm install "test-${chart}" "$REPO_ROOT/$chart" \
--namespace "$ns" \
--wait --timeout "${TIMEOUT}s" \
$extra_args 2>&1 | sed 's/^/ /'; then
log_fail "$label (helm install failed)"
dump_debug "$ns"
delete_ns "$ns"
return
fi
if ! wait_for_pods "$ns"; then
log_fail "$label (pods not ready within ${TIMEOUT}s)"
dump_debug "$ns"
helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true
delete_ns "$ns"
return
fi
# Verify HugConf exists before uninstall
if ! kubectl get hugconfs.gate.v3.haproxy.org -n "$ns" --no-headers 2>/dev/null | grep -q .; then
log_fail "$label (HugConf not found after install)"
helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true
delete_ns "$ns"
return
fi
# Uninstall and verify HugConf is cleaned up
if ! helm uninstall "test-${chart}" -n "$ns" --wait --timeout "${TIMEOUT}s" 2>&1 | sed 's/^/ /'; then
log_fail "$label (helm uninstall failed)"
delete_ns "$ns"
return
fi
# Check that HugConf is gone
if kubectl get hugconfs.gate.v3.haproxy.org -n "$ns" --no-headers 2>/dev/null | grep -q .; then
log_fail "$label (HugConf still exists after uninstall)"
else
log_pass "$label"
fi
delete_ns "$ns"
}
# --- cleanup trap ---
cleanup() {
if [ "$KEEP_NS" = "1" ]; then
if [ ${#NAMESPACES[@]} -gt 0 ]; then
echo -e "\n${YELLOW}KEEP_NS=1, these namespaces were left:${NC}"
printf ' %s\n' "${NAMESPACES[@]}"
fi
else
# In case of early exit, clean up any remaining namespaces
for ns in "${NAMESPACES[@]}"; do
kubectl delete namespace "$ns" --wait=false --ignore-not-found >/dev/null 2>&1 || true
done
fi
delete_kind_cluster
}
trap cleanup EXIT
# --- main ---
main() {
local filter="${1:-}"
echo -e "${BOLD}Helm Chart Integration Test${NC}"
echo "Repo: $REPO_ROOT"
echo "Helm: $(helm version --short 2>/dev/null)"
echo "Timeout: ${TIMEOUT}s per install"
[ -n "$CI_FILTER" ] && echo "CI filter: $CI_FILTER"
[ -n "$TEST_FILTER" ] && echo "Test: $TEST_FILTER"
# Create or reuse Kind cluster
create_kind_cluster
check_prerequisites
echo "Cluster: $(kubectl config current-context 2>/dev/null || echo 'unknown')"
echo "K8s server: $(kubectl version 2>/dev/null | grep 'Server Version' | head -1 || true)"
local charts
read -ra charts <<< "$(find_charts "$filter")"
echo "Charts: ${charts[*]}"
for chart in "${charts[@]}"; do
log_section "$chart"
if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "defaults" ]; then test_install_defaults "$chart"; fi
if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "daemonset" ]; then test_install_daemonset "$chart"; fi
if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "hpa" ]; then test_install_hpa "$chart"; fi
if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "pdb" ]; then test_install_pdb "$chart"; fi
if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "metrics-port" ]; then test_install_metrics_port "$chart"; fi
if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "monitoring" ]; then test_install_monitoring "$chart"; fi
if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "hugconf-cleanup" ]; then test_hugconf_cleanup "$chart"; fi
if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "ci" ]; then test_install_ci_values "$chart"; fi
done
# Summary
log_section "Summary"
echo -e " ${GREEN}Passed:${NC} $PASSED"
echo -e " ${RED}Failed:${NC} $FAILED"
echo -e " ${YELLOW}Skipped:${NC} $SKIPPED"
if [ ${#FAILURES[@]} -gt 0 ]; then
echo -e "\n${RED}Failures:${NC}"
for f in "${FAILURES[@]}"; do
echo " - $f"
done
exit 1
fi
echo -e "\n${GREEN}All integration tests passed.${NC}"
}
main "$@"

401
test/local-test.sh Executable file
View File

@@ -0,0 +1,401 @@
#!/bin/bash
#
# Local Helm chart testing script.
# Runs lint and template rendering for each ci/ values file.
#
# Usage:
# ./test/local-test.sh # test all charts
# ./test/local-test.sh haproxy-unified-gateway # test only HUG
# ./test/local-test.sh kubernetes-ingress # test only ingress
#
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASSED=0
FAILED=0
SKIPPED=0
FAILURES=()
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BOLD='\033[1m'
NC='\033[0m'
log_pass() { echo -e " ${GREEN}PASS${NC} $1"; PASSED=$((PASSED + 1)); }
log_fail() { echo -e " ${RED}FAIL${NC} $1"; FAILED=$((FAILED + 1)); FAILURES+=("$1"); }
log_skip() { echo -e " ${YELLOW}SKIP${NC} $1"; SKIPPED=$((SKIPPED + 1)); }
log_section() { echo -e "\n${BOLD}=== $1 ===${NC}"; }
# Determine which charts to test
find_charts() {
local filter="${1:-}"
local charts=()
for dir in "$REPO_ROOT"/*/; do
[ -f "$dir/Chart.yaml" ] || continue
local name
name="$(basename "$dir")"
if [ -n "$filter" ] && [ "$name" != "$filter" ]; then
continue
fi
charts+=("$name")
done
if [ ${#charts[@]} -eq 0 ]; then
echo "No charts found${filter:+ matching '$filter'}." >&2
exit 1
fi
echo "${charts[@]}"
}
# 1. helm lint with default values
test_lint() {
local chart="$1"
local label="$chart: helm lint (defaults)"
if helm lint "$REPO_ROOT/$chart" --quiet 2>/dev/null; then
log_pass "$label"
else
log_fail "$label"
fi
}
# 2. helm template with default values
test_template_defaults() {
local chart="$1"
local label="$chart: helm template (defaults)"
if helm template test-release "$REPO_ROOT/$chart" >/dev/null 2>&1; then
log_pass "$label"
else
log_fail "$label"
fi
}
# 3. helm lint + template for each ci/ values file
test_ci_values() {
local chart="$1"
local ci_dir="$REPO_ROOT/$chart/ci"
if [ ! -d "$ci_dir" ]; then
log_skip "$chart: no ci/ directory"
return
fi
local count=0
for values_file in "$ci_dir"/*.yaml "$ci_dir"/*.yml; do
[ -f "$values_file" ] || continue
count=$((count + 1))
local fname
fname="$(basename "$values_file")"
# lint
local lint_label="$chart: lint ci/$fname"
if helm lint "$REPO_ROOT/$chart" -f "$values_file" --quiet 2>/dev/null; then
log_pass "$lint_label"
else
log_fail "$lint_label"
fi
# template
local tmpl_label="$chart: template ci/$fname"
local output
if output=$(helm template test-release "$REPO_ROOT/$chart" -f "$values_file" 2>&1); then
log_pass "$tmpl_label"
else
log_fail "$tmpl_label"
echo "$output" | head -5 | sed 's/^/ /'
fi
done
if [ "$count" -eq 0 ]; then
log_skip "$chart: ci/ directory is empty"
fi
}
# 4. Check Chart.yaml required fields
test_chart_metadata() {
local chart="$1"
local chart_yaml="$REPO_ROOT/$chart/Chart.yaml"
local label="$chart: Chart.yaml metadata"
local ok=true
for field in name version appVersion description; do
if ! grep -q "^${field}:" "$chart_yaml"; then
echo -e " ${RED}FAIL${NC} $chart: Chart.yaml missing '$field'"
FAILED=$((FAILED + 1))
FAILURES+=("$chart: Chart.yaml missing '$field'")
ok=false
fi
done
if $ok; then
log_pass "$label"
fi
}
# 5. Verify template renders different output for Deployment vs DaemonSet (if applicable)
test_kind_switch() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
# Only test if chart has both Deployment and DaemonSet templates
if ! grep -q 'kind: Deployment' "$values_yaml" 2>/dev/null; then
return
fi
if [ ! -f "$REPO_ROOT/$chart/templates/controller-daemonset.yaml" ]; then
return
fi
local label="$chart: Deployment vs DaemonSet renders differently"
local deploy_out daemon_out
deploy_out=$(helm template test-release "$REPO_ROOT/$chart" --set controller.kind=Deployment 2>&1)
daemon_out=$(helm template test-release "$REPO_ROOT/$chart" --set controller.kind=DaemonSet 2>&1)
if [ $? -ne 0 ]; then
log_fail "$label (DaemonSet template failed)"
return
fi
if echo "$deploy_out" | grep -q 'kind: Deployment' && echo "$daemon_out" | grep -q 'kind: DaemonSet'; then
log_pass "$label"
else
log_fail "$label"
fi
}
# 6. Verify HugConf cleanup hook renders correctly
test_hugconf_cleanup() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
if ! grep -q 'hugconf:' "$values_yaml" 2>/dev/null; then
return
fi
# Cleanup resources should render when hugconf.create=true (default)
local label="$chart: HugConf cleanup hook renders when hugconf.create=true"
local output
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set hugconf.create=true 2>&1)
if echo "$output" | grep -q 'hugconf-cleanup' && echo "$output" | grep -q '"helm.sh/hook": pre-delete'; then
log_pass "$label"
else
log_fail "$label"
fi
# Cleanup resources should NOT render when hugconf.create=false
label="$chart: HugConf cleanup hook skipped when hugconf.create=false"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set hugconf.create=false 2>&1)
if echo "$output" | grep -q 'hugconf-cleanup'; then
log_fail "$label"
else
log_pass "$label"
fi
# Verify the cleanup Job uses the correct HugConf name
label="$chart: HugConf cleanup Job targets correct resource name"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set hugconf.create=true \
--set hugconf.name=my-custom-hugconf 2>&1)
if echo "$output" | grep -q 'hugconfs/my-custom-hugconf'; then
log_pass "$label"
else
log_fail "$label"
fi
}
# 7. Verify controller metrics port and metricsAuth flag render correctly
test_metrics_port() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
if ! grep -q 'metricsAuth:' "$values_yaml" 2>/dev/null; then
return
fi
# Verify metrics container port renders
local label="$chart: metrics container port (31060) renders"
local output
output=$(helm template test-release "$REPO_ROOT/$chart" 2>&1)
if echo "$output" | grep -q 'containerPort: 31060'; then
log_pass "$label"
else
log_fail "$label"
fi
# Verify --metrics-auth=kube-rbac is the default arg
label="$chart: --metrics-auth=kube-rbac in default args"
if echo "$output" | grep -q '\-\-metrics-auth=kube-rbac'; then
log_pass "$label"
else
log_fail "$label"
fi
# Verify metricsAuth=none omits kube-rbac and sets none
label="$chart: --metrics-auth=none when metricsAuth=none"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set controller.metricsAuth=none 2>&1)
if echo "$output" | grep -q '\-\-metrics-auth=none' && \
! echo "$output" | grep -q '\-\-metrics-auth=kube-rbac'; then
log_pass "$label"
else
log_fail "$label"
fi
# Verify metrics Service includes both stat and metrics ports when ServiceMonitor is enabled
label="$chart: metrics Service has stat and metrics ports"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set controller.serviceMonitor.enabled=true \
--api-versions monitoring.coreos.com/v1 2>&1)
if echo "$output" | grep -A1 'name: stat' | grep -q 'port: 31024' && \
echo "$output" | grep -A1 'name: metrics' | grep -q 'port: 31060'; then
log_pass "$label"
else
log_fail "$label"
fi
# Verify DaemonSet also gets the metrics port and --metrics-auth flag
if [ -f "$REPO_ROOT/$chart/templates/controller-daemonset.yaml" ]; then
label="$chart: DaemonSet renders metrics port and --metrics-auth"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set controller.kind=DaemonSet 2>&1)
if echo "$output" | grep -q 'containerPort: 31060' && \
echo "$output" | grep -q '\-\-metrics-auth=kube-rbac'; then
log_pass "$label"
else
log_fail "$label"
fi
fi
}
# 8. Verify ServiceMonitor/PodMonitor render correctly when API is available
test_monitoring() {
local chart="$1"
local values_yaml="$REPO_ROOT/$chart/values.yaml"
local tmpl_dir="$REPO_ROOT/$chart/templates"
if ! grep -q 'serviceMonitor:' "$values_yaml" 2>/dev/null; then
return
fi
# Determine the values path for serviceMonitor.enabled
# Some charts use controller.serviceMonitor.enabled, others use serviceMonitor.enabled
local sm_set="controller.serviceMonitor.enabled=true"
if grep -q '^serviceMonitor:' "$values_yaml" 2>/dev/null; then
sm_set="serviceMonitor.enabled=true"
fi
# ServiceMonitor: should render when API version is available
local label="$chart: ServiceMonitor renders with monitoring.coreos.com/v1 API"
local output
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set "$sm_set" \
--api-versions monitoring.coreos.com/v1 2>&1)
if echo "$output" | grep -q 'kind: ServiceMonitor'; then
log_pass "$label"
else
log_fail "$label"
fi
# ServiceMonitor: should NOT render without the API
label="$chart: ServiceMonitor skipped without monitoring.coreos.com/v1 API"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set "$sm_set" 2>&1)
if echo "$output" | grep -q 'kind: ServiceMonitor'; then
log_fail "$label"
else
log_pass "$label"
fi
# Metrics service: only test if chart has a metrics service template
if ls "$tmpl_dir"/*service-metrics* &>/dev/null; then
label="$chart: metrics Service created with ServiceMonitor"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set "$sm_set" \
--api-versions monitoring.coreos.com/v1 2>&1)
if echo "$output" | grep -q 'name: test-release.*-metrics'; then
log_pass "$label"
else
log_fail "$label"
fi
fi
# PodMonitor: only test if chart has a podmonitor template
if ls "$tmpl_dir"/*podmonitor* &>/dev/null; then
local pm_set="controller.podMonitor.enabled=true"
if grep -q '^podMonitor:' "$values_yaml" 2>/dev/null; then
pm_set="podMonitor.enabled=true"
fi
label="$chart: PodMonitor renders with monitoring.coreos.com/v1 API"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set "$pm_set" \
--api-versions monitoring.coreos.com/v1 2>&1)
if echo "$output" | grep -q 'kind: PodMonitor'; then
log_pass "$label"
else
log_fail "$label"
fi
# PodMonitor: should NOT create metrics service
label="$chart: no metrics Service with PodMonitor only"
output=$(helm template test-release "$REPO_ROOT/$chart" \
--set "$pm_set" \
--api-versions monitoring.coreos.com/v1 2>&1)
if echo "$output" | grep -q 'name: test-release.*-metrics'; then
log_fail "$label"
else
log_pass "$label"
fi
fi
}
main() {
local filter="${1:-}"
echo -e "${BOLD}Helm Chart Local Test${NC}"
echo "Repo: $REPO_ROOT"
echo "Helm: $(helm version --short 2>/dev/null)"
local charts
read -ra charts <<< "$(find_charts "$filter")"
echo "Charts: ${charts[*]}"
for chart in "${charts[@]}"; do
log_section "$chart"
test_chart_metadata "$chart"
test_lint "$chart"
test_template_defaults "$chart"
test_kind_switch "$chart"
test_hugconf_cleanup "$chart"
test_metrics_port "$chart"
test_monitoring "$chart"
test_ci_values "$chart"
done
# Summary
log_section "Summary"
echo -e " ${GREEN}Passed:${NC} $PASSED"
echo -e " ${RED}Failed:${NC} $FAILED"
echo -e " ${YELLOW}Skipped:${NC} $SKIPPED"
if [ ${#FAILURES[@]} -gt 0 ]; then
echo -e "\n${RED}Failures:${NC}"
for f in "${FAILURES[@]}"; do
echo " - $f"
done
exit 1
fi
echo -e "\n${GREEN}All tests passed.${NC}"
}
main "$@"