Files
tf_provider/TOOLS/yaml-generator/internal/config/config.go
T
Repinoid 12b3932817 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.
2026-09-30 09:28:41 +03:00

152 lines
4.4 KiB
Go

// Package config — загрузка конфигурации, токенов и списка сервисов.
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"yaml-generator/internal/normalize"
)
// Config — конфигурация генератора.
type Config struct {
APIEndpoint string
APIToken string
Services []ServiceRef
SingleServiceID int
SingleServiceName string
OutputDir string
}
// ServiceRef — ссылка на сервис (ID + имя).
type ServiceRef struct {
ID int
Name string
}
// Load читает конфигурацию из переменных окружения и файлов.
func Load() (Config, error) {
// 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 {
return Config{}, err
}
singleID := 0
if raw := strings.TrimSpace(os.Getenv("NUBES_SERVICE_ID")); raw != "" {
val, err := strconv.Atoi(raw)
if err != nil {
return Config{}, fmt.Errorf("invalid NUBES_SERVICE_ID: %s", raw)
}
singleID = val
}
singleName := strings.TrimSpace(os.Getenv("NUBES_SERVICE_NAME"))
outputDir := strings.TrimSpace(os.Getenv("NUBES_OUTPUT_DIR"))
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 == "" {
return Config{}, errors.New("NUBES_SERVICES_FILE is required when NUBES_SERVICE_ID is not set")
}
list, err := ReadServicesList(listPath)
if err != nil {
return Config{}, err
}
services = list
}
return Config{
APIEndpoint: apiEndpoint,
APIToken: apiToken,
Services: services,
SingleServiceID: singleID,
SingleServiceName: singleName,
OutputDir: outputDir,
}, nil
}
func loadToken() (string, error) {
if tok := strings.TrimSpace(os.Getenv("NUBES_API_TOKEN")); tok != "" {
return tok, nil
}
if tf := strings.TrimSpace(os.Getenv("TOKEN_FILE")); tf != "" {
b, err := os.ReadFile(tf)
if err != nil {
return "", err
}
return strings.TrimSpace(string(b)), nil
}
// Легаси-фолбэк «найти последний *.token в корне репо» удален (2026-09-30):
// токенов там нет, а такой поиск мог молча подхватить чужой токен.
return "", errors.New("NUBES_API_TOKEN or TOKEN_FILE is required")
}
// ReadServicesList читает список сервисов из файла.
func ReadServicesList(path string) ([]ServiceRef, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
lines := strings.Split(string(b), "\n")
services := []ServiceRef{}
for _, raw := range lines {
line := strings.TrimSpace(strings.ReplaceAll(raw, "\r", ""))
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.Contains(line, "#") {
line = strings.TrimSpace(strings.SplitN(line, "#", 2)[0])
}
parts := strings.Fields(line)
if len(parts) == 0 {
continue
}
id, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("invalid service id in %s: %s", path, parts[0])
}
name := ""
if len(parts) > 1 {
name = parts[1]
}
services = append(services, ServiceRef{ID: id, Name: name})
}
return services, nil
}
// FindRepoRoot ищет корень репозитория по директории provider.
func FindRepoRoot() (string, error) {
wd, err := os.Getwd()
if err != nil {
return "", err
}
current := wd
for i := 0; i < 8; i++ {
candidate := filepath.Join(current, "provider")
info, err := os.Stat(candidate)
if err == nil && info.IsDir() {
return candidate, nil
}
parent := filepath.Dir(current)
if parent == current {
break
}
current = parent
}
return "", errors.New("failed to locate provider repo root")
}