From 12b3932817c1d49e52f25edcb2597030bbdb426a Mon Sep 17 00:00:00 2001 From: Repinoid Date: Wed, 30 Sep 2026 09:28:41 +0300 Subject: [PATCH] =?UTF-8?q?fix(tools):=20=D0=B1=D0=B5=D0=B7=D0=BE=D0=BF?= =?UTF-8?q?=D0=B0=D1=81=D0=BD=D0=B0=D1=8F=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20YAML=20=E2=80=94=20staging=20+=20?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D0=BC=D0=B0=D1=80=D0=BD=D0=B0=D1=8F=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D0=BD=D0=B0,=20=D0=B1=D0=B5=D0=B7=20=D0=BB?= =?UTF-8?q?=D0=B5=D0=B3=D0=B0=D1=81=D0=B8-=D1=84=D0=BE=D0=BB=D0=B1=D1=8D?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 01_generate_yamls.sh: - генерация в staging-каталог; рабочий каталог не удаляется заранее; атомарная замена (mv) только при полном успехе, старый каталог → бэкап resources_yaml.bak- (ротация KEEP_BACKUPS=5); - убран легаси-фолбэк токена 'ls -t ROOT/*.token' (подхватывал чужой токен); - NUBES_API_ENDPOINT обязателен (убран молчаливый PROD-дефолт); - имя сервиса берётся из 2-го поля services_list.txt (убран лишний HTTP-запрос); - убран безусловный rm старого YAML перед генерацией (неатомарность); - удалён дубль SERVICES_FILE_DEFAULT; маркер .stand защищает от чужого стенда; - синхронизированы комментарии (REQUEST_DELAY 0.5, пути, формат списка). yaml-generator/internal/config: - NUBES_API_ENDPOINT обязателен (убран PROD-дефолт); - убран легаси-поиск последнего *.token в корне репо (loadToken/findLatestToken); - убран несуществующий путь provider/devops/config/services_list.txt (теперь требуется явный NUBES_SERVICES_FILE); - удалены мёртвые getenvDefault/findLatestToken. Проверено: bash -n OK, go vet/go build OK. --- TOOLS/scripts/01_generate_yamls.sh | 168 +++++++++--------- .../yaml-generator/internal/config/config.go | 66 ++----- 2 files changed, 96 insertions(+), 138 deletions(-) diff --git a/TOOLS/scripts/01_generate_yamls.sh b/TOOLS/scripts/01_generate_yamls.sh index 8d39e5c..2468568 100755 --- a/TOOLS/scripts/01_generate_yamls.sh +++ b/TOOLS/scripts/01_generate_yamls.sh @@ -43,10 +43,9 @@ resolve_root_path() { echo "${ROOT_DIR}/${path_value}" } +# Список сервисов — только из профиля стенда (TOOLS/config//services_list.txt). +# Общий TOOLS/config/services_list.txt («объединение») не используется ни одним профилем. SERVICES_FILE_DEFAULT="${PROFILE_DIR}/services_list.txt" -if [[ -f "${PROFILE_DIR}/services_list.txt" ]]; then - SERVICES_FILE_DEFAULT="${PROFILE_DIR}/services_list.txt" -fi SERVICES_FILE="${SERVICES_FILE:-$SERVICES_FILE_DEFAULT}" SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")" @@ -54,7 +53,7 @@ SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")" REQUEST_DELAY="${REQUEST_DELAY:-0.5}" ATTEMPTS="${ATTEMPTS:-3}" -# Auth: either an explicit token or a token file (latest *.token if not set). +# Auth: токен из NUBES_API_TOKEN или из файла TOKEN_FILE (profile.env). TOKEN_FILE="${TOKEN_FILE:-}" NUBES_API_TOKEN="${NUBES_API_TOKEN:-}" if [[ -n "$TOKEN_FILE" ]]; then @@ -62,37 +61,39 @@ if [[ -n "$TOKEN_FILE" ]]; then fi # Назначение: -# - Берет список сервисов из services_list.txt. +# - Берет список сервисов из services_list.txt профиля стенда. # - Для каждого сервиса запрашивает единый spec через API и пишет YAML в -# ${ROOT_DIR}/provider/resources_yaml. +# ${ROOT_DIR}/generated//resources_yaml (per-stand, см. ниже). # - Формат имени файла: ID_имя.yaml (например, 13_s3bucket.yaml). # +# Безопасность записи (best practice, с 2026-09-30): +# - Генерация идёт во временный каталог (staging), старый каталог не удаляется заранее. +# - Замена каталога атомарна и делается ТОЛЬКО при полном успехе; предыдущий +# каталог уезжает в бэкап resources_yaml.bak-, хранятся последние KEEP_BACKUPS. +# - При любом сбое рабочий каталог остаётся нетронутым, частичный результат — в staging. +# # Источники данных: # - Список сервисов: TOOLS/config/{stand}/services_list.txt -# - API endpoint: ${API_ENDPOINT} -# - Токен: переменная NUBES_API_TOKEN или файл .token. +# - API endpoint: ${API_ENDPOINT} (обязателен, только из profile.env) +# - Токен: переменная NUBES_API_TOKEN или файл TOKEN_FILE (из profile.env). # # Формат services_list.txt: -# - В начале строки: service_id. -# - Остальное в строке — произвольная заметка для девопса (не используется). +# - Поле 1: service_id (обязательно). +# - Поле 2: имя сервиса (используется для лога; можно опустить). +# - Всё после # — заметка для девопса (не используется). # # Задержки и попытки: -# - REQUEST_DELAY (секунды) между запросами и ретраями (по умолчанию 0.2). +# - REQUEST_DELAY (секунды) между запросами и ретраями (по умолчанию 0.5). # - ATTEMPTS — число попыток на сервис (по умолчанию 3). # # Логи ошибок: -# - Список неуспешных сервисов пишется в /tmp/yaml_gen_failures.txt. +# - Список неуспешных сервисов пишется в generated//tmp/yaml_gen_failures.txt. # -# Токен берется из переменной NUBES_API_TOKEN. -# Если переменная пуста, берется файл .token из ${ROOT_DIR}: -# - "последний" = файл с самым новым временем изменения (ls -t | head -n 1). -if [[ -z "$NUBES_API_TOKEN" ]]; then - if [[ -z "$TOKEN_FILE" ]]; then - TOKEN_FILE=$(ls -t "${ROOT_DIR}"/*.token 2>/dev/null | head -n 1 || true) - fi - if [[ -n "$TOKEN_FILE" && -f "$TOKEN_FILE" ]]; then - NUBES_API_TOKEN=$(cat "$TOKEN_FILE") - fi +# Токен: NUBES_API_TOKEN, иначе файл TOKEN_FILE (из profile.env). +# Легаси-поиск «последнего» *.token в корне репо удален (2026-09-30): токенов там +# нет, а такой поиск мог молча подхватить чужой токен. +if [[ -z "$NUBES_API_TOKEN" && -n "$TOKEN_FILE" && -f "$TOKEN_FILE" ]]; then + NUBES_API_TOKEN=$(cat "$TOKEN_FILE") fi if [[ -z "$NUBES_API_TOKEN" ]]; then @@ -105,12 +106,18 @@ if [[ ! -f "$SERVICES_FILE" ]]; then exit 2 fi -# ⛔ LEGACY: deck-api.ngcloud.ru ЗАКРЫВАЕТСЯ. Default = Gateway. Override via NUBES_API_ENDPOINT. -API_ENDPOINT="${NUBES_API_ENDPOINT:-https://lk-api-gateway.ngcloud.ru/api/v1/svc}" +# Endpoint ОБЯЗАТЕЛЕН: берется ТОЛЬКО из NUBES_API_ENDPOINT (profile.env). +# Легаси-фолбэк на PROD gateway удален (2026-09-30): молчаливый уход в прод недопустим. +if [[ -z "${NUBES_API_ENDPOINT:-}" ]]; then + echo "Error: NUBES_API_ENDPOINT is required (set it in ${PROFILE_DIR}/profile.env)." >&2 + exit 2 +fi +API_ENDPOINT="$NUBES_API_ENDPOINT" # Auto-detect API style: if endpoint contains "index.cfm" → legacy proxy (?endpoint=), # otherwise → new REST gateway (direct paths). No forced /index.cfm normalization. -GENERATED_DIR="${ROOT_DIR}/generated/$(basename "$PROFILE_DIR")" +STAND="$(basename "$PROFILE_DIR")" +GENERATED_DIR="${ROOT_DIR}/generated/${STAND}" YAML_OUTPUT_DIR_DEFAULT="${GENERATED_DIR}/resources_yaml" FAILURES_FILE_DEFAULT="${GENERATED_DIR}/tmp/yaml_gen_failures.txt" @@ -119,12 +126,27 @@ FAILURES_FILE="${FAILURES_FILE:-$FAILURES_FILE_DEFAULT}" YAML_OUTPUT_DIR="$(resolve_root_path "$YAML_OUTPUT_DIR")" FAILURES_FILE="$(resolve_root_path "$FAILURES_FILE")" -# Убедимся, что папка есть. Полной очистки нет: обрабатываем только сервисы из списка. -mkdir -p "$YAML_OUTPUT_DIR" +YAML_PARENT_DIR="$(dirname "$YAML_OUTPUT_DIR")" +YAML_BASE_NAME="$(basename "$YAML_OUTPUT_DIR")" +mkdir -p "$YAML_PARENT_DIR" mkdir -p "$(dirname "$FAILURES_FILE")" -if [[ ! -f "${YAML_OUTPUT_DIR}/embed.go" ]]; then - cat > "${YAML_OUTPUT_DIR}/embed.go" <<'EOF' +# Защита от генерации не в тот стенд: каталог помечается маркером .stand. +if [[ -d "$YAML_OUTPUT_DIR" && -f "${YAML_OUTPUT_DIR}/.stand" ]]; then + existing_stand="$(cat "${YAML_OUTPUT_DIR}/.stand" 2>/dev/null || true)" + if [[ -n "$existing_stand" && "$existing_stand" != "$STAND" ]]; then + echo "Error: ${YAML_OUTPUT_DIR} belongs to stand '${existing_stand}', not '${STAND}'. Refusing." >&2 + exit 2 + fi +fi + +# Staging: старый каталог НЕ удаляется заранее, замена — атомарная (mv в конце). +STAGING_DIR="${YAML_OUTPUT_DIR}.staging.$$" +rm -rf "$STAGING_DIR" +mkdir -p "$STAGING_DIR" + +# embed.go обязателен: пакет resources_yaml используется через go:embed *.yaml. +cat > "${STAGING_DIR}/embed.go" <<'EOF' package resources_yaml import "embed" @@ -134,7 +156,7 @@ import "embed" //go:embed *.yaml var Files embed.FS EOF -fi +printf '%s\n' "$STAND" > "${STAGING_DIR}/.stand" rm -f "$FAILURES_FILE" @@ -164,7 +186,12 @@ while IFS= read -r line; do continue fi sid=$(echo "$line" | awk '{print $1}') - svc_name="" + # Имя сервиса берётся из 2-го поля services_list.txt. API ради имени не дёргаем: + # Go-генератор сам запрашивает spec и нормализует имя (это убирает лишний запрос). + svc_name="$(echo "$line" | awk '{print $2}')" + if [[ "$svc_name" == \#* ]]; then + svc_name="" + fi if [[ -z "$sid" ]]; then continue @@ -176,59 +203,13 @@ while IFS= read -r line; do continue fi - # Если имя не указано в списке — подтягиваем по API. - if [[ -z "$svc_name" ]]; then - svc_name=$(python3 - <&2 - echo "${sid} api_error" >> "$FAILURES_FILE" - continue - fi - fi - - # Один YAML на сервис формируется Go-генератором. + # Один YAML на сервис формируется Go-генератором — пишем во временный каталог. echo "Generating unified spec for ${sid} (${svc_name})" success=0 # Generator normalizes names, so verify output by ID prefix only. - output_glob="${YAML_OUTPUT_DIR}/${sid}_*.yaml" - - # Для выбранного сервиса удаляем старый YAML и генерируем заново с ретраями. - rm -f "$output_glob" + output_glob="${STAGING_DIR}/${sid}_*.yaml" + # Старый YAML НЕ удаляем заранее: полная замена каталога — атомарная, в конце. # Генерация YAML для выбранного сервиса с ретраями. for attempt in $(seq 1 "$ATTEMPTS"); do ( @@ -237,7 +218,7 @@ PY NUBES_SERVICE_ID="$sid" \ NUBES_SERVICE_NAME="$svc_name" \ NUBES_API_ENDPOINT="$API_ENDPOINT" \ - NUBES_OUTPUT_DIR="$YAML_OUTPUT_DIR" \ + NUBES_OUTPUT_DIR="$STAGING_DIR" \ "${ROOT_DIR}/TOOLS/bin/yaml-generator" ) && success=1 || success=0 @@ -265,9 +246,28 @@ PY done < "$SERVICES_FILE" -echo "Done. YAML files are in ${YAML_OUTPUT_DIR}" -if [[ -f "$FAILURES_FILE" ]]; then +if [[ -s "$FAILURES_FILE" ]]; then echo "Failed services (see $FAILURES_FILE):" >&2 cat "$FAILURES_FILE" >&2 + echo "" >&2 + echo "Замена каталога ОТМЕНЕНА: рабочий ${YAML_OUTPUT_DIR} не тронут." >&2 + echo "Частичный результат оставлен для разбора: $STAGING_DIR" >&2 exit 1 -fi \ No newline at end of file +fi + +# Атомарная замена: старый каталог уезжает в бэкап, новый встаёт на его место. +BACKUP_DIR="${YAML_PARENT_DIR}/${YAML_BASE_NAME}.bak-$(date -u +%Y%m%dT%H%M%SZ)" +if [[ -d "$YAML_OUTPUT_DIR" ]]; then + mv "$YAML_OUTPUT_DIR" "$BACKUP_DIR" + echo "Backup of previous specs: $BACKUP_DIR" +fi +mv "$STAGING_DIR" "$YAML_OUTPUT_DIR" + +# Ротация бэкапов: держим последние KEEP_BACKUPS (по умолчанию 5). +KEEP_BACKUPS="${KEEP_BACKUPS:-5}" +mapfile -t stale_backups < <(ls -1dt "${YAML_PARENT_DIR}/${YAML_BASE_NAME}.bak-"* 2>/dev/null | tail -n +"$((KEEP_BACKUPS + 1))") +if [[ "${#stale_backups[@]}" -gt 0 ]]; then + rm -rf -- "${stale_backups[@]}" +fi + +echo "Done. YAML files are in ${YAML_OUTPUT_DIR}" \ No newline at end of file diff --git a/TOOLS/yaml-generator/internal/config/config.go b/TOOLS/yaml-generator/internal/config/config.go index 64bca67..401a132 100644 --- a/TOOLS/yaml-generator/internal/config/config.go +++ b/TOOLS/yaml-generator/internal/config/config.go @@ -30,7 +30,12 @@ type ServiceRef struct { // Load читает конфигурацию из переменных окружения и файлов. func Load() (Config, error) { - apiEndpoint := getenvDefault("NUBES_API_ENDPOINT", "https://lk-api-gateway.ngcloud.ru/api/v1/svc") + // Endpoint ОБЯЗАТЕЛЕН. Легаси-дефолт на PROD gateway удален (2026-09-30): + // провайдер не должен молча ходить в прод при незаданном endpoint. + apiEndpoint := strings.TrimSpace(os.Getenv("NUBES_API_ENDPOINT")) + if apiEndpoint == "" { + return Config{}, errors.New("NUBES_API_ENDPOINT is required (no default: refusing to guess a stand endpoint)") + } apiEndpoint = normalize.APIEndpoint(apiEndpoint) apiToken, err := loadToken() if err != nil { @@ -51,13 +56,11 @@ func Load() (Config, error) { services := []ServiceRef{} if singleID == 0 { + // Без per-service ID нужен явный список. Угадывание пути удалено (2026-09-30): + // старый дефолт указывал на несуществующий provider/devops/config/services_list.txt. listPath := strings.TrimSpace(os.Getenv("NUBES_SERVICES_FILE")) if listPath == "" { - repoRoot, err := FindRepoRoot() - if err != nil { - return Config{}, err - } - listPath = filepath.Join(repoRoot, "devops", "config", "services_list.txt") + return Config{}, errors.New("NUBES_SERVICES_FILE is required when NUBES_SERVICE_ID is not set") } list, err := ReadServicesList(listPath) if err != nil { @@ -76,14 +79,6 @@ func Load() (Config, error) { }, nil } -func getenvDefault(key string, def string) string { - val := strings.TrimSpace(os.Getenv(key)) - if val == "" { - return def - } - return val -} - func loadToken() (string, error) { if tok := strings.TrimSpace(os.Getenv("NUBES_API_TOKEN")); tok != "" { return tok, nil @@ -95,46 +90,9 @@ func loadToken() (string, error) { } return strings.TrimSpace(string(b)), nil } - repoRoot, err := FindRepoRoot() - if err != nil { - return "", err - } - latest, err := findLatestToken(repoRoot) - if err != nil { - return "", err - } - if latest == "" { - return "", errors.New("NUBES_API_TOKEN or TOKEN_FILE is required") - } - b, err := os.ReadFile(latest) - if err != nil { - return "", err - } - return strings.TrimSpace(string(b)), nil -} - -func findLatestToken(dir string) (string, error) { - entries, err := os.ReadDir(dir) - if err != nil { - return "", err - } - var latest string - var latestTime int64 - for _, e := range entries { - if e.IsDir() || !strings.HasSuffix(e.Name(), ".token") { - continue - } - info, err := e.Info() - if err != nil { - continue - } - mt := info.ModTime().Unix() - if mt > latestTime { - latestTime = mt - latest = filepath.Join(dir, e.Name()) - } - } - return latest, nil + // Легаси-фолбэк «найти последний *.token в корне репо» удален (2026-09-30): + // токенов там нет, а такой поиск мог молча подхватить чужой токен. + return "", errors.New("NUBES_API_TOKEN or TOKEN_FILE is required") } // ReadServicesList читает список сервисов из файла.