docs(plan): детальный план редизайна модификаторов (10 шагов + коммиты)
This commit is contained in:
@@ -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), где держать эти флаги.
|
||||
Reference in New Issue
Block a user