diff --git a/NOTES/30_analysis/OPUS_QA_MODIFIER_DESIGN_2026-09-24.md b/NOTES/30_analysis/OPUS_QA_MODIFIER_DESIGN_2026-09-24.md new file mode 100644 index 0000000..711ce99 --- /dev/null +++ b/NOTES/30_analysis/OPUS_QA_MODIFIER_DESIGN_2026-09-24.md @@ -0,0 +1,75 @@ +# Q&A с Opus: дизайн ресурсов-модификаторов (2026-09-24) + +> Кто: вопросы составлены нами (Copilot), ответы — Opus (внешний агент, по разрешению пользователя). +> Контекст: решено делать два ресурса-модификатора (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`). +> Статус: ответы приняты к сведению, **код не писался**, часть утверждений Opus мною не проверена (пометки ниже). + +## Вопросы и ответы + +### 1. Инварианты Read/Delete ресурса-модификатора +**Ответ Opus:** +- Read: `RemoveResource` только если родитель исчез (404/deleted) — у нас есть `ShouldRemoveFromState` + (Opus ссылается на `modifier.go`). Расхождение значения параметра ≠ повод удалять ресурс: это дрейф, + обновить поле в state. +- Delete = inverse modify (`count=0` / `needEnableAVI=false` / `ipSpaceName="no-needed"`) — «шаблон + `DeleteStrategy=inverse` + `override` уже реализован». +- Если родитель уже удалён: inverse пропустить, ресурс убрать из state (no-op + Warning), не падать на ошибке API. + +### 2. Reset-to-default в `*WithDefaults` +**Ответ Opus:** защита «уже встроена»: и `RunInstanceOperationUniversalWithDefaults` (`operation_run.go:138`), +и by-code путь (`operation_run_bycode.go:108`) досылают незаданные параметры с приоритетом +**live `state.params` → `paramValue` формы → `defaultValue`**. Достаточно шлать только `ipSpaceName`. +Отдельный pre-read live + merge делать не нужно; `ByCode`/`ByIdempotent` — не нужны. +Дополнительно `RunOperationByCodeIdempotent` (`check_before_run`) сверяет desired == current и пропускает лишний run. + +### 3. Генератор: modify-only параметр с `required: true` +**Ответ Opus (минимальный набор):** +- **(а)** modify-only → всегда Optional (снять Required в схеме). Обязательно. +- **(б)** исключить modify-only из create-read-back (не добавлять его `InputField` в Create/Read). Обязательно. +- **(в)** «после create догонять modify» — **не нужно**: это ответственность отдельного modifier-ресурса. +- Breaking: снятие Required — не breaking (Optional шире). Breaking — если **удалить** атрибут из схемы + instance у тех, кто его уже прописал в `.tf`. Формулировка Opus: «modify-only параметров в схеме instance + быть не должно вовсе — их место в modifier-ресурсе». + +### 4. Диагноз «inconsistent result after apply» на создании орги +**Ответ Opus: подтверждает.** `state_refresh.go`, цикл `inputs`: берёт `paramsMap["vIPConfigure"]` из +`state.params` (платформа отдаёт `[{}]`), через `setFieldValue`/`ParseString` перекрывает план; для +Required-атрибута TF требует final == config → ошибка. Корректно: не читать modify-only обратно в Create +(п.3б) и вернуть запланированное значение, либо Optional+Computed со схлопыванием `[{}]`→null. + +### 5. Порядок destroy +**Ответ Opus:** явный `depends_on` нужен — связь между org-IP и SNAT идёт по **имени** ipSpace, ребра графа +TF не видит. Цепочка: `vdc → org → org-IP → edge → SNAT → кластер`; при корректных `depends_on` destroy +пойдёт в обратном порядке. Обязательные рёбра: SNAT → org-IP, modifier → родитель. Достаточно при условии, +что inverse-Delete терпит уже удалённого родителя (п.1). + +### 6. Трактовка `[{}]` в Read +**Ответ Opus:** `[{}]` = «не выделено», нормализовать в null/пусто. `count=0` и `[{}]` — одно состояние +«пусто», иначе ложный дрейф на каждом plan. + +## Мои замечания к ответам (не проверено кодом, требует внимания) + +1. **Opus опирается на machinery отменённого захода.** Он говорит про `modifier.go`, `DeleteStrategy=inverse`, + `override`, «уже реализовано». Это шаблон генератора из эпохи `kind: modifier`, которую мы **сознательно + отменили** (см. баннер LEGACY в `NOTES/20_prompts/**`, `docs/60_strategy/modifier_resources_ideology_and_specification.md`). + Ответы про «уже встроено» нельзя принимать как готовое решение — это код отменённой ветки. +2. **Противоречие внутри п.3:** сначала «modify-only → всегда Optional (оставить в схеме instance)», + потом «modify-only в схеме instance быть не должно вовсе». Это разные изменения: Optional+Computed vs удаление. + Нужно выбрать одно, иначе получим двух владельцев одного параметра (instance-ресурс и модификатор). +3. **Два владельца параметра.** Если `ip_space_name` остаётся в `nubes_vc_nsxt` **и** появляется + `nubes_vc_nsxt_snat`, Terraform не увидит конфликт: оба будут шлать 372. Значит, из `Update` + сгенерированного `nubes_vc_nsxt` параметр надо убирать — иначе fight/drift. В ответах Opus этого нет. +4. **П.2 не проверял сам.** Утверждение «приоритет live → paramValue → defaultValue уже встроен» противоречит + комментарию в `19_vc_org_resource.go` про reset-баг (`state_params["needEnableAVI"]="false"`, когда на + платформе `true`). Нужна проверка `operation_run.go:138` и `operation_run_bycode.go:108` по коду. +5. **Политика destroy для не-нашей орги.** Орга не в state (адресация по uid). При `destroy` конфигурации + родитель не удаляется — но org-IP-модификатор по §1 выполнит inverse (`count=0`). Нужно решение: + снимать квоту или оставлять (`keep_on_destroy`)— у Opus этого нет. +6. **Один элемент vs весь массив.** `vIPConfigure` — массив. Если ресурс управляет одним элементом + (по `ip_space_name`), то два ресурса на разные ipSpace возможны; если всем массивом — нет. `count=0` + как inverse предполагает поэлементную модель, но в ответах это не зафиксировано. + +## Что дальше (после решения пользователя) + +- Проверить по коду п.2 и п.4 замечаний (чтение, без правок). +- Дописать план реализации 2 ресурсов с учётом п.3 и п.5.