# ПЛАН реализации: редизайн ресурсов-модификаторов (kind: modifier) Основа: `HISTORY/OPUS/2026-09-22_modifier_architecture_project.md`. Цель — закрыть все классы багов A–E, без костылей, по согласованной архитектуре. Порядок шагов строгий: контракт → загрузка → шаблон → core → yaml → пересборка. --- ## Шаг 1. Контракт YAML в `TOOLS/lib/types.go` Файл: `TOOLS/lib/types.go`, `OperationSpec`. Добавить поля (тег yaml, omitempty): ```go DeleteStrategy string `yaml:"delete_strategy,omitempty"` // "" → noop_warn Idempotency string `yaml:"idempotency,omitempty"` // "" → none DeleteParams []ParamSpec `yaml:"delete_params,omitempty"` ``` Enum `delete_strategy`: `noop_warn` | `inverse` | `error`. Enum `idempotency`: `none` | `check_before_run`. Проверка: `TOOLS/resource-generator` получает поля через алиас `OperationSpec = lib.OperationSpec` — отдельной правки не нужно, но `go build ./...` в lib и в resource-generator. --- ## Шаг 2. GenModifier — производные поля Файл: `TOOLS/resource-generator/internal/types/types.go`, `GenModifier`. Добавить: ```go DeleteStrategy string // нормализованный enum (noop_warn|inverse|error) Idempotency string // none|check_before_run DeleteParams []Param // из spec.DeleteParams (ConvertParams), только при inverse ``` --- ## Шаг 3. LoadSpecs — заполнение modifier + валидация Файл: `TOOLS/resource-generator/internal/loader/loader.go`, ветка `op.Kind == "modifier"`. До `continue`: - `modifier.DeleteStrategy = normalizeDeleteStrategy(op.DeleteStrategy)` (пусто → `noop_warn`); - `modifier.Idempotency = normalizeIdempotency(op.Idempotency)` (пусто → `none`); - `modifier.DeleteParams = ConvertParams(op.DeleteParams)` (при inverse). Файл: `loader.go`, `ValidateSpec` — расширить fail-fast для modifier: - `delete_strategy` вне enum → ошибка; - `delete_strategy == "inverse"` и пуст `delete_params` → ошибка; - каждый `delete_params.code` обязан существовать в `op.Params` (сравнение по lower-code) → иначе ошибка; - `idempotency` вне enum → ошибка. --- ## Шаг 4. Шаблон `modifier.go` — редизайн Файл: `TOOLS/resource-generator/internal/templates/modifier.go`. 4.1. **Убрать `CompactParams`** — в Create/Update передавать map напрямую (все заданные поля; решение о досылке — в core). 4.2. **Единый `reconcile()`** — вынести общее тело Create/Update в приватный метод `reconcile(ctx, plan *Model)`, вызываемый из Create и Update. Устраняет дубль веток. 4.3. **ID = identity** — `plan.ID = BuildActionID(instanceUID, modifierName)` (убрать operation из ID). Реализовать через существующий `BuildActionID(instanceUID, "", modifierName)` или новый helper `BuildModifierID(instanceUID, modifierName)`. 4.4. **Delete по стратегии**: ``` {{- if eq .DeleteStrategy "error" }} Delete → AddError (запрет destroy) {{- else if eq .DeleteStrategy "inverse" }} Delete → modify с DeleteParams + досылка live остальных (reconcile-вариант) {{- else }} Delete → RemoveResource + AddWarning («эффект остаётся на платформе») {{- end }} ``` 4.5. **Pre-check idempotency** — в reconcile при `eq .Idempotency "check_before_run"`: передавать флаг в вызов операции (см. шаг 6). --- ## Шаг 5. JSON-эквивалентность в нейтральный пакет (снять цикл импорта) Проблема: `JSONStringsEquivalent` в `resources_core`, а comparison нужен в `core`. - создать `provider/internal/core/jsonutil/jsonutil.go`: перенести `JSONStringsEquivalent` + `normalizeJSONIfPossible` + `encodeCanonicalJSON` + `writeCanonicalJSON` + `normalizeJSONScalarsToStrings` из `resources_core/json_normalize.go`; - `resources_core/json_normalize.go` оставить как обёртку (реэкспорт `jsonutil.JSONStringsEquivalent`) или заменить вызовы на `jsonutil.JSONStringsEquivalent`. Проверка: `go build ./...`, нет цикла импорта. --- ## Шаг 6. core — pre-check `modifierDesiredEqualsCurrent` Файл: `provider/internal/core/operation_run_bycode.go` (или новый `modifier_compare.go`). Добавить экспортированный: ```go func (c *UniversalClient) modifierDesiredEqualsCurrent( desired map[string]string, cfsParams []universalCfsParam) bool ``` Логика: - для каждого desired-кода → найти `universalCfsParam` → live `ParamValue`; - нормализовать обе стороны `normalizeUniversalValueV6`; - map-fixed/array-map-fixed → `jsonutil.JSONStringsEquivalent`; - все поля совпали → true. Опционально: добавить в `RunInstanceOperationUniversalByCode` параметр `idempotent bool` (или новый метод-обёртка). В `operation_run_bycode.go` после `fetchOperationCfsParams`: ``` if idempotent && c.modifierDesiredEqualsCurrent(paramsByID, cfsParams) { return nil // skip run } ``` idle-гейт (`waitForInstanceIdle`) уже стоит выше — не трогать. --- ## Шаг 7. Передача флага `idempotent` вплоть до client Цепочка: шаблон → `resources_core.RunOperationByCodeWithTimeout` → `core.RunInstanceOperationUniversalByCode`. - добавить вариант `RunOperationByCodeIdempotent(...)` в `resources_core/crud.go` (или расширить сигнатуру существующей, не ломая другие вызовы); - пробросить флаг в `RunInstanceOperationUniversalByCode`. --- ## Шаг 8. YAML-разметка (источник-канон) в `TOOLS/yaml-generator` Источник-канон — реестр исключений `serviceSpecificModifiers` в `TOOLS/yaml-generator/main.go` (Ключ — имя сервиса → имя modifier). `generated/dev` перегенерируется — туда НЕ вносить вручную. Контракт в `lib.OperationSpec` (алиас в обоих генераторах), значит yaml-generator должен проставлять флаги при маршале. Расширить реестр со `map[string]string` до структуры, несущей: `ModifierName`, `DeleteStrategy`, `Idempotency`, `DeleteParams []struct{Code,Value}`: ```go type modifierException struct { ModifierName string DeleteStrategy string // noop_warn | inverse | error Idempotency string // none | check_before_run DeleteParams []deleteParam // только для inverse } type deleteParam struct { Code, Value string } var serviceSpecificModifiers = map[string]modifierException{ "vc_org": {ModifierName: "ip_space", DeleteStrategy: "error", Idempotency: "check_before_run"}, "vc_nsxt": {ModifierName: "network", DeleteStrategy: "inverse", DeleteParams: []deleteParam{{"needEnableAVI", "false"}}}, } ``` В цикле над ops (там, где `Kind="modifier"`): проставить `op.DeleteStrategy`, `op.Idempotency`, `op.DeleteParams`. --- ## Порядок коммитов (по смыслу) 1. `feat(lib): delete_strategy/idempotency/delete_params в OperationSpec` 2. `feat(gen): GenModifier расширение + LoadSpecs + ValidateSpec` 3. `refactor(gen): шаблон modifier — reconcile, без CompactParams, ID identity, Delete стратегия` 4. `refactor(core): вынести JSON-эквивалентность в jsonutil` 5. `feat(core): modifierDesiredEqualsCurrent + флаг idempotent` 6. `feat(yaml): реестр исключений модификаторов (delete_strategy/idempotency)` 7. `test(core,gen): unit-кейсы` 8. `chore(dev): bump версии` --- ## Открытый вопрос — закрыт Источник-канон — реестр `serviceSpecificModifiers` в `TOOLS/yaml-generator/main.go`. Разметка `delete_strategy`/`idempotency` расширяет этот реестр, а НЕ правится вручную в `generated/dev`.