235 lines
20 KiB
Markdown
235 lines
20 KiB
Markdown
# 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` придётся получать не из дефолтного эндпоинта, а из контекста инстанса. Это вопрос рантайма провайдера, а не генератора.
|
||
|
||
### Итоговая оценка
|
||
|
||
Ошибок, которые ломали бы выводы отчёта, не выявлено. Отчёт можно использовать как подтверждённую основу для дальнейшего проектирования.
|