From 64bd495cf9825047ee38a1f478b3b12e8f460690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Wed, 11 Mar 2026 16:33:26 +0400 Subject: [PATCH] =?UTF-8?q?chore:=20=D1=83=D0=B4=D0=B0=D0=BB=D1=91=D0=BD?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B4=D0=BE=D1=87=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20push-sample=20(Harbor=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=BA=D0=BE=D0=BD=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/DESTROY_ROUTE_CLEANUP_BUG.md | 143 ----------- examples/push-sample/Dockerfile | 7 - examples/push-sample/README.md | 28 --- examples/push-sample/build_and_push.sh | 53 ---- examples/run_terraform_examples.sh | 333 ------------------------- 5 files changed, 564 deletions(-) delete mode 100644 examples/DESTROY_ROUTE_CLEANUP_BUG.md delete mode 100644 examples/push-sample/Dockerfile delete mode 100644 examples/push-sample/README.md delete mode 100755 examples/push-sample/build_and_push.sh delete mode 100755 examples/run_terraform_examples.sh diff --git a/examples/DESTROY_ROUTE_CLEANUP_BUG.md b/examples/DESTROY_ROUTE_CLEANUP_BUG.md deleted file mode 100644 index 4dba243..0000000 --- a/examples/DESTROY_ROUTE_CLEANUP_BUG.md +++ /dev/null @@ -1,143 +0,0 @@ -# Bug Report: HTTP route is not removed after Terraform destroy - -## Summary - -При удалении примера `hello-node` через Terraform команда `terraform destroy` завершается успешно, но публичный HTTP endpoint не удаляется. - -Фактическое поведение после `destroy` такое: - -1. Сразу после удаления endpoint ещё некоторое время отвечает `HTTP 200` и возвращает корректный ответ функции. -2. Затем backend функции действительно исчезает, но публичный маршрут остаётся опубликованным и начинает отвечать `HTTP 502 function unreachable`. -3. Даже через 120 секунд endpoint не исчезает. - -Это выглядит как баг cleanup в platform/backend/provider lifecycle для HTTP trigger/route. - -## Affected Example - -- Example: `hello-node` -- Terraform files: `hello-node/main.tf`, `hello-node/http.tf`, `hello-node/job.tf` -- Public URL: `https://sless-api.kube5s.ru/fn/default/hello-http` -- Function name: `hello-http` -- Trigger name: `hello-http-trigger` - -## Reproduction - -Использовался репозиторий examples и скрипт: - -- Script: `./run_terraform_examples.sh` - -Шаги воспроизведения: - -1. Выполнить `terraform init` в `hello-node` -2. Выполнить `terraform apply` -3. Убедиться, что endpoint живой -4. Выполнить `terraform destroy` -5. Проверять публичный URL после destroy - -Логика проверки встроена в `run_terraform_examples.sh`: - -1. После `apply` endpoint обязан отвечать `200` -2. После `destroy` endpoint должен исчезнуть -3. Скрипт ждёт до 120 секунд и перепроверяет endpoint каждые 5 секунд - -## Expected Result - -После успешного `terraform destroy`: - -1. Публичный URL должен перестать существовать -2. Запрос на URL должен вернуть `404` или другой явный признак отсутствия маршрута -3. Provider не должен возвращать успешный destroy раньше, чем cleanup HTTP route завершён - -## Actual Result - -После успешного `terraform destroy`: - -1. Terraform сообщает `Destroy complete! Resources: 4 destroyed.` -2. Endpoint `https://sless-api.kube5s.ru/fn/default/hello-http` продолжает отвечать `200` -3. Через некоторое время тот же endpoint начинает отвечать `502` -4. Тело ответа на `502`: - -```json -{"error":"function unreachable: Post \"http://hello-http.sless-fn-default.svc.cluster.local:8080\": dial tcp 10.106.128.167:8080: connect: operation not permitted"} -``` - -Это означает: - -1. внешний HTTP маршрут всё ещё существует; -2. запрос по нему всё ещё направляется внутрь платформы; -3. backend функции уже удалён или недоступен; -4. cleanup маршрута не завершён. - -## Timeline From Real Run - -Подтверждённая последовательность из фактического прогона: - -1. `terraform destroy` завершился успешно -2. первые проверки после destroy возвращали `HTTP 200` -3. затем проверки начали возвращать `HTTP 502 function unreachable` -4. в течение всех 24 проверок по 5 секунд endpoint не исчез -5. итоговое время ожидания: 120 секунд - -Итоговый summary из скрипта: - -```text -ERROR SUMMARY -example: hello-node -step: endpoint cleanup after clean destroy -reason: route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable); endpoint was still published after 120s -``` - -## Why This Is A Real Platform Bug - -Это не похоже на проблему тестового скрипта или Terraform CLI по следующим причинам: - -1. `terraform destroy` завершается без ошибки -2. state Terraform очищается как ожидалось -3. сначала endpoint отвечает `200`, значит маршрут реально жив после destroy -4. потом endpoint отвечает `502 function unreachable`, значит backend уже исчез, но route ещё остался -5. скрипт ждёт 120 секунд, то есть это не мгновенная eventual consistency на 1-2 секунды - -Иными словами: удаление backend и удаление публичного маршрута расходятся по времени, а route cleanup либо не выполняется, либо не дожидается завершения. - -## Most Likely Broken Layer - -Наиболее вероятные точки проблемы: - -1. API/backend destroy trigger возвращает success до фактического удаления HTTP route -2. Controller удаляет function workload, но не удаляет route/ingress/virtualservice/gateway mapping -3. Удаление route запускается асинхронно, но его результат не awaited -4. В системе остаётся запись маршрута на имя функции, хотя service/backend уже удалён - -## What To Check In The Development Repo - -Нужно проверить destroy flow именно для HTTP trigger: - -1. Удаляется ли объект trigger только в metadata/storage или реально удаляется и внешний маршрут -2. Какие Kubernetes/ingress объекты создаются для HTTP trigger и все ли они удаляются -3. Есть ли race condition между удалением function/service и удалением route -4. Не возвращает ли provider success раньше, чем backend подтверждает полное удаление маршрута -5. Есть ли финальный polling/wait на исчезновение route перед возвратом успешного destroy - -Если архитектура использует отдельные сущности route/service/function, то destroy должен идти в таком порядке: - -1. disable/remove public routing -2. дождаться, что endpoint больше не публикуется снаружи -3. удалить backend/service/workload -4. завершить destroy success - -Сейчас по фактическому поведению порядок либо обратный, либо неполный. - -## Minimal Acceptance Criteria For Fix - -Исправление можно считать рабочим, если после `terraform destroy` для `hello-node` выполняются все условия: - -1. URL `https://sless-api.kube5s.ru/fn/default/hello-http` перестаёт отвечать как живой маршрут -2. URL не возвращает `502 function unreachable` -3. URL исчезает в разумное время после destroy -4. `./run_terraform_examples.sh` проходит шаг `endpoint cleanup after clean destroy` - -## Current Status - -На данный момент массовый прогон examples корректно останавливается на `hello-node`, потому что это первый воспроизводимый failure. - -Дальше прогонять остальные примеры без исправления destroy cleanup смысла нет: тест уже доказал platform bug на базовом HTTP сценарии. \ No newline at end of file diff --git a/examples/push-sample/Dockerfile b/examples/push-sample/Dockerfile deleted file mode 100644 index faa19a5..0000000 --- a/examples/push-sample/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index 51043a0..0000000 --- a/examples/push-sample/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Пример для пуша в 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 deleted file mode 100755 index 92f563f..0000000 --- a/examples/push-sample/build_and_push.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/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/run_terraform_examples.sh b/examples/run_terraform_examples.sh deleted file mode 100755 index 0ea985a..0000000 --- a/examples/run_terraform_examples.sh +++ /dev/null @@ -1,333 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -LOG_DIR="$ROOT_DIR/.test-logs" -mkdir -p "$LOG_DIR" - -EXAMPLES=( - "hello-node" - "simple-node" - "simple-python" - "notes-python" -) - -declare -A CHECK_METHOD=( - [hello-node]="POST" - [simple-node]="GET" - [simple-python]="GET" - [notes-python]="GET" -) - -declare -A CHECK_URL=( - [hello-node]="https://sless-api.kube5s.ru/fn/default/hello-http" - [simple-node]="https://sless-api.kube5s.ru/fn/default/simple-node-time-display" - [simple-python]="https://sless-api.kube5s.ru/fn/default/simple-py-time-display" - [notes-python]="https://sless-api.kube5s.ru/fn/default/notes-list" -) - -declare -A CHECK_DATA=( - [hello-node]='{"name":"Smoke"}' - [simple-node]='' - [simple-python]='' - [notes-python]='' -) - -declare -a PREEXISTING_EXAMPLES=() -LAST_LOG_FILE="" -CURRENT_EXAMPLE="" -CURRENT_STEP="" -LAST_ERROR_SUMMARY="" - -fail_run() { - local example="$1" - local step="$2" - local details="$3" - - echo - echo "ERROR SUMMARY" - echo "example: $example" - echo "step: $step" - echo "reason: $details" - - if [ -n "$LAST_LOG_FILE" ] && [ -f "$LAST_LOG_FILE" ]; then - echo "log: $LAST_LOG_FILE" - echo "last log lines:" - tail -n 20 "$LAST_LOG_FILE" - fi - - exit 1 -} - -run_step() { - local example="$1" - local step="$2" - shift 2 - - CURRENT_EXAMPLE="$example" - CURRENT_STEP="$step" - LAST_ERROR_SUMMARY="" - - if ! "$@"; then - local details="$LAST_ERROR_SUMMARY" - if [ -z "$details" ]; then - details="step failed without explicit summary" - fi - fail_run "$example" "$step" "$details" - fi -} - -restore_any_backups() { - local backup - while IFS= read -r backup; do - [ -n "$backup" ] || continue - if [ -f "$backup" ]; then - mv "$backup" "${backup%.copilot.bak}" - fi - done < <(find "$ROOT_DIR" -name '*.copilot.bak' | sort) -} - -trap restore_any_backups EXIT - -clean_local_artifacts() { - local example="$1" - rm -rf \ - "$ROOT_DIR/$example/.terraform" \ - "$ROOT_DIR/$example/.terraform.lock.hcl" \ - "$ROOT_DIR/$example/terraform.tfstate" \ - "$ROOT_DIR/$example/terraform.tfstate.backup" \ - "$ROOT_DIR/$example"/terraform.tfstate.*.backup \ - "$ROOT_DIR/$example/dist" -} - -retry_tf() { - local example="$1" - local label="$2" - shift 2 - - local attempt=1 - while [ "$attempt" -le 3 ]; do - LAST_LOG_FILE="$LOG_DIR/${example//\//_}-${label// /_}-${attempt}.log" - echo "==> [$example] $label (attempt $attempt/3)" - - ( - cd "$ROOT_DIR/$example" - "$@" - ) 2>&1 | tee "$LAST_LOG_FILE" - - local status=${PIPESTATUS[0]} - if [ "$status" -eq 0 ]; then - return 0 - fi - - if grep -Eiq 'Unauthorized|401|403' "$LAST_LOG_FILE"; then - LAST_ERROR_SUMMARY="authorization error during $label" - echo "[$example] authorization error during $label" - return 41 - fi - - if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE" && [ "$attempt" -lt 3 ]; then - attempt=$((attempt + 1)) - sleep 2 - continue - fi - - if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE"; then - LAST_ERROR_SUMMARY="network/provider download failure during $label after retries" - else - LAST_ERROR_SUMMARY="terraform command failed during $label with exit code $status" - fi - - return "$status" - done - - LAST_ERROR_SUMMARY="terraform command failed during $label after exhausting retries" - return 1 -} - -record_preexisting_if_needed() { - local example="$1" - if grep -Fq 'No changes. Your infrastructure matches the configuration.' "$LAST_LOG_FILE"; then - PREEXISTING_EXAMPLES+=("$example") - echo "[$example] detected preexisting remote resources on clean apply" - fi -} - -probe_endpoint() { - local example="$1" - local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt" - local status_file="$LOG_DIR/${example//\//_}-endpoint-status.txt" - local method="${CHECK_METHOD[$example]}" - local url="${CHECK_URL[$example]}" - local data="${CHECK_DATA[$example]}" - - if [ "$method" = "POST" ]; then - curl -sS -X POST -H 'Content-Type: application/json' -d "$data" -o "$body_file" -w '%{http_code}' "$url" > "$status_file" - else - curl -sS -o "$body_file" -w '%{http_code}' "$url" > "$status_file" - fi -} - -assert_live_endpoint() { - local example="$1" - probe_endpoint "$example" - - local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt" - local status - status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")" - - if [ "$status" != "200" ]; then - LAST_ERROR_SUMMARY="live endpoint check failed with HTTP $status" - echo "[$example] live endpoint check failed with HTTP $status" - cat "$body_file" - return 1 - fi - - if grep -Fq 'function unreachable' "$body_file"; then - LAST_ERROR_SUMMARY="live endpoint returned function unreachable" - echo "[$example] live endpoint check returned unreachable function" - cat "$body_file" - return 1 - fi -} - -assert_destroyed_endpoint() { - local example="$1" - probe_endpoint "$example" - - local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt" - local status - status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")" - - if [ "$status" = "404" ] || [ "$status" = "000" ]; then - return 0 - fi - - if grep -Eiq 'not found|404 page not found' "$body_file"; then - return 0 - fi - - if [ "$status" = "502" ] && grep -Fq 'function unreachable' "$body_file"; then - LAST_ERROR_SUMMARY="route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable)" - echo "[$example] route still exists after destroy, but backend is already gone (HTTP 502 function unreachable)" - cat "$body_file" - return 1 - fi - - LAST_ERROR_SUMMARY="endpoint still responds after destroy with HTTP $status" - echo "[$example] endpoint still responds after destroy with HTTP $status" - cat "$body_file" - return 1 -} - -wait_for_destroyed_endpoint() { - local example="$1" - local attempts=24 - local sleep_sec=5 - local try=1 - - while [ "$try" -le "$attempts" ]; do - if assert_destroyed_endpoint "$example"; then - echo "[$example] endpoint disappeared after destroy" - return 0 - fi - - echo "[$example] endpoint still present after destroy, waiting (${try}/${attempts})" - try=$((try + 1)) - sleep "$sleep_sec" - done - - echo "[$example] endpoint did not disappear after destroy within $((attempts * sleep_sec))s" - if [ -z "$LAST_ERROR_SUMMARY" ]; then - LAST_ERROR_SUMMARY="endpoint remained reachable for more than $((attempts * sleep_sec))s after destroy" - else - LAST_ERROR_SUMMARY="$LAST_ERROR_SUMMARY; endpoint was still published after $((attempts * sleep_sec))s" - fi - return 1 -} - -backup_and_modify() { - local example="$1" - case "$example" in - hello-node) - cp "$ROOT_DIR/$example/http.tf" "$ROOT_DIR/$example/http.tf.copilot.bak" - perl -0pi -e 's/enabled\s+=\s+true/enabled = false/' "$ROOT_DIR/$example/http.tf" - ;; - simple-node) - cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak" - perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf" - ;; - simple-python) - cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak" - perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf" - ;; - notes-python) - cp "$ROOT_DIR/$example/notes-list.tf" "$ROOT_DIR/$example/notes-list.tf.copilot.bak" - perl -0pi -e 's/memory_mb\s+=\s+128/memory_mb = 160/' "$ROOT_DIR/$example/notes-list.tf" - ;; - esac -} - -restore_modified_files() { - local example="$1" - case "$example" in - hello-node) - mv "$ROOT_DIR/$example/http.tf.copilot.bak" "$ROOT_DIR/$example/http.tf" - ;; - simple-node) - mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf" - ;; - simple-python) - mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf" - ;; - notes-python) - mv "$ROOT_DIR/$example/notes-list.tf.copilot.bak" "$ROOT_DIR/$example/notes-list.tf" - ;; - esac -} - -run_example() { - local example="$1" - - echo - echo "==== $example ====" - - clean_local_artifacts "$example" - run_step "$example" "terraform init" retry_tf "$example" "terraform init" terraform init -input=false -no-color - run_step "$example" "terraform apply clean" retry_tf "$example" "terraform apply clean" terraform apply -auto-approve -input=false -no-color - record_preexisting_if_needed "$example" - run_step "$example" "endpoint check after clean apply" assert_live_endpoint "$example" - - run_step "$example" "terraform destroy clean" retry_tf "$example" "terraform destroy clean" terraform destroy -auto-approve -input=false -no-color - run_step "$example" "endpoint cleanup after clean destroy" wait_for_destroyed_endpoint "$example" - - run_step "$example" "terraform apply second" retry_tf "$example" "terraform apply second" terraform apply -auto-approve -input=false -no-color - run_step "$example" "endpoint check after second apply" assert_live_endpoint "$example" - - backup_and_modify "$example" - run_step "$example" "terraform apply modified" retry_tf "$example" "terraform apply modified" terraform apply -auto-approve -input=false -no-color - restore_modified_files "$example" - - run_step "$example" "terraform destroy final" retry_tf "$example" "terraform destroy final" terraform destroy -auto-approve -input=false -no-color - run_step "$example" "endpoint cleanup after final destroy" wait_for_destroyed_endpoint "$example" - clean_local_artifacts "$example" -} - -main() { - local example - for example in "${EXAMPLES[@]}"; do - run_example "$example" - done - - if [ "${#PREEXISTING_EXAMPLES[@]}" -gt 0 ]; then - echo - echo "Preexisting remote resources were detected on first apply for: ${PREEXISTING_EXAMPLES[*]}" - exit 2 - fi - - echo - echo "All Terraform example lifecycles completed successfully." -} - -main "$@" \ No newline at end of file