Files
tf_provider/HISTORY/OPUS/2026-09-22_modifier_architecture_project.md
T

7.3 KiB

Opus: архитектура модификаторов (project, полный) — 2026-09-22

Источник: ответ Opus на prompt_for_opus_modifier_architecture_full.md.

Ключевая модель

Модификатор — декларативная проекция подмножества полей родителя, а не «действие». Отсюда:

  • один reconcile (Create ≡ Update), не два разных пути;
  • payload всегда полный по своим полям (не дельта);
  • источник истины — родитель; модификатор в state хранит только read-back.

1. Сравнить и применить — полный payload, не дельта

Дельта запрещена: бэкенд трактует отсутствующий параметр как reset-to-default (класс A). Reconcile:

  1. взять все SchemaParams;
  2. заданные пользователем → значение из плана;
  3. незаданные → live → default (уже в operation_run_bycode.go);
  4. drift в Read — сравнение модели с state_params родителя (state_refresh.go).

2. Досылка незаданных (заливы A и B) — канон

CompactParams в шаблоне + досылка в клиенте — два конца одного бага. Правило по приоритету (уже в operation_run_bycode.go:98-118):

Ситуация Что слать
задан пользователем значение из плана
не задан, есть live ParamValue live
не задан, нет live, есть DefaultValue дефолт
не задан, ничего нет пропустить (не синтезировать)

Дыра: CompactParams в modifier.go:92 выкидывает пустые ДО клиента (теряется «задал пусто» vs «не задал»). → Убрать CompactParams из шаблона модификатора, передавать map напрямую. Единственная точка решения — клиент. CompactParams оставить только для instance-ресурсов.

3. Delete / rollback

No-op Delete = скрытый drift (класс D). Пока обратный payload не подтверждён — допустимы 3 стратегии через флаг YAML delete_strategy:

  1. noop_warn — удалить из state + AddWarning (дефолт для необратимых: ip_space);
  2. inverse — если есть «выключающие» значения в modify (напр. needEnableAVI:false);
  3. error — запретить destroy (AddError), если откат критичен.

Обратный payload — та же modify с выключающими значениями. Для ip_space его нет → только noop_warn.

4. Idempotency + ID

  • ID = идентичность (родитель + имя модификатора) = instanceUID:modifierName. Это правильно и не должен меняться per-apply. opUid в ID не класть (иначе replace). opUid — только в лог/приватный state.
  • Двойная аллокация (класс E) защищается не ID, а идемпотентностью modify: pre-check «desired == current» → пропустить run. Для ip_space перед modify читать state_params; если целевое достигнуто — skip.

5. Связь с родителем

  • <service>_id — ссылка на родителя (Required, уже так). depends_on не нужен — пользователь передаёт UUID.
  • Borrow state не нужен: Read тянет state_params родителя по UUID.
  • Родителя нет (ShouldRemoveFromState) → модификатор удаляется из state (уже есть).

6. Create vs Update

Единый reconcile(ctx, plan); Create и Update вызывают его (устраняет дубль веток).

7. Полный перечень кейсов (13 шт)

# Кейс Поведение
1 create родителя → create модификатора reconcile, полный payload
2 изменение одного поля полный payload, соседние не сбрасываются (A)
3 partial params досылка live→default→skip (B)
4 integer > 0 без значения/дефолта пропустить (не слать "0")
5 is_modifiable:true (needEnableAVI) не CreateOnly, менять без replace (C)
6 destroy модификатора по delete_strategy (D)
7 replace/taint reconcile + idempotency pre-check (E)
8 повторный apply без изменений desired==current → skip
9 родитель удалён remove из state
10 API не вернул код в state_params unknown→null (уже)
11 два модификатора разных типов разные ID
12 operation in progress waitForInstanceIdle (уже)
13 drift на платформе Read → план показывает изменение

Сводка мест правки

Место Правка
modifier.go:92 убрать CompactParams → прямой map (п.2)
modifier.go:77 единый reconcile() (п.6)
modifier.go:156 delete_strategy (п.3)
modifier.go:100 ID = instanceUID:modifierName (п.4)
RunOperationByCodeWithTimeout / reconcile idempotency pre-check (п.4,7)
params.go:116 учитывать modifier-канал/is_modifiable (класс C, кейс 5)
loader модификаторов YAML-поля delete_strategy, idempotency
operation_run_bycode.go оставить как есть (guard корректен)

Открытые вопросы к Opus (не закрыты ответом)

  1. Где брать значения для inverse-стратегии Delete? Для network «выключающие» значения — это хардкод per-modifier? Как их задать декларативно в YAML, без хардкода в генераторе?
  2. Формат YAML новых полей. Точная схема delete_strategy и idempotency: enum-значения, дефолты, валидация (fail-fast на неизвестных).
  3. Pre-check «desired == current» — где читать current? Через RefreshResourceState/state_params или отдельный GET? Как сериализовать сравнение для map-fixed/array-map-fixed (порядок ключей)?
  4. Как пометить модификатор «idempotency: check_before_run» на уровне YAML (а не хардкодом в коде reconcile)?
  5. Что если желаемое == текущее, но была «частичная» ошибка ранее — пропускать run безопасно всегда, или есть исключения?