From a96e38fb1e4e58626597aac472fbf059bf470a3e Mon Sep 17 00:00:00 2001 From: Repinoid Date: Wed, 23 Sep 2026 19:23:31 +0300 Subject: [PATCH] chore: save current changes --- !/network.tf.tmpl | 87 +++++++ !/vdc.tf | 67 +++++ !/vmware_org.tf | 54 ++++ docs/FORENSIC_ANALYSIS_BRIEF_2026-09-23.md | 217 ++++++++++++++++ docs/FORENSIC_ANALYSIS_REPORT_2026-09-23.md | 234 ++++++++++++++++++ ...S_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md | 115 +++++++++ ...SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md | 169 +++++++++++++ .../prompt_for_opus_iac_shturval_modify.md | 51 ++++ 8 files changed, 994 insertions(+) create mode 100755 !/network.tf.tmpl create mode 100755 !/vdc.tf create mode 100755 !/vmware_org.tf create mode 100644 docs/FORENSIC_ANALYSIS_BRIEF_2026-09-23.md create mode 100644 docs/FORENSIC_ANALYSIS_REPORT_2026-09-23.md create mode 100644 docs/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md create mode 100644 docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md create mode 100644 docs/prompts/prompt_for_opus_iac_shturval_modify.md diff --git a/!/network.tf.tmpl b/!/network.tf.tmpl new file mode 100755 index 0000000..6a1d9be --- /dev/null +++ b/!/network.tf.tmpl @@ -0,0 +1,87 @@ +data "vcd_external_network_v2" "nsxt-ext-net" { + name = var.providerGateway +} + +<% if (vdcType == "vdcGroup") { %> +data "vcd_vdc_group" "groupvdc" { + name = var.vdcGroupName +} + +data "vcd_org_vdc" "mainvdc" { + name = var.vmwareVdc +} +<% } %> + +resource "vcd_nsxt_edgegateway" "nsxt-edge" { + name = var.nsxName + description = "Nsxt edge" + + org = var.vmwareOrg + + <% if (vdcType == "vdc") { %> + owner_id = var.vmwareServicesId + <% } else { %> + owner_id = data.vcd_vdc_group.groupvdc.id + starting_vdc_id = data.vcd_org_vdc.mainvdc.id + <% } %> + + external_network_id = data.vcd_external_network_v2.nsxt-ext-net.id +} + +resource "vcd_network_routed_v2" "test_routed_net" { + name = var.routedNet + + edge_gateway_id = vcd_nsxt_edgegateway.nsxt-edge.id + gateway = "${ application.ipGw }" + + prefix_length = ${ application.ipMask } + +# dns1 = "185.247.187.83" +# dns2 = "81.22.46.43" + + dns1 = "${ routedNetConfiguration.mainDns }" + dns2 = "${ routedNetConfiguration.secondDns }" + + static_ip_pool { + start_address = "${ application.ipStartPool }" + end_address = "${ application.ipEndPool }" + } + + depends_on = [vcd_nsxt_edgegateway.nsxt-edge] +} + +# Включаем AVI +resource "vcd_nsxt_alb_settings" "avi" { + count = var.alb_enable ? 1 : 0 + + org = var.vmwareOrg + + edge_gateway_id = vcd_nsxt_edgegateway.nsxt-edge.id + is_active = var.alb_enable + + # Optional definition of service network for the ALB. "192.168.255.125/25" is the default one. + # service_network_specification = "192.168.255.125/25" + + depends_on = [vcd_nsxt_edgegateway.nsxt-edge] +} + +## Добавляем ServiceEngine Group +# Получаем SEGroup +data "vcd_nsxt_alb_service_engine_group" "provider-gateway" { + count = var.alb_enable ? 1 : 0 + + name = var.alb_segroup_name + sync_on_refresh = false +} + +# Создаем SE +resource "vcd_nsxt_alb_edgegateway_service_engine_group" "first" { + count = var.alb_enable ? 1 : 0 + + edge_gateway_id = vcd_nsxt_edgegateway.nsxt-edge.id + service_engine_group_id = data.vcd_nsxt_alb_service_engine_group.provider-gateway[0].id + + max_virtual_services = 100 + reserved_virtual_services = var.alb_segroup_count + depends_on = [vcd_nsxt_alb_settings.avi[0]] +} \ No newline at end of file diff --git a/!/vdc.tf b/!/vdc.tf new file mode 100755 index 0000000..2c05cd5 --- /dev/null +++ b/!/vdc.tf @@ -0,0 +1,67 @@ +resource "vcd_org_vdc" "vdc_services" { + name = var.vmwareVdc + description = "vcd description" + org = var.vmwareOrg + + allocation_model = "Flex" + elasticity = true + include_vm_memory_overhead = false + network_pool_name = var.providerNetworkPoolName + provider_vdc_name = var.providerVdcName + network_quota = 1 + cpu_guaranteed = var.cpuGuaranteed + cpu_speed = var.vmwareCpuspeed + memory_guaranteed = var.memGuaranteed + + compute_capacity { + cpu { + allocated = var.cpuAllocated + limit = var.cpuAllocated + } + + # limit ставится в unlimited, чтобы можно было создать ВМки по размеру аллоцирования (впритык), уместив overhead по памяти + # Клиент выйти за allocated не сможет, но и дополнительно забираться память для гипервизора не будет + memory { + allocated = var.memAllocated + limit = 0 + } + } + + metadata_entry { + key = "instanceUid" + type = "MetadataStringValue" + value = var.instanceUid + user_access = "PRIVATE" + is_system = true # Requires System admin privileges + } + + dynamic "metadata_entry" { + for_each = var.enabled ? [] : [1] + content { + key = "dtStopped" + type = "MetadataStringValue" + value = var.dtStopped + user_access = "PRIVATE" + is_system = true # Requires System admin privileges + } + } + + dynamic "storage_profile" { + for_each = local.storage_config_with_default + content { + name = storage_profile.value.name + limit = storage_profile.value.size + enabled = true + default = storage_profile.value.default + } + } + + default_compute_policy_id = var.defaultComputePolicyId + vm_sizing_policy_ids = local.all_sizing_policies + + enabled = var.enabled + enable_thin_provisioning = true + enable_fast_provisioning = false # Если включить параметр, то диски менять системные не получится + delete_force = true + delete_recursive = true +} diff --git a/!/vmware_org.tf b/!/vmware_org.tf new file mode 100755 index 0000000..64b4ba3 --- /dev/null +++ b/!/vmware_org.tf @@ -0,0 +1,54 @@ +resource "vcd_org" "org" { + name = var.tenantOrgName + full_name = var.tenantOrgName + description = var.contragentCode + is_enabled = var.vcdEnable + delete_recursive = true + delete_force = true + + vapp_lease { + maximum_runtime_lease_in_sec = 0 + power_off_on_runtime_lease_expiration = true + maximum_storage_lease_in_sec = 0 + delete_on_storage_lease_expiration = false + } + vapp_template_lease { + maximum_storage_lease_in_sec = 0 + delete_on_storage_lease_expiration = true + } + + metadata_entry { + key = "clientid" + type = "MetadataStringValue" + value = var.contragentCode + user_access = "PRIVATE" + is_system = true # Requires System admin privileges + } + + metadata_entry { + key = "status" + type = "MetadataStringValue" + value = "${ var.clientStatus }" + user_access = "PRIVATE" + is_system = true # Requires System admin privileges + } + + metadata_entry { + key = "instanceUid" + type = "MetadataStringValue" + value = "${ var.instanceUid }" + user_access = "PRIVATE" + is_system = true # Requires System admin privileges + } + + dynamic "metadata_entry" { + for_each = var.vcdEnable ? [] : [1] + content { + key = "dtStopped" + type = "MetadataStringValue" + value = var.dtStopped + user_access = "PRIVATE" + is_system = true # Requires System admin privileges + } + } +} diff --git a/docs/FORENSIC_ANALYSIS_BRIEF_2026-09-23.md b/docs/FORENSIC_ANALYSIS_BRIEF_2026-09-23.md new file mode 100644 index 0000000..a8a456e --- /dev/null +++ b/docs/FORENSIC_ANALYSIS_BRIEF_2026-09-23.md @@ -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. + +На текущем этапе никаких решений по этим компонентам принимать нельзя. diff --git a/docs/FORENSIC_ANALYSIS_REPORT_2026-09-23.md b/docs/FORENSIC_ANALYSIS_REPORT_2026-09-23.md new file mode 100644 index 0000000..fc2e402 --- /dev/null +++ b/docs/FORENSIC_ANALYSIS_REPORT_2026-09-23.md @@ -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` придётся получать не из дефолтного эндпоинта, а из контекста инстанса. Это вопрос рантайма провайдера, а не генератора. + +### Итоговая оценка + +Ошибок, которые ломали бы выводы отчёта, не выявлено. Отчёт можно использовать как подтверждённую основу для дальнейшего проектирования. diff --git a/docs/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md b/docs/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md new file mode 100644 index 0000000..57f9e56 --- /dev/null +++ b/docs/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md @@ -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) чтение цепочки, после чего фиксировать, что реально блокирует, а что уже закрыто. diff --git a/docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md b/docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md new file mode 100644 index 0000000..4f07f61 --- /dev/null +++ b/docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md @@ -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» заработает без правки провайдера. diff --git a/docs/prompts/prompt_for_opus_iac_shturval_modify.md b/docs/prompts/prompt_for_opus_iac_shturval_modify.md new file mode 100644 index 0000000..f2d6aee --- /dev/null +++ b/docs/prompts/prompt_for_opus_iac_shturval_modify.md @@ -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) до того, как мы начинаем кодить, а что можем сделать на стороне провайдера уже сейчас? + +Отвечай по номерам, кратко.