# ПЛАН реализации: редизайн ресурсов-модификаторов (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-разметка в `generated/dev/resources_yaml/` `19_vc_org.yaml` (modify 207, modifier ip_space): ```yaml - name: modify id: 207 kind: modifier modifier: ip_space delete_strategy: error idempotency: check_before_run ``` `22_vc_nsxt.yaml` (modify 111, modifier network): ```yaml - name: modify id: 111 kind: modifier modifier: network delete_strategy: inverse delete_params: - code: needEnableAVI value: "false" ``` ВАЖНО: `generated/dev` — gitignored/генерируемый. Канон YAML — в yaml-generator (источник из API), поэтому разметку `delete_strategy`/`idempotency` надо вносить в **источник** (механизм исключений yaml-generator, а не в generated), иначе перегенерация её сотрёт. Уточнить: где живёт источник правил модификаторов (registry исключений). --- ## Шаг 9. Тесты `provider/internal/core/` — unit: - `modifierDesiredEqualsCurrent`: совпадение/расхождение по bool/int/map-fixed/array-map-fixed; - досылка: live→default→skip (integer>0 без дефолта не шлёт `"0"`). `TOOLS/resource-generator` — unit: - `ValidateSpec`: неизвестный enum, inverse без delete_params, delete_params.code вне params. --- ## Шаг 10. Сборка и заливка - `go build ./...` + `go vet ./...` по всем 5 модулям; - подать сборку `TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev` (bump версии отдельным коммитом). --- ## Порядок коммитов (по смыслу) 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): разметка ip_space/network` 7. `test(core,gen): unit-кейсы` 8. `chore(dev): bump версии` --- ## Открытый вопрос (блокирует шаг 8) **Где живёт источник-канон YAML модификаторов** (не `generated/dev`)? Разметку `delete_strategy`/`idempotency` надо вносить в источник, а не в сгенерированный файл, иначе перегенерация (`01_generate_yamls.sh`) сотрёт. Нужно найти механизм (yaml-generator exceptions / registry), где держать эти флаги.