chore: save current changes

This commit is contained in:
Repinoid
2026-09-23 19:23:31 +03:00
parent d78573de45
commit a96e38fb1e
8 changed files with 994 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
# Forensic Analysis: API Model to Ordinary YAML
## Цель
Исследовать существующую цепочку:
```text
API model
↓
ordinary generator
↓
API YAML
```
Цель этапа — получить подтверждённую картину движения данных и установить, что сохраняется, преобразуется или теряется до формирования API YAML.
Этот документ предназначен для передачи Opus перед анализом.
## Строгие ограничения
На этом этапе запрещено:
- изменять файлы;
- писать код;
- менять ordinary generator;
- проектировать `ModifierSpec`;
- проектировать `modifiers.yaml`;
- проектировать `delete_rule`;
- проектировать output layout или orchestration;
- обсуждать inverse и dependency ordering;
- придумывать API identifiers;
- придумывать `parameter_path`;
- придумывать HTTP method или payload structure;
- считать API YAML полным источником данных без доказательства.
Если факт невозможно установить, его нужно обозначить как `unknown` или как требующий проверки фактическим API. Нельзя закрывать неизвестность архитектурным предположением.
## Что исследовать
### 1. Исходная API-модель
Установить:
- где находится canonical API model;
- в каком формате она представлена;
- как представлены services и operations;
- как представлены параметры;
- как представлены nested objects и arrays;
- как представлены типы параметров;
- существует ли стабильный operation ID;
- существует ли стабильный parameter ID;
- какие данные доступны до запуска ordinary generator.
### 2. Ordinary generator
Установить:
- какой input получает generator;
- где он читает API-модель;
- какие внутренние структуры строит;
- какие преобразования выполняет;
- какие поля нормализует или переименовывает;
- какие поля вычисляет;
- какие поля отбрасывает;
- где формируется API YAML;
- где именно могут происходить потери данных.
Обязательно различать:
```text
данные отсутствуют уже в API
```
и:
```text
данные присутствуют в API, но теряются ordinary generator
```
### 3. API YAML
Установить:
- какие поля сохраняются;
- какие поля представлены иначе, чем в API-модели;
- сохраняются ли nested objects и arrays;
- сохраняются ли типы;
- сохраняются ли operation identity и parameter identity;
- сохраняются ли HTTP method и path, если они есть в исходной модели;
- какие данные доступны будущему modifier layer;
- какие данные потенциально доступны только в исходной API-модели.
## Обязательные трассировки
### `vc_org`
Отдельно проследить `vIPConfigure` и `count`:
```text
API model
→ generator input
→ internal generator representation
→ generator transformation
→ API YAML
```
Для каждого этапа указать:
- присутствует ли `vIPConfigure`;
- присутствует ли `count`;
- в каком типе они представлены;
- в какой структуре находятся;
- изменяются ли их имена или типы;
- теряются ли они;
- если теряются, в какой точке.
Не считать заранее известной структуру `vIPConfigure` или `count`.
### `vc_nsxt`
Отдельно проследить `ipSpaceName` и связанные параметры по той же цепочке:
```text
API model
→ generator input
→ internal generator representation
→ generator transformation
→ API YAML
```
Установить:
- где появляется `ipSpaceName`;
- к какой operation или структуре он относится;
- в каком типе представлен;
- является ли обычным полем, nested field или частью массива;
- какие связанные параметры находятся рядом;
- сохраняется ли он в API YAML;
- изменяются ли его значение или тип;
- теряются ли связанные поля.
## Требования к доказательности
Каждый вывод разделять на:
- **Подтверждённый факт** — непосредственно виден из кода, структуры данных, фактического API input, generator input или API YAML.
- **Неизвестное** — информация отсутствует или неоднозначна.
- **Предположение** — не использовать как основание для архитектуры; только явно перечислять как неподтверждённое.
Для каждого важного вывода указывать конкретное основание: файл, функцию, структуру, входной документ или фрагмент YAML/JSON. Если точное место невозможно назвать, это указать как ограничение анализа.
## Формат итогового отчёта
Отчёт должен содержать только следующие разделы:
1. **Подтверждённые факты**
2. **Исходная API-модель**
3. **Как ordinary generator читает модель**
4. **Как формируется API YAML**
5. **Цепочка данных для `vc_org.vIPConfigure.count`**
6. **Цепочка данных для `vc_nsxt.ipSpaceName` и связанных параметров**
7. **Что сохраняется**
8. **Что преобразуется или нормализуется**
9. **Что теряется**
10. **Где происходят потери**
11. **Какие данные доступны в API YAML**
12. **Какие данные доступны только в API-модели**
13. **Неизвестные места**
14. **Что необходимо проверить фактическим API**
## Критерий завершения
Forensic analysis завершён только тогда, когда для каждого потенциально необходимого modifier-элемента можно проследить происхождение:
```text
API model
→ generator input
→ internal representation
→ transformation
→ API YAML
```
без неизвестных промежуточных преобразований.
Для `vc_org` и `vc_nsxt` должны быть подтверждены:
```text
operation identity
parameter identity
parameter structure
parameter type
API model representation
generator representation
YAML representation
transformation
loss or absence
```
Если на существенном этапе остаётся `???`, исследование не завершено. Этот пункт нужно зафиксировать как `unknown`, а не проектировать решение.
## Главный принцип
Сначала:
```text
исследовать
↓
зафиксировать факты
↓
зафиксировать неизвестное
↓
отличить отсутствие данных от потери данных
```
Только после отдельного согласования forensic report можно переходить к проектированию `ModifierSpec`, `modifiers.yaml`, `delete_rule`, modifier generator, output boundary и orchestration.
На текущем этапе никаких решений по этим компонентам принимать нельзя.
+234
View File
@@ -0,0 +1,234 @@
# 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` придётся получать не из дефолтного эндпоинта, а из контекста инстанса. Это вопрос рантайма провайдера, а не генератора.
### Итоговая оценка
Ошибок, которые ломали бы выводы отчёта, не выявлено. Отчёт можно использовать как подтверждённую основу для дальнейшего проектирования.
@@ -0,0 +1,115 @@
# Ответ Opus: анализ решения IaC-развёртывания Штурвала (модификаторы + скрытые зависимости)
**Дата:** 2026-09-23
**Связанный промпт:** `docs/prompts/prompt_for_opus_iac_shturval_modify.md`
**Связанный анализ:** `docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md`
**Статус:** документирование ответа. Конкретный план НЕ составляется.
---
## 1. Суть ответа (главный вывод)
Форма IaC-ресурсов фиксируется **уже сейчас**, потому что она диктуется моделью Terraform (декларативность, идемпотентность, inverse), а не спеками платформы.
НО есть **три блокера от платформы**, без которых идемпотентность и Delete принципиально недостижимы на стороне провайдера.
---
## 2. Ответ Opus по пунктам
### Пункт 1 — накопительный `vIPConfigure` → идемпотентный ресурс
- Ресурс отдельный (`nubes_org_vip_allocation`) с `depends_on` на оргу, НЕ операция внутри орги.
- **Ключ идемпотентности — желаемое состояние, а не дельта.** Юзер задаёт целевой `count` на `name`; провайдер сам считает `target − current` и модифицирует только разницу.
- **Create:** Read текущего числа vIP → выделить `target − current`. Если API не отдаёт «сколько уже есть» — нужен серверный счётчик/тег, иначе идемпотентность недостижима.
- **Read:** читать родителя (оргу), извлекать фактическое число IP по `name` в state. Если API не различает «кем/зачем выделено» — Read вернёт общий пул, drift неизбежен.
- **Update:** та же дельта-логика (target изменился → доначислить/освободить).
- **Delete (inverse):** `modify` с обратным знаком до `count=0` по этому `name`. Требует адресного освобождения конкретных IP. Если освобождение — тоже накопительный modify без адресации, inverse корректно сделать нельзя.
**Риск:** накопительный API без «прочитать текущее» и без «освободить конкретное» несовместим с декларативной моделью. Чинится на стороне платформы, не провайдера.
### Пункт 2 — `ipSpaceName` (цепочка providerVdc → providerGateway → ipSpace)
- Это **выводимое значение из инфраструктуры, НЕ пользовательский ввод** → data-source, а не аргумент ресурса.
- Правильно: `data "nubes_ip_space" { org/vdc = ... }`, который проходит цепочку providerVdc → providerGateway → ipSpace и возвращает `name`. Ресурс берёт значение по ссылке.
- **Граница «данные vs логика»:** в реестре хранить **тип поля и его источник** (что это computed-from-parent, а не user-input). Сама цепочка обхода — логика data-source, не данные реестра.
- НЕ вычислять из state родителя вручную в ресурсе (скрытая связанность, ломается при >1 T0). Data-source явно выражает зависимость в графе tf.
- Пока платформа «подкладывает» значение сама — data-source должен уметь то же читать. Если API этой цепочки нет на чтение — **блокер**.
### Пункт 3 — не завязываться на «один T0»
- Закладывать **явный селектор шлюза** уже сейчас: `provider_gateway` / `t0_id` как аргумент (или ключ data-source), даже если сегодня один и выводится автоматически (optional + computed default).
- vIP-аллокация и SNAT привязывать к **конкретному gateway id**, а не к «дефолтному в орге».
- **Что сломается при >1 T0, если не заложить:** `ipSpaceName` станет неоднозначным (несколько ipSpace), vIP-аллокация не будет знать, к какому шлюзу. Придётся менять схему (добавлять обязательный селектор) → breaking change.
- **Заложив optional-селектор сейчас:** при росте T0 меняется только default-резолвинг, схема остаётся совместимой.
### Пункт 4 — ждать спеки или фиксировать форму сейчас
- **Форму ресурсов можно и нужно фиксировать сейчас** — она диктуется моделью Terraform, а не спеками.
- **Не блокер (делаем сейчас):** раздельные ресурсы + `depends_on`; целевое состояние вместо дельты; селектор шлюза; data-source для `ipSpaceName`; inverse через обратный modify.
- **Блокер (нужно от платформы):**
- a) чтение текущего числа vIP по `name` (иначе нет Read/идемпотентности);
- b) адресное освобождение IP (иначе нет корректного Delete);
- c) API чтения цепочки providerVdc → providerGateway → ipSpace (иначе data-source невозможен).
- **Вывод:** проектируем форму сейчас, помечаем a/b/c как зависимости от платформы. Новые спеки повлияют на **резолвинг значений**, не на форму ресурсов — если форма построена на «целевое состояние + селектор + data-source».
### Пункт 5 — минимально-инвазивный порядок внедрения
**От платформы (до кодинга ресурсов) — обязательно:**
- Read-эндпоинт: текущее количество vIP по `name` (для идемпотентности/Read).
- Освобождение конкретных vIP (для Delete/inverse).
- Read цепочки → `ipSpaceName` (для data-source).
- Подтверждение, что генератор умеет строить схему из объединения `create`+`modify` полей (иначе `vIPConfigure`/`ipSpaceName` вообще не попадут в схему).
**На стороне провайдера — можно сейчас, не дожидаясь:**
- Раздельные ресурсы vip-allocation / nsxt-snat с `depends_on`.
- Логика «target − current = дельта» (заглушка current, пока нет Read).
- Data-source-скелет для `ipSpaceName` (с TODO на реальный обход цепочки).
- Optional+computed селектор шлюза.
- Inverse-контракт (Delete = обратный modify до нуля).
**Главный неустранимый на нашей стороне блокер:** накопительный API без чтения текущего состояния и без адресного освобождения. Без этого идемпотентность и Delete принципиально недостижимы. Правится в спеках/API платформы, а не в провайдере.
---
## 3. Что нового vs то, что уже собирались делать
### Совпадает со старыми планами (НЕ новое)
- Отдельный ресурс под модификацию + `depends_on` — было (`PLAN_modifier_redesign.md`, «resource association»).
- Inverse через обратный modify (`count→0`) — было (`inverse_rollback_analysis_2026-09-23.md`).
- Идемпотентность (skip run, если live уже целевое) — было.
- «Не ждать спеки для формы, а фиксировать сейчас» — по сути было.
### Реально новое у Опуса
1. **«Целевое состояние, а не дельта»** — строгий принцип: юзер задаёт целевой `count`, провайдер сам считает `target − current`. Старые планы просто «досылали заданные поля», не формализовали желаемое состояние.
2. **`ipSpaceName` — data-source, не аргумент ресурса** — сдвиг от «юзер вписывает значение» к «computed-from-parent». Раньше виделось как ввод.
3. **Селектор шлюза (`t0_id`/`provider_gateway`) как optional+computed сейчас** — в старых планах про «один T0» вообще не было (пришло только из реплики Виталия).
4. **Чёткая граница «данные vs логика»** — в реестре только «тип поля + что computed-from-parent», цепочка обхода — логика data-source.
5. **Три конкретных блокера от платформы** как API-требования (Read счётчика, адресное освобождение, Read цепочки) — раньше это было «unknown, проверить», теперь жёсткий список.
### Главное отличие одной фразой
Старые планы отвечали на «**как сделать модификатор в tf**». Опус отвечает на «**как сделать его идемпотентным и IaC-честным при накопительном API и скрытых зависимостях**» — и выявил, что ядро проблемы не в провайдере, а в платформе (Read счётчика + адресное освобождение + Read цепочки).
---
## 4. Спорный/непроверенный момент (требует живой проверки)
Опус утверждает: накопительный `vIPConfigure` без Read-счётчика и без адресного освобождения «несовместим с декларативной моделью».
Проверено частично: из HAR (`org2.har`, `org_enough_.har`) видно, что live `vIPConfigure` с `count` читается (это уже потенциально «Read текущего числа»). Значит блокер (a) — чтение счётчика — возможно, уже закрыт.
НЕ проверено: умеет ли API **адресно освободить** конкретный `name`/`count` (откат), или освобождение тоже накопительное. Это блокер (b), и его надо проверить живым API, прежде чем принимать вывод Опуса за окончательный.
---
## 5. Резюме
- Форма ресурсов — проектируем сейчас, она не зависит от спеков.
- Три блокера платформы: Read счётчика vIP, адресное освобождение, Read цепочки ipSpace.
- Из трёх блокеров (a) частично подтверждён HAR-дампами; (b) и (c) — непроверены.
- Конкретный план внедрения пока НЕ составляется (по решению пользователя).
**Следующий возможный шаг (только по запросу):** проверить живым API блокеры (b) адресное освобождение и (c) чтение цепочки, после чего фиксировать, что реально блокирует, а что уже закрыто.
@@ -0,0 +1,169 @@
# Штурвал через IaC: анализ проблемы `modify` и скрытых платформенных зависимостей
**Дата:** 2026-09-23
**Контекст:** дискуссия в Telegram про запуск цепочки Штурвал полностью через Terraform.
---
## 1. Исходная задача
Клиенту нужен IaC (Infrastructure as Code): вся инфраструктура описывается одним конфигом, команда `terraform apply` разворачивает её целиком, `plan`/`destroy` дают полную картину. Никаких обязательных ручных шагов посередине.
Цепочка для стенда Штурвал:
```text
vcOrg -> create
vcVdc -> create
vcNsxt -> create
------------------------------
vcOrg -> modify (аллоцировать внешние IP в организацию)
vcNsxt -> modify (включить SNAT, указать внешний IP из vcOrg)
------------------------------
k8sShturval -> create
```
Ключевой конфликт: `create` у Terraform работает штатно, а операции `modify` в текущем провайдере никак не выражаются — Terraform не умеет «создать ресурс, а через несколько шагов поменять в нём же параметр».
---
## 2. Почему `modify` не выражается в текущем провайдере
### 2.1. Генератор строит схему только из `create`
Провайдер генерируется из YAML-спеков (`generated/{stand}/resources_yaml/*.yaml`). Схема ресурса (какие поля можно писать в `.tf`) строится **только из операции `create`**. Параметры, которые есть только в `modify`, в схему не попадают.
Подтверждено по файлам:
- `generated/dev/resources_yaml/19_vc_org.yaml`:
- `create` (id 136) → только `resourceRealm` (418), `organizationType` (556), `orgSuffix` (1125);
- `vIPConfigure` (выделение внешних IP) есть **только** в `modify` (id 207), с sub-полями `name` (39) и `count` (40).
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`:
- `create` (id 10) → `vdcUid`, `needEnableAVI` (340), `virtualServicesCount` (341), `qosProfile` (825), `routedNetConfiguration` (1110) и др.;
- `ipSpaceName` (372) есть **только** в `modify` (id 111).
Вывод: `vIPConfigure` (vc_org) и `ipSpaceName` (vc_nsxt) живут только в `modify`, в схеме tf-ресурсов их нет. Поэтому «прописать параметр в tf и сделать apply» падает ещё на `plan` (атрибут не известен провайдеру).
### 2.2. Эти параметры — не «настройки», а отложенные действия
- `vIPConfigure=[{name,count}]` — транзакция «выделить N внешних IP» на ресурсной платформе. Повторный вызов **накапливает** (аккумулирует квоту), а не задаёт состояние. Это не декларативное значение.
- `ipSpaceName` — включение SNAT на конкретный ipSpace, который возникает **только после** того, как на орге выделены IP.
- Эти операции требуют порядка (org.modify → затем nsxt.modify) и зависят от живого состояния инстанса, а не от дефолтов формы.
### 2.3. Схема в state ≠ реальное состояние
Вписывать недостающие параметры «насильно» в tfstate нельзя и бесполезно:
1. Terraform валидирует атрибуты по **схеме провайдера**, а не по state — неизвестный атрибут будет отброшен/вызовет ошибку.
2. Записывать в state «SNAT включён», когда этого нет на площадке, — значит получить ложный `plan` (чистый) при сломанной инфраструктуре.
3. Ручная правка tfstate/`state push` ломает целостность (серийник, конфликты на следующем apply).
Работает только косвенно: `terraform_data`/`null_resource` + `local-exec` → в state попадает **факт** «операция выполнена» (маркер с `triggers`), но не **состояние** SNAT/IP. Порядок задаётся через `depends_on`, но дрейф по самим параметрам `plan` не видит.
---
## 3. Каноничное решение: отдельный ресурс (resource association)
Это принятая в Terraform практика — «resource association / separate resource». Классические примеры:
- `aws_security_group` + `aws_security_group_rule`
- `aws_vpc` + `aws_route_table_association`
- `google_project` + `google_project_iam_member`
Базовый ресурс создаётся отдельно, а донастройка/привязка — отдельным ресурсом с `depends_on`. Граф сам выстраивает порядок, `destroy` разворачивает его корректно.
### 3.1. Прецедент из Cloud Director (VCD)
В репозитории лежит сторонний шаблон — `/home/naeel/TF/tf_provider/!/` (network.tf.tmpl, vmware_org.tf, vdc.tf), показывающий, как та же цепочка делается провайдером VMware Cloud Director:
- `resource "vcd_nsxt_alb_settings"` — включение ALB, `count = var.alb_enable ? 1 : 0`, `depends_on = [vcd_nsxt_edgegateway...]`;
- `resource "vcd_nsxt_alb_edgegateway_service_engine_group"` — выделение SE, `reserved_virtual_services = var.alb_segroup_count`;
- `resource "vcd_network_routed_v2"` — routed-сеть, `edge_gateway_id`, `dns1/dns2/static_ip_pool`;
- `resource "vcd_ip_space_custom_quota"` — квота IP на **оргу**, `depends_on = [edge]`.
Приём «включить/выключить» = `count`. Обратная операция (выключить ALB / снять квоту) получается **удалением ресурса** — inverse логика не нужна.
Маппинг на наши сервисы:
| Nubes API | Канон VCD |
|---|---|
| `needEnableAVI` | `vcd_nsxt_alb_settings` (+ `count`) |
| `virtualServicesCount` | `reserved_virtual_services` в SE-группе |
| `routedNetConfiguration` (mainDns/secondDns/ipAddrPool) | `vcd_network_routed_v2` (`dns1/dns2/static_ip_pool`) |
| `vIPConfigure` (IP на оргу) | `vcd_ip_space_custom_quota` (на оргу) |
### 3.2. Чем наш случай сложнее канона
В классическом паттерне ребёнок — **отдельный объект API** со своим CRUD (правило, association, attachment). Его можно создать/прочитать/удалить.
У нас отдельного объекта нет — есть **операция `modify` над родителем**. Поэтому требуются:
1. `Read` — не свой объект, а чтение состояния родителя;
2. `Delete` — не удаление, а **обратный modify** (inverse);
3. `Create/Update` — вызов той же операции с параметрами;
4. идемпотентность (не дёргать `run`, если live уже целевое) и live-сверку.
Именно поэтому «просто завести поля из modify в схему» не работает — нужна полноценная механика, а не одна правка.
---
## 4. Более глубокая проблема: скрытые платформенные зависимости
Это главное из всей дискуссии (реплики Виталия Зайцева, 18:30–18:34).
### 4.1. `ipSpaceName` нельзя ввести вручную — он выводится
```text
имя ipSpace → зависит от providerGateway
providerGateway → зависит от providerVdc
providerVdc → никто не знает изначально
```
Пользователь **не может** заполнить `ipSpaceName`, потому что это значение выводится из внутренней топологии (providerVdc → providerGateway → ipSpace), а не из того, что он видел в ЛК. Это не «поле, которое забыли отдать через API», а **вычисляемое от скрытых зависимостей** значение.
### 4.2. Текущий костыль платформы
«При создании орги/vdc/edge, если организация ничего не знает про недостающие параметры — они подкладываются». То есть одноразовая подстановка при создании пустой орги, чтобы избавить пользователя от «мучительных приседаний» в ЛК.
### 4.3. Ограничение модели: один T0
«Другая проблема — что будет, если в облаке появится больше 1 T0». Пока принято допущение на уровне кода: **в организации всё одно подключение**. Решение осознанно отложено («пара лет спокойствия»), но для IaC это риск: текущее решение завязано на «в орге всегда один провайдер-шлюз».
### 4.4. Ожидание изменений спеков
«Мне надо увидеть, как спеки поменяются, чтобы понять, что исправлять… Надеюсь, появится сначала в sandbox.nubes.ru, а не в ngcloud». То есть платформа меняется, форма ресурсов зависит от **новых спеков**, и строить модификатор сейчас = работать по устаревшим спекам.
---
## 5. Итог: где правда
1. **Ручной ЛК и скрипт не подходят** — клиент требует IaC (Георгий прав). Это не «костыль против красоты», это невыполнение требования.
2. **Отдельный ресурс под модификацию — необходимое, но не достаточное условие.** Он закрывает «как expressить modify», но не закрывает «откуда юзер возьмёт значения».
3. **Главная блокировка — не Terraform, а платформа.** `ipSpaceName` (и подобные) выводятся из `providerVdc → providerGateway → ipSpace`, которые юзер не знает. Пока платформа не отдаёт эти значения в спеках (или провайдер не резолвит их data-источником), честный IaC не собрать — ни модификаторами, ни скриптом, ни руками.
4. **«Ломается агностичность» — верно, но это не порок, а цена.** Ресурсы-модификаторы доменные и «ручные», как в VCD. Без них IaC невозможен, прецедент — перед глазами (`!/network.tf.tmpl`).
5. **Состояние дел:** платформа в движении (ждут новые спеки). Правильная последовательность — дождаться, что придёт в спеках (snandbox), а затем решать форму ресурса; не строить по старым спекам.
---
## 6. Возможные пути (по убыванию «честности» перед IaC)
| Вариант | Что делает | Вердикт |
|---|---|---|
| **A. Полноценные ресурсы-модификаторы + data-источники** | отдельный tf-ресурс на modify + data-source, резолвящий `ipSpace`. Полный IaC. | правильно, но только после новых спеков |
| **B. Data-source через `http`/`external` + `jsondecode`** | DevOps сам дёргает API и подставляет динамические списки, без правки провайдера | рабочая «дожималка», не полный IaC |
| **C. `terraform_data`/`null_resource` + `local-exec`** | модификации скриптом, факт в state, порядок через `depends_on` | полумера, состояние SNAT/IP вне state |
| **D. Прессеты/дефолтное окружение** | готовый набор компонентов, экспорт через провайдер | снижает боль на старте, IaC не заменяет |
| **E. Ручной ЛК / скрипт вне tf** | модификации руками | не подходит (требование клиента) |
---
## 7. Моё мнение
**Коротко:** для настоящего IaC нужны обе вещи одновременно — **отдельный ресурс под `modify`** и **механизм получения динамических значений** (`ipSpace` и пр.). Пока платформа не отдаёт второе через API/спеки, все «быстрые» способы (скрипт, руками, пресеты) закрывают только симптом, а не требование клиента.
**Рекомендация:** не городить модификатор сейчас по устаревшим спекам. Дождаться изменений спеков (сначала sandbox), параллельно — обсчитать два blockers: (1) как провайдер будет резолвить `providerVdc → providerGateway → ipSpace` без ручного ввода; (2) допущение «один T0». После этого проектировать форму ресурсов.
**Что точно не делать:** вписывать параметры «насильно» в tfstate; дёргать `modify` через скрипт на каждый apply без live-сверки (накопит `vIPConfigure`); ждать, что «прописал поле в tf → apply» заработает без правки провайдера.
@@ -0,0 +1,51 @@
# Промпт для Opus: IaC-развёртывание Штурвала, проблема `modify` и скрытых зависимостей
## Правила ответа (жёстко)
1. НЕ лезь в файлы/репозиторий/сеть. Отвечай ТОЛЬКО по материалу ниже.
2. Отвечай КРАТКО, тезисами, по номерам вопросов. Без простыней.
3. Токены/секреты/креды НЕ нужны — если захочешь, не упоминай и не проси.
4. Если для ответа не хватает данных — прямо пиши «неизвестно», не выдумывай.
5. Не предлагай «ручной ЛК / скрипт / пресеты дефолтного окружения» как решение IaC — это уже отклонено (клиенту нужен полноценный IaC).
## Контекст
Terraform-провайдер для Nubes Cloud. Клиенту нужен IaC: один конфиг + `terraform apply` = вся инфраструктура. Цепочка Штурвала:
```
vcOrg -> create
vcVdc -> create
vcNsxt -> create
vcOrg -> modify (аллокация внешних IP)
vcNsxt -> modify (включить SNAT, указать внешний IP из vcOrg)
k8sShturval -> create
```
Операции строго последовательны.
Факты (подтверждены):
- Провайдер генерируется из YAML-спеков. Схема tf-ресурса строится ТОЛЬКО из операции `create`.
- `vIPConfigure` (array-map-fixed, sub: name/count) есть только в `modify` vc_org (id 207); в `create` (136) его нет.
- `ipSpaceName` (string) есть только в `modify` vc_nsxt (id 111); в `create` (10) его нет.
- `vIPConfigure` — накопительный: повторный `modify` выделяет IP заново, а не задаёт состояние.
- `ipSpaceName` выводится из цепочки `providerVdc -> providerGateway -> ipSpace`, которую пользователь не знает. Сейчас платформа «подкладывает» недостающие параметры при создании пустой орги.
- Допущение платформы: в организации один T0/провайдер-шлюз. Рост числа T0 отложен.
- Платформа в движении: форма ресурсов зависит от новых спеков (ждут, придут сначала в sandbox).
Прецедент (VCD): та же цепочка делается отдельными ресурсами с `depends_on` — `vcd_nsxt_alb_settings` (count + is_active), `vcd_nsxt_alb_edgegateway_service_engine_group` (reserved_virtual_services), `vcd_network_routed_v2`, `vcd_ip_space_custom_quota` (на оргу). Включение/выключение = `count`, inverse = удаление ресурса.
Разница с каноном: у нас нет отдельного API-объекта под модификацию — только операция `modify` над родителем (Read = чтение родителя, Delete = обратный modify, нужна идемпотентность).
## Вопросы
1. Как корректно выразить накопительный `vIPConfigure` идемпотентным tf-ресурсом (чтобы повторный apply не выделял IP заново)? Как устроить Read и Delete (inverse: обнулить count), если отдельного API-объекта нет?
2. Как провайдер должен получать выводимое значение `ipSpaceName` (цепочка providerVdc → providerGateway → ipSpace): data-source, вычисляемое из state родителя, или иное? Где граница «данные vs логика», что хранить в реестре, что выводить из типа/state?
3. Как спроектировать форму ресурсов, чтобы не завязываться на допущение «в организации один T0», и что сломается/что менять, если T0 станет больше одного?
4. Стоит ли ждать новых спеков платформы перед проектированием ресурсов, или форму ресурсов можно зафиксировать уже сейчас так, чтобы она пережила изменение спеков? Что в спеках — блокер, что — нет?
5. Минимально-инвазивный порядок внедрения: что должно прийти от платформы (spec/API) до того, как мы начинаем кодить, а что можем сделать на стороне провайдера уже сейчас?
Отвечай по номерам, кратко.