// service_spec_gen — генератор YAML-спеков сервисов из API Nubes. // // Читает список сервисов, дёргает API, собирает операции/параметры/MAN, // пишет один YAML на сервис в resources_yaml/. package main import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" "yaml-generator/internal/client" "yaml-generator/internal/config" "yaml-generator/internal/normalize" "yaml-generator/internal/spec" "yaml-generator/internal/types" ) func main() { cfg, err := config.Load() if err != nil { panic(err) } cli := client.New(cfg.APIEndpoint, cfg.APIToken) services := cfg.Services if cfg.SingleServiceID > 0 { services = []config.ServiceRef{{ID: cfg.SingleServiceID, Name: cfg.SingleServiceName}} } outputDir := cfg.OutputDir if outputDir == "" { panic("NUBES_OUTPUT_DIR is required — generated files must go to generated/{stand}/resources_yaml/") } if err := spec.ValidateOutputDir(outputDir); err != nil { panic(err) } if err := os.MkdirAll(outputDir, 0o755); err != nil { panic(err) } for _, svc := range services { if svc.ID <= 0 { continue } fmt.Fprintf(os.Stderr, " service %d (%s): fetching...\n", svc.ID, svc.Name) info, err := cli.GetService(svc.ID) if err != nil { fmt.Fprintf(os.Stderr, " ⚠ service %d (%s) skipped: %v\n", svc.ID, svc.Name, err) continue } name := strings.TrimSpace(svc.Name) if name == "" { name = strings.TrimSpace(info.ShortName) } if name == "" { name = strings.TrimSpace(info.DisplayName) } name = normalize.ServiceName(name) if name == "" { name = fmt.Sprintf("service_%d", svc.ID) } fmt.Fprintf(os.Stderr, " %d operations to fetch...\n", len(info.Operations)) // === Универсальная выгрузка из API (БЕЗ доменных знаний) === // // CollectOperations возвращает операции сервиса ТАКИМИ, КАК ИХ ОТДАЁТ API: // create/delete/modify/suspend/... с их настоящими действиями и параметрами. // Здесь НЕЛЬЗЯ привносить доменную семантику (помечать операцию как // "модификатор", задавать delete_strategy, delete_params и т.п.) — это // сломало бы главный принцип: YAML должен быть ЧИСТОЙ, ПОЛНОЙ выгрузкой // того, что доступно по API для сервиса, без каких-либо знаний о слое // "модификаторов" провайдера. // // Раньше (до рефакторинга 2026-09-23) здесь стоял реестр // `serviceSpecificModifiers`, который помечал отдельные modify как // kind=modifier. Это было инвазивно: обычные ресурсы зависели от // доменного реестра, а YAML переставал быть чистой выгрузкой. // Модификаторы теперь живут ОТДЕЛЬНЫМ оверлеем (см. modifiers.yaml + // второй проход), базовый YAML их не содержит и не знает о них. ops, hasSuspend, _, _, err := cli.CollectOperations(info.Operations) if err != nil { panic(err) } specYAML := types.ServiceSpec{ Name: name, ServiceID: svc.ID, ServiceDisplayName: strings.TrimSpace(info.DisplayName), ServiceShortName: strings.TrimSpace(info.ShortName), ServiceMan: strings.TrimSpace(info.Man), Lifecycle: types.Lifecycle{ SuspendOnDestroyDefault: hasSuspend, AdoptExistingOnCreateDefault: false, }, Outputs: types.OutputSection{Params: spec.DefaultOutputParams()}, Operations: ops, } outPath := filepath.Join(outputDir, fmt.Sprintf("%d_%s.yaml", svc.ID, name)) buf, err := yaml.Marshal(specYAML) if err != nil { panic(err) } if err := os.WriteFile(outPath, buf, 0o644); err != nil { panic(err) } fmt.Printf("written %s\n", outPath) } }