From fba4cbda374fbc6e85f47083c36be928c1badfbd Mon Sep 17 00:00:00 2001 From: Repinoid Date: Tue, 22 Sep 2026 20:50:37 +0300 Subject: [PATCH] =?UTF-8?q?docs(plan):=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=D0=B0=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=20(10=20=D1=88=D0=B0=D0=B3=D0=BE=D0=B2=20+=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BC=D0=B8=D1=82=D1=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLAN_modifier_redesign.md | 202 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 PLAN_modifier_redesign.md diff --git a/PLAN_modifier_redesign.md b/PLAN_modifier_redesign.md new file mode 100644 index 0000000..8596349 --- /dev/null +++ b/PLAN_modifier_redesign.md @@ -0,0 +1,202 @@ +# ПЛАН реализации: редизайн ресурсов-модификаторов (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), где держать эти флаги.