# Forensic Analysis: API Model to Ordinary YAML **Анализ от Gemini 3.8 flash.** Исследование проведено строго в границах требований [docs/FORENSIC_ANALYSIS_BRIEF_2026-09-23.md](FORENSIC_ANALYSIS_BRIEF_2026-09-23.md). --- ## 1. Подтверждённые факты - **Цепочка генерации YAML:** Инструмент `yaml-generator` ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go)) опрашивает live HTTP REST Gateway API Nubes по сети и сериализует результат в YAML-файлы спецификаций (`generated/{stand}/resources_yaml/{service_id}_{name}.yaml`), используя структуры контракта [TOOLS/lib/types.go](../TOOLS/lib/types.go). - **Спецификации сервисов в репозитории:** Канонические сгенерированные спеки сервисов физически размещены в [generated/dev/resources_yaml/](../generated/dev/resources_yaml/). В частности, [19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml) и [22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml). - **В `vc_org.yaml`:** - Операция `modify` имеет числовой ID `207`, `kind: instance`, `action: modify`. - Параметр `vIPConfigure` присутствует как параметр операции `modify` с числовым ID `662`, типом `data_type: array-map-fixed`, `required: true`. - Поле `count` присутствует внутри `sub_params` параметра `vIPConfigure` с числовым ID `40`, `data_type: integer > 0`, `required: true`, `is_modifiable: false`. - Поле `name` присутствует внутри `sub_params` параметра `vIPConfigure` с числовым ID `39`, `data_type: string`, `required: true`, `is_modifiable: false`. - **В `vc_nsxt.yaml`:** - Операция `modify` имеет числовой ID `111`, `kind: instance`, `action: modify`. - Параметр `ipSpaceName` присутствует в операции `modify` с числовым ID `372`, `data_type: string`, `required: false`, `sort: 50`. - С ним рядом в операции `modify` присутствуют: - `needEnableAVI` (ID `368`, `data_type: boolean`, `required: false`, `value_list: ["false", "true"]`); - `virtualServicesCount` (ID `369`, `data_type: integer > 0`, `required: false`, `minvalue: 1`, `maxvalue: 4`); - `qosProfile` (ID `856`, `data_type: string`, `required: false`); - `routedNetConfiguration` (ID `1112`, `data_type: map-fixed`, `required: true`, с вложенными подполями `ipAddrPool`, `mainDns`, `secondDns`). - **Генератор ресурсов (Ordinary Resource Generator):** - Расположен в [TOOLS/resource-generator/main.go](../TOOLS/resource-generator/main.go). - При `op.Kind == "instance"` (что установлено для `modify` в [generated/dev/resources_yaml/19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml) и [generated/dev/resources_yaml/22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml)) генератор объединяет параметры `create` и `modify` в схему одного общего ресурса `nubes_vc_org` / `nubes_vc_nsxt`. - Параметры, отсутствующие в операции `create`, но присутствующие в операции `modify` (как `vIPConfigure` в `vc_org`), не включаются в жизненный цикл `create`, а при отсутствии отдельной разметки `kind: modifier` в YAML генератор не создаёт под них отдельного ресурса модификатора. --- ## 2. Исходная API-модель - **Источник и формат:** Модель получается HTTP-клиентом ([TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L44-L62)) по протоколу HTTP GET в формате JSON. - **Эндпоинты API:** - Метаданные сервиса и список операций: `GET /services/{svcId}` (возвращает `types.ServiceResponse`). - Метаданные конкретной операции: `GET /instanceOperations/default/{svcOperationId}` (возвращает `types.ServiceOperationResponse`). - **Идентификаторы операций и параметров:** - Операция идентифицируется стабильным числовым `svcOperationId` (int) и строковым именем `operation` (например, `"modify"`, `"create"`). - Параметры идентифицируются стабильным числовым `svcOperationCfsParamId` (int) и строковым кодом `svcOperationCfsParam` (например, `"vIPConfigure"`, `"ipSpaceName"`). - **Вложенные объекты и массивы (`dataDescriptor`):** - В ответе эндпоинта `/instanceOperations/default/{id}` сложная структура передаётся в поле `dataDescriptor: map[string]CfsSubParam`. - Каждое подполе имеет свой числовой `svcOperationCfsSubparamId`, строковый ключ (код подполя), `dataType`, `isRequired`, `isModifiableDefinition`, `defaultValue`, `valueList` (строка через запятую или массив). --- ## 3. Как ordinary generator читает модель - **Входной поток:** `yaml-generator` вызывает `cli.GetService(svc.ID)` и перебирает список `info.Operations`. - **Чтение операций и параметров:** Для каждой операции вызывается `cli.GetServiceOperation(op.SvcOperationID)` ([TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L182-L240)). - **Преобразования и нормализация:** - Имена сервисов и операций нормализуются в snake_case функцией `normalize.Identifier` ([TOOLS/yaml-generator/internal/normalize/normalize.go](../TOOLS/yaml-generator/internal/normalize/normalize.go#L17-L50)). - Классификация операции: функция `classifyOperation` делит операции на `instance` (для `create`, `modify`, `delete`, `suspend`, `resume`), `subresource` (если есть символ подчеркивания) или `action`. - Разворачивание `dataDescriptor`: генератор обходит `map[string]CfsSubParam`, преобразует подполя в срез `types.ParamSpec` и детерминированно сортирует по `subParams[i].ID`. - Сортировка верхнеуровневых параметров по `params[i].ID`. - Сортировка операций по имени и ID. - **Что отбрасывается / не сохраняется в YAML:** - Конкретные HTTP method и URL-пути эндпоинтов API платформы (они зашиты в код клиента, в спек YAML не пишутся). - Вспомогательные поля `CfsParam`, не имеющие тега `yaml:` в [TOOLS/lib/types.go](../TOOLS/lib/types.go#L70-L101), если они не сериализуются или приходят пустыми: `IsModifiable`, `IsSensitive` (указаны с `omitempty`). Поле `dataDescriptor` как мапа отбрасывается — сохраняется преобразованный срез `sub_params`. --- ## 4. Как формируется API YAML - Сериализация структуры `types.ServiceSpec` в YAML выполняется через библиотеку `gopkg.in/yaml.v3` ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go#L107-L114)). - В результирующий файл пишутся: - Метаданные сервиса (`name`, `service_id`, `service_display_name`, `service_short_name`, `service_man`). - Стандартная секция `lifecycle` и `outputs`. - Секция `operations` со списком операций и всеми их параметрами (`id`, `code`, `data_type`, `required`, `default`, `value_list`, `descr`, `man`, `sort`, `sub_params`). --- ## 5. Цепочка данных для `vc_org.vIPConfigure.count` 1. **API Model:** - Эндпоинт `/instanceOperations/default/207` возвращает параметр с `svcOperationCfsParamId: 662`, `svcOperationCfsParam: "vIPConfigure"`, `dataType: "array-map-fixed"`. - Внутри него поле `dataDescriptor` содержит ключ `"count"`: - `svcOperationCfsSubparamId: 40`, - `dataType: "integer > 0"`, - `isRequired: true`, - `defaultValue: ""`, - `descr: "Пример: \`1\`"`. 2. **Generator Input:** - Читается в структуру `types.CfsParam` с мапой `DataDescriptor map[string]CfsSubParam` ([TOOLS/yaml-generator/internal/types/types.go](../TOOLS/yaml-generator/internal/types/types.go#L49-L72)). 3. **Internal Generator Representation:** - В [TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L210-L232) мапа `dataDescriptor` разворачивается в `types.ParamSpec.SubParams`. Поле `count` становится элементом среза `SubParams` с `ID: 40`, `Code: "count"`, `DataType: "integer > 0"`. 4. **Generator Transformation:** - Сортируется по ID (`sort.Slice(subParams, ...)`). 5. **API YAML:** - Записывается в [generated/dev/resources_yaml/19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml#L131-L150): ```yaml - id: 662 code: vIPConfigure data_type: array-map-fixed required: true sort: 10 sub_params: - id: 39 code: name data_type: string required: true default: "" is_modifiable: false - id: 40 code: count data_type: integer > 0 required: true default: "" descr: 'Пример: `1`' is_modifiable: false ``` - **Потерь в YAML нет:** `vIPConfigure` и `count` полностью и без искажений сохранены в каноническом YAML спецификации. --- ## 6. Цепочка данных для `vc_nsxt.ipSpaceName` и связанных параметров 1. **API Model:** - Эндпоинт `/instanceOperations/default/111` возвращает операцию `modify` сервиса 22. - Параметр `ipSpaceName` возвращается с: - `svcOperationCfsParamId: 372`, - `svcOperationCfsParam: "ipSpaceName"`, - `dataType: "string"`, - `isRequired: false`, - `descr: "Имя ip Space для внешнего IP"`, - `man: "Необходимо указывать, если включён параметр \`Выделить VIP для SNAT\`"`, - `sort: 50`. - В HAR-дампах ([HAR/edge_.har](../HAR/edge_.har#L7985)) на живом инстансе в рантайме возвращается `valueList: ["no-needed", ...]`. 2. **Generator Input:** - Читается в структуру `types.CfsParam` ([TOOLS/yaml-generator/internal/types/types.go](../TOOLS/yaml-generator/internal/types/types.go#L49-L72)). 3. **Internal Generator Representation:** - Преобразуется в `types.ParamSpec` со значениями `ID: 372`, `Code: "ipSpaceName"`, `DataType: "string"`. 4. **Generator Transformation:** - Нормализуются defaults и value_list через `normalizeDefault` и `normalizeValueList`. 5. **API YAML:** - Записывается в [generated/dev/resources_yaml/22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml#L149-L155): ```yaml - id: 372 code: ipSpaceName data_type: string required: false descr: Имя ip Space для внешнего IP man: Необходимо указывать, если включён параметр `Выделить VIP для SNAT` sort: 50 ``` - Рядом в той же операции сохранены: `needEnableAVI` (ID 368), `virtualServicesCount` (ID 369), `qosProfile` (ID 856), `routedNetConfiguration` (ID 1112 с sub_params). - **Особенность по `value_list`:** в статическом `22_vc_nsxt.yaml` поле `value_list` для `ipSpaceName` отсутствует (`null` в ответе static-дефолтов эндпоинта `/instanceOperations/default/111`), хотя в рантайме на конкретном инстансе `valueList` динамически содержит `["no-needed", ...]`. --- ## 7. Что сохраняется - Полная идентичность сущностей: `service_id`, `svcOperationId` (как `id` операции), `svcOperationCfsParamId` (как `id` параметра), `svcOperationCfsSubparamId` (как `id` в `sub_params`). - Строковые коды: `operation`, коды параметров (`code`). - Исходные типы платформы: `dataType` (`string`, `boolean`, `integer > 0`, `array-map-fixed`, `map-fixed`). - Вся структура вложенности (`dataDescriptor` → `sub_params`). - Флаги `required`, валидационные regex, min/max, описания (`descr`, `man`), порядок (`sort`). --- ## 8. Что преобразуется или нормализуется - Имена операций и сервисов приводятся к ASCII snake_case через `normalize.Identifier`. - `dataDescriptor` из мапы ключей преобразуется в упорядоченный срез `sub_params` с сортировкой по числовому `id`. - Значения `valueList` и `default` приводятся к строковым представлениям (убираются пробелы, пустые значения приводятся к `nil`). --- ## 9. Что теряется - HTTP-метод и путь обращения к API (в YAML отсутствуют; генератор считает их внешним знанием рантайма). - Динамические значения списков выбора (`valueList`): эндпоинт дефолтов `/instanceOperations/default/{id}` возвращает пустой `valueList` для полей, зависящих от конкретного тенанта/инстанса (например, доступные `ipSpaceName` для конкретного VDC/Org). --- ## 10. Где происходят потери - Потери HTTP-метаданных (метод, URL) происходят на этапе маршалинга структуры `types.ServiceSpec` в YAML ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go#L94-L105)), так как они изначально отсутствуют в контракте [TOOLS/lib/types.go](../TOOLS/lib/types.go). - Отсутствие runtime `valueList` обусловлено вызовом шаблонного эндпоинта `/instanceOperations/default/{id}` вместо запроса контекста живого инстанса. --- ## 11. Какие данные доступны в API YAML - Полный перечень всех сервисов, операций (`create`, `modify`, `delete`, `suspend`, `resume`, сабресурсов) и их параметров. - Точные типы платформы (`data_type`) и иерархия подполей (`sub_params`). - Идентификаторы `id` (CFS param IDs) и символические коды (`code`). - Метаданные валидации (обязательность, регулярные выражения, ограничения диапазонов). --- ## 12. Какие данные доступны только в API-модели - Динамические списки допустимых значений (`valueList`), вычисляемые бэкендом для конкретного состояния конкретного инстанса (например, список реально существующих ipSpaces организации при вызове modify на Edge). - Внутренние служебные поля платформы CFS, отфильтрованные моделью генератора (`contractId`, `contragentId`, `nestedRefData`, `config`, `statePath`, `expression`). --- ## 13. Неизвестные места - Неизвестно, возвращает ли платформа Nubes какую-либо схему валидации для эндпоинтов отката/деаллокации (например, принимает ли `vc_org.modify` пустой массив `vIPConfigure: []` для полного снятия или требует только уменьшения `count: 0`), так как в дефолтной модели операции 207 описан только общий формат `vIPConfigure`. --- ## 14. Что необходимо проверить фактическим API - Поведение `vc_org` (операция 207) при передаче `vIPConfigure: []` против `vIPConfigure: [{"name": "...", "count": 0}]` при попытке полной деаллокации IP-пространства. - Поведение `vc_nsxt` (операция 111) при передаче `ipSpaceName: "no-needed"` на различных окружениях (Dev/Test/Prod). --- ## Мнение Opus: финальное ревью отчёта Отчёт признан годным и принят как вход для дальнейшего архитектурного этапа. ### Достаточно для дальнейшей работы - Цепочка `API model → yaml-generator → API YAML` прослежена по коду, а не по догадкам: `classifyOperation`, `params.Merge`, разворачивание `dataDescriptor` в `sub_params`. - Обе обязательные трассировки (`vc_org.vIPConfigure.count`, `vc_nsxt.ipSpaceName`) доведены до YAML с подтверждением «потерь нет». - Зафиксирован ключевой факт: динамический `valueList` есть только в рантайме живого инстанса, а в API YAML его нет. Это прямое ограничение для будущего modifier-слоя. - Неизвестное поведение payload деаллокации (`vIPConfigure: []` против `count: 0`) помечено как `unknown`, а не закрыто предположением. ### Следствия перед архитектурным этапом - Под ярлыком «Ordinary Resource Generator» в отчёте упоминаются два разных инструмента: `yaml-generator` пишет спеки, а `resource-generator` создаёт Go-ресурсы. Для проектирования `ModifierSpec` это две разные точки вмешательства. - `vIPConfigure` и `ipSpaceName` присутствуют только в `modify` и отсутствуют в `create`. В текущей схеме они сливаются в общий ресурс и не имеют отдельного жизненного цикла. Это корень задачи модификаторов. - Runtime-`valueList` придётся получать не из дефолтного эндпоинта, а из контекста инстанса. Это вопрос рантайма провайдера, а не генератора. ### Итоговая оценка Ошибок, которые ломали бы выводы отчёта, не выявлено. Отчёт можно использовать как подтверждённую основу для дальнейшего проектирования.