# Ответ Opus: анализ решения IaC-развёртывания Штурвала (модификаторы + скрытые зависимости) **Дата:** 2026-09-23 **Связанный промпт:** `docs/prompts/prompt_for_opus_iac_shturval_modify.md` **Связанный анализ:** `docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md` **Статус:** документирование ответа. Конкретный план НЕ составляется. > ⚠️ **ВАЖНАЯ ПОПРАВКА (2026-09-23, позже).** Ответ Опуса ниже строился на НЕВЕРНОЙ посылке «`vIPConfigure` — накопительный API». Это опровергнуто тестом `docs/ORG_IP_MODIFIER_TEST_2026-09-22.md`: `vIPConfigure` ведёт себя как **replace-состояние** — идемпотентно (1→1), работает в обе стороны (вверх/вниз/до 0), `count` читается из `state.params`. Соответственно «блокеры» (a) Read счётчика и (b) адресное освобождение **сняты как ложные**. Остаётся только (c) Read цепочки `providerVdc → providerGateway → ipSpace`. НЕ использовать прежнюю формулировку «накопительный API несовместим с декларативной моделью» как источник истины. --- ## 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». Факт: `vIPConfigure` — replace-состояние, дельта `target − current` по факту не нужна — достаточно слать целевой `count`, платформа сама выставляет его (идемпотентно). См. `ORG_IP_MODIFIER_TEST_2026-09-22.md`. ### Пункт 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. - **Блокер (нужно от платформы) — только один подтверждённый:** - c) API чтения цепочки providerVdc → providerGateway → ipSpace (иначе data-source невозможен). - **Сняты как ложные (опровергнуты тестом 2026-09-22):** - a) чтение текущего числа vIP — УЖЕ работает через `state.params.vIPConfigure`; - b) адресное освобождение IP — УЖЕ работает: `count` меньше/`0` задаётся тем же `modify`, в обе стороны. - **Вывод:** проектируем форму сейчас, блокер только (c). Новые спеки повлияют на **резолвинг значений**, не на форму ресурсов — если форма построена на «целевое состояние + селектор + data-source». ### Пункт 5 — минимально-инвазивный порядок внедрения **От платформы (до кодинга ресурсов) — обязательно:** - Read цепочки → `ipSpaceName` (для data-source). - Подтверждение, что генератор умеет строить схему из объединения `create`+`modify` полей (иначе `vIPConfigure`/`ipSpaceName` вообще не попадут в схему). > ⚠️ Read счётчика vIP и адресное освобождение — НЕ блокеры (уже подтверждено тестом). Исключены. **На стороне провайдера — можно сейчас, не дожидаясь:** - Раздельные ресурсы vip-allocation / nsxt-snat с `depends_on`. - Логика «target − current = дельта» (заглушка current, пока нет Read). - Data-source-скелет для `ipSpaceName` (с TODO на реальный обход цепочки). - Optional+computed селектор шлюза. - Inverse-контракт (Delete = обратный modify до нуля). **Главный неустранимый на нашей стороне блокер:** ❌ СНЯТ — строился на ложной посылке «накопительный API». Реальный остаточный блокер — только (c) чтение цепочки providerVdc → providerGateway → ipSpace (для data-source `ipSpaceName`). --- ## 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. **Блокеры от платформы** — из трёх заявленных Опуса два (Read счётчика, адресное освобождение) **ложны** (опровергнуты тестом), остаётся один реальный: Read цепочки providerVdc→providerGateway→ipSpace. ### Главное отличие одной фразой Старые планы отвечали на «**как сделать модификатор в tf**». Опус отвечает на «**как сделать его идемпотентным и IaC-честным**» — но его центральный вывод «ядро проблемы в платформе (накопительный API)» **оказался ошибочным**, т.к. исходная посылка «накопительный» неверна (см. поправку в шапке). Реальный остаток — только `ipSpaceName` (цепочка providerVdc→providerGateway→ipSpace) и селектор шлюза. --- ## 4. Спорный/непроверенный момент — РАЗРЕШЁН Посылка Опуса «накопительный `vIPConfigure` без Read-счётчика и адресного освобождения несовместим с декларативной моделью» **опровергнута** тестом `docs/ORG_IP_MODIFIER_TEST_2026-09-22.md`: - повторный `modify` с тем же `count` не аккумулирует IP (1→1) → идемпотентно; - `count` меняется в обе стороны (2→1→0) через тот же `modify` → «адресное освобождение» не нужно, достаточно задать меньший/нулевой `count`; - `count=0` принимается (несмотря на `minvalue:1` в схеме), элемент ipSpace остаётся в `state.params`; - Read уже есть: `state.params.vIPConfigure = [{"name":"internet-ipv4-v1","count":N}]`. Единственный реально непроверенный момент: полное удаление ipSpace (`[]` / отсутствие элемента) — тест этого не покрывал. Для IaC-задачи «обнулить» достаточно, полное удаление — опционально. --- ## 5. Резюме (с поправкой) - Форма ресурсов — проектируем сейчас, она не зависит от спеков. - `vIPConfigure` — **НЕ блокер**: идемпотентно, обе стороны, `count` читается/задаётся из `state.params` (тест 2026-09-22). - Единственный подтверждённый блокер: **Read цепочки `providerVdc → providerGateway → ipSpace`** для data-source `ipSpaceName` (c). - Непроверено: полное удаление ipSpace (`[]`), но для задачи достаточно `count=0`. - Конкретный план внедрения пока НЕ составляется (по решению пользователя). **Следующий возможный шаг (только по запросу):** проверить (c) чтение цепочки ipSpace живым API.