From bfa9207fbd39fbbbbfaca11640a6977258ea8270 Mon Sep 17 00:00:00 2001 From: Noam Ben Simon Date: Tue, 19 May 2026 22:08:38 -0400 Subject: [PATCH] Some files! --- scripts/dsvclient-k8s-helpers.sh | 182 ++++++++++++++++++ scripts/test-dsvclient-failure.sh | 69 +++++++ scripts/test-dsvclient-gauntlet.sh | 43 +++++ scripts/test-dsvclient-high-concurrency.sh | 66 +++++++ scripts/test-dsvclient-mixed-concurrency.sh | 73 +++++++ scripts/test-dsvclient-same-key-namespaces.sh | 65 +++++++ 6 files changed, 498 insertions(+) create mode 100644 scripts/dsvclient-k8s-helpers.sh create mode 100644 scripts/test-dsvclient-failure.sh create mode 100644 scripts/test-dsvclient-gauntlet.sh create mode 100644 scripts/test-dsvclient-high-concurrency.sh create mode 100644 scripts/test-dsvclient-mixed-concurrency.sh create mode 100644 scripts/test-dsvclient-same-key-namespaces.sh diff --git a/scripts/dsvclient-k8s-helpers.sh b/scripts/dsvclient-k8s-helpers.sh new file mode 100644 index 0000000..cd0bd55 --- /dev/null +++ b/scripts/dsvclient-k8s-helpers.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Shared helpers for DSVClient-driven Kubernetes integration/load tests. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REPOS_ROOT="$(cd "${ROOT}/.." && pwd)" + +NAMESPACE="${NAMESPACE:-dsv}" +STATEFULSET="${STATEFULSET:-dsv-app}" +SERVICE="${SERVICE:-dsv-app-service}" +SERVICE_PORT="${SERVICE_PORT:-9080}" +LOCAL_PORT="${LOCAL_PORT:-19080}" +BASE_URL="${BASE_URL:-http://127.0.0.1:${LOCAL_PORT}}" +DSV_CLIENT_DIR="${DSV_CLIENT_DIR:-${REPOS_ROOT}/DSVClient}" +CLIENT_CLI="${CLIENT_CLI:-${DSV_CLIENT_DIR}/cli.py}" +PYTHON_BIN="${PYTHON_BIN:-python3}" +RUN_ID="${RUN_ID:-$(date +%Y%m%d%H%M%S)-${RANDOM}}" +WORK_DIR="${WORK_DIR:-${ROOT}/target/dsvclient-k8s-${RUN_ID}}" +PORT_FORWARD_PID="" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +TOTAL_COUNT=0 +PASS_COUNT=0 +FAIL_COUNT=0 + +info() { + echo -e "${CYAN}==>${NC} $*" +} + +section() { + echo "" + echo -e "${YELLOW}--- $* ---${NC}" +} + +pass() { + TOTAL_COUNT=$((TOTAL_COUNT + 1)) + PASS_COUNT=$((PASS_COUNT + 1)) + echo -e " ${GREEN}[PASS]${NC} [${TOTAL_COUNT}] $*" +} + +fail() { + TOTAL_COUNT=$((TOTAL_COUNT + 1)) + FAIL_COUNT=$((FAIL_COUNT + 1)) + echo -e " ${RED}[FAIL]${NC} [${TOTAL_COUNT}] $*" +} + +die() { + echo -e "${RED}ERROR:${NC} $*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1" +} + +setup_suite() { + mkdir -p "$WORK_DIR" + require_command kubectl + require_command curl + require_command "$PYTHON_BIN" + [[ -f "$CLIENT_CLI" ]] || die "DSVClient CLI not found at ${CLIENT_CLI}. Set DSV_CLIENT_DIR or CLIENT_CLI." + + info "Using namespace=${NAMESPACE}, service=${SERVICE}, base_url=${BASE_URL}" + kubectl get namespace "$NAMESPACE" >/dev/null + kubectl -n "$NAMESPACE" rollout status "statefulset/${STATEFULSET}" --timeout=240s + ensure_gateway + wait_for_gateway +} + +ensure_gateway() { + if [[ -n "${NO_PORT_FORWARD:-}" || -n "${EXTERNAL_BASE_URL:-}" ]]; then + BASE_URL="${EXTERNAL_BASE_URL:-${BASE_URL}}" + return + fi + + info "Starting kubectl port-forward svc/${SERVICE} ${LOCAL_PORT}:${SERVICE_PORT}" + kubectl -n "$NAMESPACE" port-forward "svc/${SERVICE}" "${LOCAL_PORT}:${SERVICE_PORT}" \ + >"${WORK_DIR}/port-forward.log" 2>&1 & + PORT_FORWARD_PID="$!" + trap cleanup_suite EXIT +} + +cleanup_suite() { + local status=$? + if [[ -n "$PORT_FORWARD_PID" ]]; then + kill "$PORT_FORWARD_PID" >/dev/null 2>&1 || true + wait "$PORT_FORWARD_PID" >/dev/null 2>&1 || true + fi + return "$status" +} + +wait_for_gateway() { + local timeout="${GATEWAY_TIMEOUT_SECONDS:-180}" + local elapsed=0 + while ! curl -sf --connect-timeout 2 --max-time 10 "${BASE_URL}/actuator/health" >/dev/null 2>&1; do + if (( elapsed >= timeout )); then + [[ -f "${WORK_DIR}/port-forward.log" ]] && tail -40 "${WORK_DIR}/port-forward.log" || true + die "Gateway ${BASE_URL} did not become healthy within ${timeout}s" + fi + sleep 3 + elapsed=$((elapsed + 3)) + done +} + +wait_for_rollout() { + kubectl -n "$NAMESPACE" rollout status "statefulset/${STATEFULSET}" --timeout="${ROLLOUT_TIMEOUT:-300s}" + wait_for_gateway +} + +client_home() { + local username="$1" + local home_dir="${WORK_DIR}/homes/${username}-$$-${RANDOM}" + mkdir -p "${home_dir}/.dsv_client" + cat > "${home_dir}/.dsv_client/config.json" < "$file" +} + +count_status() { + local dir="$1" + local status="$2" + local count=0 + local file + for file in "$dir"/status-*; do + [[ -f "$file" ]] || continue + [[ "$(cat "$file")" == "$status" ]] && count=$((count + 1)) + done + printf "%s" "$count" +} + +print_summary() { + echo "" + echo -e "${CYAN}Summary${NC}" + echo " total: ${TOTAL_COUNT}" + echo -e " passed: ${GREEN}${PASS_COUNT}${NC}" + if (( FAIL_COUNT > 0 )); then + echo -e " failed: ${RED}${FAIL_COUNT}${NC}" + return 1 + fi + echo " failed: 0" + return 0 +} diff --git a/scripts/test-dsvclient-failure.sh b/scripts/test-dsvclient-failure.sh new file mode 100644 index 0000000..0742403 --- /dev/null +++ b/scripts/test-dsvclient-failure.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# DSVClient Kubernetes failure test. Deletes StatefulSet pods while client traffic is active. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/dsvclient-k8s-helpers.sh" + +LOAD_REQUESTS="${LOAD_REQUESTS:-120}" +PARALLELISM="${PARALLELISM:-30}" +FAIL_POD_INDEX="${FAIL_POD_INDEX:-2}" + +setup_suite +RESULTS_DIR="${WORK_DIR}/failure" +mkdir -p "$RESULTS_DIR" + +USER="failure-client-${RUN_ID}" +BASE_SECRET="failure-baseline-${RUN_ID}" + +section "Seed baseline secret" +out="$(dsvc "$USER" create "$BASE_SECRET" "baseline-value" 2>&1)" || true +expect_output_contains "$out" "Secret created" "Created baseline secret" + +section "Run client traffic while one pod is deleted" +( + sleep 2 + info "Deleting pod ${STATEFULSET}-${FAIL_POD_INDEX} in namespace ${NAMESPACE}" + kubectl -n "$NAMESPACE" delete pod "${STATEFULSET}-${FAIL_POD_INDEX}" --wait=false +) & + +for i in $(seq 1 "$LOAD_REQUESTS"); do + ( + user="failure-load-$((i % 10))-${RUN_ID}" + name="failure-load-${RUN_ID}-${i}" + case $((i % 4)) in + 0) out="$(dsvc "$USER" get "$BASE_SECRET" 2>&1)" ;; + 1) out="$(dsvc "$USER" update "$BASE_SECRET" "baseline-update-${i}" 2>&1)" ;; + 2) out="$(dsvc "$user" create "$name" "value-${i}" 2>&1)" ;; + 3) out="$(dsvc "$USER" get "$BASE_SECRET" --all 2>&1)" ;; + esac + printf "%s\n" "$out" > "${RESULTS_DIR}/op-${i}.log" + if printf "%s" "$out" | grep -Eq "Secret created|Secret updated|baseline"; then + write_status "${RESULTS_DIR}/status-${i}" "PASS" + else + write_status "${RESULTS_DIR}/status-${i}" "CHECK" + fi + ) & + if (( i % PARALLELISM == 0 )); then + wait + fi +done +wait + +section "Wait for StatefulSet recovery" +wait_for_rollout + +section "Verify baseline after recovery" +out="$(dsvc "$USER" get "$BASE_SECRET" 2>&1)" || true +expect_output_contains "$out" "baseline" "Baseline remains readable after pod recovery" + +passed="$(count_status "$RESULTS_DIR" PASS)" +check="$(count_status "$RESULTS_DIR" CHECK)" +if (( passed > 0 )); then + pass "Traffic during pod failure produced ${passed} successful client operations; review=${check}" +else + fail "No successful operations during failure. Logs: ${RESULTS_DIR}" +fi + +print_summary + diff --git a/scripts/test-dsvclient-gauntlet.sh b/scripts/test-dsvclient-gauntlet.sh new file mode 100644 index 0000000..268671f --- /dev/null +++ b/scripts/test-dsvclient-gauntlet.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Full DSVClient Kubernetes gauntlet: simple load, mixed load, namespace contention, and pod failure. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +export NAMESPACE="${NAMESPACE:-dsv}" +export RUN_ID="${RUN_ID:-gauntlet-$(date +%Y%m%d%H%M%S)-${RANDOM}}" +export WORK_DIR="${WORK_DIR:-$(cd "${SCRIPT_DIR}/.." && pwd)/target/dsvclient-k8s-${RUN_ID}}" +export LOCAL_PORT="${LOCAL_PORT:-19080}" + +echo "DSVClient Kubernetes gauntlet" +echo " namespace: ${NAMESPACE}" +echo " run id: ${RUN_ID}" +echo " work dir: ${WORK_DIR}" + +# Run each phase with an external base URL after one shared port-forward is established. +source "${SCRIPT_DIR}/dsvclient-k8s-helpers.sh" +setup_suite +export EXTERNAL_BASE_URL="${BASE_URL}" +export NO_PORT_FORWARD=1 + +REQUESTS="${GAUNTLET_HIGH_REQUESTS:-250}" \ +PARALLELISM="${GAUNTLET_HIGH_PARALLELISM:-50}" \ +bash "${SCRIPT_DIR}/test-dsvclient-high-concurrency.sh" + +SEED_COUNT="${GAUNTLET_MIXED_SEED_COUNT:-80}" \ +ROUNDS="${GAUNTLET_MIXED_ROUNDS:-10}" \ +PARALLELISM="${GAUNTLET_MIXED_PARALLELISM:-100}" \ +bash "${SCRIPT_DIR}/test-dsvclient-mixed-concurrency.sh" + +USERS="${GAUNTLET_NAMESPACE_USERS:-5}" \ +KEYS="${GAUNTLET_NAMESPACE_KEYS:-12}" \ +REQUESTS_PER_USER="${GAUNTLET_NAMESPACE_REQUESTS_PER_USER:-120}" \ +PARALLELISM="${GAUNTLET_NAMESPACE_PARALLELISM:-100}" \ +bash "${SCRIPT_DIR}/test-dsvclient-same-key-namespaces.sh" + +LOAD_REQUESTS="${GAUNTLET_FAILURE_REQUESTS:-180}" \ +PARALLELISM="${GAUNTLET_FAILURE_PARALLELISM:-45}" \ +bash "${SCRIPT_DIR}/test-dsvclient-failure.sh" + +echo "" +echo "Gauntlet completed. Logs are under ${WORK_DIR}" diff --git a/scripts/test-dsvclient-high-concurrency.sh b/scripts/test-dsvclient-high-concurrency.sh new file mode 100644 index 0000000..ad60421 --- /dev/null +++ b/scripts/test-dsvclient-high-concurrency.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# High-concurrency DSVClient test: many independent create/get/update/get-all/delete flows. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/dsvclient-k8s-helpers.sh" + +REQUESTS="${REQUESTS:-200}" +PARALLELISM="${PARALLELISM:-40}" +USER_COUNT="${USER_COUNT:-25}" + +setup_suite +section "High concurrency independent client flows" + +RESULTS_DIR="${WORK_DIR}/high-concurrency" +mkdir -p "$RESULTS_DIR" + +run_flow() { + local i="$1" + local user="hc-user-$((i % USER_COUNT))-${RUN_ID}" + local name="hc-secret-${RUN_ID}-${i}" + local value="hc-value-${i}" + local updated="hc-updated-${i}" + local out + + out="$(dsvc "$user" create "$name" "$value" 2>&1)" || true + printf "%s\n" "$out" > "${RESULTS_DIR}/create-${i}.log" + [[ "$out" == *"Secret created"* ]] || { write_status "${RESULTS_DIR}/status-${i}" "FAIL"; return; } + + out="$(dsvc "$user" get "$name" 2>&1)" || true + printf "%s\n" "$out" > "${RESULTS_DIR}/get1-${i}.log" + [[ "$out" == *"$value"* ]] || { write_status "${RESULTS_DIR}/status-${i}" "FAIL"; return; } + + out="$(dsvc "$user" update "$name" "$updated" 2>&1)" || true + printf "%s\n" "$out" > "${RESULTS_DIR}/update-${i}.log" + [[ "$out" == *"Secret updated"* ]] || { write_status "${RESULTS_DIR}/status-${i}" "FAIL"; return; } + + out="$(dsvc "$user" get "$name" --all 2>&1)" || true + printf "%s\n" "$out" > "${RESULTS_DIR}/all-${i}.log" + [[ "$out" == *"$value"* && "$out" == *"$updated"* ]] || { write_status "${RESULTS_DIR}/status-${i}" "FAIL"; return; } + + out="$(dsvc "$user" delete "$name" 2>&1)" || true + printf "%s\n" "$out" > "${RESULTS_DIR}/delete-${i}.log" + [[ "$out" == *"Delete succeeded"* ]] || { write_status "${RESULTS_DIR}/status-${i}" "FAIL"; return; } + + write_status "${RESULTS_DIR}/status-${i}" "PASS" +} + +for i in $(seq 1 "$REQUESTS"); do + run_flow "$i" & + if (( i % PARALLELISM == 0 )); then + wait + fi +done +wait + +passed="$(count_status "$RESULTS_DIR" PASS)" +failed="$(count_status "$RESULTS_DIR" FAIL)" +if [[ "$passed" == "$REQUESTS" ]]; then + pass "All ${REQUESTS} concurrent client flows completed" +else + fail "High concurrency flows completed ${passed}/${REQUESTS}; failed=${failed}. Logs: ${RESULTS_DIR}" +fi + +print_summary + diff --git a/scripts/test-dsvclient-mixed-concurrency.sh b/scripts/test-dsvclient-mixed-concurrency.sh new file mode 100644 index 0000000..d8f7f1f --- /dev/null +++ b/scripts/test-dsvclient-mixed-concurrency.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Mixed operation concurrency: creates, reads, updates, deletes, and version reads in parallel. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/dsvclient-k8s-helpers.sh" + +SEED_COUNT="${SEED_COUNT:-50}" +ROUNDS="${ROUNDS:-8}" +PARALLELISM="${PARALLELISM:-80}" +USER_COUNT="${USER_COUNT:-10}" + +setup_suite +section "Seeding mixed-concurrency data" + +RESULTS_DIR="${WORK_DIR}/mixed-concurrency" +mkdir -p "$RESULTS_DIR" + +for i in $(seq 1 "$SEED_COUNT"); do + user="mixed-user-$((i % USER_COUNT))-${RUN_ID}" + name="mixed-secret-${RUN_ID}-${i}" + out="$(dsvc "$user" create "$name" "seed-${i}" 2>&1)" || true + printf "%s\n" "$out" > "${RESULTS_DIR}/seed-${i}.log" +done + +section "Running mixed concurrent load" + +op_id=0 +for round in $(seq 1 "$ROUNDS"); do + for i in $(seq 1 "$SEED_COUNT"); do + user="mixed-user-$((i % USER_COUNT))-${RUN_ID}" + name="mixed-secret-${RUN_ID}-${i}" + op_id=$((op_id + 1)) + ( + case $((op_id % 6)) in + 0) out="$(dsvc "$user" get "$name" 2>&1)" ;; + 1) out="$(dsvc "$user" update "$name" "round-${round}-value-${i}" 2>&1)" ;; + 2) out="$(dsvc "$user" get "$name" --all 2>&1)" ;; + 3) out="$(dsvc "$user" get "$name" --version 1 2>&1)" ;; + 4) + new_name="mixed-new-${RUN_ID}-${round}-${i}" + out="$(dsvc "$user" create "$new_name" "new-${round}-${i}" 2>&1)" + ;; + 5) + delete_name="mixed-delete-${RUN_ID}-${round}-${i}" + create_out="$(dsvc "$user" create "$delete_name" "delete-me-${round}-${i}" 2>&1)" + delete_out="$(dsvc "$user" delete "$delete_name" 2>&1)" + out="${create_out}"$'\n'"${delete_out}" + ;; + esac + printf "%s\n" "$out" > "${RESULTS_DIR}/op-${op_id}.log" + if printf "%s" "$out" | grep -Eq "Secret created|Secret updated|Delete succeeded|seed-|round-|new-|\\{|\\["; then + write_status "${RESULTS_DIR}/status-${op_id}" "PASS" + else + write_status "${RESULTS_DIR}/status-${op_id}" "CHECK" + fi + ) & + if (( op_id % PARALLELISM == 0 )); then + wait + fi + done +done +wait + +passed="$(count_status "$RESULTS_DIR" PASS)" +check="$(count_status "$RESULTS_DIR" CHECK)" +if (( passed > 0 && check == 0 )); then + pass "Mixed concurrent load completed with ${passed} successful operations" +else + fail "Mixed concurrent load needs review: pass=${passed}, check=${check}. Logs: ${RESULTS_DIR}" +fi + +print_summary diff --git a/scripts/test-dsvclient-same-key-namespaces.sh b/scripts/test-dsvclient-same-key-namespaces.sh new file mode 100644 index 0000000..ec19067 --- /dev/null +++ b/scripts/test-dsvclient-same-key-namespaces.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# High concurrency against the same small key namespace across several users. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/dsvclient-k8s-helpers.sh" + +USERS="${USERS:-5}" +KEYS="${KEYS:-8}" +REQUESTS_PER_USER="${REQUESTS_PER_USER:-80}" +PARALLELISM="${PARALLELISM:-60}" + +setup_suite +section "Five users writing the same key names concurrently" + +RESULTS_DIR="${WORK_DIR}/same-key-namespaces" +mkdir -p "$RESULTS_DIR" + +# Pre-create the shared key names for each user, so the heavy phase can hammer updates/reads. +for u in $(seq 1 "$USERS"); do + user="namespace-user-${u}-${RUN_ID}" + for k in $(seq 1 "$KEYS"); do + name="shared-key-${k}" + dsvc "$user" create "$name" "initial-${u}-${k}" >"${RESULTS_DIR}/seed-${u}-${k}.log" 2>&1 || true + done +done + +op_id=0 +for u in $(seq 1 "$USERS"); do + user="namespace-user-${u}-${RUN_ID}" + for r in $(seq 1 "$REQUESTS_PER_USER"); do + op_id=$((op_id + 1)) + key_index=$((((r - 1) % KEYS) + 1)) + name="shared-key-${key_index}" + ( + case $((r % 4)) in + 0) out="$(dsvc "$user" get "$name" 2>&1)" ;; + 1) out="$(dsvc "$user" update "$name" "user-${u}-write-${r}" 2>&1)" ;; + 2) out="$(dsvc "$user" get "$name" --all 2>&1)" ;; + 3) out="$(dsvc "$user" get "$name" --version 1 2>&1)" ;; + esac + printf "%s\n" "$out" > "${RESULTS_DIR}/op-${op_id}.log" + if printf "%s" "$out" | grep -Eq "Secret updated|initial-|user-${u}-write-|\\{|\\["; then + write_status "${RESULTS_DIR}/status-${op_id}" "PASS" + else + write_status "${RESULTS_DIR}/status-${op_id}" "CHECK" + fi + ) & + if (( op_id % PARALLELISM == 0 )); then + wait + fi + done +done +wait + +passed="$(count_status "$RESULTS_DIR" PASS)" +check="$(count_status "$RESULTS_DIR" CHECK)" +if (( check == 0 )); then + pass "Same-key namespace load completed: users=${USERS}, keys=${KEYS}, operations=${passed}" +else + fail "Same-key namespace load needs review: pass=${passed}, check=${check}. Logs: ${RESULTS_DIR}" +fi + +print_summary +