#!/bin/bash # test_v2_suite.sh — Комплексный тест-сьют SQS Operator v0.1.5 # Создан: 2026-04-07 # Покрытие: # Phase 0 — Timing: provisioning нового тенанта с нуля (время + ресурсы) # Phase 1 — Базовые SQS API операции (T01–T11) # Phase 2 — Ошибочные параметры (E01–E09) # Phase 3 — Продвинутые фичи: batch, longpoll, attrs, DLQ (A01–A09) # Phase 4 — Multi-tenant изоляция (MT01–MT05; MT03=WONTFIX/Keycloak) # Phase 5 — Self-Healing (SH01–SH06): Deployment, Service, ConfigMap, Ingress # Phase 6 — Ресурсы: CPU/MEM operator + tenantPod по kubectl top # Phase 7 — Concurrent load: параллельные запросы # Phase 8 — Стресс-марафон 30 минут с per-minute resource snapshots set -o pipefail # ════════════════════════════════════════════ # КОНФИГ # ════════════════════════════════════════════ TENANT01="test001" NAMESPACE01="sless-fn-${TENANT01}" CR_NAMESPACE="sqs-operator-system" CR_NAME01="test-tenant-001" # Имя QueueService CR для test001 TIMING_TENANT="timing-ts" # Новый тенант только для timing-фазы TIMING_CR="test-tenant-timing-ts" TENANT02="test002" # Используется в multi-tenant фазе CR_NAME02="test-tenant-002" SQS_HOST="sqs.kube5s.ru" EP="https://${SQS_HOST}/sqs/${TENANT01}" EP2="https://${SQS_HOST}/sqs/${TENANT02}" EP_TIMING="https://${SQS_HOST}/sqs/${TIMING_TENANT}" MARATHON_SEC=1800 # 30 минут стресс-марафон MAX_CONCURRENT=10 # Параллельных воркеров для concurrent-фазы # ════════════════════════════════════════════ # УТИЛИТЫ # ════════════════════════════════════════════ PASS=0; FAIL=0; WARN=0; SKIP=0 FAIL_LIST=""; WARN_LIST="" ok() { echo " ✅ PASS[$1]"; PASS=$((PASS+1)); } fail() { echo " ❌ FAIL[$1]: $2"; FAIL=$((FAIL+1)); FAIL_LIST="${FAIL_LIST}\n - $1: $2"; } warn() { echo " ⚠️ WARN[$1]: $2"; WARN=$((WARN+1)); WARN_LIST="${WARN_LIST}\n - $1: $2"; } skip() { echo " ⏭️ SKIP[$1]: $2"; SKIP=$((SKIP+1)); } hdr() { echo "" echo "══════════════════════════════════════════════════" echo " $1" echo "══════════════════════════════════════════════════" } ts() { date '+%H:%M:%S'; } elapsed() { echo $(( $(date +%s) - $1 )); } # SQS GET sqs() { curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$AK:$SK" "$EP/?$1" } # SQS POST (для ReceiptHandle с '#') sqspost() { curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$AK:$SK" \ -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "$1" "$EP/" } # SQS с произвольным endpoint/credentials sqsraw() { local ep="$1" ak="$2" sk="$3" data="$4" curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "$data" "$ep/" } encode_receipt() { echo "$1" | sed 's/#/%23/g'; } # Ждать Phase у QueueService CR wait_qs_phase() { local name="$1" ns="$2" target_phase="$3" max_sec="${4:-120}" local start=$(date +%s) while true; do local p=$(kubectl get queueservice "$name" -n "$ns" \ -o jsonpath='{.status.phase}' 2>/dev/null) [ "$p" = "$target_phase" ] && { echo "$p"; return 0; } [ $(( $(date +%s) - start )) -ge "$max_sec" ] && { echo "timeout:$p"; return 1; } sleep 2 done } # kubectl top — безопасный вызов (fallback если metrics-server недоступен) ktopns() { local ns="$1" local out out=$(kubectl top pod -n "$ns" 2>&1) if echo "$out" | grep -q "Metrics API not available\|metrics-server\|error"; then echo " [metrics-server недоступен]" else echo "$out" | head -20 fi } # Получить creds тенанта get_creds() { local ns="$1" tenant="$2" local ak sk ak=$(kubectl -n "$ns" get secret "sqs-creds-${tenant}" \ -o jsonpath='{.data.accessKey}' 2>/dev/null | base64 -d 2>/dev/null) sk=$(kubectl -n "$ns" get secret "sqs-creds-${tenant}" \ -o jsonpath='{.data.secretKey}' 2>/dev/null | base64 -d 2>/dev/null) echo "$ak $sk" } # ════════════════════════════════════════════ # ИНИЦИАЛИЗАЦИЯ # ════════════════════════════════════════════ SUITE_START=$(date +%s) echo "" echo "╔══════════════════════════════════════════════════╗" echo "║ SQS Operator Test Suite v2.0 ║" echo "║ $(date '+%Y-%m-%d %H:%M:%S') ║" echo "╚══════════════════════════════════════════════════╝" echo "" # Загружаем creds test001 read AK SK <<< "$(get_creds "$NAMESPACE01" "$TENANT01")" echo " Tenant: $TENANT01 AK: $AK" echo " Endpoint: $EP" [ -z "$AK" ] && { echo "FATAL: нет credentials для $TENANT01"; exit 1; } # Проверим доступность кластера kubectl cluster-info --request-timeout=5s > /dev/null 2>&1 \ || { echo "FATAL: kubectl не работает"; exit 1; } # Проверим что test001 в Ready PHASE01=$(kubectl get queueservice "$CR_NAME01" -n "$CR_NAMESPACE" \ -o jsonpath='{.status.phase}' 2>/dev/null) if [ "$PHASE01" != "Ready" ]; then echo " ⚠️ QueueService $CR_NAME01 не в Ready (phase=$PHASE01), ждём 60с..." wait_qs_phase "$CR_NAME01" "$CR_NAMESPACE" "Ready" 60 fi echo " QS test001: $(kubectl get qs "$CR_NAME01" -n "$CR_NAMESPACE" \ -o jsonpath='{.status.phase}' 2>/dev/null)" echo "" # ════════════════════════════════════════════ hdr "PHASE 0 — Provisioning Timing (новый тенант с нуля)" # ════════════════════════════════════════════ # Цель: измерить точное время от `kubectl apply` до полностью работающего SQS API. # Измеряем каждый переход фазы. echo " Тенант: $TIMING_TENANT CR: $TIMING_CR" echo " Очищаем если остался от прошлого прогона..." kubectl delete queueservice "$TIMING_CR" -n "$CR_NAMESPACE" --ignore-not-found=true \ --wait=true --timeout=30s 2>&1 | grep -v "^$" | head -3 kubectl delete ns "sless-fn-${TIMING_TENANT}" --ignore-not-found=true \ --wait=false 2>/dev/null # Ждём полного удаления NS — если завис (Terminating), принудительно убираем finalizer for i in $(seq 1 20); do STATUS=$(kubectl get ns "sless-fn-${TIMING_TENANT}" -o jsonpath='{.status.phase}' 2>/dev/null) [ -z "$STATUS" ] && break if [ "$STATUS" = "Terminating" ] && [ "$i" -ge 10 ]; then # NS завис в Terminating — принудительно убираем finalizer kubectl get ns "sless-fn-${TIMING_TENANT}" -o json \ | python3 -c "import sys,json; d=json.load(sys.stdin); d['spec']['finalizers']=[]; print(json.dumps(d))" \ | kubectl replace --raw "/api/v1/namespaces/sless-fn-${TIMING_TENANT}/finalize" -f - >/dev/null 2>&1 fi sleep 2 done echo " Создаём QueueService..." T0_APPLY=$(date +%s) kubectl apply -f - <&1 apiVersion: sqs.kube5s.ru/v1alpha1 kind: QueueService metadata: name: ${TIMING_CR} namespace: ${CR_NAMESPACE} spec: tenantId: "${TIMING_TENANT}" memoryMB: 64 storageMB: 512 persistence: false EOF echo " [$(ts)] Apply complete, watching phases..." # Ждём Pending T0_PENDING_WAIT=$(date +%s) for i in $(seq 1 30); do P=$(kubectl get qs "$TIMING_CR" -n "$CR_NAMESPACE" -o jsonpath='{.status.phase}' 2>/dev/null) [ -n "$P" ] && break sleep 1 done T0_PENDING=$(date +%s) TIME_TO_PENDING=$(( T0_PENDING - T0_APPLY )) echo " [$(ts)] Phase appeared: $P (+${TIME_TO_PENDING}s от apply)" # Ждём Provisioning for i in $(seq 1 60); do P=$(kubectl get qs "$TIMING_CR" -n "$CR_NAMESPACE" -o jsonpath='{.status.phase}' 2>/dev/null) [ "$P" = "Provisioning" ] && break [ "$P" = "Ready" ] && break # иногда быстро перескакивает sleep 1 done T0_PROVISIONING=$(date +%s) echo " [$(ts)] Phase: $P (+$(( T0_PROVISIONING - T0_APPLY ))s от apply)" # Ждём Ready T0_READY_WAIT=$(date +%s) RESULT=$(wait_qs_phase "$TIMING_CR" "$CR_NAMESPACE" "Ready" 180) T0_READY=$(date +%s) TIME_TO_READY=$(( T0_READY - T0_APPLY )) if [ "$RESULT" = "Ready" ]; then ok "P01_Phase_Ready в ${TIME_TO_READY}s" else fail P01_Phase_Ready "timeout/error: $RESULT" fi # Ждём первый успешный API-запрос read AK_T SK_T <<< "$(get_creds "sless-fn-${TIMING_TENANT}" "$TIMING_TENANT")" T0_API_WAIT=$(date +%s) API_OK=0 if [ -n "$AK_T" ]; then for i in $(seq 1 30); do R=$(curl -sk --max-time 5 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$AK_T:$SK_T" \ "$EP_TIMING/?Action=ListQueues&Version=2012-11-05") if echo "$R" | grep -q "ListQueuesResponse"; then API_OK=1; break fi sleep 2 done fi T0_API=$(date +%s) TIME_TO_API=$(( T0_API - T0_APPLY )) if [ "$API_OK" = "1" ]; then ok "P02_API_Ready в ${TIME_TO_API}s" else fail P02_API_Ready "API не ответил за ${TIME_TO_API}s" fi echo "" echo " ─── Timing Summary ───────────────────────" printf " apply → CR exists: %3ds\n" "$TIME_TO_PENDING" printf " apply → Phase=Ready: %3ds\n" "$TIME_TO_READY" printf " apply → первый API ответ: %3ds\n" "$TIME_TO_API" echo " ──────────────────────────────────────────" # Ресурсы нового тенанта echo "" echo " Ресурсы нового тенанта (kubectl top):" ktopns "sless-fn-${TIMING_TENANT}" echo " Оператор:" ktopns "$CR_NAMESPACE" # Финальный cleanup timing-тенанта echo "" echo " Удаляем timing-тенант..." kubectl delete queueservice "$TIMING_CR" -n "$CR_NAMESPACE" \ --wait=true --timeout=60s 2>&1 | tail -1 # ════════════════════════════════════════════ hdr "PHASE 1 — Базовые операции (T01–T11)" # ════════════════════════════════════════════ # T01: ListQueues echo "--- T01 ListQueues" R=$(sqs "Action=ListQueues&Version=2012-11-05") echo "$R" | grep -q "ListQueuesResponse" && ok T01 || fail T01 "нет ListQueuesResponse: $R" # T02: CreateQueue echo "--- T02 CreateQueue" R=$(sqs "Action=CreateQueue&QueueName=t-basic&Version=2012-11-05") BASIC_URL=$(echo "$R" | grep -oP "(?<=)[^<]+") [ -n "$BASIC_URL" ] && ok "T02 ($BASIC_URL)" || fail T02 "нет QueueUrl: $R" # T03: CreateQueue duplicate (idempotency) echo "--- T03 CreateQueue idempotent" R=$(sqs "Action=CreateQueue&QueueName=t-basic&Version=2012-11-05") URL2=$(echo "$R" | grep -oP "(?<=)[^<]+") [ "$URL2" = "$BASIC_URL" ] && ok T03 || fail T03 "URL изменился: $BASIC_URL vs $URL2" # T04: GetQueueUrl echo "--- T04 GetQueueUrl" R=$(sqs "Action=GetQueueUrl&QueueName=t-basic&Version=2012-11-05") echo "$R" | grep -q "QueueUrl" && ok T04 || fail T04 "$R" # T05: SendMessage echo "--- T05 SendMessage" R=$(sqs "Action=SendMessage&QueueUrl=${BASIC_URL}&MessageBody=hello-world&Version=2012-11-05") MSG_ID=$(echo "$R" | grep -oP "(?<=)[^<]+") [ -n "$MSG_ID" ] && ok "T05 (id=$MSG_ID)" || fail T05 "$R" # T06: ReceiveMessage echo "--- T06 ReceiveMessage" R=$(sqs "Action=ReceiveMessage&QueueUrl=${BASIC_URL}&MaxNumberOfMessages=1&Version=2012-11-05") BODY=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1) RECEIPT06=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1) [ "$BODY" = "hello-world" ] && ok "T06 (body=$BODY)" || fail T06 "body=$BODY, R=$R" # T07: DeleteMessage via POST echo "--- T07 DeleteMessage" ENC=$(encode_receipt "$RECEIPT06") R=$(sqspost "Action=DeleteMessage&QueueUrl=${BASIC_URL}&ReceiptHandle=${ENC}&Version=2012-11-05") echo "$R" | grep -q "DeleteMessageResponse\|ResponseMetadata" && ok T07 || fail T07 "$R" # T08: ReceiveMessage после delete — пусто echo "--- T08 ReceiveMessage == empty after delete" sleep 1 R=$(sqs "Action=ReceiveMessage&QueueUrl=${BASIC_URL}&MaxNumberOfMessages=1&WaitTimeSeconds=0&Version=2012-11-05") echo "$R" | grep -q "" \ && fail T08 "сообщение ещё есть после delete" \ || ok T08 # T09: GetQueueAttributes echo "--- T09 GetQueueAttributes" R=$(sqs "Action=GetQueueAttributes&QueueUrl=${BASIC_URL}&AttributeName.1=All&Version=2012-11-05") echo "$R" | grep -q "VisibilityTimeout" && ok T09 || fail T09 "$R" # T10: SetQueueAttributes echo "--- T10 SetQueueAttributes" R=$(sqs "Action=SetQueueAttributes&QueueUrl=${BASIC_URL}&Attribute.1.Name=VisibilityTimeout&Attribute.1.Value=10&Version=2012-11-05") echo "$R" | grep -q "ResponseMetadata" && ok T10 || fail T10 "$R" # T11: DeleteQueue echo "--- T11 DeleteQueue" R=$(sqs "Action=DeleteQueue&QueueUrl=${BASIC_URL}&Version=2012-11-05") echo "$R" | grep -q "ResponseMetadata" && ok T11 || fail T11 "$R" # ════════════════════════════════════════════ hdr "PHASE 2 — Ошибочные параметры (E01–E09)" # ════════════════════════════════════════════ # Служебная очередь для error-тестов sqs "Action=CreateQueue&QueueName=t-errtmp&Version=2012-11-05" > /dev/null ERRTMP_URL=$(sqs "Action=GetQueueUrl&QueueName=t-errtmp&Version=2012-11-05" | grep -oP "(?<=)[^<]+") # E01: InvalidQueueName echo "--- E01 CreateQueue invalid name" R=$(sqs "Action=CreateQueue&QueueName=bad%20name%21&Version=2012-11-05") echo "$R" | grep -qi "error\|invalid" && ok E01 || warn E01 "elasticmq принял: $R" # E02: VisibilityTimeout > 43200 echo "--- E02 VisibilityTimeout > 43200" R=$(sqs "Action=SetQueueAttributes&QueueUrl=${ERRTMP_URL}&Attribute.1.Name=VisibilityTimeout&Attribute.1.Value=99999&Version=2012-11-05") echo "$R" | grep -qi "error\|invalid" && ok E02 || warn E02 "accepted oversize timeout: $R" # E03: ReceiveMessage несуществующей очереди echo "--- E03 ReceiveMessage non-existent" R=$(sqs "Action=ReceiveMessage&QueueUrl=${EP}/test001/queue-xyz-does-not-exist&Version=2012-11-05") echo "$R" | grep -qi "error\|NonExistent\|not exist" && ok E03 || fail E03 "нет ошибки: $R" # E04: DeleteMessage с невалидным ReceiptHandle echo "--- E04 DeleteMessage invalid receipt" R=$(sqspost "Action=DeleteMessage&QueueUrl=${ERRTMP_URL}&ReceiptHandle=totally-invalid-receipt-xyz&Version=2012-11-05") echo "$R" | grep -qi "error\|ReceiptHandle\|invalid" && ok E04 || fail E04 "нет ошибки: $R" # E05: GetQueueUrl несуществующей очереди echo "--- E05 GetQueueUrl non-existent" R=$(sqs "Action=GetQueueUrl&QueueName=absolutely-not-exist-xyz-2026&Version=2012-11-05") echo "$R" | grep -qi "error\|NonExistent" && ok E05 || fail E05 "нет ошибки: $R" # E06: Double delete (дублированное удаление) echo "--- E06 DeleteMessage double-delete" sqs "Action=SendMessage&QueueUrl=${ERRTMP_URL}&MessageBody=test&Version=2012-11-05" > /dev/null R_RECV=$(sqs "Action=ReceiveMessage&QueueUrl=${ERRTMP_URL}&MaxNumberOfMessages=1&Version=2012-11-05") RCPT6=$(echo "$R_RECV" | grep -oP "(?<=)[^<]+" | head -1) if [ -n "$RCPT6" ]; then ENC6=$(encode_receipt "$RCPT6") sqspost "Action=DeleteMessage&QueueUrl=${ERRTMP_URL}&ReceiptHandle=${ENC6}&Version=2012-11-05" > /dev/null R=$(sqspost "Action=DeleteMessage&QueueUrl=${ERRTMP_URL}&ReceiptHandle=${ENC6}&Version=2012-11-05") echo "$R" | grep -qi "error\|invalid\|ResponseMetadata" \ && ok "E06 (double-delete handled)" || warn E06 "$R" else warn E06 "ReceiveMessage не вернул сообщение" fi # E07: Неверные credentials (ElasticMQ не проверяет SigV4 — known limitation) echo "--- E07 Wrong credentials (WONTFIX: ElasticMQ не верифицирует SigV4)" R=$(curl -sk --max-time 5 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "FAKEKEY123:FAKESECRET456" \ "$EP/?Action=ListQueues&Version=2012-11-05") echo "$R" | grep -qi "error\|Unauthorized\|Signature" \ && ok E07 \ || skip E07 "WONTFIX: изоляция через Keycloak в проде (не через SigV4)" # E08: SendMessage с пустым телом echo "--- E08 SendMessage empty body" R=$(sqspost "Action=SendMessage&QueueUrl=${ERRTMP_URL}&MessageBody=&Version=2012-11-05") echo "$R" | grep -qi "error\|empty\|invalid" && ok E08 || warn E08 "empty body принято: $R" # E09: SendMessage oversized >256KB echo "--- E09 SendMessage 300KB oversized" BIG=$(python3 -c "print('X'*307200)" 2>/dev/null) R=$(curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$AK:$SK" \ -X POST -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "Action=SendMessage" \ --data-urlencode "QueueUrl=${ERRTMP_URL}" \ --data-urlencode "MessageBody=${BIG}" \ --data-urlencode "Version=2012-11-05" \ "$EP/") echo "$R" | grep -qi "error\|MessageTooLong\|too large" && ok E09 || warn E09 "oversized принято" # ════════════════════════════════════════════ hdr "PHASE 3 — Продвинутые фичи (A01–A09)" # ════════════════════════════════════════════ # Очередь с VisibilityTimeout=5s sqs "Action=CreateQueue&QueueName=t-adv&Version=2012-11-05" > /dev/null ADV_URL=$(sqs "Action=GetQueueUrl&QueueName=t-adv&Version=2012-11-05" | grep -oP "(?<=)[^<]+") sqs "Action=SetQueueAttributes&QueueUrl=${ADV_URL}&Attribute.1.Name=VisibilityTimeout&Attribute.1.Value=5&Version=2012-11-05" > /dev/null # A01: VisibilityTimeout — возврат сообщения после таймаута echo "--- A01 VisibilityTimeout (5s return)" sqs "Action=SendMessage&QueueUrl=${ADV_URL}&MessageBody=will-return&Version=2012-11-05" > /dev/null R=$(sqs "Action=ReceiveMessage&QueueUrl=${ADV_URL}&MaxNumberOfMessages=1&Version=2012-11-05") BODY_A01=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1) echo " Взяли: '$BODY_A01', ждём 6с..." sleep 6 R2=$(sqs "Action=ReceiveMessage&QueueUrl=${ADV_URL}&MaxNumberOfMessages=1&WaitTimeSeconds=0&Version=2012-11-05") BODY_A01_2=$(echo "$R2" | grep -oP "(?<=)[^<]+" | head -1) [ "$BODY_A01_2" = "will-return" ] && ok "A01 (вернулось: '$BODY_A01_2')" || fail A01 "не вернулось: '$BODY_A01_2'" # A02: ChangeMessageVisibility → 0 (немедленная доступность) echo "--- A02 ChangeMessageVisibility → 0" RCPT_A02=$(echo "$R2" | grep -oP "(?<=)[^<]+" | head -1) ENC_A02=$(encode_receipt "$RCPT_A02") R=$(sqspost "Action=ChangeMessageVisibility&QueueUrl=${ADV_URL}&ReceiptHandle=${ENC_A02}&VisibilityTimeout=0&Version=2012-11-05") echo "$R" | grep -q "ResponseMetadata" && ok A02 || warn A02 "$R" # A03: SendMessageBatch 10 сообщений echo "--- A03 SendMessageBatch 10" BATCH="Action=SendMessageBatch&Version=2012-11-05&QueueUrl=${ADV_URL}" for i in $(seq 1 10); do BATCH="${BATCH}&SendMessageBatchRequestEntry.${i}.Id=m${i}&SendMessageBatchRequestEntry.${i}.MessageBody=batch-${i}" done R=$(sqspost "$BATCH") CNT=$(echo "$R" | grep -o "" | wc -l) [ "$CNT" -ge 10 ] && ok "A03 (sent=$CNT)" || fail A03 "sent=$CNT" # A04: ReceiveMessage batch MaxNumberOfMessages=10 echo "--- A04 ReceiveMessageBatch MaxNumberOfMessages=10" sleep 1 R=$(sqs "Action=ReceiveMessage&QueueUrl=${ADV_URL}&MaxNumberOfMessages=10&Version=2012-11-05") RECV=$(echo "$R" | grep -o "" | wc -l) [ "$RECV" -ge 9 ] && ok "A04 (recv=$RECV)" || warn A04 "recv=$RECV из 10+" BATCH_RCPTS=$(echo "$R" | grep -oP "(?<=)[^<]+") # A05: DeleteMessageBatch echo "--- A05 DeleteMessageBatch" BDEL="Action=DeleteMessageBatch&Version=2012-11-05&QueueUrl=${ADV_URL}" IDX=1 while IFS= read -r r; do [ -z "$r" ] && continue ENC_D=$(encode_receipt "$r") BDEL="${BDEL}&DeleteMessageBatchRequestEntry.${IDX}.Id=d${IDX}&DeleteMessageBatchRequestEntry.${IDX}.ReceiptHandle=${ENC_D}" IDX=$((IDX+1)) done <<< "$BATCH_RCPTS" R=$(sqspost "$BDEL") echo "$R" | grep -q "DeleteMessageBatchResponse\|ResultCode\|ResponseMetadata" \ && ok "A05 (deleted=$((IDX-1)))" || fail A05 "$R" # A06: Long polling (WaitTimeSeconds=3, пустая очередь) echo "--- A06 Long polling 3s" sleep 6 # Ждём VisibilityTimeout для сообщения из A02 # Чистим очередь — для long polling нужна гарантированно пустая очередь sqs "Action=PurgeQueue&QueueUrl=${ADV_URL}&Version=2012-11-05" > /dev/null 2>&1 sleep 1 START=$(date +%s) R=$(sqs "Action=ReceiveMessage&QueueUrl=${ADV_URL}&MaxNumberOfMessages=1&WaitTimeSeconds=3&Version=2012-11-05") WAITED=$(elapsed $START) [ "$WAITED" -ge 2 ] && ok "A06 (waited=${WAITED}s)" || warn A06 "слишком быстро: ${WAITED}s" # A07: MessageAttributes (String) echo "--- A07 MessageAttributes" R=$(sqspost "Action=SendMessage&QueueUrl=${ADV_URL}&MessageBody=with-attrs&Version=2012-11-05\ &MessageAttribute.1.Name=Color&MessageAttribute.1.Value.StringValue=Blue&MessageAttribute.1.Value.DataType=String\ &MessageAttribute.2.Name=Priority&MessageAttribute.2.Value.StringValue=High&MessageAttribute.2.Value.DataType=String") if echo "$R" | grep -q "MessageId"; then R2=$(sqs "Action=ReceiveMessage&QueueUrl=${ADV_URL}&MaxNumberOfMessages=1&MessageAttributeName.1=Color&MessageAttributeName.2=Priority&Version=2012-11-05") echo "$R2" | grep -qi "Color\|Blue" && ok A07 || warn A07 "атрибуты не вернулись" else fail A07 "SendMessage с атрибутами: $R" fi # A08: PurgeQueue echo "--- A08 PurgeQueue" for i in $(seq 1 5); do sqs "Action=SendMessage&QueueUrl=${ADV_URL}&MessageBody=purge${i}&Version=2012-11-05" > /dev/null done R=$(sqs "Action=PurgeQueue&QueueUrl=${ADV_URL}&Version=2012-11-05") echo "$R" | grep -q "ResponseMetadata" && ok A08 || fail A08 "$R" sleep 1 CNT_AFTER=$(sqs "Action=GetQueueAttributes&QueueUrl=${ADV_URL}&AttributeName.1=ApproximateNumberOfMessages&Version=2012-11-05" \ | grep -oP "(?<=)[^<]+" | head -1) echo " Сообщений после Purge: $CNT_AFTER" # A09: Dead Letter Queue с RedrivePolicy echo "--- A09 Dead Letter Queue" sqs "Action=CreateQueue&QueueName=t-dlq&Version=2012-11-05" > /dev/null DLQ_URL=$(sqs "Action=GetQueueUrl&QueueName=t-dlq&Version=2012-11-05" | grep -oP "(?<=)[^<]+") DLQ_ARN=$(sqs "Action=GetQueueAttributes&QueueUrl=${DLQ_URL}&AttributeName.1=QueueArn&Version=2012-11-05" \ | grep -oP "(?<=)[^<]+" | head -1) echo " DLQ ARN: $DLQ_ARN" if [ -n "$DLQ_ARN" ]; then REDRIVE=$(python3 -c "import urllib.parse,json; print(urllib.parse.quote(json.dumps({'deadLetterTargetArn':'$DLQ_ARN','maxReceiveCount':'1'})))" 2>/dev/null) R=$(sqspost "Action=CreateQueue&QueueName=t-src-dlq&Version=2012-11-05&Attribute.1.Name=RedrivePolicy&Attribute.1.Value=${REDRIVE}") echo "$R" | grep -q "QueueUrl\|CreateQueueResponse" && ok A09 || warn A09 "DLQ не принят: $R" else warn A09 "Не получил DLQ ARN" fi # ════════════════════════════════════════════ hdr "PHASE 4 — Multi-tenant изоляция (MT01–MT05)" # ════════════════════════════════════════════ # MT01: Создаём второй тенант и измеряем время echo "--- MT01 Создаём QueueService $TENANT02" kubectl delete queueservice "$CR_NAME02" -n "$CR_NAMESPACE" --ignore-not-found=true --wait=false 2>/dev/null sleep 3 MT01_START=$(date +%s) kubectl apply -f - <&1 apiVersion: sqs.kube5s.ru/v1alpha1 kind: QueueService metadata: name: ${CR_NAME02} namespace: ${CR_NAMESPACE} spec: tenantId: "${TENANT02}" memoryMB: 64 storageMB: 512 persistence: false EOF echo " Ждём Ready (до 120с)..." RESULT=$(wait_qs_phase "$CR_NAME02" "$CR_NAMESPACE" "Ready" 120) MT01_ELAPSED=$(elapsed $MT01_START) [ "$RESULT" = "Ready" ] && ok "MT01 (tenant002 Ready за ${MT01_ELAPSED}s)" || fail MT01 "Phase=$RESULT" read AK2 SK2 <<< "$(get_creds "sless-fn-${TENANT02}" "$TENANT02")" [ -n "$AK2" ] && ok "MT01b creds OK (AK2=$AK2)" || fail MT01b "нет credentials для $TENANT02" # MT02: Одинаковое имя очереди в разных тенантах — разные URL echo "--- MT02 Одинаковое имя → разные URL" sqsraw "$EP" "$AK" "$SK" "Action=CreateQueue&QueueName=shared-q&Version=2012-11-05" > /dev/null sqsraw "$EP2" "$AK2" "$SK2" "Action=CreateQueue&QueueName=shared-q&Version=2012-11-05" > /dev/null URL_T1=$(sqsraw "$EP" "$AK" "$SK" "Action=GetQueueUrl&QueueName=shared-q&Version=2012-11-05" | grep -oP "(?<=)[^<]+") URL_T2=$(sqsraw "$EP2" "$AK2" "$SK2" "Action=GetQueueUrl&QueueName=shared-q&Version=2012-11-05" | grep -oP "(?<=)[^<]+") echo " T1: $URL_T1" echo " T2: $URL_T2" [ -n "$URL_T1" ] && [ -n "$URL_T2" ] && [ "$URL_T1" != "$URL_T2" ] \ && ok MT02 || warn MT02 "URL пустые или совпадают" # MT03: Cross-tenant isolation — WONTFIX (Keycloak в проде) echo "--- MT03 Cross-tenant message isolation [WONTFIX/Keycloak]" sqsraw "$EP" "$AK" "$SK" "Action=SendMessage&QueueUrl=${URL_T1}&MessageBody=tenant001-secret&Version=2012-11-05" > /dev/null R=$(sqsraw "$EP" "$AK2" "$SK2" "Action=ReceiveMessage&QueueUrl=${URL_T1}&MaxNumberOfMessages=1&Version=2012-11-05") BODY_CROSS=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1) if [ "$BODY_CROSS" = "tenant001-secret" ]; then skip MT03 "WONTFIX: изоляция будет через Keycloak JWT в проде (не через SigV4); ElasticMQ не проверяет подпись" else ok "MT03 (tenant002 не прочитал сообщение tenant001)" fi # MT04: Независимые операции в tenant002 echo "--- MT04 Операции tenant002 независимы" sqsraw "$EP2" "$AK2" "$SK2" "Action=SendMessage&QueueUrl=${URL_T2}&MessageBody=t2-msg&Version=2012-11-05" > /dev/null R=$(sqsraw "$EP2" "$AK2" "$SK2" "Action=ReceiveMessage&QueueUrl=${URL_T2}&MaxNumberOfMessages=1&Version=2012-11-05") BODY_T2=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1) [ "$BODY_T2" = "t2-msg" ] && ok "MT04 (tenant002 работает независимо)" || fail MT04 "body='$BODY_T2'" # MT05: Удаление tenant002 чистит все его ресурсы echo "--- MT05 Удаляем tenant002, ресурсы должны исчезнуть" kubectl delete queueservice "$CR_NAME02" -n "$CR_NAMESPACE" 2>&1 | head -1 echo " Ждём удаления namespace sless-fn-${TENANT02} (до 30с)..." for i in $(seq 1 15); do NS=$(kubectl get ns "sless-fn-${TENANT02}" -o name 2>/dev/null) [ -z "$NS" ] && break sleep 2 done [ -z "$NS" ] && ok "MT05 (namespace удалён)" || warn MT05 "namespace ещё существует: $NS" # ════════════════════════════════════════════ hdr "PHASE 5 — Self-Healing (SH01–SH06)" # ════════════════════════════════════════════ # Тест работает благодаря v0.1.5 fix: ensureHealthy проверяет все 4 ресурса. # При исчезновении → статус переходит в Pending → оператор вызывает provision. echo " Текущие ресурсы test001:" kubectl -n "$NAMESPACE01" get all -o name 2>&1 | head -10 # SH01: Deployment удалён — должен пересоздаться echo "--- SH01 Delete Deployment → auto-recreate" SH01_START=$(date +%s) kubectl -n "$NAMESPACE01" delete deployment -l "sqs.kube5s.ru/tenant=$TENANT01" 2>&1 | head -1 echo " Ждём пересоздания pod..." for i in $(seq 1 45); do READY=$(kubectl -n "$NAMESPACE01" get deployment \ -l "sqs.kube5s.ru/tenant=$TENANT01" \ -o jsonpath='{.items[0].status.readyReplicas}' 2>/dev/null) [ "$READY" = "1" ] && break sleep 2 done SH01_ELAPSED=$(elapsed $SH01_START) [ "$READY" = "1" ] && ok "SH01 Deployment восстановлен за ${SH01_ELAPSED}s" \ || fail SH01 "не восстановился (readyReplicas=$READY)" # SH02: Service удалён — должен пересоздаться (FIXED v0.1.5) echo "--- SH02 Delete Service → auto-recreate (v0.1.5 fix)" SH02_START=$(date +%s) kubectl -n "$NAMESPACE01" delete service "sqs-svc-${TENANT01}" 2>&1 | head -1 # ensureHealthy обнаружит пропажу за ≤15с и перейдёт в Pending → provision for i in $(seq 1 30); do SVC=$(kubectl -n "$NAMESPACE01" get service "sqs-svc-${TENANT01}" -o name 2>/dev/null) [ -n "$SVC" ] && break sleep 2 done SH02_ELAPSED=$(elapsed $SH02_START) [ -n "$SVC" ] && ok "SH02 Service восстановлен за ${SH02_ELAPSED}s" \ || fail SH02 "Service не пересоздан за ${SH02_ELAPSED}s" # SH03: ConfigMap удалён — должен пересоздаться (FIXED v0.1.5) echo "--- SH03 Delete ConfigMap → auto-recreate (v0.1.5 fix)" SH03_START=$(date +%s) kubectl -n "$NAMESPACE01" delete configmap "sqs-cfg-${TENANT01}" 2>&1 | head -1 for i in $(seq 1 30); do CM=$(kubectl -n "$NAMESPACE01" get configmap "sqs-cfg-${TENANT01}" -o name 2>/dev/null) [ -n "$CM" ] && break sleep 2 done SH03_ELAPSED=$(elapsed $SH03_START) [ -n "$CM" ] && ok "SH03 ConfigMap восстановлен за ${SH03_ELAPSED}s" \ || fail SH03 "ConfigMap не пересоздан за ${SH03_ELAPSED}s" # SH04: Ingress удалён — должен пересоздаться с auth-snippet (FIXED v0.1.5) echo "--- SH04 Delete Ingress → auto-recreate with auth-snippet" SH04_START=$(date +%s) kubectl -n "$NAMESPACE01" delete ingress "sqs-ing-${TENANT01}" 2>&1 | head -1 for i in $(seq 1 30); do ING=$(kubectl -n "$NAMESPACE01" get ingress "sqs-ing-${TENANT01}" -o name 2>/dev/null) [ -n "$ING" ] && break sleep 2 done SH04_ELAPSED=$(elapsed $SH04_START) if [ -n "$ING" ]; then ok "SH04 Ingress восстановлен за ${SH04_ELAPSED}s" # Дополнительно проверяем аннотацию HAS_SNIPPET=$(kubectl -n "$NAMESPACE01" get ingress "sqs-ing-${TENANT01}" \ -o jsonpath='{.metadata.annotations.nginx\.ingress\.kubernetes\.io/configuration-snippet}' 2>/dev/null) [ -n "$HAS_SNIPPET" ] && ok "SH04b Ingress имеет auth-snippet" \ || warn SH04b "auth-snippet отсутствует в Ingress (nginx-controller блокирует snippets?)" else fail SH04 "Ingress не пересоздан за ${SH04_ELAPSED}s" fi # SH05: Одновременное удаление всех ресурсов echo "--- SH05 Удаляем Service+ConfigMap+Ingress одновременно" SH05_START=$(date +%s) kubectl -n "$NAMESPACE01" delete service "sqs-svc-${TENANT01}" \ configmap "sqs-cfg-${TENANT01}" \ ingress "sqs-ing-${TENANT01}" --ignore-not-found=true 2>&1 | head -1 echo " Ждём восстановления всех трёх ресурсов..." for i in $(seq 1 45); do SVC=$(kubectl -n "$NAMESPACE01" get svc "sqs-svc-${TENANT01}" -o name 2>/dev/null) CM=$(kubectl -n "$NAMESPACE01" get cm "sqs-cfg-${TENANT01}" -o name 2>/dev/null) ING=$(kubectl -n "$NAMESPACE01" get ingress "sqs-ing-${TENANT01}" -o name 2>/dev/null) [ -n "$SVC" ] && [ -n "$CM" ] && [ -n "$ING" ] && break sleep 2 done SH05_ELAPSED=$(elapsed $SH05_START) if [ -n "$SVC" ] && [ -n "$CM" ] && [ -n "$ING" ]; then ok "SH05 Все ресурсы восстановлены за ${SH05_ELAPSED}s" else fail SH05 "svc=${SVC:+ok}, cm=${CM:+ok}, ing=${ING:+ok} / ${SH05_ELAPSED}s" fi # SH06: API работает после self-healing echo "--- SH06 API доступен после self-healing" echo " Ждём pod Ready после всех манипуляций..." kubectl -n "$NAMESPACE01" wait --for=condition=Ready pod \ -l "sqs.kube5s.ru/tenant=$TENANT01" --timeout=90s 2>&1 | head -1 sleep 5 R=$(sqs "Action=ListQueues&Version=2012-11-05") PHASE_AFTER=$(kubectl get qs "$CR_NAME01" -n "$CR_NAMESPACE" -o jsonpath='{.status.phase}' 2>/dev/null) echo "$R" | grep -q "ListQueuesResponse" \ && ok "SH06 SQS API доступен (Phase=$PHASE_AFTER)" \ || fail SH06 "API недоступен: $R (Phase=$PHASE_AFTER)" echo "" echo " Self-Healing recovery times:" echo " SH01 Deployment: ${SH01_ELAPSED}s" echo " SH02 Service: ${SH02_ELAPSED}s" echo " SH03 ConfigMap: ${SH03_ELAPSED}s" echo " SH04 Ingress: ${SH04_ELAPSED}s" echo " SH05 All-3: ${SH05_ELAPSED}s" # ════════════════════════════════════════════ hdr "PHASE 6 — Ресурсы" # ════════════════════════════════════════════ echo "--- R01 kubectl top: ElasticMQ pod (test001)" ktopns "$NAMESPACE01" echo "" echo "--- R02 kubectl top: Operator pod" ktopns "$CR_NAMESPACE" echo "" echo "--- R03 Limits/Requests ElasticMQ container" kubectl -n "$NAMESPACE01" get pod \ -l "sqs.kube5s.ru/tenant=$TENANT01" \ -o jsonpath='{range .items[0].spec.containers[*]}{.name}{"\t"}{.resources}{"\n"}{end}' 2>/dev/null ok R03 echo "" echo "--- R04 PVC usage" kubectl -n "$NAMESPACE01" get pvc 2>/dev/null PVC_STATUS=$(kubectl -n "$NAMESPACE01" get pvc "sqs-data-${TENANT01}" \ -o jsonpath='{.status.phase}' 2>/dev/null) [ "$PVC_STATUS" = "Bound" ] && ok "R04 PVC Bound" || warn R04 "PVC status=$PVC_STATUS" echo "" echo "--- R05 QueueService Spec" kubectl get qs "$CR_NAME01" -n "$CR_NAMESPACE" \ -o jsonpath='spec={.spec}{"\n"}status={.status}{"\n"}' 2>/dev/null | python3 -m json.tool 2>/dev/null || true ok R05 # ════════════════════════════════════════════ hdr "PHASE 7 — Concurrent load (параллельные запросы)" # ════════════════════════════════════════════ # Создаём очередь для concurrent-тестов sqs "Action=CreateQueue&QueueName=t-concurrent&Version=2012-11-05" > /dev/null CONC_URL=$(sqs "Action=GetQueueUrl&QueueName=t-concurrent&Version=2012-11-05" | grep -oP "(?<=)[^<]+") echo "--- CL01 ${MAX_CONCURRENT} параллельных SendMessage" CL_START=$(date +%s) for i in $(seq 1 $MAX_CONCURRENT); do curl -sk --max-time 10 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$AK:$SK" \ "$EP/?Action=SendMessage&QueueUrl=${CONC_URL}&MessageBody=concurrent-${i}&Version=2012-11-05" & done wait # Ждём все curl CL01_ELAPSED=$(elapsed $CL_START) sleep 2 # Проверяем сколько получили R=$(sqs "Action=GetQueueAttributes&QueueUrl=${CONC_URL}&AttributeName.1=ApproximateNumberOfMessages&Version=2012-11-05") MSG_CNT=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1) echo " Отправлено $MAX_CONCURRENT за ${CL01_ELAPSED}s, в очереди: $MSG_CNT" [ "${MSG_CNT:-0}" -ge "$((MAX_CONCURRENT-2))" ] \ && ok "CL01 (в очереди=$MSG_CNT /expected=$MAX_CONCURRENT)" \ || warn CL01 "в очереди=$MSG_CNT, ожидалось ~$MAX_CONCURRENT" echo "--- CL02 5 параллельных ReceiveMessage" CL02_START=$(date +%s) TMPDIR_CL=$(mktemp -d) for i in $(seq 1 5); do curl -sk --max-time 10 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$AK:$SK" \ "$EP/?Action=ReceiveMessage&QueueUrl=${CONC_URL}&MaxNumberOfMessages=1&WaitTimeSeconds=0&Version=2012-11-05" \ > "${TMPDIR_CL}/recv_${i}.xml" & done wait CL02_ELAPSED=$(elapsed $CL02_START) RECV_CNT=$(grep -l "MessageId" "${TMPDIR_CL}/"*.xml 2>/dev/null | wc -l) echo " Получили сообщений за ${CL02_ELAPSED}s: $RECV_CNT" [ "$RECV_CNT" -ge 4 ] && ok "CL02 (recv=$RECV_CNT/5)" || warn CL02 "recv=$RECV_CNT/5" rm -rf "$TMPDIR_CL" echo "--- CL03 Race: Одновременный send+receive (RPS throughput)" CL03_START=$(date +%s) SEND_OK=0; RECV_OK=0 for i in $(seq 1 20); do curl -sk --max-time 5 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$AK:$SK" \ "$EP/?Action=SendMessage&QueueUrl=${CONC_URL}&MessageBody=racetest-${i}&Version=2012-11-05" > /dev/null & if [ $((i % 4)) -eq 0 ]; then curl -sk --max-time 5 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$AK:$SK" \ "$EP/?Action=ReceiveMessage&QueueUrl=${CONC_URL}&MaxNumberOfMessages=5&Version=2012-11-05" > /dev/null & fi done wait CL03_ELAPSED=$(elapsed $CL03_START) # Проверяем что система живая после гонки R=$(sqs "Action=ListQueues&Version=2012-11-05") echo "$R" | grep -q "ListQueuesResponse" \ && ok "CL03 система живая после concurrent send/recv (${CL03_ELAPSED}s)" \ || fail CL03 "API не отвечает после concurrent: $R" # RPS оценка RPS=0 [ "$CL01_ELAPSED" -gt 0 ] && RPS=$(( MAX_CONCURRENT / CL01_ELAPSED )) echo "" echo " Throughput estimates (crude):" printf " SendMessage RPS: ~%d/s (%ds для %d req)\n" "$RPS" "$CL01_ELAPSED" "$MAX_CONCURRENT" # ════════════════════════════════════════════ hdr "PHASE 8 — Стресс-марафон 30 минут" # ════════════════════════════════════════════ # Очереди для марафона for q in m-alpha m-beta m-gamma; do sqs "Action=CreateQueue&QueueName=$q&Version=2012-11-05" > /dev/null done MARATHON_START=$(date +%s) ITER=0; MERR=0; MSEND=0; MRECV=0; MDEL=0; MERR_SQS=0 LAST_REPORT=$(date +%s) RESOURCE_SNAP="" echo " Старт: $(ts)" echo " Конец: $(date -d "+${MARATHON_SEC} seconds" '+%H:%M:%S' 2>/dev/null || \ date -r $(($(date +%s)+MARATHON_SEC)) '+%H:%M:%S' 2>/dev/null || \ echo '+30min')" echo " Паттерны: normal-send, batch-send, batch-receive, batch-delete, purge, error-injection, attrs" echo "" while true; do NOW_M=$(date +%s) ELAPSED_M=$(( NOW_M - MARATHON_START )) [ "$ELAPSED_M" -ge "$MARATHON_SEC" ] && break ITER=$((ITER+1)) # Ротация очередей case $((ITER % 3)) in 0) Q="m-alpha" ;; 1) Q="m-beta" ;; 2) Q="m-gamma" ;; esac Q_URL="${EP}/test001/${Q}" # === Обычный send === R=$(sqs "Action=SendMessage&QueueUrl=${Q_URL}&MessageBody=iter${ITER}-${NOW_M}&Version=2012-11-05") if echo "$R" | grep -q "MessageId"; then MSEND=$((MSEND+1)); else MERR=$((MERR+1)); fi # === Batch send каждые 5 итераций === if [ $((ITER % 5)) -eq 0 ]; then B="Action=SendMessageBatch&Version=2012-11-05&QueueUrl=${Q_URL}" for j in $(seq 1 5); do B="${B}&SendMessageBatchRequestEntry.${j}.Id=b${j}&SendMessageBatchRequestEntry.${j}.MessageBody=batch-${ITER}-${j}" done R=$(sqspost "$B") echo "$R" | grep -q "SendMessageBatchResponse\|Id>" || MERR=$((MERR+1)) fi # === Receive + batch delete каждые 3 итерации === if [ $((ITER % 3)) -eq 0 ]; then R=$(sqs "Action=ReceiveMessage&QueueUrl=${Q_URL}&MaxNumberOfMessages=10&WaitTimeSeconds=0&Version=2012-11-05") CNT_R=$(echo "$R" | grep -o "" | wc -l) MRECV=$((MRECV+CNT_R)) if [ "$CNT_R" -gt 0 ]; then RCPTS=$(echo "$R" | grep -oP "(?<=)[^<]+") BDEL="Action=DeleteMessageBatch&Version=2012-11-05&QueueUrl=${Q_URL}" DX=1 while IFS= read -r rc; do [ -z "$rc" ] && continue EN=$(encode_receipt "$rc") BDEL="${BDEL}&DeleteMessageBatchRequestEntry.${DX}.Id=d${DX}&DeleteMessageBatchRequestEntry.${DX}.ReceiptHandle=${EN}" DX=$((DX+1)) done <<< "$RCPTS" R=$(sqspost "$BDEL") echo "$R" | grep -q "DeleteMessageBatchResponse\|ResponseMetadata" \ && MDEL=$((MDEL+CNT_R)) || MERR=$((MERR+1)) fi fi # === GetQueueAttributes health check каждые 20 итераций === if [ $((ITER % 20)) -eq 0 ]; then R=$(sqs "Action=GetQueueAttributes&QueueUrl=${Q_URL}&AttributeName.1=ApproximateNumberOfMessages&Version=2012-11-05") echo "$R" | grep -q "GetQueueAttributesResponse" || MERR=$((MERR+1)) fi # === Намеренно ошибочные запросы каждые 7 итераций === if [ $((ITER % 7)) -eq 0 ]; then sqs "Action=ReceiveMessage&QueueUrl=${EP}/test001/no-such-queue-${RANDOM}&Version=2012-11-05" > /dev/null sqspost "Action=DeleteMessage&QueueUrl=${Q_URL}&ReceiptHandle=fake-${RANDOM}&Version=2012-11-05" > /dev/null MERR_SQS=$((MERR_SQS+1)) fi # === SendMessage с атрибутами каждые 11 итераций === if [ $((ITER % 11)) -eq 0 ]; then sqspost "Action=SendMessage&QueueUrl=${Q_URL}&MessageBody=with-attr-${ITER}&Version=2012-11-05\ &MessageAttribute.1.Name=Iteration&MessageAttribute.1.Value.StringValue=${ITER}&MessageAttribute.1.Value.DataType=Number" > /dev/null fi # === Burst: 5 параллельных send каждые 30 итераций === if [ $((ITER % 30)) -eq 0 ]; then for bx in $(seq 1 5); do sqs "Action=SendMessage&QueueUrl=${Q_URL}&MessageBody=burst-${bx}&Version=2012-11-05" > /dev/null & done wait fi # === PurgeQueue каждые 100 итераций === if [ $((ITER % 100)) -eq 0 ]; then for q2 in m-alpha m-beta m-gamma; do sqs "Action=PurgeQueue&QueueUrl=${EP}/test001/${q2}&Version=2012-11-05" > /dev/null done fi # === Отчёт + ресурсы каждую минуту === if [ $(( NOW_M - LAST_REPORT )) -ge 60 ]; then MIN=$((ELAPSED_M/60)); SEC=$((ELAPSED_M%60)) QA=$(sqs "Action=GetQueueAttributes&QueueUrl=${EP}/test001/m-alpha&AttributeName.1=ApproximateNumberOfMessages&Version=2012-11-05" \ | grep -oP "(?<=)[^<]+" | head -1) QB=$(sqs "Action=GetQueueAttributes&QueueUrl=${EP}/test001/m-beta&AttributeName.1=ApproximateNumberOfMessages&Version=2012-11-05" \ | grep -oP "(?<=)[^<]+" | head -1) POD_RESTARTS=$(kubectl -n "$NAMESPACE01" get pod \ -l "sqs.kube5s.ru/tenant=$TENANT01" \ -o jsonpath='{.items[0].status.containerStatuses[0].restartCount}' 2>/dev/null) QS_PHASE=$(kubectl get qs "$CR_NAME01" -n "$CR_NAMESPACE" \ -o jsonpath='{.status.phase}' 2>/dev/null) printf " [%02d:%02d | iter=%-4d] send=%-4d recv=%-4d del=%-4d err_infra=%d err_sqs=%d | q: α=%s β=%s | restarts=%s phase=%s\n" \ "$MIN" "$SEC" "$ITER" "$MSEND" "$MRECV" "$MDEL" "$MERR" "$MERR_SQS" \ "$QA" "$QB" "$POD_RESTARTS" "$QS_PHASE" # Resource snapshot раз в 5 минут if [ $((MIN % 5)) -eq 0 ] && [ "$MIN" -gt 0 ]; then echo " --- Resource snapshot [${MIN}min] ---" ktopns "$NAMESPACE01" fi LAST_REPORT=$NOW_M fi done MARATHON_ELAPSED=$(elapsed $MARATHON_START) echo "" echo " ─── Marathon Summary ─────────────────────" printf " Итераций: %d\n" "$ITER" printf " Sent: %d\n" "$MSEND" printf " Received: %d\n" "$MRECV" printf " Deleted: %d\n" "$MDEL" printf " Infra errors:%d\n" "$MERR" printf " SQS errors: %d (намеренные)\n" "$MERR_SQS" echo " ──────────────────────────────────────────" ERR_RATE=0 [ "$ITER" -gt 0 ] && ERR_RATE=$(( MERR * 100 / ITER )) # Финальные ресурсы echo "" echo " Финальные ресурсы после марафона:" ktopns "$NAMESPACE01" echo " Operator:" ktopns "$CR_NAMESPACE" # Pod restarts за всё время FINAL_RESTARTS=$(kubectl -n "$NAMESPACE01" get pod \ -l "sqs.kube5s.ru/tenant=$TENANT01" \ -o jsonpath='{.items[0].status.containerStatuses[0].restartCount}' 2>/dev/null) echo " ElasticMQ pod restarts за тест: $FINAL_RESTARTS" if [ "$MERR" -eq 0 ]; then ok "ST01 Marathon ${ITER}iter/${MARATHON_ELAPSED}s — 0 инфра-ошибок" elif [ "$ERR_RATE" -lt 1 ]; then ok "ST01 Marathon ${ITER}iter — ${MERR} ошибок (<1%)" elif [ "$ERR_RATE" -lt 5 ]; then warn ST01 "Marathon: ${MERR}/${ITER} ошибок (${ERR_RATE}%)" else fail ST01 "Marathon: ${MERR}/${ITER} ошибок (${ERR_RATE}%)" fi [ "${FINAL_RESTARTS:-0}" -eq 0 ] \ && ok "ST02 Нет pod restarts за марафон" \ || warn ST02 "Pod перезапускался ${FINAL_RESTARTS} раз" # ════════════════════════════════════════════ hdr "ИТОГОВЫЙ ОТЧЁТ" # ════════════════════════════════════════════ SUITE_ELAPSED=$(elapsed $SUITE_START) TOTAL=$((PASS+FAIL+WARN+SKIP)) echo "" echo " Завершено: $(date '+%Y-%m-%d %H:%M:%S')" echo " Длительность: ${SUITE_ELAPSED}s ($((SUITE_ELAPSED/60))min $((SUITE_ELAPSED%60))sec)" echo "" printf " ✅ PASS: %3d\n" "$PASS" printf " ❌ FAIL: %3d\n" "$FAIL" printf " ⚠️ WARN: %3d\n" "$WARN" printf " ⏭️ SKIP: %3d\n" "$SKIP" printf " TOTAL: %3d\n" "$TOTAL" if [ "$FAIL" -gt 0 ]; then echo "" echo " Провалившиеся тесты:" echo -e "$FAIL_LIST" fi if [ "$WARN" -gt 0 ]; then echo "" echo " Предупреждения:" echo -e "$WARN_LIST" fi echo "" echo "╔══════════════════════════════════════════════════╗" [ "$FAIL" -eq 0 ] \ && echo "║ ✅ ALL PASSED ║" \ || echo "║ ❌ ЕСТЬ ПРОВАЛЫ ║" echo "╚══════════════════════════════════════════════════╝" exit "$FAIL"