Files
tf_provider/docs/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md
T
2026-09-24 07:25:27 +03:00

14 KiB
Raw Blame History

Ответ 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.