#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}" PROVIDER_DIR="${ROOT_DIR}/provider" PROFILE_DIR="${PROFILE_DIR:-}" if [[ "${1:-}" == "--profile" ]]; then PROFILE_DIR="${2:-}" shift 2 fi if [[ -n "$PROFILE_DIR" ]]; then if [[ ! -d "$PROFILE_DIR" ]]; then echo "Error: profile directory not found: $PROFILE_DIR" >&2 exit 2 fi PROFILE_ENV_FILE="${PROFILE_DIR}/profile.env" if [[ -f "$PROFILE_ENV_FILE" ]]; then set -a # shellcheck disable=SC1090 source "$PROFILE_ENV_FILE" set +a fi fi if [[ -z "$PROFILE_DIR" ]]; then echo "Error: --profile is required. Strict mode stores generated files per stand only." >&2 exit 2 fi resolve_root_path() { local path_value="$1" if [[ -z "$path_value" ]]; then echo "" return fi if [[ "$path_value" = /* ]]; then echo "$path_value" return fi echo "${ROOT_DIR}/${path_value}" } # Список сервисов — только из профиля стенда (TOOLS/config//services_list.txt). # Общий TOOLS/config/services_list.txt («объединение») не используется ни одним профилем. SERVICES_FILE_DEFAULT="${PROFILE_DIR}/services_list.txt" SERVICES_FILE="${SERVICES_FILE:-$SERVICES_FILE_DEFAULT}" SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")" # Retry behavior (per service): attempts and delay between attempts. REQUEST_DELAY="${REQUEST_DELAY:-0.5}" ATTEMPTS="${ATTEMPTS:-3}" # Auth: токен из NUBES_API_TOKEN или из файла TOKEN_FILE (profile.env). TOKEN_FILE="${TOKEN_FILE:-}" NUBES_API_TOKEN="${NUBES_API_TOKEN:-}" if [[ -n "$TOKEN_FILE" ]]; then TOKEN_FILE="$(resolve_root_path "$TOKEN_FILE")" fi # Назначение: # - Берет список сервисов из services_list.txt профиля стенда. # - Для каждого сервиса запрашивает единый spec через API и пишет 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} (обязателен, только из profile.env) # - Токен: переменная NUBES_API_TOKEN или файл TOKEN_FILE (из profile.env). # # Формат services_list.txt: # - Поле 1: service_id (обязательно). # - Поле 2: имя сервиса (используется для лога; можно опустить). # - Всё после # — заметка для девопса (не используется). # # Задержки и попытки: # - REQUEST_DELAY (секунды) между запросами и ретраями (по умолчанию 0.5). # - ATTEMPTS — число попыток на сервис (по умолчанию 3). # # Логи ошибок: # - Список неуспешных сервисов пишется в generated//tmp/yaml_gen_failures.txt. # # Токен: 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 echo "Error: NUBES_API_TOKEN or TOKEN_FILE is required" >&2 exit 2 fi if [[ ! -f "$SERVICES_FILE" ]]; then echo "Error: services file not found: $SERVICES_FILE" >&2 exit 2 fi # 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. 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" YAML_OUTPUT_DIR="${YAML_OUTPUT_DIR:-$YAML_OUTPUT_DIR_DEFAULT}" FAILURES_FILE="${FAILURES_FILE:-$FAILURES_FILE_DEFAULT}" YAML_OUTPUT_DIR="$(resolve_root_path "$YAML_OUTPUT_DIR")" FAILURES_FILE="$(resolve_root_path "$FAILURES_FILE")" YAML_PARENT_DIR="$(dirname "$YAML_OUTPUT_DIR")" YAML_BASE_NAME="$(basename "$YAML_OUTPUT_DIR")" mkdir -p "$YAML_PARENT_DIR" mkdir -p "$(dirname "$FAILURES_FILE")" # Защита от генерации не в тот стенд: каталог помечается маркером .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" // Files embeds unified service YAML specs for provider runtime lookup. // //go:embed *.yaml var Files embed.FS EOF printf '%s\n' "$STAND" > "${STAGING_DIR}/.stand" rm -f "$FAILURES_FILE" # Auto-rebuild yaml-generator if sources are newer than binary BIN="${ROOT_DIR}/TOOLS/bin/yaml-generator" SRC="${ROOT_DIR}/TOOLS/yaml-generator/" if [[ ! -x "$BIN" ]] || find "$SRC" -type f -newer "$BIN" -print -quit | grep -q .; then echo "Building yaml-generator..." (cd "${ROOT_DIR}/TOOLS/yaml-generator" && go build -o "$BIN" .) fi # Проверка что список сервисов не пустой svc_count=$(grep -cv '^\s*$\|^\s*#' "$SERVICES_FILE" || true) if [[ "$svc_count" -eq 0 ]]; then echo "Error: services list is empty or all commented out: $SERVICES_FILE" >&2 exit 2 fi echo "Generating YAML for ${svc_count} services from ${SERVICES_FILE}" # Читаем список сервисов, пропуская пустые строки и комментарии. while IFS= read -r line; do line="${line//$'\r'/}" if [[ -z "$line" ]]; then continue fi if [[ "$line" == \#* ]]; then continue fi sid=$(echo "$line" | awk '{print $1}') # Имя сервиса берётся из 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 fi if [[ ! "$sid" =~ ^[0-9]+$ ]]; then echo "Error: invalid service id '${sid}' in ${SERVICES_FILE}" >&2 echo "${sid} invalid_id" >> "$FAILURES_FILE" continue fi # Один YAML на сервис формируется Go-генератором — пишем во временный каталог. echo "Generating unified spec for ${sid} (${svc_name})" success=0 # Generator normalizes names, so verify output by ID prefix only. output_glob="${STAGING_DIR}/${sid}_*.yaml" # Старый YAML НЕ удаляем заранее: полная замена каталога — атомарная, в конце. # Генерация YAML для выбранного сервиса с ретраями. for attempt in $(seq 1 "$ATTEMPTS"); do ( cd "$PROVIDER_DIR" NUBES_API_TOKEN="$NUBES_API_TOKEN" \ NUBES_SERVICE_ID="$sid" \ NUBES_SERVICE_NAME="$svc_name" \ NUBES_API_ENDPOINT="$API_ENDPOINT" \ NUBES_OUTPUT_DIR="$STAGING_DIR" \ "${ROOT_DIR}/TOOLS/bin/yaml-generator" ) && success=1 || success=0 if [[ "$success" -eq 1 ]]; then # Success means output file exists for the service ID. if ! compgen -G "$output_glob" > /dev/null; then echo "Error: output not created ${output_glob} for ${sid} (${svc_name})" >&2 success=0 fi fi if [[ "$success" -eq 1 ]]; then break fi echo "Retry ${attempt}/${ATTEMPTS} failed for ${sid} (${svc_name})" >&2 sleep "$REQUEST_DELAY" done # Фиксируем неудачные сервисы для анализа. if [[ "$success" -ne 1 ]]; then echo "${sid} ${svc_name}" >> "$FAILURES_FILE" fi sleep "$REQUEST_DELAY" done < "$SERVICES_FILE" 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 # Атомарная замена: старый каталог уезжает в бэкап, новый встаёт на его место. 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}"