- баннеры «ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО» на 4 файла HISTORY/OPUS/2026-09-22_modifier_* и docs/60_strategy/modifier_resources_ideology_and_specification.md - vIPConfigure: replace-семантика, НЕ накопительная (по тесту docs/ORG_IP_MODIFIER_TEST_2026-09-22.md) - обновлены ссылки на перенесённые материалы (docs/... -> NOTES/..., HOW_TO/...)
15 KiB
⛔ ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ. Архитектура отменённого захода (
kind: modifierв YAML + реестр в генераторе). История. Актуально:NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md.
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:
- взять все
SchemaParams; - заданные пользователем → значение из плана;
- незаданные → live → default (уже в
operation_run_bycode.go); - 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:
noop_warn— удалить из state +AddWarning(дефолт для необратимых:ip_space);inverse— если есть «выключающие» значения в modify (напр.needEnableAVI:false);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 (не закрыты ответом)
- Где брать значения для
inverse-стратегии Delete? Дляnetwork«выключающие» значения — это хардкод per-modifier? Как их задать декларативно в YAML, без хардкода в генераторе? - Формат YAML новых полей. Точная схема
delete_strategyиidempotency: enum-значения, дефолты, валидация (fail-fast на неизвестных). - Pre-check «desired == current» — где читать current? Через
RefreshResourceState/state_paramsили отдельный GET? Как сериализовать сравнение для map-fixed/array-map-fixed (порядок ключей)? - Как пометить модификатор «idempotency: check_before_run» на уровне YAML (а не хардкодом в коде reconcile)?
- Что если желаемое == текущее, но была «частичная» ошибка ранее — пропускать run безопасно всегда, или есть исключения?
Ответы Opus №2 (уточнения по 5 вопросам)
1. inverse-Delete — только декларативно в YAML, хардкод запрещён
Обратный payload зависит от параметров: needEnableAVI:false валиден, а
virtualServicesCount (integer > 0) обнулить нечем → 0 невозможен.
Значит inverse-значения задаются явным блоком в YAML. Если хоть один параметр
не имеет валидного inverse — стратегия inverse недопустима (fail-fast в загрузчике).
Для ip_space inverse нет вообще → только noop_warn.
2. Точная схема YAML новых полей
operations:
- kind: modifier
modifier: network
action: modify
delete_strategy: noop_warn # enum: noop_warn | inverse | error
idempotency: check_before_run # enum: none | check_before_run
delete_params: # обязателен ТОЛЬКО при delete_strategy: inverse
- code: needEnableAVI
value: "false"
params: [...]
Go-контракт (OperationSpec):
DeleteStrategy string `yaml:"delete_strategy,omitempty"` // "" → noop_warn
Idempotency string `yaml:"idempotency,omitempty"` // "" → none
DeleteParams []ParamSpec `yaml:"delete_params,omitempty"`
Дефолты: delete_strategy → noop_warn; idempotency → none.
Fail-fast в ValidateSpec: значение вне enum → ошибка; inverse с пустым
delete_params → ошибка; delete_params.code нет в params → ошибка; inverse-значение
нарушает constraint параметра → ошибка на этапе генерации.
GenModifier получает DeleteStrategy, Idempotency, DeleteParams.
3. Откуда читать current + как сравнивать
Читать из state_params, отдельный GET не делать (это уже источник истины для Read;
второй источник = риск рассогласования).
Сравнение по типу:
| Тип | Как сравнивать |
|---|---|
| bool/int/string | равенство после normalizeUniversalValueV6 |
| map-fixed | JSONStringsEquivalent (игнор порядка ключей) |
| array-map-fixed | deep-equal с сохранением порядка элементов (порядок значим) |
Порядок ключей map-fixed — нормализовать (не значим). Порядок элементов array-map-fixed — НЕ нормализовать (значим).
4. idempotency декларативно
Поле idempotency на modify-операции в YAML → ValidateSpec → GenModifier.Idempotency
→ шаблон modifier.go в reconcile() эмитит pre-check {{- if eq .Idempotency "check_before_run" }}.
Для ip_space — в YAML; для остальных — дефолт none.
5. Когда безопасно skip run при desired == current (НЕ всегда)
Три условия безопасного skip:
- Инстанс idle — если pending/in-progress, сначала
waitForInstanceIdle, потом перечитатьstate_params(иначе mid-flight аллокация даст ложное «уже равно»). - current из живого state_params, НЕ из TF-state — после частичной ошибки TF-state может врать, а state_params отражает реальную платформу.
- Сравнение по всем полям, не по одному — skip только при совпадении ВСЕХ полей.
Итог:
idle? нет → wait, re-read
всё-live == всё-desired? да → skip run
иначе → reconcile (полный payload)
Ответы Opus №3 (сверка с фактическим кодом, расхождения + сомнения)
Факт №1: контракт — в lib, поведение — в GenModifier
Подтверждено: delete_strategy/idempotency/delete_params добавляются в
lib.OperationSpec (TOOLS/lib/types.go), resource-generator получает через алиас
(types.go:19). Ссылка «types.go:39» была неточной — канон в lib.
Граница:
| Где | Что |
|---|---|
lib.OperationSpec / lib.ParamSpec |
всё из YAML, видно обоим генераторам |
GenModifier (локально) |
производные для шаблона, флаги Needs*, готовый inverse-список |
Правило: парсится из YAML → lib; вычисляется загрузчиком для шаблона → GenModifier.
Факт №2: pre-check — в core (вариант A), экспортировать сравнение
normalizeUniversalValueV6 приватная и требует universalCfsParam — в resources_core
этих данных нет. Pre-check делать в core, не в resources_core и не в шаблоне.
Конкретно — экспортированный метод в core, вызывается из operation_run_bycode.go
сразу после fetchOperationCfsParams:
func (c *UniversalClient) modifierDesiredEqualsCurrent(
desired map[string]string, cfsParams []universalCfsParam) bool
Сравнение: нормализовать обе стороны через normalizeUniversalValueV6; для
map-fixed/array-map-fixed — JSON-эквивалентность. Но JSONStringsEquivalent лежит в
resources_core → импорт в core даст цикл. Вынести JSON-эквивалентность в
нейтральный пакет (core/jsonutil или в сам core) и переиспользовать в обоих местах.
Вариант C (только JSONStringsEquivalent без нормализации) — отклонён (ложный diff
true/1).
Управление: RunInstanceOperationUniversalByCode получает флаг idempotent (из
GenModifier.Idempotency → шаблон → параметр вызова); idle-гейт выше pre-check.
Дополнительные сомнения (ответы)
- Idempotency и полный payload НЕ конфликтуют (разные уровни). Бинарно на весь
модификатор:
ALL == ALL→ skip целиком; любое расхождение → полный payload. Полудельты нет. - Частичный inverse — допустим и правилен.
delete_paramsпокрывает только обратимые поля; необратимые/constraint просто не входят. Fail-fast смягчить: ошибка не «inverse обязан покрыть всё», а «код в delete_params обязан существовать в params и value удовлетворять constraint». Delete при inverse = modify с delete_params + досылка live остальных (полный payload). noop_warnдефолт — оставить, но критичные — вручнуюerror. Дефолт мягкий (noop_warn, всегда сAddWarning), а необратимые (ip_space) автор спеки явно помечаетdelete_strategy: errorв YAML. Генератор сам не решает обратимо/необратимо.