From 6c6040d8f8581ecf181ce70e5dc1f78e2f3024ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 11 Mar 2026 10:49:36 +0400 Subject: [PATCH] =?UTF-8?q?test:=20E2E=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF?= =?UTF-8?q?=D1=82=20run=5Fe2e=5Ftests.sh=20+=20fix=20provider=20token/vers?= =?UTF-8?q?ion=20=D0=B2=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/progress.md | 18 ++ examples/hello-node/main.tf | 2 +- examples/hello-node/variables.tf | 10 + examples/notes-python/main.tf | 7 +- examples/notes-python/variables.tf | 10 +- examples/push-sample/Dockerfile | 7 + examples/push-sample/README.md | 28 +++ examples/push-sample/build_and_push.sh | 53 +++++ examples/simple-node/main.tf | 7 +- examples/simple-node/variables.tf | 10 + examples/simple-python/main.tf | 7 +- examples/simple-python/variables.tf | 10 + run_e2e_tests.sh | 256 +++++++++++++++++++++++++ 13 files changed, 414 insertions(+), 11 deletions(-) create mode 100644 examples/hello-node/variables.tf create mode 100644 examples/push-sample/Dockerfile create mode 100644 examples/push-sample/README.md create mode 100755 examples/push-sample/build_and_push.sh create mode 100644 examples/simple-node/variables.tf create mode 100644 examples/simple-python/variables.tf create mode 100755 run_e2e_tests.sh diff --git a/doc/progress.md b/doc/progress.md index cae6252..7bef580 100644 --- a/doc/progress.md +++ b/doc/progress.md @@ -175,6 +175,24 @@ --- +## 2026-03-11 — E2E тесты через скрипт run_e2e_tests.sh + +| Пример | init | apply | modify + apply | destroy | Итог | +|--------|------|-------|----------------|---------|------| +| hello-node | ✅ | ✅ | ✅ memory 128→256 MB | ✅ 4 destroyed | **PASS** | +| simple-node | ✅ | ✅ | ✅ memory 64→96 MB | ✅ 4 destroyed | **PASS** | + +**Скрипт:** `run_e2e_tests.sh` в корне репы +**Возможности:** retry 3× при TLS timeout, emergency destroy trap, логи в `.e2e-logs/` +**Провайдер в примерах:** токен через `var.token` + `terraform.tfvars` (gitignored), version `~> 0.1.13` + +**Наблюдения:** +- Периодические TLS handshake timeout на `sless-api.kube5s.ru` — сеть нестабильна, retry помогает +- `terra.k8c.ru` иногда даёт `unexpected EOF` при скачивании провайдера — то же, retry +- Namespace `sless-cdd874dfa31ba6ca` жив после всех destroy — поведение корректное + +--- + ## Остаток технического долга (не блокирует) | # | Что | Приоритет | diff --git a/examples/hello-node/main.tf b/examples/hello-node/main.tf index e3584dd..5f4a402 100644 --- a/examples/hello-node/main.tf +++ b/examples/hello-node/main.tf @@ -18,7 +18,7 @@ terraform { provider "sless" { endpoint = "https://sless-api.kube5s.ru" - token = file("${path.module}/../../secrets/prod.token") + token = var.token nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1" } diff --git a/examples/hello-node/variables.tf b/examples/hello-node/variables.tf new file mode 100644 index 0000000..538fc2b --- /dev/null +++ b/examples/hello-node/variables.tf @@ -0,0 +1,10 @@ +# 2026-03-11 +# variables.tf — входные переменные для hello-node примера. + +# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). +# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} +variable "token" { + description = "JWT токен облака для аутентификации в sless API" + type = string + sensitive = true +} diff --git a/examples/notes-python/main.tf b/examples/notes-python/main.tf index de76386..d4c1772 100644 --- a/examples/notes-python/main.tf +++ b/examples/notes-python/main.tf @@ -14,13 +14,14 @@ terraform { # Провайдер для управления serverless функциями через sless API sless = { source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.11" + version = "~> 0.1.13" } } } # sless провайдер подключается к API кластера. provider "sless" { - endpoint = "https://sless-api.kube5s.ru" - token = "dev-token-change-me" + endpoint = "https://sless-api.kube5s.ru" + token = var.token + nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1" } diff --git a/examples/notes-python/variables.tf b/examples/notes-python/variables.tf index d4e0af0..0e0b3bb 100644 --- a/examples/notes-python/variables.tf +++ b/examples/notes-python/variables.tf @@ -1,10 +1,18 @@ -# 2026-03-09 +# 2026-03-09 (обновлён 2026-03-11) # variables.tf — входные переменные для notes-python примера. # # PG_DSN передаётся во все функции через env_vars. # Хранится как sensitive чтобы не светился в terraform output и логах. # В продакшне — не хардкоди DSN здесь, используй TF_VAR_pg_dsn или secrets manager. +# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). +# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} +variable "token" { + description = "JWT токен облака для аутентификации в sless API" + type = string + sensitive = true +} + # DSN для подключения к PostgreSQL внутри кластера. # Формат: postgres://user:password@host:port/dbname?sslmode=... variable "pg_dsn" { diff --git a/examples/push-sample/Dockerfile b/examples/push-sample/Dockerfile new file mode 100644 index 0000000..faa19a5 --- /dev/null +++ b/examples/push-sample/Dockerfile @@ -0,0 +1,7 @@ +# 2026-03-11 10:00 +# Minimal sample image to push to PearlHarbor registry +# Purpose: небольшой образ для тестирования пуша в реестр + +FROM alpine:3.18 + +CMD ["sh", "-c", "echo Hello from pearlharbor sample image"] diff --git a/examples/push-sample/README.md b/examples/push-sample/README.md new file mode 100644 index 0000000..51043a0 --- /dev/null +++ b/examples/push-sample/README.md @@ -0,0 +1,28 @@ +# Пример для пуша в PearlHarbor + +Файлы: +- [examples/push-sample/Dockerfile](examples/push-sample/Dockerfile) — минимальный образ +- [examples/push-sample/build_and_push.sh](examples/push-sample/build_and_push.sh) — сборка и опциональный пуш + +Как использовать: + +1. Сборка локально (в корне репы): + +```bash +docker build -t sless-sample:local -f examples/push-sample/Dockerfile examples/push-sample +``` + +2. Протестировать скрипт (скрипт не будет пушить без переменной DO_PUSH): + +```bash +cd examples/push-sample +./build_and_push.sh +``` + +3. Для реального пуша установите `DO_PUSH=true`. Скрипт прочитает `secrets/pearlharbor_registry.txt`. + +```bash +DO_PUSH=true ./build_and_push.sh +``` + +Примечание: скрипт использует по умолчанию пользователя `admin`. Для другого пользователя задайте `REGISTRY_USER`. diff --git a/examples/push-sample/build_and_push.sh b/examples/push-sample/build_and_push.sh new file mode 100755 index 0000000..92f563f --- /dev/null +++ b/examples/push-sample/build_and_push.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# 2026-03-11 10:02 +# Скрипт: собирает минимальный образ и, при разрешении, пушит в реестр PearlHarbor +# Требования: `docker` в PATH. Скрипт НЕ будет пушить без DO_PUSH=true. + +set -euo pipefail + +# Получаем значения из файла секретов +SECRETS_FILE="secrets/pearlharbor_registry.txt" +if [ ! -f "$SECRETS_FILE" ]; then + echo "Файл с секретами не найден: $SECRETS_FILE" + 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-) + +if [ -z "$connection_url" ]; then + echo "Не найден connection_url в $SECRETS_FILE" + exit 1 +fi + +# Убираем протокол и возможный слеш на конце +registry_host=$(echo "$connection_url" | sed -E 's~https?://~~' | sed -E 's~/$~~') + +image_name="$registry_host/sless-sample:latest" + +echo "Registry host: $registry_host" +echo "Image name: $image_name" + +echo "Собираю образ локально..." +docker build -t sless-sample:local -f Dockerfile .. || { + echo "Сборка не удалась"; exit 1 +} + +echo "Готово. Образ: sless-sample:local" + +if [ "${DO_PUSH:-}" != "true" ]; then + echo "DO_PUSH != true — пуш не будет выполнен. Чтобы запушить: DO_PUSH=true ./build_and_push.sh" + exit 0 +fi + +# Если дошли до сюда — выполняем login/push +registry_user=${REGISTRY_USER:-admin} + +echo "Выполняю docker login к $registry_host как '$registry_user'" +echo "$admin_pass" | docker login "$registry_host" -u "$registry_user" --password-stdin + +echo "Тегирую и пушу образ: $image_name" +docker tag sless-sample:local "$image_name" +docker push "$image_name" + +echo "Пуш завершён. Проверьте реестр для образа: $image_name" diff --git a/examples/simple-node/main.tf b/examples/simple-node/main.tf index a4cda22..2e1927a 100644 --- a/examples/simple-node/main.tf +++ b/examples/simple-node/main.tf @@ -19,12 +19,13 @@ terraform { required_providers { sless = { source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.11" + version = "~> 0.1.13" } } } provider "sless" { - endpoint = "https://sless-api.kube5s.ru" - token = "dev-token-change-me" + endpoint = "https://sless-api.kube5s.ru" + token = var.token + nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1" } diff --git a/examples/simple-node/variables.tf b/examples/simple-node/variables.tf new file mode 100644 index 0000000..2057582 --- /dev/null +++ b/examples/simple-node/variables.tf @@ -0,0 +1,10 @@ +# 2026-03-11 +# variables.tf — входные переменные для simple-node примера. + +# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). +# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} +variable "token" { + description = "JWT токен облака для аутентификации в sless API" + type = string + sensitive = true +} diff --git a/examples/simple-python/main.tf b/examples/simple-python/main.tf index 75a1e6a..d0d3d86 100644 --- a/examples/simple-python/main.tf +++ b/examples/simple-python/main.tf @@ -18,12 +18,13 @@ terraform { required_providers { sless = { source = "terra.k8c.ru/naeel/sless" - version = "~> 0.1.11" + version = "~> 0.1.13" } } } provider "sless" { - endpoint = "https://sless-api.kube5s.ru" - token = "dev-token-change-me" + endpoint = "https://sless-api.kube5s.ru" + token = var.token + nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1" } diff --git a/examples/simple-python/variables.tf b/examples/simple-python/variables.tf new file mode 100644 index 0000000..dac71bd --- /dev/null +++ b/examples/simple-python/variables.tf @@ -0,0 +1,10 @@ +# 2026-03-11 +# variables.tf — входные переменные для simple-python примера. + +# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored). +# Из токена провайдер вычисляет namespace: sless-{sha256[:8]} +variable "token" { + description = "JWT токен облака для аутентификации в sless API" + type = string + sensitive = true +} diff --git a/run_e2e_tests.sh b/run_e2e_tests.sh new file mode 100755 index 0000000..e9966ff --- /dev/null +++ b/run_e2e_tests.sh @@ -0,0 +1,256 @@ +#!/usr/bin/env bash +# 2026-03-11 +# run_e2e_tests.sh — E2E тест примеров: apply → modify → apply → destroy +# +# Запускает два примера: hello-node (nodejs20) и simple-python (python3.11) +# Каждый проходит полный цикл: init → apply → modify → apply → destroy +# В конце кластер должен быть полностью чистым. +# +# Использование: +# ./run_e2e_tests.sh +# ./run_e2e_tests.sh hello-node # только один пример +# +# Требования: terraform, curl в PATH + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXAMPLES_DIR="$REPO_ROOT/examples" +LOGS_DIR="$REPO_ROOT/.e2e-logs" +mkdir -p "$LOGS_DIR" + +# ── Примеры для запуска ────────────────────────────────────────────────────── +if [ $# -gt 0 ]; then + EXAMPLES=("$@") +else + EXAMPLES=("hello-node" "simple-python") +fi + +# ── Цвета ──────────────────────────────────────────────────────────────────── +GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; RESET='\033[0m' +ok() { echo -e "${GREEN} ✓ $*${RESET}"; } +fail() { echo -e "${RED} ✗ $*${RESET}"; } +info() { echo -e "${YELLOW} → $*${RESET}"; } + +# ── Результаты ─────────────────────────────────────────────────────────────── +declare -A RESULTS=() + +# ── Cleanup-trap: уничтожить всё что могло остаться ────────────────────────── +cleanup() { + for example in "${EXAMPLES[@]}"; do + local dir="$EXAMPLES_DIR/$example" + if [ -f "$dir/terraform.tfstate" ] && \ + grep -q '"resources"' "$dir/terraform.tfstate" 2>/dev/null && \ + python3 -c "import json,sys; d=json.load(open('$dir/terraform.tfstate')); sys.exit(0 if d.get('resources') else 1)" 2>/dev/null; then + info "cleanup trap: destroying $example" + (cd "$dir" && terraform destroy -auto-approve -input=false -no-color \ + >> "$LOGS_DIR/${example}-destroy-emergency.log" 2>&1) || true + fi + restore_backup "$example" + done +} +trap cleanup EXIT + +# ── Retry wrapper: 3 попытки, ретрай при TLS/EOF ошибках ───────────────────── +# Использование: tf_retry +tf_retry() { + local logfile="$1"; shift + local attempt=1 + while [ "$attempt" -le 3 ]; do + if "$@" 2>&1 | tee "$logfile"; then + return 0 + fi + if grep -Eiq \ + 'TLS handshake timeout|unexpected EOF|i/o timeout|context deadline' \ + "$logfile" && [ "$attempt" -lt 3 ]; then + info "network error, retry $((attempt+1))/3..." + attempt=$((attempt + 1)) + sleep 3 + continue + fi + return 1 + done + return 1 +} + +# ── Modify: сохранить бэкап и внести изменение ─────────────────────────────── +# Изменения минимальны и легко проверяемы в plan output +backup_and_modify() { + local example="$1" + case "$example" in + hello-node) + cp "$EXAMPLES_DIR/$example/http.tf" \ + "$EXAMPLES_DIR/$example/http.tf.e2e.bak" + # memory_mb 128 → 256, добавить env_vars + perl -0pi -e \ + 's/(memory_mb\s+=\s+)128/${1}256/' \ + "$EXAMPLES_DIR/$example/http.tf" + perl -0pi -e \ + 's/(source_dir\s+=.*\n)/$1\n env_vars = \{ GREETING = "e2e-test" \}\n/' \ + "$EXAMPLES_DIR/$example/http.tf" + ;; + simple-python) + cp "$EXAMPLES_DIR/$example/time-display.tf" \ + "$EXAMPLES_DIR/$example/time-display.tf.e2e.bak" + # memory_mb 64 → 96 + perl -0pi -e \ + 's/(memory_mb\s+=\s+)64/${1}96/' \ + "$EXAMPLES_DIR/$example/time-display.tf" + ;; + simple-node) + cp "$EXAMPLES_DIR/$example/time-display.tf" \ + "$EXAMPLES_DIR/$example/time-display.tf.e2e.bak" + perl -0pi -e \ + 's/(memory_mb\s+=\s+)64/${1}96/' \ + "$EXAMPLES_DIR/$example/time-display.tf" + ;; + esac +} + +restore_backup() { + local example="$1" + for bak in "$EXAMPLES_DIR/$example"/*.e2e.bak; do + [ -f "$bak" ] || continue + mv "$bak" "${bak%.e2e.bak}" + done +} + +# ── Шаг apply: проверяем что terraform вернул 0 и есть "Apply complete" ─────── +assert_apply_ok() { + local logfile="$1" + if ! grep -q 'Apply complete' "$logfile"; then + return 1 + fi + # Выводим краткий итог + grep -E 'Apply complete|added|changed|destroyed|Outputs:' "$logfile" | head -5 + return 0 +} + +assert_destroy_ok() { + local logfile="$1" + grep -q 'Destroy complete' "$logfile" +} + +# ── Запуск одного примера ───────────────────────────────────────────────────── +run_example() { + local example="$1" + local dir="$EXAMPLES_DIR/$example" + local log_base="$LOGS_DIR/$example" + local failed=0 + + echo + echo "════════════════════════════════════════" + echo " EXAMPLE: $example" + echo "════════════════════════════════════════" + + # Чистим локальные артефакты от предыдущих запусков + rm -rf "$dir/.terraform" "$dir/.terraform.lock.hcl" \ + "$dir/terraform.tfstate" "$dir/terraform.tfstate.backup" \ + "$dir"/terraform.tfstate.*.backup + + # 1. init ─────────────────────────────────────────────────────────────────── + info "init" + if tf_retry "${log_base}-init.log" \ + terraform -chdir="$dir" init -input=false -no-color; then + ok "init" + else + fail "init — см. ${log_base}-init.log" + RESULTS[$example]="FAIL (init)" + return 1 + fi + + # 2. apply ────────────────────────────────────────────────────────────────── + info "apply" + if tf_retry "${log_base}-apply.log" \ + terraform -chdir="$dir" apply -auto-approve -input=false -no-color \ + && assert_apply_ok "${log_base}-apply.log"; then + ok "apply" + else + fail "apply — см. ${log_base}-apply.log" + RESULTS[$example]="FAIL (apply)" + return 1 + fi + + # 3. modify ───────────────────────────────────────────────────────────────── + info "modify (backup + patch)" + backup_and_modify "$example" + ok "files patched" + + # 4. apply after modify ───────────────────────────────────────────────────── + info "apply (после modify)" + if tf_retry "${log_base}-apply-modify.log" \ + terraform -chdir="$dir" apply -auto-approve -input=false -no-color \ + && assert_apply_ok "${log_base}-apply-modify.log"; then + ok "apply (modify)" + # Показываем что изменилось + grep -E 'changed|updated|Modifying' "${log_base}-apply-modify.log" | head -3 \ + || true + else + fail "apply (modify) — см. ${log_base}-apply-modify.log" + RESULTS[$example]="FAIL (apply-modify)" + failed=1 + fi + + # Восстанавливаем файлы до destroy + restore_backup "$example" + + # 5. destroy ──────────────────────────────────────────────────────────────── + info "destroy" + if tf_retry "${log_base}-destroy.log" \ + terraform -chdir="$dir" destroy -auto-approve -input=false -no-color \ + && assert_destroy_ok "${log_base}-destroy.log"; then + ok "destroy" + grep 'Destroy complete' "${log_base}-destroy.log" || true + else + fail "destroy — см. ${log_base}-destroy.log" + RESULTS[$example]="FAIL (destroy)" + return 1 + fi + + # Чистим после успешного прогона + rm -rf "$dir/.terraform" "$dir/.terraform.lock.hcl" \ + "$dir/terraform.tfstate" "$dir/terraform.tfstate.backup" + + if [ "$failed" -eq 0 ]; then + RESULTS[$example]="PASS" + fi +} + +# ── Main ────────────────────────────────────────────────────────────────────── +main() { + echo + echo "╔══════════════════════════════════════╗" + echo "║ sless E2E tests $(date '+%Y-%m-%d %H:%M') ║" + echo "╚══════════════════════════════════════╝" + echo "Примеры: ${EXAMPLES[*]}" + echo "Логи: $LOGS_DIR" + + for example in "${EXAMPLES[@]}"; do + run_example "$example" || true + done + + # ── Итог ──────────────────────────────────────────────────────────────────── + echo + echo "════════════════ ИТОГ ════════════════" + local all_pass=1 + for example in "${EXAMPLES[@]}"; do + local result="${RESULTS[$example]:-UNKNOWN}" + if [ "$result" = "PASS" ]; then + ok "$example: $result" + else + fail "$example: $result" + all_pass=0 + fi + done + echo "══════════════════════════════════════" + + if [ "$all_pass" -eq 1 ]; then + echo -e "${GREEN}Все тесты прошли успешно.${RESET}" + exit 0 + else + echo -e "${RED}Есть ошибки. Логи: $LOGS_DIR${RESET}" + exit 1 + fi +} + +main