docs: agent models summary, available models list, cluster migration guide (2026-04-13)
This commit is contained in:
+552
@@ -0,0 +1,552 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# compare_sqs.sh — Сравнительный тест двух SQS реализаций
|
||||||
|
# Created: 2026-04-10
|
||||||
|
#
|
||||||
|
# Сравниваем:
|
||||||
|
# A) sqs-operator — https://sqs.kube5s.ru/sqs/{tenant} (1 тенант = 1 namespace в k8s)
|
||||||
|
# B) shared-sqs — https://qu.kube5s.ru (Single-process, multitenancy через API)
|
||||||
|
#
|
||||||
|
# Метрики:
|
||||||
|
# - Время создания тенанта
|
||||||
|
# - Время CRUD очереди (create/send/receive/delete)
|
||||||
|
# - Время удаления тенанта
|
||||||
|
# - Ресурсы pods после N тенантов (kubectl top)
|
||||||
|
#
|
||||||
|
# Запуск:
|
||||||
|
# bash compare_sqs.sh [TENANTS]
|
||||||
|
# TENANTS=10 bash compare_sqs.sh
|
||||||
|
# TENANTS=20 bash compare_sqs.sh
|
||||||
|
# TENANTS=50 bash compare_sqs.sh
|
||||||
|
#
|
||||||
|
# Требования:
|
||||||
|
# - kubectl с доступом к кластеру
|
||||||
|
# - curl
|
||||||
|
# - python3
|
||||||
|
# - aws CLI (для shared-sqs CRUD)
|
||||||
|
#
|
||||||
|
# Для sqs-operator: тенант создаётся через kubectl apply QueueService CR
|
||||||
|
# Для shared-sqs: тенант создаётся через POST /admin/tenants
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
# КОНФИГ
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Количество тенантов — передаётся аргументом или переменной или по умолчанию 10
|
||||||
|
TENANTS="${1:-${TENANTS:-10}}"
|
||||||
|
|
||||||
|
# sqs-operator
|
||||||
|
OP_HOST="https://sqs.kube5s.ru"
|
||||||
|
OP_CR_NAMESPACE="sqs-operator-system"
|
||||||
|
# Namespace тенантов в sqs-operator: sless-fn-{tenant}
|
||||||
|
OP_TENANT_NS_PREFIX="sless-fn"
|
||||||
|
# Шаблон QueueService CR (apiVersion из реального кода)
|
||||||
|
OP_CR_TEMPLATE='apiVersion: sqs.kube5s.ru/v1alpha1
|
||||||
|
kind: QueueService
|
||||||
|
metadata:
|
||||||
|
name: CRNAME
|
||||||
|
namespace: sqs-operator-system
|
||||||
|
spec:
|
||||||
|
tenantId: TENANTID
|
||||||
|
memoryMB: 64
|
||||||
|
storageMB: 512
|
||||||
|
persistence: false'
|
||||||
|
|
||||||
|
# shared-sqs
|
||||||
|
SS_HOST="https://qu.kube5s.ru"
|
||||||
|
SS_ADMIN_TOKEN="${SS_ADMIN_TOKEN:-sqs-admin-7a7d8bd0c060a75c198d48680f34077a}"
|
||||||
|
SS_REGION="us-east-1"
|
||||||
|
SS_NAMESPACE="sless" # namespace где живёт shared-sqs pod (для kubectl top)
|
||||||
|
|
||||||
|
# Имя пода shared-sqs в k8s (для ресурсов)
|
||||||
|
SS_DEPLOY_LABEL="app=shared-sqs"
|
||||||
|
OP_MANAGER_LABEL="control-plane=controller-manager"
|
||||||
|
OP_TENANT_LABEL_PREFIX="sless.dev/tenant"
|
||||||
|
|
||||||
|
# Таймаут ожидания QueueService Ready (секунд)
|
||||||
|
OP_READY_TIMEOUT=120
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
# УТИЛИТЫ
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
RESULTS_FILE="/tmp/sqs_compare_$(date +%s).log"
|
||||||
|
|
||||||
|
ok() { echo " ✅ $1"; PASS=$((PASS+1)); }
|
||||||
|
fail() { echo " ❌ $1"; FAIL=$((FAIL+1)); }
|
||||||
|
hdr() { echo ""; echo "══════════════════════════════════════════"; echo " $1"; echo "══════════════════════════════════════════"; }
|
||||||
|
ts() { date '+%H:%M:%S'; }
|
||||||
|
elapsed_ms() {
|
||||||
|
# $1 = start seconds (с дробной частью от date +%s%3N)
|
||||||
|
local start="$1"
|
||||||
|
local end
|
||||||
|
end=$(date +%s%3N)
|
||||||
|
echo $(( end - start ))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Запись результата в файл для итоговой таблицы
|
||||||
|
record() {
|
||||||
|
# $1=impl $2=phase $3=n_tenants $4=value_ms $5=label
|
||||||
|
echo "$1,$2,$3,$4,$5" >> "$RESULTS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── sqs-operator API ───
|
||||||
|
op_sqs() {
|
||||||
|
# $1=endpoint $2=ak $3=sk $4=query_params
|
||||||
|
curl -sk --max-time 15 \
|
||||||
|
--aws-sigv4 "aws:amz:us-east-1:sqs" \
|
||||||
|
--user "$2:$3" \
|
||||||
|
"${1}/?${4}&Version=2012-11-05"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ожидать QueueService Phase=Ready
|
||||||
|
op_wait_ready() {
|
||||||
|
local crname="$1" max_sec="$2"
|
||||||
|
local start
|
||||||
|
start=$(date +%s)
|
||||||
|
while true; do
|
||||||
|
local phase
|
||||||
|
phase=$(kubectl get queueservice "$crname" -n "$OP_CR_NAMESPACE" \
|
||||||
|
-o jsonpath='{.status.phase}' 2>/dev/null || echo "")
|
||||||
|
[ "$phase" = "Ready" ] && return 0
|
||||||
|
local now
|
||||||
|
now=$(date +%s)
|
||||||
|
[ $(( now - start )) -ge "$max_sec" ] && return 1
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверить что QueueService CR API доступен (идемпотентно)
|
||||||
|
op_check_api() {
|
||||||
|
local tenantid="$1"
|
||||||
|
local ep="${OP_HOST}/sqs/${tenantid}"
|
||||||
|
# Для credentials нужен secret в k8s
|
||||||
|
local ns="${OP_TENANT_NS_PREFIX}-${tenantid}"
|
||||||
|
local ak sk
|
||||||
|
ak=$(kubectl -n "$ns" get secret "sqs-creds-${tenantid}" \
|
||||||
|
-o jsonpath='{.data.accessKey}' 2>/dev/null | base64 -d 2>/dev/null || echo "")
|
||||||
|
sk=$(kubectl -n "$ns" get secret "sqs-creds-${tenantid}" \
|
||||||
|
-o jsonpath='{.data.secretKey}' 2>/dev/null | base64 -d 2>/dev/null || echo "")
|
||||||
|
echo "$ak:$sk:$ep"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── shared-sqs Admin API ───
|
||||||
|
ss_admin() {
|
||||||
|
local method="$1" path="$2" body="${3:-}"
|
||||||
|
if [[ -n "$body" ]]; then
|
||||||
|
curl -sf --max-time 15 \
|
||||||
|
-X "$method" \
|
||||||
|
-H "Authorization: Bearer $SS_ADMIN_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$body" \
|
||||||
|
"${SS_HOST}${path}" 2>&1
|
||||||
|
else
|
||||||
|
curl -sf --max-time 15 \
|
||||||
|
-X "$method" \
|
||||||
|
-H "Authorization: Bearer $SS_ADMIN_TOKEN" \
|
||||||
|
"${SS_HOST}${path}" 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ss_sqs() {
|
||||||
|
# $1=ak $2=sk $3=action_params
|
||||||
|
curl -sk --max-time 15 \
|
||||||
|
-u "$1:$2" \
|
||||||
|
"${SS_HOST}/?${3}&Version=2012-11-05" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
# ПРОВЕРКА ДОСТУПНОСТИ
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
hdr "0. Проверка доступности (N=$TENANTS тенантов)"
|
||||||
|
|
||||||
|
OP_HEALTH=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "${OP_HOST}/sqs/test001" 2>/dev/null || echo "0")
|
||||||
|
SS_HEALTH=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "${SS_HOST}/health" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
echo " sqs-operator (${OP_HOST}): HTTP $OP_HEALTH"
|
||||||
|
echo " shared-sqs (${SS_HOST}): HTTP $SS_HEALTH"
|
||||||
|
|
||||||
|
# kubectl доступен?
|
||||||
|
KUBECTL_OK=false
|
||||||
|
if kubectl cluster-info &>/dev/null; then
|
||||||
|
KUBECTL_OK=true
|
||||||
|
echo " kubectl: OK ($(kubectl config current-context))"
|
||||||
|
else
|
||||||
|
echo " kubectl: НЕДОСТУПЕН — метрики ресурсов будут пропущены"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# aws cli доступен?
|
||||||
|
AWS_CLI_OK=false
|
||||||
|
if command -v aws &>/dev/null; then
|
||||||
|
AWS_CLI_OK=true
|
||||||
|
echo " aws CLI: OK"
|
||||||
|
else
|
||||||
|
echo " aws CLI: НЕДОСТУПЕН — shared-sqs CRUD будет через curl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
# ФАЗА 1 — Создание N тенантов
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
hdr "1. Создание $TENANTS тенантов"
|
||||||
|
|
||||||
|
# Массивы для хранения созданных тенантов
|
||||||
|
declare -a OP_TENANTS=() # tenantId
|
||||||
|
declare -a OP_CR_NAMES=() # имя CR
|
||||||
|
declare -a SS_TENANT_IDS=() # UUID тенанта
|
||||||
|
declare -a SS_AK_LIST=() # Access Keys
|
||||||
|
declare -a SS_SK_LIST=() # Secret Keys
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ── A) sqs-operator ──"
|
||||||
|
echo " (каждый тенант = QueueService CR → новый namespace + deployment + secret)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
OP_TOTAL_CREATE_MS=0
|
||||||
|
OP_CREATE_TIMES=()
|
||||||
|
|
||||||
|
for i in $(seq 1 "$TENANTS"); do
|
||||||
|
tid="cmp-op-$(printf '%03d' $i)-$$"
|
||||||
|
crname="cmp-cr-$(printf '%03d' $i)-$$"
|
||||||
|
|
||||||
|
# Применяем CR
|
||||||
|
CR_YAML=$(echo "$OP_CR_TEMPLATE" | sed "s/CRNAME/$crname/g" | sed "s/TENANTID/$tid/g")
|
||||||
|
t_start=$(date +%s%3N)
|
||||||
|
echo "$CR_YAML" | kubectl apply -f - &>/dev/null
|
||||||
|
apply_ok=$?
|
||||||
|
|
||||||
|
if [[ $apply_ok -ne 0 ]]; then
|
||||||
|
echo " ❌ Тенант #$i ($tid): kubectl apply failed"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ждём Ready
|
||||||
|
if op_wait_ready "$crname" "$OP_READY_TIMEOUT"; then
|
||||||
|
t_ms=$(elapsed_ms "$t_start")
|
||||||
|
OP_CREATE_TIMES+=("$t_ms")
|
||||||
|
OP_TOTAL_CREATE_MS=$(( OP_TOTAL_CREATE_MS + t_ms ))
|
||||||
|
printf " ✅ #%-3d %-40s %dms\n" "$i" "$tid" "$t_ms"
|
||||||
|
record "sqs-operator" "tenant_create" "$i" "$t_ms" "$tid"
|
||||||
|
else
|
||||||
|
echo " ⚠️ #$i ($tid): timeout — не стал Ready за ${OP_READY_TIMEOUT}s"
|
||||||
|
record "sqs-operator" "tenant_create_timeout" "$i" "$OP_READY_TIMEOUT\000" "$tid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
OP_TENANTS+=("$tid")
|
||||||
|
OP_CR_NAMES+=("$crname")
|
||||||
|
done
|
||||||
|
|
||||||
|
OP_CREATED=${#OP_TENANTS[@]}
|
||||||
|
if [[ $OP_CREATED -gt 0 ]]; then
|
||||||
|
OP_AVG_CREATE=$(( OP_TOTAL_CREATE_MS / OP_CREATED ))
|
||||||
|
# Медиана
|
||||||
|
OP_SORTED_TIMES=($(printf '%s\n' "${OP_CREATE_TIMES[@]}" | sort -n))
|
||||||
|
OP_MEDIAN_CREATE=${OP_SORTED_TIMES[$(( OP_CREATED / 2 ))]}
|
||||||
|
printf "\n Итого sqs-operator: создано %d/%d avg=%dms median=%dms\n" \
|
||||||
|
"$OP_CREATED" "$TENANTS" "$OP_AVG_CREATE" "$OP_MEDIAN_CREATE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ── B) shared-sqs ──"
|
||||||
|
echo " (тенант = POST /admin/tenants, всё в памяти одного процесса)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
SS_TOTAL_CREATE_MS=0
|
||||||
|
SS_CREATE_TIMES=()
|
||||||
|
|
||||||
|
for i in $(seq 1 "$TENANTS"); do
|
||||||
|
tname="cmp-ss-$(printf '%03d' $i)-$$"
|
||||||
|
t_start=$(date +%s%3N)
|
||||||
|
|
||||||
|
RESP=$(ss_admin POST /admin/tenants "{\"name\":\"${tname}\",\"max_queues\":50}" 2>&1)
|
||||||
|
t_ms=$(elapsed_ms "$t_start")
|
||||||
|
|
||||||
|
if echo "$RESP" | grep -q "access_key"; then
|
||||||
|
ak=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_key'])" 2>/dev/null || echo "")
|
||||||
|
sk=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['secret_key'])" 2>/dev/null || echo "")
|
||||||
|
tid=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
SS_CREATE_TIMES+=("$t_ms")
|
||||||
|
SS_TOTAL_CREATE_MS=$(( SS_TOTAL_CREATE_MS + t_ms ))
|
||||||
|
printf " ✅ #%-3d %-40s %dms (id=%s)\n" "$i" "$tname" "$t_ms" "${tid:0:12}..."
|
||||||
|
record "shared-sqs" "tenant_create" "$i" "$t_ms" "$tname"
|
||||||
|
|
||||||
|
SS_TENANT_IDS+=("$tid")
|
||||||
|
SS_AK_LIST+=("$ak")
|
||||||
|
SS_SK_LIST+=("$sk")
|
||||||
|
else
|
||||||
|
echo " ❌ #$i ($tname): ошибка: ${RESP:0:100}"
|
||||||
|
record "shared-sqs" "tenant_create_error" "$i" "0" "$tname"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
SS_CREATED=${#SS_TENANT_IDS[@]}
|
||||||
|
if [[ $SS_CREATED -gt 0 ]]; then
|
||||||
|
SS_AVG_CREATE=$(( SS_TOTAL_CREATE_MS / SS_CREATED ))
|
||||||
|
SS_SORTED_TIMES=($(printf '%s\n' "${SS_CREATE_TIMES[@]}" | sort -n))
|
||||||
|
SS_MEDIAN_CREATE=${SS_SORTED_TIMES[$(( SS_CREATED / 2 ))]}
|
||||||
|
printf "\n Итого shared-sqs: создано %d/%d avg=%dms median=%dms\n" \
|
||||||
|
"$SS_CREATED" "$TENANTS" "$SS_AVG_CREATE" "$SS_MEDIAN_CREATE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
# ФАЗА 2 — CRUD очереди (первые 3 тенанта)
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
hdr "2. CRUD очереди (send+receive+delete, первые 3 тенанта каждой реализации)"
|
||||||
|
|
||||||
|
CRUD_LIMIT=3
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ── A) sqs-operator CRUD ──"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
OP_CRUD_TIMES=()
|
||||||
|
for idx in $(seq 0 $(( CRUD_LIMIT - 1 ))); do
|
||||||
|
[[ $idx -ge ${#OP_TENANTS[@]} ]] && break
|
||||||
|
tid="${OP_TENANTS[$idx]}"
|
||||||
|
crname="${OP_CR_NAMES[$idx]}"
|
||||||
|
creds=$(op_check_api "$tid")
|
||||||
|
ak=$(echo "$creds" | cut -d: -f1)
|
||||||
|
sk=$(echo "$creds" | cut -d: -f2)
|
||||||
|
ep=$(echo "$creds" | cut -d: -f3-)
|
||||||
|
|
||||||
|
if [[ -z "$ak" || -z "$sk" ]]; then
|
||||||
|
echo " ⚠️ #$idx skipped — нет credentials (тенант не Ready?)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
qname="crud-q-$$-$idx"
|
||||||
|
t_start=$(date +%s%3N)
|
||||||
|
|
||||||
|
# CreateQueue
|
||||||
|
R=$(curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${ep}/?Action=CreateQueue&QueueName=${qname}&Version=2012-11-05")
|
||||||
|
QURL=$(echo "$R" | grep -oP "(?<=<QueueUrl>)[^<]+" || echo "")
|
||||||
|
|
||||||
|
if [[ -z "$QURL" ]]; then
|
||||||
|
echo " ❌ #$idx CreateQueue failed: ${R:0:80}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SendMessage
|
||||||
|
curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${ep}/?Action=SendMessage&QueueUrl=${QURL}&MessageBody=compare-test-${idx}&Version=2012-11-05" >/dev/null
|
||||||
|
|
||||||
|
# ReceiveMessage
|
||||||
|
R=$(curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${ep}/?Action=ReceiveMessage&QueueUrl=${QURL}&MaxNumberOfMessages=1&Version=2012-11-05")
|
||||||
|
RECEIPT=$(echo "$R" | grep -oP "(?<=<ReceiptHandle>)[^<]+" | head -1 || echo "")
|
||||||
|
|
||||||
|
# DeleteMessage
|
||||||
|
if [[ -n "$RECEIPT" ]]; then
|
||||||
|
RECEIPT_ENC=$(echo "$RECEIPT" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
|
||||||
|
curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${ep}/?Action=DeleteMessage&QueueUrl=${QURL}&ReceiptHandle=${RECEIPT_ENC}&Version=2012-11-05" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DeleteQueue
|
||||||
|
curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${ep}/?Action=DeleteQueue&QueueUrl=${QURL}&Version=2012-11-05" >/dev/null
|
||||||
|
|
||||||
|
t_ms=$(elapsed_ms "$t_start")
|
||||||
|
OP_CRUD_TIMES+=("$t_ms")
|
||||||
|
printf " ✅ Tenant #%d %-36s CRUD %dms\n" "$idx" "$tid" "$t_ms"
|
||||||
|
record "sqs-operator" "crud" "$idx" "$t_ms" "$tid"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ── B) shared-sqs CRUD ──"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
SS_CRUD_TIMES=()
|
||||||
|
for idx in $(seq 0 $(( CRUD_LIMIT - 1 ))); do
|
||||||
|
[[ $idx -ge ${#SS_TENANT_IDS[@]} ]] && break
|
||||||
|
tid="${SS_TENANT_IDS[$idx]}"
|
||||||
|
ak="${SS_AK_LIST[$idx]}"
|
||||||
|
sk="${SS_SK_LIST[$idx]}"
|
||||||
|
qname="crud-q-$$-$idx"
|
||||||
|
|
||||||
|
t_start=$(date +%s%3N)
|
||||||
|
|
||||||
|
# CreateQueue — shared-sqs требует AWS SigV4, не Basic Auth
|
||||||
|
R=$(curl -sk --max-time 15 \
|
||||||
|
--aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${SS_HOST}/?Action=CreateQueue&QueueName=${qname}&Version=2012-11-05")
|
||||||
|
QURL=$(echo "$R" | grep -oP "(?<=<QueueUrl>)[^<]+" || echo "")
|
||||||
|
|
||||||
|
if [[ -z "$QURL" ]]; then
|
||||||
|
echo " ❌ #$idx CreateQueue failed: ${R:0:80}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SendMessage
|
||||||
|
curl -sk --max-time 15 \
|
||||||
|
--aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${SS_HOST}/?Action=SendMessage&QueueUrl=${QURL}&MessageBody=compare-test-${idx}&Version=2012-11-05" >/dev/null
|
||||||
|
|
||||||
|
# ReceiveMessage
|
||||||
|
R=$(curl -sk --max-time 15 \
|
||||||
|
--aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${SS_HOST}/?Action=ReceiveMessage&QueueUrl=${QURL}&MaxNumberOfMessages=1&Version=2012-11-05")
|
||||||
|
RECEIPT=$(echo "$R" | grep -oP "(?<=<ReceiptHandle>)[^<]+" | head -1 || echo "")
|
||||||
|
|
||||||
|
# DeleteMessage
|
||||||
|
if [[ -n "$RECEIPT" ]]; then
|
||||||
|
RECEIPT_ENC=$(echo "$RECEIPT" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
|
||||||
|
curl -sk --max-time 15 \
|
||||||
|
--aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${SS_HOST}/?Action=DeleteMessage&QueueUrl=${QURL}&ReceiptHandle=${RECEIPT_ENC}&Version=2012-11-05" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DeleteQueue
|
||||||
|
curl -sk --max-time 15 \
|
||||||
|
--aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \
|
||||||
|
"${SS_HOST}/?Action=DeleteQueue&QueueUrl=${QURL}&Version=2012-11-05" >/dev/null
|
||||||
|
|
||||||
|
t_ms=$(elapsed_ms "$t_start")
|
||||||
|
SS_CRUD_TIMES+=("$t_ms")
|
||||||
|
printf " ✅ Tenant #%d %-36s CRUD %dms\n" "$idx" "${tid:0:20}..." "$t_ms"
|
||||||
|
record "shared-sqs" "crud" "$idx" "$t_ms" "${tid:0:12}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
# ФАЗА 3 — Ресурсы (kubectl top)
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
hdr "3. Ресурсы при $TENANTS тенантах (kubectl top)"
|
||||||
|
|
||||||
|
if [[ "$KUBECTL_OK" == "true" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo " ── A) sqs-operator pods ──"
|
||||||
|
# Controller manager
|
||||||
|
echo " -- Controller manager:"
|
||||||
|
kubectl top pods -n "$OP_CR_NAMESPACE" -l "$OP_MANAGER_LABEL" 2>/dev/null || echo " (kubectl top недоступен — metrics-server?)"
|
||||||
|
# Tenant pods — все ns с prefix sless-fn-cmp-
|
||||||
|
echo " -- Tenant pods (cmp-* namespaces):"
|
||||||
|
for tid in "${OP_TENANTS[@]}"; do
|
||||||
|
ns="${OP_TENANT_NS_PREFIX}-${tid}"
|
||||||
|
kubectl top pods -n "$ns" 2>/dev/null | grep -v "^NAME" | \
|
||||||
|
awk -v ns="$ns" '{printf " %-50s CPU=%-8s MEM=%s\n", ns"/"$1, $2, $3}' 2>/dev/null || true
|
||||||
|
done
|
||||||
|
# Суммарно: кол-во pods
|
||||||
|
OP_NS_COUNT=$(kubectl get ns | grep -c "sless-fn-cmp-" 2>/dev/null || echo "?")
|
||||||
|
echo " -- Итого namespace под тенанты: $OP_NS_COUNT"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ── B) shared-sqs pod ──"
|
||||||
|
kubectl top pods -n "$SS_NAMESPACE" -l "$SS_DEPLOY_LABEL" 2>/dev/null || \
|
||||||
|
kubectl top pods -n "$SS_NAMESPACE" 2>/dev/null | grep -i "shared-sqs\|sqs" | \
|
||||||
|
awk '{printf " %-50s CPU=%-8s MEM=%s\n", $1, $2, $3}' || \
|
||||||
|
echo " (pod не найден в namespace $SS_NAMESPACE)"
|
||||||
|
echo " -- Тенанты в памяти: $SS_CREATED (0 дополнительных pods)"
|
||||||
|
else
|
||||||
|
echo " kubectl недоступен — пропускаем"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
# ФАЗА 4 — Cleanup
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
hdr "4. Cleanup"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ── A) sqs-operator ──"
|
||||||
|
OP_DEL_TIMES=()
|
||||||
|
for i in "${!OP_CR_NAMES[@]}"; do
|
||||||
|
crname="${OP_CR_NAMES[$i]}"
|
||||||
|
t_start=$(date +%s%3N)
|
||||||
|
kubectl delete queueservice "$crname" -n "$OP_CR_NAMESPACE" --wait=false &>/dev/null
|
||||||
|
t_ms=$(elapsed_ms "$t_start")
|
||||||
|
OP_DEL_TIMES+=("$t_ms")
|
||||||
|
record "sqs-operator" "tenant_delete" "$i" "$t_ms" "$crname"
|
||||||
|
done
|
||||||
|
echo " Удалено ${#OP_CR_NAMES[@]} CR (без ожидания термин.)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " ── B) shared-sqs ──"
|
||||||
|
SS_DEL_TIMES=()
|
||||||
|
for i in "${!SS_TENANT_IDS[@]}"; do
|
||||||
|
tid="${SS_TENANT_IDS[$i]}"
|
||||||
|
t_start=$(date +%s%3N)
|
||||||
|
ss_admin DELETE "/admin/tenants/$tid" >/dev/null 2>&1
|
||||||
|
t_ms=$(elapsed_ms "$t_start")
|
||||||
|
SS_DEL_TIMES+=("$t_ms")
|
||||||
|
record "shared-sqs" "tenant_delete" "$i" "$t_ms" "${tid:0:12}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#SS_DEL_TIMES[@]} -gt 0 ]]; then
|
||||||
|
SS_AVG_DEL=$(python3 -c "t=[${SS_DEL_TIMES[*]}]; print(int(sum(t)/len(t)))" 2>/dev/null || echo "?")
|
||||||
|
echo " Удалено ${#SS_DEL_TIMES[@]} тенантов avg=${SS_AVG_DEL}ms"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
# ИТОГОВАЯ ТАБЛИЦА
|
||||||
|
# ════════════════════════════════════════════
|
||||||
|
hdr "ИТОГ — Сравнительная таблица (N=$TENANTS)"
|
||||||
|
|
||||||
|
python3 - << PYEOF
|
||||||
|
import csv, sys
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Парсим results file
|
||||||
|
try:
|
||||||
|
with open("$RESULTS_FILE") as f:
|
||||||
|
for row in csv.reader(f):
|
||||||
|
if len(row) < 5:
|
||||||
|
continue
|
||||||
|
impl, phase, n, val_str, label = row
|
||||||
|
try:
|
||||||
|
val = int(val_str)
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
key = (impl, phase)
|
||||||
|
data.setdefault(key, []).append(val)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Нет результатов")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def stats(vals):
|
||||||
|
if not vals:
|
||||||
|
return "нет данных"
|
||||||
|
vals_s = sorted(vals)
|
||||||
|
n = len(vals_s)
|
||||||
|
avg = int(sum(vals_s) / n)
|
||||||
|
med = vals_s[n // 2]
|
||||||
|
mn = vals_s[0]
|
||||||
|
mx = vals_s[-1]
|
||||||
|
return f"avg={avg}ms med={med}ms min={mn}ms max={mx}ms n={n}"
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print(f"{'Метрика':<30} {'sqs-operator':<50} {'shared-sqs':<50}")
|
||||||
|
print("-" * 130)
|
||||||
|
|
||||||
|
phases = [
|
||||||
|
("tenant_create", "Создание тенанта"),
|
||||||
|
("crud", "CRUD очереди (create+send+recv+del)"),
|
||||||
|
("tenant_delete", "Удаление тенанта"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for phase_key, phase_label in phases:
|
||||||
|
op_vals = data.get(("sqs-operator", phase_key), [])
|
||||||
|
ss_vals = data.get(("shared-sqs", phase_key), [])
|
||||||
|
print(f" {phase_label:<28} {stats(op_vals):<50} {stats(ss_vals):<50}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
# Вывод победителя по скорости создания
|
||||||
|
op_create = data.get(("sqs-operator", "tenant_create"), [])
|
||||||
|
ss_create = data.get(("shared-sqs", "tenant_create"), [])
|
||||||
|
if op_create and ss_create:
|
||||||
|
op_avg = sum(op_create) / len(op_create)
|
||||||
|
ss_avg = sum(ss_create) / len(ss_create)
|
||||||
|
ratio = op_avg / ss_avg if ss_avg > 0 else float('inf')
|
||||||
|
faster = "shared-sqs" if ss_avg < op_avg else "sqs-operator"
|
||||||
|
print(f" Создание тенанта: {faster} быстрее в {ratio:.1f}x")
|
||||||
|
print(f" sqs-operator: {op_avg:.0f}ms | shared-sqs: {ss_avg:.0f}ms")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print(f" Файл подробных данных: $RESULTS_FILE")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo " Готово! $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Сводка агентов (моделей)
|
||||||
|
|
||||||
|
Дата: 2026-04-13
|
||||||
|
|
||||||
|
| Агент | Контекст | Цена | Главная фишка |
|
||||||
|
|-------|---------:|:----:|---------------|
|
||||||
|
| GPT-5.4 | 400K | 1x | «Видит» всё. Лидер по объему знаний. |
|
||||||
|
| GPT-5.3-Codex | 400K | 1x | Ваш основной инструмент для WSL/Terraform/Go. |
|
||||||
|
| Claude Opus 4.6 | 192K | 3x | Элитный кодер. Самый чистый и логичный код. |
|
||||||
|
| Claude Sonnet 4.6 | 160K | 1x | Баланс для повседневных фич. |
|
||||||
|
| GPT-5.4 mini | 400K | 0.33x | Лучший для тестов и мелких правок «пачками». |
|
||||||
|
| Raptor mini | 264K | 0x | Бесплатный анализ больших логов и дампов. |
|
||||||
|
| Grok Code Fast | 173K | 0.25x | Ультра‑свежие данные и библиотеки. |
|
||||||
|
| GPT-4o / GPT-4.1 | ~100K | 0x | Для элементарных задач и Bash‑скриптов. |
|
||||||
|
|
||||||
|
> Примечание: сохранённая версия также будет скопирована в домашнюю папку ВМ как `~/agent_models.md`.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Список доступных языковых моделей (скриншот)
|
||||||
|
|
||||||
|
Дата: 2026-04-13
|
||||||
|
|
||||||
|
Колонки: Имя | Размер контекста | Возможности | Множитель запроса
|
||||||
|
|
||||||
|
- Claude Haiku 4.5 — 160K — Инструменты, Видение — 0.33x
|
||||||
|
- Claude Opus 4.5 — 160K — Инструменты, Видение — 3x
|
||||||
|
- Claude Opus 4.6 — 192K — Инструменты, Видение — 3x
|
||||||
|
- Claude Sonnet 4 — 144K — Инструменты, Видение — 1x
|
||||||
|
- Claude Sonnet 4.5 — 160K — Инструменты, Видение — 1x
|
||||||
|
- Claude Sonnet 4.6 — 160K — Инструменты, Видение — 1x
|
||||||
|
- Gemini 2.5 Pro — 173K — Инструменты, Видение — 1x
|
||||||
|
- Gemini 3 Flash (Preview) — 173K — Инструменты, Видение — 0.33x
|
||||||
|
- Gemini 3.1 Pro (Preview) — 173K — Инструменты, Видение — 1x
|
||||||
|
- GPT-4.1 — 128K — Инструменты, Видение — 0x
|
||||||
|
- GPT-4o — 68K — Инструменты, Видение — 0x
|
||||||
|
- GPT-5 mini — 192K — Инструменты, Видение — 1x
|
||||||
|
- GPT-5.1 — 192K — Инструменты, Видение — 1x
|
||||||
|
- GPT-5.2 — 192K — Инструменты, Видение — 1x
|
||||||
|
- GPT-5.2-Codex — 400K — Инструменты, Видение — 1x
|
||||||
|
- GPT-5.3-Codex — 400K — Инструменты, Видение — 1x
|
||||||
|
- GPT-5.4 — 400K — Инструменты, Видение — 1x
|
||||||
|
- GPT-5.4 mini — 400K — Инструменты, Видение — 0.33x
|
||||||
|
- Grok Code Fast 1 — 173K — Инструменты, Видение — 0.25x
|
||||||
|
- Raptor mini (Preview) — 264K — Инструменты, Видение — 0x
|
||||||
|
|
||||||
|
Примечание: транскрипция выполнена по приложенному скриншоту. Если нужно другое форматирование (CSV, JSON или добавить дополнительные колонки), скажите, сохраню в нужном виде.
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 2026-04-09 — Полный набор тестов SQS API для ручного / автоматического запуска
|
||||||
|
# Покрывает: happy path, невалидный ввод, edge cases, безопасность
|
||||||
|
# Запуск: bash sqs_api_tests.sh
|
||||||
|
# Требования: curl, base64, доступ к https://sqs.kube5s.ru
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# КОНФИГУРАЦИЯ
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
SQS_HOST="https://sqs.kube5s.ru"
|
||||||
|
SQS_BASE="${SQS_HOST}/sqs/test001"
|
||||||
|
AK="SQSAK-test001-ae4dc8"
|
||||||
|
SK="J8mElXq8Nwww8iFRtGJCm8KfxM9XSTluTqS2A67xsEE"
|
||||||
|
AUTH="${AK}:${SK}"
|
||||||
|
VERSION="2012-11-05"
|
||||||
|
|
||||||
|
# Счётчики результатов
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
SKIP=0
|
||||||
|
TOTAL=0
|
||||||
|
|
||||||
|
# Уникальный суффикс чтобы тесты не конфликтовали при повторном запуске
|
||||||
|
TS=$(date +%s)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# УТИЛИТЫ
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
sqs_call() {
|
||||||
|
# $1 = параметры запроса (после ?)
|
||||||
|
# $2 = auth (опционально, по умолчанию $AUTH)
|
||||||
|
local auth="${2:-$AUTH}"
|
||||||
|
curl -s --max-time 15 -u "$auth" "${SQS_BASE}?${1}&Version=${VERSION}" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
# $1 = название теста
|
||||||
|
# $2 = ответ
|
||||||
|
# $3 = ожидаемая подстрока
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
if echo "$2" | grep -q "$3"; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: $1"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " ❌ FAIL: $1"
|
||||||
|
echo " Ожидалось: '$3'"
|
||||||
|
echo " Получено: $(echo "$2" | head -3)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_contains() {
|
||||||
|
# $1 = название теста
|
||||||
|
# $2 = ответ
|
||||||
|
# $3 = подстрока которой НЕ должно быть
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
if echo "$2" | grep -q "$3"; then
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " ❌ FAIL: $1"
|
||||||
|
echo " НЕ ожидалось: '$3'"
|
||||||
|
echo " Получено: $(echo "$2" | head -3)"
|
||||||
|
else
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_http_code() {
|
||||||
|
# $1 = название теста
|
||||||
|
# $2 = URL (полный)
|
||||||
|
# $3 = ожидаемый HTTP код
|
||||||
|
# $4 = auth (опционально)
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
local auth="${4:-$AUTH}"
|
||||||
|
local code
|
||||||
|
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 -u "$auth" "$2" 2>&1)
|
||||||
|
if [ "$code" = "$3" ]; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: $1 (HTTP $code)"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " ❌ FAIL: $1 (ожидался HTTP $3, получен $code)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_queue() {
|
||||||
|
# Тихое удаление очереди
|
||||||
|
sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/$1" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "╔══════════════════════════════════════════════════╗"
|
||||||
|
echo "║ SQS API Test Suite — $(date '+%Y-%m-%d %H:%M:%S') ║"
|
||||||
|
echo "║ Endpoint: ${SQS_BASE} ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 1: БАЗОВЫЕ ОПЕРАЦИИ (Happy Path)
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 1: Базовые операции (Happy Path) ──"
|
||||||
|
|
||||||
|
Q1="testq-hp-${TS}"
|
||||||
|
|
||||||
|
# T01: Создание очереди
|
||||||
|
R=$(sqs_call "Action=CreateQueue&QueueName=${Q1}")
|
||||||
|
assert_contains "T01 CreateQueue" "$R" "CreateQueueResult"
|
||||||
|
assert_contains "T01 CreateQueue URL" "$R" "${Q1}"
|
||||||
|
|
||||||
|
# T02: Повторное создание той же очереди — не ошибка, возвращает ту же URL
|
||||||
|
R=$(sqs_call "Action=CreateQueue&QueueName=${Q1}")
|
||||||
|
assert_contains "T02 CreateQueue idempotent" "$R" "CreateQueueResult"
|
||||||
|
|
||||||
|
# T03: ListQueues — очередь видна
|
||||||
|
R=$(sqs_call "Action=ListQueues")
|
||||||
|
assert_contains "T03 ListQueues" "$R" "${Q1}"
|
||||||
|
|
||||||
|
# T04: SendMessage
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&MessageBody=Hello")
|
||||||
|
assert_contains "T04 SendMessage" "$R" "MessageId"
|
||||||
|
assert_contains "T04 SendMessage MD5" "$R" "MD5OfMessageBody"
|
||||||
|
|
||||||
|
# T05: ReceiveMessage
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q1}")
|
||||||
|
assert_contains "T05 ReceiveMessage Body" "$R" "Hello"
|
||||||
|
assert_contains "T05 ReceiveMessage ReceiptHandle" "$R" "ReceiptHandle"
|
||||||
|
|
||||||
|
# Извлекаем ReceiptHandle для T06
|
||||||
|
RH=$(echo "$R" | grep -oP '(?<=<ReceiptHandle>)[^<]+' | head -1)
|
||||||
|
|
||||||
|
# T06: DeleteMessage
|
||||||
|
# ReceiptHandle содержит +/= — URL-кодируем чтобы не сломать query string
|
||||||
|
if [ -n "$RH" ]; then
|
||||||
|
RH_ENC=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$RH")
|
||||||
|
R=$(sqs_call "Action=DeleteMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&ReceiptHandle=${RH_ENC}")
|
||||||
|
assert_contains "T06 DeleteMessage" "$R" "DeleteMessageResponse"
|
||||||
|
else
|
||||||
|
TOTAL=$((TOTAL + 1)); SKIP=$((SKIP + 1))
|
||||||
|
echo " ⚠️ SKIP: T06 DeleteMessage — не удалось получить ReceiptHandle"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# T07: ReceiveMessage из пустой очереди — пустой результат
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q1}")
|
||||||
|
assert_not_contains "T07 ReceiveMessage empty queue" "$R" "<Body>"
|
||||||
|
|
||||||
|
# T08: GetQueueAttributes
|
||||||
|
R=$(sqs_call "Action=GetQueueAttributes&QueueUrl=${SQS_BASE}/test001/${Q1}&AttributeName.1=All")
|
||||||
|
assert_contains "T08 GetQueueAttributes" "$R" "GetQueueAttributesResponse"
|
||||||
|
|
||||||
|
# T09: PurgeQueue
|
||||||
|
sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&MessageBody=ToPurge" >/dev/null
|
||||||
|
R=$(sqs_call "Action=PurgeQueue&QueueUrl=${SQS_BASE}/test001/${Q1}")
|
||||||
|
assert_contains "T09 PurgeQueue" "$R" "PurgeQueueResponse"
|
||||||
|
|
||||||
|
# T10: DeleteQueue
|
||||||
|
R=$(sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q1}")
|
||||||
|
assert_contains "T10 DeleteQueue" "$R" "DeleteQueueResponse"
|
||||||
|
|
||||||
|
# T11: ListQueues — очередь исчезла
|
||||||
|
R=$(sqs_call "Action=ListQueues")
|
||||||
|
assert_not_contains "T11 ListQueues after delete" "$R" "${Q1}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 2: НЕВАЛИДНЫЙ ВВОД
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 2: Невалидный ввод ──"
|
||||||
|
|
||||||
|
# T12: Неизвестный Action
|
||||||
|
R=$(sqs_call "Action=BogusAction")
|
||||||
|
assert_contains "T12 Unknown Action" "$R" "InvalidAction\|AWS.SimpleQueueService.NonExistentAction\|UnknownAction\|InvalidParameterValue"
|
||||||
|
|
||||||
|
# T13: Пустой Action
|
||||||
|
R=$(sqs_call "Action=")
|
||||||
|
assert_contains "T13 Empty Action" "$R" "Error\|MissingAction\|InvalidAction\|Value for parameter Action is not valid"
|
||||||
|
|
||||||
|
# T14: Без Action вообще
|
||||||
|
R=$(curl -s --max-time 15 -u "$AUTH" "${SQS_BASE}?Version=${VERSION}" 2>&1)
|
||||||
|
assert_contains "T14 Missing Action" "$R" "Error\|MissingAction\|request"
|
||||||
|
|
||||||
|
# T15: CreateQueue без QueueName
|
||||||
|
R=$(sqs_call "Action=CreateQueue")
|
||||||
|
assert_contains "T15 CreateQueue no name" "$R" "Error\|MissingParameter\|Value for parameter QueueName is not valid"
|
||||||
|
|
||||||
|
# T16: CreateQueue с пустым QueueName
|
||||||
|
R=$(sqs_call "Action=CreateQueue&QueueName=")
|
||||||
|
assert_contains "T16 CreateQueue empty name" "$R" "Error\|InvalidParameterValue"
|
||||||
|
|
||||||
|
# T17: CreateQueue с недопустимыми символами (пробелы, кириллица)
|
||||||
|
# Akka HTTP (ElasticMQ) отвечает "Illegal request-target" на raw кириллицу
|
||||||
|
R=$(sqs_call "Action=CreateQueue&QueueName=очередь%20тест")
|
||||||
|
assert_contains "T17 CreateQueue invalid chars" "$R" "Error\|InvalidParameterValue\|Illegal"
|
||||||
|
|
||||||
|
# T18: CreateQueue с очень длинным именем (>80 символов)
|
||||||
|
LONGNAME=$(printf 'q%.0s' {1..100})
|
||||||
|
R=$(sqs_call "Action=CreateQueue&QueueName=${LONGNAME}")
|
||||||
|
assert_contains "T18 CreateQueue name too long" "$R" "Error\|InvalidParameterValue\|CreateQueueResult"
|
||||||
|
# Примечание: ElasticMQ может создать — это допустимо если не strict AWS
|
||||||
|
|
||||||
|
# T19: SendMessage без MessageBody
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/nonexist")
|
||||||
|
assert_contains "T19 SendMessage no body" "$R" "Error\|MissingParameter\|The request must contain the parameter MessageBody"
|
||||||
|
|
||||||
|
# T20: SendMessage без QueueUrl
|
||||||
|
R=$(sqs_call "Action=SendMessage&MessageBody=test")
|
||||||
|
assert_contains "T20 SendMessage no QueueUrl" "$R" "Error\|MissingParameter\|AWS.SimpleQueueService.NonExistentQueue\|The request must contain"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 3: НЕСУЩЕСТВУЮЩИЕ РЕСУРСЫ
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 3: Несуществующие ресурсы ──"
|
||||||
|
|
||||||
|
# T21: SendMessage в несуществующую очередь (autoCreateQueues=false!)
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}&MessageBody=fail")
|
||||||
|
assert_contains "T21 SendMessage nonexistent queue" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error"
|
||||||
|
|
||||||
|
# T22: ReceiveMessage из несуществующей очереди
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}")
|
||||||
|
assert_contains "T22 ReceiveMessage nonexistent queue" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error"
|
||||||
|
|
||||||
|
# T23: DeleteQueue несуществующей очереди
|
||||||
|
R=$(sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}")
|
||||||
|
assert_contains "T23 DeleteQueue nonexistent" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error"
|
||||||
|
|
||||||
|
# T24: GetQueueAttributes несуществующей очереди
|
||||||
|
R=$(sqs_call "Action=GetQueueAttributes&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}&AttributeName.1=All")
|
||||||
|
assert_contains "T24 GetQueueAttributes nonexistent" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error"
|
||||||
|
|
||||||
|
# T25: DeleteMessage с фейковым ReceiptHandle
|
||||||
|
Q25="testq-rh-${TS}"
|
||||||
|
sqs_call "Action=CreateQueue&QueueName=${Q25}" >/dev/null
|
||||||
|
R=$(sqs_call "Action=DeleteMessage&QueueUrl=${SQS_BASE}/test001/${Q25}&ReceiptHandle=fake-receipt-handle-12345")
|
||||||
|
assert_contains "T25 DeleteMessage fake receipt" "$R" "Error\|ReceiptHandleIsInvalid\|DeleteMessageResponse"
|
||||||
|
# Примечание: ElasticMQ может молча игнорировать — это допустимо
|
||||||
|
cleanup_queue "$Q25"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 4: АУТЕНТИФИКАЦИЯ И БЕЗОПАСНОСТЬ
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 4: Аутентификация и безопасность ──"
|
||||||
|
|
||||||
|
# T26: Неправильный Access Key
|
||||||
|
R=$(sqs_call "Action=ListQueues" "WRONG-KEY:${SK}")
|
||||||
|
assert_contains "T26 Wrong access key" "$R" "InvalidClientTokenId\|SignatureDoesNotMatch\|Error\|403\|401\|Unauthorized"
|
||||||
|
|
||||||
|
# T27: Неправильный Secret Key
|
||||||
|
R=$(sqs_call "Action=ListQueues" "${AK}:WRONG-SECRET")
|
||||||
|
assert_contains "T27 Wrong secret key" "$R" "SignatureDoesNotMatch\|Error\|403\|401\|Unauthorized"
|
||||||
|
|
||||||
|
# T28: Пустые credentials
|
||||||
|
R=$(curl -s --max-time 15 "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" 2>&1)
|
||||||
|
assert_contains "T28 No credentials" "$R" "Error\|AuthFailure\|MissingAuthenticationToken\|ListQueuesResult\|403\|401"
|
||||||
|
# Примечание: ElasticMQ может не требовать auth — если так, это проблема безопасности
|
||||||
|
|
||||||
|
# T29: SQL injection в QueueName
|
||||||
|
R=$(sqs_call "Action=CreateQueue&QueueName=test';DROP%20TABLE%20queues;--")
|
||||||
|
assert_contains "T29 SQL injection in QueueName" "$R" "Error\|InvalidParameterValue\|CreateQueueResult"
|
||||||
|
# Если создалась — OK, ElasticMQ in-memory, нет SQL. Убираем.
|
||||||
|
cleanup_queue "test';DROP TABLE queues;--"
|
||||||
|
|
||||||
|
# T30: XSS в MessageBody
|
||||||
|
Q30="testq-xss-${TS}"
|
||||||
|
sqs_call "Action=CreateQueue&QueueName=${Q30}" >/dev/null
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q30}&MessageBody=%3Cscript%3Ealert(1)%3C/script%3E")
|
||||||
|
assert_contains "T30 XSS in MessageBody — accepted" "$R" "MessageId"
|
||||||
|
# SQS должен принять любой body — это не HTML, а очередь. Проверяем что возвращает как есть:
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q30}")
|
||||||
|
assert_contains "T30 XSS body returned" "$R" "script\|<script"
|
||||||
|
cleanup_queue "$Q30"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 5: СПЕЦСИМВОЛЫ И КОДИРОВКИ
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 5: Спецсимволы и кодировки ──"
|
||||||
|
|
||||||
|
Q5="testq-enc-${TS}"
|
||||||
|
sqs_call "Action=CreateQueue&QueueName=${Q5}" >/dev/null
|
||||||
|
|
||||||
|
# T31: Unicode в MessageBody
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82")
|
||||||
|
assert_contains "T31 Unicode body" "$R" "MessageId"
|
||||||
|
|
||||||
|
# T32: Receive Unicode
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q5}")
|
||||||
|
assert_contains "T32 Receive Unicode" "$R" "Привет\|%D0%9F"
|
||||||
|
|
||||||
|
# T33: Амперсанд и XML-спецсимволы в body
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=a%26b%3Cc%3Ed")
|
||||||
|
assert_contains "T33 XML special chars" "$R" "MessageId"
|
||||||
|
|
||||||
|
# T34: JSON в body
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=%7B%22key%22%3A%22value%22%7D")
|
||||||
|
assert_contains "T34 JSON body" "$R" "MessageId"
|
||||||
|
|
||||||
|
# T35: Пустой MessageBody
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=")
|
||||||
|
assert_contains "T35 Empty body" "$R" "Error\|MissingParameter\|MessageId"
|
||||||
|
|
||||||
|
cleanup_queue "$Q5"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 6: НАГРУЗКА И ПОРЯДОК
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 6: Множественные сообщения ──"
|
||||||
|
|
||||||
|
Q6="testq-multi-${TS}"
|
||||||
|
sqs_call "Action=CreateQueue&QueueName=${Q6}" >/dev/null
|
||||||
|
|
||||||
|
# T36: Отправить 10 сообщений подряд
|
||||||
|
ALL_OK=true
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q6}&MessageBody=msg-${i}")
|
||||||
|
if ! echo "$R" | grep -q "MessageId"; then
|
||||||
|
ALL_OK=false
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
if $ALL_OK; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: T36 Send 10 messages"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " ❌ FAIL: T36 Send 10 messages"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# T37: Receive все 10 (может потребовать несколько вызовов)
|
||||||
|
RECEIVED=0
|
||||||
|
for _ in $(seq 1 15); do
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q6}&MaxNumberOfMessages=10")
|
||||||
|
COUNT=$(echo "$R" | grep -c "<Message>" || true)
|
||||||
|
RECEIVED=$((RECEIVED + COUNT))
|
||||||
|
if [ "$RECEIVED" -ge 10 ]; then break; fi
|
||||||
|
done
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
if [ "$RECEIVED" -ge 10 ]; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: T37 Receive all 10 messages (got ${RECEIVED})"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " ❌ FAIL: T37 Receive all 10 messages (got ${RECEIVED}/10)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_queue "$Q6"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 7: HTTP-УРОВЕНЬ
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 7: HTTP-уровень ──"
|
||||||
|
|
||||||
|
# T38: Проверка HTTPS
|
||||||
|
assert_http_code "T38 HTTPS endpoint" "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" "200"
|
||||||
|
|
||||||
|
# T39: Health endpoint
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
HC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${SQS_BASE%/sqs/test001}/sqs/test001/health" 2>&1 || echo "000")
|
||||||
|
# Примечание: health может быть на другом пути
|
||||||
|
if [ "$HC" = "200" ] || [ "$HC" = "404" ]; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: T39 Health endpoint (HTTP $HC)"
|
||||||
|
else
|
||||||
|
SKIP=$((SKIP + 1))
|
||||||
|
echo " ⚠️ SKIP: T39 Health endpoint (HTTP $HC — может быть не на этом пути)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# T40: Очень большой запрос (64KB body)
|
||||||
|
Q40="testq-big-${TS}"
|
||||||
|
sqs_call "Action=CreateQueue&QueueName=${Q40}" >/dev/null
|
||||||
|
BIGBODY=$(python3 -c "print('A'*65000)" 2>/dev/null || printf 'A%.0s' $(seq 1 65000))
|
||||||
|
R=$(curl -s --max-time 15 -u "$AUTH" -X POST "${SQS_BASE}?Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q40}&Version=${VERSION}" --data-urlencode "MessageBody=${BIGBODY}" 2>&1)
|
||||||
|
assert_contains "T40 Large body 64KB" "$R" "MessageId\|Error\|TooLong\|InvalidParameterValue"
|
||||||
|
cleanup_queue "$Q40"
|
||||||
|
|
||||||
|
# T41: POST вместо GET
|
||||||
|
R=$(curl -s --max-time 15 -u "$AUTH" -X POST "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" 2>&1)
|
||||||
|
assert_contains "T41 POST method" "$R" "ListQueuesResult\|ListQueuesResponse"
|
||||||
|
|
||||||
|
# T42: Несуществующий путь
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
HC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -u "$AUTH" "${SQS_HOST}/sqs/nonexistent-tenant?Action=ListQueues&Version=${VERSION}" 2>&1)
|
||||||
|
if [ "$HC" = "404" ] || [ "$HC" = "403" ] || [ "$HC" = "502" ] || [ "$HC" = "503" ] || [ "$HC" = "302" ]; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: T42 Wrong tenant path (HTTP $HC)"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " ❌ FAIL: T42 Wrong tenant path (HTTP $HC — ожидался 302/404/403)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 8: ТАЙМАУТЫ И LONG POLLING
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 8: Long polling и таймауты ──"
|
||||||
|
|
||||||
|
Q8="testq-poll-${TS}"
|
||||||
|
sqs_call "Action=CreateQueue&QueueName=${Q8}" >/dev/null
|
||||||
|
|
||||||
|
# T43: WaitTimeSeconds=1 на пустой очереди — должен вернуть пустой через ~1 сек
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
T_START=$(date +%s)
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q8}&WaitTimeSeconds=1")
|
||||||
|
T_END=$(date +%s)
|
||||||
|
ELAPSED=$((T_END - T_START))
|
||||||
|
if [ "$ELAPSED" -ge 1 ] && [ "$ELAPSED" -le 5 ]; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: T43 Long poll 1s empty queue (${ELAPSED}s)"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " ❌ FAIL: T43 Long poll 1s (${ELAPSED}s — ожидалось 1-5s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# T44: WaitTimeSeconds=0 — мгновенный ответ
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
T_START=$(date +%s)
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q8}&WaitTimeSeconds=0")
|
||||||
|
T_END=$(date +%s)
|
||||||
|
ELAPSED=$((T_END - T_START))
|
||||||
|
if [ "$ELAPSED" -le 2 ]; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
echo " ✅ PASS: T44 Short poll instant response (${ELAPSED}s)"
|
||||||
|
else
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
echo " ❌ FAIL: T44 Short poll (${ELAPSED}s — ожидалось <2s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_queue "$Q8"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# ГРУППА 9: VISIBILITY TIMEOUT
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "── ГРУППА 9: Visibility timeout ──"
|
||||||
|
|
||||||
|
Q9="testq-vis-${TS}"
|
||||||
|
sqs_call "Action=CreateQueue&QueueName=${Q9}" >/dev/null
|
||||||
|
sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&MessageBody=VisTest" >/dev/null
|
||||||
|
|
||||||
|
# T45: Receive с VisibilityTimeout=2 — сообщение скрыто, повторный receive пуст
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&VisibilityTimeout=2")
|
||||||
|
assert_contains "T45a Receive with visibility" "$R" "VisTest"
|
||||||
|
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&WaitTimeSeconds=0")
|
||||||
|
assert_not_contains "T45b Hidden during visibility" "$R" "VisTest"
|
||||||
|
|
||||||
|
# T46: После visibility timeout — сообщение снова доступно
|
||||||
|
sleep 3
|
||||||
|
R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&WaitTimeSeconds=0")
|
||||||
|
assert_contains "T46 Re-visible after timeout" "$R" "VisTest"
|
||||||
|
|
||||||
|
cleanup_queue "$Q9"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
# РЕЗУЛЬТАТЫ
|
||||||
|
# ═══════════════════════════════════════════
|
||||||
|
echo "╔══════════════════════════════════════════════════╗"
|
||||||
|
echo "║ РЕЗУЛЬТАТЫ ║"
|
||||||
|
echo "╠══════════════════════════════════════════════════╣"
|
||||||
|
echo "║ Всего: ${TOTAL} ║"
|
||||||
|
echo "║ ✅ PASS: ${PASS} ║"
|
||||||
|
echo "║ ❌ FAIL: ${FAIL} ║"
|
||||||
|
echo "║ ⚠️ SKIP: ${SKIP} ║"
|
||||||
|
echo "╚══════════════════════════════════════════════════╝"
|
||||||
|
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
Executable
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 2026-04-09
|
||||||
|
# Нагрузочный тест Harbor: параллельные curl-запросы, замер латентности,
|
||||||
|
# фиксация зависаний по таймауту. Позволяет воспроизвести и измерить
|
||||||
|
# intermittent-проблему с HTTP-протоколом на конкретном хосте.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# ── Конфигурация ─────────────────────────────────────────────────────────────
|
||||||
|
SECRETS_FILE="${SECRETS_FILE:-secrets/pearlharbor_registry.txt}"
|
||||||
|
DURATION=${DURATION:-60} # секунд
|
||||||
|
CONCURRENCY=${CONCURRENCY:-10} # параллельных воркеров
|
||||||
|
TIMEOUT=${TIMEOUT:-8} # таймаут одного запроса (сек)
|
||||||
|
LOGDIR="${LOGDIR:-/tmp/harbor_load_$(date +%Y%m%d_%H%M%S)}"
|
||||||
|
|
||||||
|
# ── Читаем секреты ────────────────────────────────────────────────────────────
|
||||||
|
if [ ! -f "$SECRETS_FILE" ]; then
|
||||||
|
echo "ERROR: secrets file not found: $SECRETS_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
connection_url=$(grep -E '^connection_url=' "$SECRETS_FILE" | cut -d'=' -f2-)
|
||||||
|
admin_pass=$(grep -E '^admin_pass=' "$SECRETS_FILE" | cut -d'=' -f2-)
|
||||||
|
REGISTRY=$(echo "$connection_url" | sed -E 's~https?://~~' | sed -E 's~/$~~')
|
||||||
|
BASE_URL="https://$REGISTRY"
|
||||||
|
|
||||||
|
# ── Точки нагрузки (URL + принудительный протокол) ───────────────────────────
|
||||||
|
# Цель: нагрузить Harbor разными типами запросов и обоими протоколами,
|
||||||
|
# чтобы поймать и зафиксировать intermittent-зависания.
|
||||||
|
declare -a TARGETS=(
|
||||||
|
"h2 $BASE_URL/api/v2.0/ping"
|
||||||
|
"h1 $BASE_URL/api/v2.0/ping"
|
||||||
|
"h2 $BASE_URL/api/v2.0/projects"
|
||||||
|
"h1 $BASE_URL/api/v2.0/projects"
|
||||||
|
"h2 $BASE_URL/v2/"
|
||||||
|
"h1 $BASE_URL/v2/"
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$LOGDIR"
|
||||||
|
RESULT_LOG="$LOGDIR/results.csv"
|
||||||
|
echo "ts,worker,proto,url,http_code,time_total,timeout" > "$RESULT_LOG"
|
||||||
|
|
||||||
|
echo "=== Harbor Load Test ==="
|
||||||
|
echo "Registry : $BASE_URL"
|
||||||
|
echo "Duration : ${DURATION}s"
|
||||||
|
echo "Workers : $CONCURRENCY"
|
||||||
|
echo "Timeout : ${TIMEOUT}s per request"
|
||||||
|
echo "Log : $LOGDIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
END_TIME=$(( $(date +%s) + DURATION ))
|
||||||
|
|
||||||
|
# ── Один воркер ──────────────────────────────────────────────────────────────
|
||||||
|
harbor_worker() {
|
||||||
|
local wid="$1"
|
||||||
|
local target_idx=$(( wid % ${#TARGETS[@]} ))
|
||||||
|
local entry="${TARGETS[$target_idx]}"
|
||||||
|
local proto=$(echo "$entry" | awk '{print $1}')
|
||||||
|
local url=$(echo "$entry" | awk '{print $2}')
|
||||||
|
|
||||||
|
local proto_flag=""
|
||||||
|
if [ "$proto" = "h1" ]; then
|
||||||
|
proto_flag="--http1.1"
|
||||||
|
else
|
||||||
|
proto_flag="--http2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [ "$(date +%s)" -lt "$END_TIME" ]; do
|
||||||
|
local ts
|
||||||
|
ts=$(date +%s%3N)
|
||||||
|
# Замеряем время ответа и HTTP-код
|
||||||
|
local result
|
||||||
|
result=$(curl -sk $proto_flag \
|
||||||
|
--max-time "$TIMEOUT" \
|
||||||
|
-u "admin:$admin_pass" \
|
||||||
|
-o /dev/null \
|
||||||
|
-w "%{http_code} %{time_total}" \
|
||||||
|
"$url" 2>/dev/null) || result="000 $TIMEOUT"
|
||||||
|
|
||||||
|
local http_code time_total timed_out
|
||||||
|
http_code=$(echo "$result" | awk '{print $1}')
|
||||||
|
time_total=$(echo "$result" | awk '{print $2}')
|
||||||
|
|
||||||
|
# Считаем тайм-аут если http_code=000 или время близко к лимиту
|
||||||
|
if [ "$http_code" = "000" ]; then
|
||||||
|
timed_out=1
|
||||||
|
else
|
||||||
|
timed_out=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$ts,$wid,$proto,$url,$http_code,$time_total,$timed_out" >> "$RESULT_LOG"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
export -f harbor_worker
|
||||||
|
export END_TIME TIMEOUT LOGDIR RESULT_LOG admin_pass
|
||||||
|
export -a TARGETS
|
||||||
|
|
||||||
|
# ── Запускаем воркеров параллельно ───────────────────────────────────────────
|
||||||
|
echo "Starting $CONCURRENCY workers for ${DURATION}s..."
|
||||||
|
PIDS=()
|
||||||
|
for i in $(seq 0 $(( CONCURRENCY - 1 ))); do
|
||||||
|
harbor_worker "$i" &
|
||||||
|
PIDS+=($!)
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Прогресс каждые 5 секунд ─────────────────────────────────────────────────
|
||||||
|
while [ "$(date +%s)" -lt "$END_TIME" ]; do
|
||||||
|
sleep 5
|
||||||
|
local_count=$(wc -l < "$RESULT_LOG")
|
||||||
|
local_timeouts=$(grep -c ",1$" "$RESULT_LOG" 2>/dev/null || echo 0)
|
||||||
|
elapsed=$(( DURATION - ( END_TIME - $(date +%s) ) ))
|
||||||
|
echo "[+${elapsed}s] requests: $(( local_count - 1 )), timeouts: $local_timeouts"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Ждём завершения воркеров ─────────────────────────────────────────────────
|
||||||
|
for pid in "${PIDS[@]}"; do
|
||||||
|
wait "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Results ==="
|
||||||
|
|
||||||
|
# ── Статистика через python3 ─────────────────────────────────────────────────
|
||||||
|
python3 - "$RESULT_LOG" << 'PYEOF'
|
||||||
|
import sys, csv
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
log_file = sys.argv[1]
|
||||||
|
rows = []
|
||||||
|
with open(log_file) as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for r in reader:
|
||||||
|
rows.append(r)
|
||||||
|
|
||||||
|
total = len(rows)
|
||||||
|
timeouts = sum(1 for r in rows if r['timeout'] == '1')
|
||||||
|
success = sum(1 for r in rows if r['timeout'] == '0' and r['http_code'].startswith('2'))
|
||||||
|
|
||||||
|
times = [float(r['time_total']) for r in rows if r['timeout'] == '0']
|
||||||
|
times.sort()
|
||||||
|
|
||||||
|
def pct(lst, p):
|
||||||
|
if not lst: return 0
|
||||||
|
idx = int(len(lst) * p / 100)
|
||||||
|
return lst[min(idx, len(lst)-1)]
|
||||||
|
|
||||||
|
print(f"Total requests : {total}")
|
||||||
|
print(f"Success (2xx) : {success} ({100*success//total if total else 0}%)")
|
||||||
|
print(f"Timeouts : {timeouts} ({100*timeouts//total if total else 0}%)")
|
||||||
|
other = total - success - timeouts
|
||||||
|
print(f"Other errors : {other}")
|
||||||
|
if times:
|
||||||
|
print(f"Latency (ok) : min={min(times):.3f}s median={pct(times,50):.3f}s p95={pct(times,95):.3f}s max={max(times):.3f}s")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("--- By protocol ---")
|
||||||
|
by_proto = defaultdict(lambda: {'ok':0,'fail':0,'times':[]})
|
||||||
|
for r in rows:
|
||||||
|
p = r['proto']
|
||||||
|
if r['timeout'] == '1':
|
||||||
|
by_proto[p]['fail'] += 1
|
||||||
|
elif r['http_code'].startswith('2'):
|
||||||
|
by_proto[p]['ok'] += 1
|
||||||
|
by_proto[p]['times'].append(float(r['time_total']))
|
||||||
|
else:
|
||||||
|
by_proto[p]['fail'] += 1
|
||||||
|
|
||||||
|
for proto, d in sorted(by_proto.items()):
|
||||||
|
t = sorted(d['times'])
|
||||||
|
p95 = pct(t, 95) if t else 0
|
||||||
|
print(f" {proto}: ok={d['ok']} fail={d['fail']} p95={p95:.3f}s")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("--- By URL ---")
|
||||||
|
by_url = defaultdict(lambda: {'ok':0,'to':0})
|
||||||
|
for r in rows:
|
||||||
|
u = r['url']
|
||||||
|
if r['timeout'] == '1':
|
||||||
|
by_url[u]['to'] += 1
|
||||||
|
elif r['http_code'].startswith('2'):
|
||||||
|
by_url[u]['ok'] += 1
|
||||||
|
# else: counted as non-2xx
|
||||||
|
|
||||||
|
for url, d in sorted(by_url.items(), key=lambda x: x[0]):
|
||||||
|
print(f" {url}: ok={d['ok']} timeout={d['to']}")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Raw log: $RESULT_LOG"
|
||||||
Reference in New Issue
Block a user