docs(plan): внести правки ревью Опуса в план (порядок, ID-миграция, override, array-map-fixed)

This commit is contained in:
Repinoid
2026-09-22 20:57:54 +03:00
parent e06a2c0b11
commit 2933c57a4b
2 changed files with 126 additions and 19 deletions
+54 -19
View File
@@ -1,8 +1,11 @@
# ПЛАН реализации: редизайн ресурсов-модификаторов (kind: modifier)
Основа: `HISTORY/OPUS/2026-09-22_modifier_architecture_project.md`.
Ревью плана: `HISTORY/OPUS/2026-09-22_modifier_plan_review.md`.
Цель — закрыть все классы багов A–E, без костылей, по согласованной архитектуре.
Порядок шагов строгий: контракт → загрузка → шаблон → core → yaml → пересборка.
Порядок шагов (исправлен по ревью): шаблон (4) зависит от core/resources_core (5–7),
поэтому: 1 → 2 → 3 → 5 → 6 → 7 → 4 → 8 → регенерация → 9 → 10.
---
@@ -45,6 +48,9 @@ DeleteParams []Param // из spec.DeleteParams (ConvertParams), толь
- `modifier.Idempotency = normalizeIdempotency(op.Idempotency)` (пусто → `none`);
- `modifier.DeleteParams = ConvertParams(op.DeleteParams)` (при inverse).
Helpers `normalizeDeleteStrategy`/`normalizeIdempotency` — добавить в `loader.go`
(тот же пакет, рядом с веткой modifier).
Файл: `loader.go`, `ValidateSpec` — расширить fail-fast для modifier:
- `delete_strategy` вне enum → ошибка;
- `delete_strategy == "inverse"` и пуст `delete_params` → ошибка;
@@ -61,25 +67,34 @@ DeleteParams []Param // из spec.DeleteParams (ConvertParams), толь
(все заданные поля; решение о досылке — в core).
4.2. **Единый `reconcile()`** — вынести общее тело Create/Update в приватный метод
`reconcile(ctx, plan *Model)`, вызываемый из Create и Update. Устраняет дубль веток.
`reconcile(ctx, model *Model, override map[string]string)`, вызываемый из Create и Update
(override=nil). Устраняет дубль веток. **override нужен для Delete=inverse** (см. 4.4),
так как Delete не имеет plan — только state.
4.3. **ID = identity** — `plan.ID = BuildActionID(instanceUID, modifierName)`
(убрать operation из ID). Реализовать через существующий `BuildActionID(instanceUID, "", modifierName)`
или новый helper `BuildModifierID(instanceUID, modifierName)`.
⚠️ **миграция state:** смена формата ID изменит ID уже задеплоенных модификаторов →
Terraform форснёт replace. Принять решение ДО: сохранить старый формат ИЛИ явный
state-migration план. По умолчанию — сохранить формат `uid:operation:modifier`, не менять формат.
4.4. **Delete по стратегии**:
```
{{- if eq .DeleteStrategy "error" }}
Delete → AddError (запрет destroy)
Delete → AddError (запрет destroy); ⚠️ конфликт с replace: replace = Delete→Create,
при error пользователь не сможет заменить модификатор. Решение: запретить replace
у error-модификаторов (документировать) или отличить «чистый destroy» от replace.
{{- else if eq .DeleteStrategy "inverse" }}
Delete → modify с DeleteParams + досылка live остальных (reconcile-вариант)
Delete → reconcile(state-model, override=delete_params)
(delete_params — финальные wire-строки: "false", готовый JSON; обработать как override)
{{- else }}
Delete → RemoveResource + AddWarning («эффект остаётся на платформе»)
{{- end }}
```
4.5. **Pre-check idempotency** — в reconcile при `eq .Idempotency "check_before_run"`:
передавать флаг в вызов операции (см. шаг 6).
передавать флаг в вызов операции (см. шаг 6). ⚠️ при unknown (computed ref) pre-check
skip — сравнение невозможно.
---
@@ -89,8 +104,8 @@ DeleteParams []Param // из spec.DeleteParams (ConvertParams), толь
- создать `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`.
- `resources_core/json_normalize.go` — **оставить реэкспорт-обёртку** `JSONStringsEquivalent`
(не заменять вызовы по resources_core — иначе диф на инстансы).
Проверка: `go build ./...`, нет цикла импорта.
@@ -100,16 +115,21 @@ DeleteParams []Param // из spec.DeleteParams (ConvertParams), толь
Файл: `provider/internal/core/operation_run_bycode.go` (или новый `modifier_compare.go`).
Добавить экспортированный:
Добавить (unexported, вызов внутри core):
```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.
- маппинг code→param по **двум** алиасам: `p.Code` И `p.SvcOperationCfsParam`
(как в operation_run_bycode.go:50-58);
- для каждого desired-кода → live `ParamValue`;
- bool/int/string → нормализовать обе стороны `normalizeUniversalValueV6` + сравнение строк;
- map-fixed → `jsonutil.JSONStringsEquivalent`;
- **array-map-fixed → `jsonutil.JSONStringsEquivalent` по сырым значениям, НЕ через normalize**
(`normalizeUniversalValueV6` не строит дефолт для array-map-fixed, params.go:33);
- desired — только явно заданные коды (до досылки live/default);
- если desired содержит unknown (computed ref) — сравнение невозможно, pre-check пропустить.
Опционально: добавить в `RunInstanceOperationUniversalByCode` параметр `idempotent bool`
(или новый метод-обёртка). В `operation_run_bycode.go` после `fetchOperationCfsParams`:
@@ -125,9 +145,9 @@ idle-гейт (`waitForInstanceIdle`) уже стоит выше — не тро
## Шаг 7. Передача флага `idempotent` вплоть до client
Цепочка: шаблон → `resources_core.RunOperationByCodeWithTimeout` → `core.RunInstanceOperationUniversalByCode`.
- добавить вариант `RunOperationByCodeIdempotent(...)` в `resources_core/crud.go`
(или расширить сигнатуру существующей, не ломая другие вызовы);
- пробросить флаг в `RunInstanceOperationUniversalByCode`.
- **добавить НОВЫЙ метод `RunOperationByCodeIdempotent(...)` в `resources_core/crud.go`**,
НЕ менять сигнатуру `RunOperationByCodeWithTimeout` (его зовут инстансы);
- пробросить флаг в `RunInstanceOperationUniversalByCode` (новый параметр или обёртка).
---
@@ -161,15 +181,18 @@ var serviceSpecificModifiers = map[string]modifierException{
В цикле над ops (там, где `Kind="modifier"`): проставить `op.DeleteStrategy`,
`op.Idempotency`, `op.DeleteParams`.
⚠️ При переходе с `map[string]string` на структуру: `ModifierName` берётся из структуры
(сейчас `modName, ok := serviceSpecificModifiers[name]` — строка 95 main.go).
---
## Порядок коммитов (по смыслу)
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`
2. `feat(gen): GenModifier расширение + LoadSpecs + ValidateSpec + normalize-helpers`
3. `refactor(core): вынести JSON-эквивалентность в jsonutil (+реэкспорт)`
4. `feat(core): modifierDesiredEqualsCurrent + RunOperationByCodeIdempotent`
5. `feat(gen): шаблон modifier — reconcile(override), Delete стратегия, ID identity`
6. `feat(yaml): реестр исключений модификаторов (delete_strategy/idempotency)`
7. `test(core,gen): unit-кейсы`
8. `chore(dev): bump версии`
@@ -181,3 +204,15 @@ var serviceSpecificModifiers = map[string]modifierException{
Источник-канон — реестр `serviceSpecificModifiers` в `TOOLS/yaml-generator/main.go`.
Разметка `delete_strategy`/`idempotency` расширяет этот реестр, а НЕ правится вручную
в `generated/dev`.
---
## Решения, нуждающиеся в подтверждении (из ревью)
1. **ID=identity → миграция state.** Смена формата заставит Terraform replace уже
задеплоенных модификаторов. Предлагаю: НЕ менять формат ID (оставить
`uid:operation:modifier`), а idempotency обеспечить pre-check, не трогая ID.
2. **`error` + replace.** Пользователь не сможет заменить error-модификатор.
Предлагаю: оставить `error` только для «чистого» destroy, документировать запрет replace.
3. **Разметка по default** — `ip_space`: `delete_strategy=error`, `idempotency=check_before_run`;
`network`: `delete_strategy=inverse`, delete_params=[needEnableAVI=false], idempotency=none.