docs(plan): внести правки ревью Опуса в план (порядок, ID-миграция, override, array-map-fixed)
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
# Opus: ревью плана редизайна модификаторов — 2026-09-22
|
||||||
|
|
||||||
|
Источник: ответ на `prompt_for_opus_modifier_plan_review.md` (план `PLAN_modifier_redesign.md`).
|
||||||
|
|
||||||
|
## 1. Порядок шагов — скрытые зависимости
|
||||||
|
|
||||||
|
- Шаг 4 (шаблон) ссылается на API из шагов 6–7 → **сначала 5→6→7, потом 4**.
|
||||||
|
- Шаг 8 (yaml-generator) должен идти ДО регенерации `dev` и до сборки.
|
||||||
|
|
||||||
|
Скорректированный порядок: 1 → 2 → 3 → 5 → 6 → 7 → 4 → 8 → регенерация → 9 → 10.
|
||||||
|
|
||||||
|
## 2. Шаг 5 (вынос JSON-эквивалентности)
|
||||||
|
|
||||||
|
Путь верен. **Оставить реэкспорт-обёртку `JSONStringsEquivalent` в `json_normalize.go`**,
|
||||||
|
не заменять вызовы по всему resources_core (иначе диф на инстансы, вопрос 7).
|
||||||
|
Переносятся самодостаточные 5 функций: `JSONStringsEquivalent`, `normalizeJSONIfPossible`,
|
||||||
|
`encodeCanonicalJSON`, `writeCanonicalJSON`, `normalizeJSONScalarsToStrings`.
|
||||||
|
|
||||||
|
Вариант «готовые строки в core» — отклонить (размазывает нормализацию, не снимает
|
||||||
|
потребность в JSONStringsEquivalent в core).
|
||||||
|
|
||||||
|
## 3. Шаг 6 — сигнатура и сравнение
|
||||||
|
|
||||||
|
- Маппинг code→param по **двум** алиасам: `p.Code` И `p.SvcOperationCfsParam` (как в
|
||||||
|
operation_run_bycode.go:50-58). Один `Code` даст пропуски.
|
||||||
|
- Имя `modifierDesiredEqualsCurrent` — unexported, вызов внутри core. Слово «экспортированный» убрать.
|
||||||
|
- bool/int/string — `normalizeUniversalValueV6` + сравнение. map-fixed — `JSONStringsEquivalent`.
|
||||||
|
- **array-map-fixed — дыра:** `normalizeUniversalValueV6` строит дефолт только для
|
||||||
|
`map-fixed`/`HasPrefix "map"` (params.go:33); `array-map-fixed` туда не попадает →
|
||||||
|
сравнивать сырые значения через `jsonutil.JSONStringsEquivalent`, не через normalize.
|
||||||
|
- desired = только явно заданные коды (до досылки live/default), иначе pre-check всегда «равно».
|
||||||
|
|
||||||
|
## 4. Шаг 4.4 Delete=inverse — подводный камень
|
||||||
|
|
||||||
|
- Delete не имеет `plan` (только `req.State`). `reconcile(ctx, plan *Model)` не подходит.
|
||||||
|
→ `reconcile(ctx, model *Model, override map[string]string)`; для inverse override = delete_params.
|
||||||
|
- `deleteParams` — финальные **wire-строки** (`"false"`, готовый JSON), БЕЗ прогонки через
|
||||||
|
`ParamFormat`/тип. В реестре `deleteParam{Code, Value}` несёт готовую строку.
|
||||||
|
|
||||||
|
## 5. Шаг 8 — расширение реестра
|
||||||
|
|
||||||
|
Верно. Держать в `serviceSpecificModifiers` (main.go:34), не отдельным реестром.
|
||||||
|
Структура `modifierException` корректна. При переходе со `map[string]string` на структуру:
|
||||||
|
`ModifierName` берётся из структуры (сейчас `modName, ok := serviceSpecificModifiers[name]`
|
||||||
|
— строка 95).
|
||||||
|
|
||||||
|
## 6. Пропущенные кейсы
|
||||||
|
|
||||||
|
- **taint/replace + `delete_strategy=error`** — конфликт: replace = Delete→Create, Delete=error
|
||||||
|
блокирует → пользователь не сможет заменить error-модификатор. Решить явно:
|
||||||
|
запретить replace у error (документировать) или отличить «чистый destroy» от replace.
|
||||||
|
- **unknown в pre-check** — при unknown (computed ref) сравнение невозможно; шаг 6 должен
|
||||||
|
skip-ить pre-check при unknown (иначе пустая строка даст ложный diff/панику).
|
||||||
|
- **partial apply** — досылка live для незаданных + pre-check; проверить кейс «часть задана, часть live».
|
||||||
|
|
||||||
|
## 7. Риск сломать инстансы
|
||||||
|
|
||||||
|
Низкий при условиях:
|
||||||
|
- **НЕ удалять `CompactParams`** (helpers.go:68) — убирается только из modifier-шаблона;
|
||||||
|
функция нужна инстанс/action.
|
||||||
|
- **Шаг 7 — новый метод `RunOperationByCodeIdempotent`, НЕ менять сигнатуру**
|
||||||
|
`RunOperationByCodeWithTimeout`/`RunInstanceOperationUniversalByCode` (зовут инстансы).
|
||||||
|
- Шаг 1 (поля OperationSpec) аддитивен — безопасно.
|
||||||
|
|
||||||
|
## Дополнительно (не в вопросах)
|
||||||
|
|
||||||
|
- **Шаг 4.3 (ID=identity) — ломающая миграция state.** Смена формата ID изменит ID уже
|
||||||
|
задеплоенных модификаторов → Terraform форснёт replace. Нужно: либо сохранить старый
|
||||||
|
формат ID, либо явный state-migration plan. В плане не отмечено.
|
||||||
|
- **Шаг 3 — `normalizeDeleteStrategy`/`normalizeIdempotency`** — где живут (в loader.go, рядом
|
||||||
|
с веткой modifier). Не указано.
|
||||||
|
- **ValidateSpec** — проверка `delete_params.code ∈ op.Params` по lower-code; сверить поле `Code`.
|
||||||
+54
-19
@@ -1,8 +1,11 @@
|
|||||||
# ПЛАН реализации: редизайн ресурсов-модификаторов (kind: modifier)
|
# ПЛАН реализации: редизайн ресурсов-модификаторов (kind: modifier)
|
||||||
|
|
||||||
Основа: `HISTORY/OPUS/2026-09-22_modifier_architecture_project.md`.
|
Основа: `HISTORY/OPUS/2026-09-22_modifier_architecture_project.md`.
|
||||||
|
Ревью плана: `HISTORY/OPUS/2026-09-22_modifier_plan_review.md`.
|
||||||
Цель — закрыть все классы багов A–E, без костылей, по согласованной архитектуре.
|
Цель — закрыть все классы багов 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.Idempotency = normalizeIdempotency(op.Idempotency)` (пусто → `none`);
|
||||||
- `modifier.DeleteParams = ConvertParams(op.DeleteParams)` (при inverse).
|
- `modifier.DeleteParams = ConvertParams(op.DeleteParams)` (при inverse).
|
||||||
|
|
||||||
|
Helpers `normalizeDeleteStrategy`/`normalizeIdempotency` — добавить в `loader.go`
|
||||||
|
(тот же пакет, рядом с веткой modifier).
|
||||||
|
|
||||||
Файл: `loader.go`, `ValidateSpec` — расширить fail-fast для modifier:
|
Файл: `loader.go`, `ValidateSpec` — расширить fail-fast для modifier:
|
||||||
- `delete_strategy` вне enum → ошибка;
|
- `delete_strategy` вне enum → ошибка;
|
||||||
- `delete_strategy == "inverse"` и пуст `delete_params` → ошибка;
|
- `delete_strategy == "inverse"` и пуст `delete_params` → ошибка;
|
||||||
@@ -61,25 +67,34 @@ DeleteParams []Param // из spec.DeleteParams (ConvertParams), толь
|
|||||||
(все заданные поля; решение о досылке — в core).
|
(все заданные поля; решение о досылке — в core).
|
||||||
|
|
||||||
4.2. **Единый `reconcile()`** — вынести общее тело Create/Update в приватный метод
|
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)`
|
4.3. **ID = identity** — `plan.ID = BuildActionID(instanceUID, modifierName)`
|
||||||
(убрать operation из ID). Реализовать через существующий `BuildActionID(instanceUID, "", modifierName)`
|
(убрать operation из ID). Реализовать через существующий `BuildActionID(instanceUID, "", modifierName)`
|
||||||
или новый helper `BuildModifierID(instanceUID, modifierName)`.
|
или новый helper `BuildModifierID(instanceUID, modifierName)`.
|
||||||
|
⚠️ **миграция state:** смена формата ID изменит ID уже задеплоенных модификаторов →
|
||||||
|
Terraform форснёт replace. Принять решение ДО: сохранить старый формат ИЛИ явный
|
||||||
|
state-migration план. По умолчанию — сохранить формат `uid:operation:modifier`, не менять формат.
|
||||||
|
|
||||||
4.4. **Delete по стратегии**:
|
4.4. **Delete по стратегии**:
|
||||||
```
|
```
|
||||||
{{- if eq .DeleteStrategy "error" }}
|
{{- if eq .DeleteStrategy "error" }}
|
||||||
Delete → AddError (запрет destroy)
|
Delete → AddError (запрет destroy); ⚠️ конфликт с replace: replace = Delete→Create,
|
||||||
|
при error пользователь не сможет заменить модификатор. Решение: запретить replace
|
||||||
|
у error-модификаторов (документировать) или отличить «чистый destroy» от replace.
|
||||||
{{- else if eq .DeleteStrategy "inverse" }}
|
{{- else if eq .DeleteStrategy "inverse" }}
|
||||||
Delete → modify с DeleteParams + досылка live остальных (reconcile-вариант)
|
Delete → reconcile(state-model, override=delete_params)
|
||||||
|
(delete_params — финальные wire-строки: "false", готовый JSON; обработать как override)
|
||||||
{{- else }}
|
{{- else }}
|
||||||
Delete → RemoveResource + AddWarning («эффект остаётся на платформе»)
|
Delete → RemoveResource + AddWarning («эффект остаётся на платформе»)
|
||||||
{{- end }}
|
{{- end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
4.5. **Pre-check idempotency** — в reconcile при `eq .Idempotency "check_before_run"`:
|
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`:
|
- создать `provider/internal/core/jsonutil/jsonutil.go`:
|
||||||
перенести `JSONStringsEquivalent` + `normalizeJSONIfPossible` + `encodeCanonicalJSON` +
|
перенести `JSONStringsEquivalent` + `normalizeJSONIfPossible` + `encodeCanonicalJSON` +
|
||||||
`writeCanonicalJSON` + `normalizeJSONScalarsToStrings` из `resources_core/json_normalize.go`;
|
`writeCanonicalJSON` + `normalizeJSONScalarsToStrings` из `resources_core/json_normalize.go`;
|
||||||
- `resources_core/json_normalize.go` оставить как обёртку (реэкспорт `jsonutil.JSONStringsEquivalent`)
|
- `resources_core/json_normalize.go` — **оставить реэкспорт-обёртку** `JSONStringsEquivalent`
|
||||||
или заменить вызовы на `jsonutil.JSONStringsEquivalent`.
|
(не заменять вызовы по resources_core — иначе диф на инстансы).
|
||||||
|
|
||||||
Проверка: `go build ./...`, нет цикла импорта.
|
Проверка: `go build ./...`, нет цикла импорта.
|
||||||
|
|
||||||
@@ -100,16 +115,21 @@ DeleteParams []Param // из spec.DeleteParams (ConvertParams), толь
|
|||||||
|
|
||||||
Файл: `provider/internal/core/operation_run_bycode.go` (или новый `modifier_compare.go`).
|
Файл: `provider/internal/core/operation_run_bycode.go` (или новый `modifier_compare.go`).
|
||||||
|
|
||||||
Добавить экспортированный:
|
Добавить (unexported, вызов внутри core):
|
||||||
```go
|
```go
|
||||||
func (c *UniversalClient) modifierDesiredEqualsCurrent(
|
func (c *UniversalClient) modifierDesiredEqualsCurrent(
|
||||||
desired map[string]string, cfsParams []universalCfsParam) bool
|
desired map[string]string, cfsParams []universalCfsParam) bool
|
||||||
```
|
```
|
||||||
Логика:
|
Логика:
|
||||||
- для каждого desired-кода → найти `universalCfsParam` → live `ParamValue`;
|
- маппинг code→param по **двум** алиасам: `p.Code` И `p.SvcOperationCfsParam`
|
||||||
- нормализовать обе стороны `normalizeUniversalValueV6`;
|
(как в operation_run_bycode.go:50-58);
|
||||||
- map-fixed/array-map-fixed → `jsonutil.JSONStringsEquivalent`;
|
- для каждого desired-кода → live `ParamValue`;
|
||||||
- все поля совпали → true.
|
- 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`
|
Опционально: добавить в `RunInstanceOperationUniversalByCode` параметр `idempotent bool`
|
||||||
(или новый метод-обёртка). В `operation_run_bycode.go` после `fetchOperationCfsParams`:
|
(или новый метод-обёртка). В `operation_run_bycode.go` после `fetchOperationCfsParams`:
|
||||||
@@ -125,9 +145,9 @@ idle-гейт (`waitForInstanceIdle`) уже стоит выше — не тро
|
|||||||
## Шаг 7. Передача флага `idempotent` вплоть до client
|
## Шаг 7. Передача флага `idempotent` вплоть до client
|
||||||
|
|
||||||
Цепочка: шаблон → `resources_core.RunOperationByCodeWithTimeout` → `core.RunInstanceOperationUniversalByCode`.
|
Цепочка: шаблон → `resources_core.RunOperationByCodeWithTimeout` → `core.RunInstanceOperationUniversalByCode`.
|
||||||
- добавить вариант `RunOperationByCodeIdempotent(...)` в `resources_core/crud.go`
|
- **добавить НОВЫЙ метод `RunOperationByCodeIdempotent(...)` в `resources_core/crud.go`**,
|
||||||
(или расширить сигнатуру существующей, не ломая другие вызовы);
|
НЕ менять сигнатуру `RunOperationByCodeWithTimeout` (его зовут инстансы);
|
||||||
- пробросить флаг в `RunInstanceOperationUniversalByCode`.
|
- пробросить флаг в `RunInstanceOperationUniversalByCode` (новый параметр или обёртка).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -161,15 +181,18 @@ var serviceSpecificModifiers = map[string]modifierException{
|
|||||||
В цикле над ops (там, где `Kind="modifier"`): проставить `op.DeleteStrategy`,
|
В цикле над ops (там, где `Kind="modifier"`): проставить `op.DeleteStrategy`,
|
||||||
`op.Idempotency`, `op.DeleteParams`.
|
`op.Idempotency`, `op.DeleteParams`.
|
||||||
|
|
||||||
|
⚠️ При переходе с `map[string]string` на структуру: `ModifierName` берётся из структуры
|
||||||
|
(сейчас `modName, ok := serviceSpecificModifiers[name]` — строка 95 main.go).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Порядок коммитов (по смыслу)
|
## Порядок коммитов (по смыслу)
|
||||||
|
|
||||||
1. `feat(lib): delete_strategy/idempotency/delete_params в OperationSpec`
|
1. `feat(lib): delete_strategy/idempotency/delete_params в OperationSpec`
|
||||||
2. `feat(gen): GenModifier расширение + LoadSpecs + ValidateSpec`
|
2. `feat(gen): GenModifier расширение + LoadSpecs + ValidateSpec + normalize-helpers`
|
||||||
3. `refactor(gen): шаблон modifier — reconcile, без CompactParams, ID identity, Delete стратегия`
|
3. `refactor(core): вынести JSON-эквивалентность в jsonutil (+реэкспорт)`
|
||||||
4. `refactor(core): вынести JSON-эквивалентность в jsonutil`
|
4. `feat(core): modifierDesiredEqualsCurrent + RunOperationByCodeIdempotent`
|
||||||
5. `feat(core): modifierDesiredEqualsCurrent + флаг idempotent`
|
5. `feat(gen): шаблон modifier — reconcile(override), Delete стратегия, ID identity`
|
||||||
6. `feat(yaml): реестр исключений модификаторов (delete_strategy/idempotency)`
|
6. `feat(yaml): реестр исключений модификаторов (delete_strategy/idempotency)`
|
||||||
7. `test(core,gen): unit-кейсы`
|
7. `test(core,gen): unit-кейсы`
|
||||||
8. `chore(dev): bump версии`
|
8. `chore(dev): bump версии`
|
||||||
@@ -181,3 +204,15 @@ var serviceSpecificModifiers = map[string]modifierException{
|
|||||||
Источник-канон — реестр `serviceSpecificModifiers` в `TOOLS/yaml-generator/main.go`.
|
Источник-канон — реестр `serviceSpecificModifiers` в `TOOLS/yaml-generator/main.go`.
|
||||||
Разметка `delete_strategy`/`idempotency` расширяет этот реестр, а НЕ правится вручную
|
Разметка `delete_strategy`/`idempotency` расширяет этот реестр, а НЕ правится вручную
|
||||||
в `generated/dev`.
|
в `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.
|
||||||
|
|||||||
Reference in New Issue
Block a user