fix(tools): безопасная генерация YAML — staging + атомарная замена, без легаси-фолбэков
01_generate_yamls.sh: - генерация в staging-каталог; рабочий каталог не удаляется заранее; атомарная замена (mv) только при полном успехе, старый каталог → бэкап resources_yaml.bak-<UTC> (ротация 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.
This commit is contained in:
@@ -43,10 +43,9 @@ resolve_root_path() {
|
|||||||
echo "${ROOT_DIR}/${path_value}"
|
echo "${ROOT_DIR}/${path_value}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Список сервисов — только из профиля стенда (TOOLS/config/<stand>/services_list.txt).
|
||||||
|
# Общий TOOLS/config/services_list.txt («объединение») не используется ни одним профилем.
|
||||||
SERVICES_FILE_DEFAULT="${PROFILE_DIR}/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="${SERVICES_FILE:-$SERVICES_FILE_DEFAULT}"
|
||||||
SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")"
|
SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")"
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")"
|
|||||||
REQUEST_DELAY="${REQUEST_DELAY:-0.5}"
|
REQUEST_DELAY="${REQUEST_DELAY:-0.5}"
|
||||||
ATTEMPTS="${ATTEMPTS:-3}"
|
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:-}"
|
TOKEN_FILE="${TOKEN_FILE:-}"
|
||||||
NUBES_API_TOKEN="${NUBES_API_TOKEN:-}"
|
NUBES_API_TOKEN="${NUBES_API_TOKEN:-}"
|
||||||
if [[ -n "$TOKEN_FILE" ]]; then
|
if [[ -n "$TOKEN_FILE" ]]; then
|
||||||
@@ -62,38 +61,40 @@ if [[ -n "$TOKEN_FILE" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Назначение:
|
# Назначение:
|
||||||
# - Берет список сервисов из services_list.txt.
|
# - Берет список сервисов из services_list.txt профиля стенда.
|
||||||
# - Для каждого сервиса запрашивает единый spec через API и пишет YAML в
|
# - Для каждого сервиса запрашивает единый spec через API и пишет YAML в
|
||||||
# ${ROOT_DIR}/provider/resources_yaml.
|
# ${ROOT_DIR}/generated/<stand>/resources_yaml (per-stand, см. ниже).
|
||||||
# - Формат имени файла: ID_имя.yaml (например, 13_s3bucket.yaml).
|
# - Формат имени файла: ID_имя.yaml (например, 13_s3bucket.yaml).
|
||||||
#
|
#
|
||||||
|
# Безопасность записи (best practice, с 2026-09-30):
|
||||||
|
# - Генерация идёт во временный каталог (staging), старый каталог не удаляется заранее.
|
||||||
|
# - Замена каталога атомарна и делается ТОЛЬКО при полном успехе; предыдущий
|
||||||
|
# каталог уезжает в бэкап resources_yaml.bak-<UTC>, хранятся последние KEEP_BACKUPS.
|
||||||
|
# - При любом сбое рабочий каталог остаётся нетронутым, частичный результат — в staging.
|
||||||
|
#
|
||||||
# Источники данных:
|
# Источники данных:
|
||||||
# - Список сервисов: TOOLS/config/{stand}/services_list.txt
|
# - Список сервисов: TOOLS/config/{stand}/services_list.txt
|
||||||
# - API endpoint: ${API_ENDPOINT}
|
# - API endpoint: ${API_ENDPOINT} (обязателен, только из profile.env)
|
||||||
# - Токен: переменная NUBES_API_TOKEN или файл .token.
|
# - Токен: переменная NUBES_API_TOKEN или файл TOKEN_FILE (из profile.env).
|
||||||
#
|
#
|
||||||
# Формат services_list.txt:
|
# Формат services_list.txt:
|
||||||
# - В начале строки: service_id.
|
# - Поле 1: service_id (обязательно).
|
||||||
# - Остальное в строке — произвольная заметка для девопса (не используется).
|
# - Поле 2: имя сервиса (используется для лога; можно опустить).
|
||||||
|
# - Всё после # — заметка для девопса (не используется).
|
||||||
#
|
#
|
||||||
# Задержки и попытки:
|
# Задержки и попытки:
|
||||||
# - REQUEST_DELAY (секунды) между запросами и ретраями (по умолчанию 0.2).
|
# - REQUEST_DELAY (секунды) между запросами и ретраями (по умолчанию 0.5).
|
||||||
# - ATTEMPTS — число попыток на сервис (по умолчанию 3).
|
# - ATTEMPTS — число попыток на сервис (по умолчанию 3).
|
||||||
#
|
#
|
||||||
# Логи ошибок:
|
# Логи ошибок:
|
||||||
# - Список неуспешных сервисов пишется в /tmp/yaml_gen_failures.txt.
|
# - Список неуспешных сервисов пишется в generated/<stand>/tmp/yaml_gen_failures.txt.
|
||||||
#
|
#
|
||||||
# Токен берется из переменной NUBES_API_TOKEN.
|
# Токен: NUBES_API_TOKEN, иначе файл TOKEN_FILE (из profile.env).
|
||||||
# Если переменная пуста, берется файл .token из ${ROOT_DIR}:
|
# Легаси-поиск «последнего» *.token в корне репо удален (2026-09-30): токенов там
|
||||||
# - "последний" = файл с самым новым временем изменения (ls -t | head -n 1).
|
# нет, а такой поиск мог молча подхватить чужой токен.
|
||||||
if [[ -z "$NUBES_API_TOKEN" ]]; then
|
if [[ -z "$NUBES_API_TOKEN" && -n "$TOKEN_FILE" && -f "$TOKEN_FILE" ]]; 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")
|
NUBES_API_TOKEN=$(cat "$TOKEN_FILE")
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$NUBES_API_TOKEN" ]]; then
|
if [[ -z "$NUBES_API_TOKEN" ]]; then
|
||||||
echo "Error: NUBES_API_TOKEN or TOKEN_FILE is required" >&2
|
echo "Error: NUBES_API_TOKEN or TOKEN_FILE is required" >&2
|
||||||
@@ -105,12 +106,18 @@ if [[ ! -f "$SERVICES_FILE" ]]; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ⛔ LEGACY: deck-api.ngcloud.ru ЗАКРЫВАЕТСЯ. Default = Gateway. Override via NUBES_API_ENDPOINT.
|
# Endpoint ОБЯЗАТЕЛЕН: берется ТОЛЬКО из NUBES_API_ENDPOINT (profile.env).
|
||||||
API_ENDPOINT="${NUBES_API_ENDPOINT:-https://lk-api-gateway.ngcloud.ru/api/v1/svc}"
|
# Легаси-фолбэк на 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=),
|
# Auto-detect API style: if endpoint contains "index.cfm" → legacy proxy (?endpoint=),
|
||||||
# otherwise → new REST gateway (direct paths). No forced /index.cfm normalization.
|
# 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"
|
YAML_OUTPUT_DIR_DEFAULT="${GENERATED_DIR}/resources_yaml"
|
||||||
FAILURES_FILE_DEFAULT="${GENERATED_DIR}/tmp/yaml_gen_failures.txt"
|
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")"
|
YAML_OUTPUT_DIR="$(resolve_root_path "$YAML_OUTPUT_DIR")"
|
||||||
FAILURES_FILE="$(resolve_root_path "$FAILURES_FILE")"
|
FAILURES_FILE="$(resolve_root_path "$FAILURES_FILE")"
|
||||||
|
|
||||||
# Убедимся, что папка есть. Полной очистки нет: обрабатываем только сервисы из списка.
|
YAML_PARENT_DIR="$(dirname "$YAML_OUTPUT_DIR")"
|
||||||
mkdir -p "$YAML_OUTPUT_DIR"
|
YAML_BASE_NAME="$(basename "$YAML_OUTPUT_DIR")"
|
||||||
|
mkdir -p "$YAML_PARENT_DIR"
|
||||||
mkdir -p "$(dirname "$FAILURES_FILE")"
|
mkdir -p "$(dirname "$FAILURES_FILE")"
|
||||||
|
|
||||||
if [[ ! -f "${YAML_OUTPUT_DIR}/embed.go" ]]; then
|
# Защита от генерации не в тот стенд: каталог помечается маркером .stand.
|
||||||
cat > "${YAML_OUTPUT_DIR}/embed.go" <<'EOF'
|
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
|
package resources_yaml
|
||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
@@ -134,7 +156,7 @@ import "embed"
|
|||||||
//go:embed *.yaml
|
//go:embed *.yaml
|
||||||
var Files embed.FS
|
var Files embed.FS
|
||||||
EOF
|
EOF
|
||||||
fi
|
printf '%s\n' "$STAND" > "${STAGING_DIR}/.stand"
|
||||||
|
|
||||||
rm -f "$FAILURES_FILE"
|
rm -f "$FAILURES_FILE"
|
||||||
|
|
||||||
@@ -164,7 +186,12 @@ while IFS= read -r line; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
sid=$(echo "$line" | awk '{print $1}')
|
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=""
|
svc_name=""
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$sid" ]]; then
|
if [[ -z "$sid" ]]; then
|
||||||
continue
|
continue
|
||||||
@@ -176,59 +203,13 @@ while IFS= read -r line; do
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Если имя не указано в списке — подтягиваем по API.
|
# Один YAML на сервис формируется Go-генератором — пишем во временный каталог.
|
||||||
if [[ -z "$svc_name" ]]; then
|
|
||||||
svc_name=$(python3 - <<PY
|
|
||||||
import json, sys, time, urllib.parse, urllib.request
|
|
||||||
|
|
||||||
sid = "${sid}"
|
|
||||||
endpoint = "${API_ENDPOINT}"
|
|
||||||
token = "${NUBES_API_TOKEN}"
|
|
||||||
max_retries = 3
|
|
||||||
|
|
||||||
# Auto-detect API style: "index.cfm" → legacy proxy, otherwise → REST gateway.
|
|
||||||
if "index.cfm" in endpoint:
|
|
||||||
params = urllib.parse.urlencode({"endpoint": f"/services/{sid}"})
|
|
||||||
url = f"{endpoint}?{params}"
|
|
||||||
else:
|
|
||||||
url = f"{endpoint}/services/{sid}"
|
|
||||||
|
|
||||||
for attempt in range(1, max_retries + 1):
|
|
||||||
try:
|
|
||||||
req = urllib.request.Request(url)
|
|
||||||
if token:
|
|
||||||
req.add_header("Authorization", f"Bearer {token}")
|
|
||||||
req.add_header("User-Agent", "Mozilla/5.0 (compatible; Terraform-Provider-Nubes/BashGenerator)")
|
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
||||||
data = json.loads(resp.read().decode("utf-8"))
|
|
||||||
svc = data.get("svc", {})
|
|
||||||
name = svc.get("svcShort") or svc.get("name") or svc.get("title") or f"service_{sid}"
|
|
||||||
print(name)
|
|
||||||
sys.exit(0)
|
|
||||||
except Exception as e:
|
|
||||||
if attempt < max_retries:
|
|
||||||
time.sleep(2 * attempt)
|
|
||||||
else:
|
|
||||||
print(f"ERROR: failed to fetch service {sid} after {max_retries} attempts: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
PY
|
|
||||||
)
|
|
||||||
if [[ "$svc_name" == ERROR:* ]]; then
|
|
||||||
echo "$svc_name" >&2
|
|
||||||
echo "${sid} api_error" >> "$FAILURES_FILE"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Один YAML на сервис формируется Go-генератором.
|
|
||||||
echo "Generating unified spec for ${sid} (${svc_name})"
|
echo "Generating unified spec for ${sid} (${svc_name})"
|
||||||
success=0
|
success=0
|
||||||
# Generator normalizes names, so verify output by ID prefix only.
|
# Generator normalizes names, so verify output by ID prefix only.
|
||||||
output_glob="${YAML_OUTPUT_DIR}/${sid}_*.yaml"
|
output_glob="${STAGING_DIR}/${sid}_*.yaml"
|
||||||
|
|
||||||
# Для выбранного сервиса удаляем старый YAML и генерируем заново с ретраями.
|
|
||||||
rm -f "$output_glob"
|
|
||||||
|
|
||||||
|
# Старый YAML НЕ удаляем заранее: полная замена каталога — атомарная, в конце.
|
||||||
# Генерация YAML для выбранного сервиса с ретраями.
|
# Генерация YAML для выбранного сервиса с ретраями.
|
||||||
for attempt in $(seq 1 "$ATTEMPTS"); do
|
for attempt in $(seq 1 "$ATTEMPTS"); do
|
||||||
(
|
(
|
||||||
@@ -237,7 +218,7 @@ PY
|
|||||||
NUBES_SERVICE_ID="$sid" \
|
NUBES_SERVICE_ID="$sid" \
|
||||||
NUBES_SERVICE_NAME="$svc_name" \
|
NUBES_SERVICE_NAME="$svc_name" \
|
||||||
NUBES_API_ENDPOINT="$API_ENDPOINT" \
|
NUBES_API_ENDPOINT="$API_ENDPOINT" \
|
||||||
NUBES_OUTPUT_DIR="$YAML_OUTPUT_DIR" \
|
NUBES_OUTPUT_DIR="$STAGING_DIR" \
|
||||||
"${ROOT_DIR}/TOOLS/bin/yaml-generator"
|
"${ROOT_DIR}/TOOLS/bin/yaml-generator"
|
||||||
) && success=1 || success=0
|
) && success=1 || success=0
|
||||||
|
|
||||||
@@ -265,9 +246,28 @@ PY
|
|||||||
|
|
||||||
done < "$SERVICES_FILE"
|
done < "$SERVICES_FILE"
|
||||||
|
|
||||||
echo "Done. YAML files are in ${YAML_OUTPUT_DIR}"
|
if [[ -s "$FAILURES_FILE" ]]; then
|
||||||
if [[ -f "$FAILURES_FILE" ]]; then
|
|
||||||
echo "Failed services (see $FAILURES_FILE):" >&2
|
echo "Failed services (see $FAILURES_FILE):" >&2
|
||||||
cat "$FAILURES_FILE" >&2
|
cat "$FAILURES_FILE" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo "Замена каталога ОТМЕНЕНА: рабочий ${YAML_OUTPUT_DIR} не тронут." >&2
|
||||||
|
echo "Частичный результат оставлен для разбора: $STAGING_DIR" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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}"
|
||||||
@@ -30,7 +30,12 @@ type ServiceRef struct {
|
|||||||
|
|
||||||
// Load читает конфигурацию из переменных окружения и файлов.
|
// Load читает конфигурацию из переменных окружения и файлов.
|
||||||
func Load() (Config, error) {
|
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)
|
apiEndpoint = normalize.APIEndpoint(apiEndpoint)
|
||||||
apiToken, err := loadToken()
|
apiToken, err := loadToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,13 +56,11 @@ func Load() (Config, error) {
|
|||||||
|
|
||||||
services := []ServiceRef{}
|
services := []ServiceRef{}
|
||||||
if singleID == 0 {
|
if singleID == 0 {
|
||||||
|
// Без per-service ID нужен явный список. Угадывание пути удалено (2026-09-30):
|
||||||
|
// старый дефолт указывал на несуществующий provider/devops/config/services_list.txt.
|
||||||
listPath := strings.TrimSpace(os.Getenv("NUBES_SERVICES_FILE"))
|
listPath := strings.TrimSpace(os.Getenv("NUBES_SERVICES_FILE"))
|
||||||
if listPath == "" {
|
if listPath == "" {
|
||||||
repoRoot, err := FindRepoRoot()
|
return Config{}, errors.New("NUBES_SERVICES_FILE is required when NUBES_SERVICE_ID is not set")
|
||||||
if err != nil {
|
|
||||||
return Config{}, err
|
|
||||||
}
|
|
||||||
listPath = filepath.Join(repoRoot, "devops", "config", "services_list.txt")
|
|
||||||
}
|
}
|
||||||
list, err := ReadServicesList(listPath)
|
list, err := ReadServicesList(listPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -76,14 +79,6 @@ func Load() (Config, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getenvDefault(key string, def string) string {
|
|
||||||
val := strings.TrimSpace(os.Getenv(key))
|
|
||||||
if val == "" {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadToken() (string, error) {
|
func loadToken() (string, error) {
|
||||||
if tok := strings.TrimSpace(os.Getenv("NUBES_API_TOKEN")); tok != "" {
|
if tok := strings.TrimSpace(os.Getenv("NUBES_API_TOKEN")); tok != "" {
|
||||||
return tok, nil
|
return tok, nil
|
||||||
@@ -95,47 +90,10 @@ func loadToken() (string, error) {
|
|||||||
}
|
}
|
||||||
return strings.TrimSpace(string(b)), nil
|
return strings.TrimSpace(string(b)), nil
|
||||||
}
|
}
|
||||||
repoRoot, err := FindRepoRoot()
|
// Легаси-фолбэк «найти последний *.token в корне репо» удален (2026-09-30):
|
||||||
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")
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadServicesList читает список сервисов из файла.
|
// ReadServicesList читает список сервисов из файла.
|
||||||
func ReadServicesList(path string) ([]ServiceRef, error) {
|
func ReadServicesList(path string) ([]ServiceRef, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user