docs(plan): детальный план редизайна модификаторов (10 шагов + коммиты)

This commit is contained in:
Repinoid
2026-09-22 20:50:37 +03:00
parent c9c69a3d11
commit fba4cbda37
+202
View File
@@ -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), где держать эти флаги.