// 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") }