Files
tf_provider/PLAN_modifier_redesign.md
T

219 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ПЛАН реализации: редизайн ресурсов-модификаторов (kind: modifier)
Основа: `HISTORY/OPUS/2026-09-22_modifier_architecture_project.md`.
Ревью плана: `HISTORY/OPUS/2026-09-22_modifier_plan_review.md`.
Цель — закрыть все классы багов A–E, без костылей, по согласованной архитектуре.
Порядок шагов (исправлен по ревью): шаблон (4) зависит от core/resources_core (5–7),
поэтому: 1 → 2 → 3 → 5 → 6 → 7 → 4 → 8 → регенерация → 9 → 10.
---
## Шаг 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).
Helpers `normalizeDeleteStrategy`/`normalizeIdempotency` — добавить в `loader.go`
(тот же пакет, рядом с веткой modifier).
Файл: `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, 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); ⚠️ конфликт с replace: replace = Delete→Create,
при error пользователь не сможет заменить модификатор. Решение: запретить replace
у error-модификаторов (документировать) или отличить «чистый destroy» от replace.
{{- else if eq .DeleteStrategy "inverse" }}
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). ⚠️ при unknown (computed ref) pre-check
skip — сравнение невозможно.
---
## Шаг 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` — **оставить реэкспорт-обёртку** `JSONStringsEquivalent`
(не заменять вызовы по resources_core — иначе диф на инстансы).
Проверка: `go build ./...`, нет цикла импорта.
---
## Шаг 6. core — pre-check `modifierDesiredEqualsCurrent`
Файл: `provider/internal/core/operation_run_bycode.go` (или новый `modifier_compare.go`).
Добавить (unexported, вызов внутри core):
```go
func (c *UniversalClient) modifierDesiredEqualsCurrent(
desired map[string]string, cfsParams []universalCfsParam) bool
```
Логика:
- маппинг 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`:
```
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`**,
НЕ менять сигнатуру `RunOperationByCodeWithTimeout` (его зовут инстансы);
- пробросить флаг в `RunInstanceOperationUniversalByCode` (новый параметр или обёртка).
---
## Шаг 8. YAML-разметка (источник-канон) в `TOOLS/yaml-generator`
Источник-канон — реестр исключений `serviceSpecificModifiers` в
`TOOLS/yaml-generator/main.go` (Ключ — имя сервиса → имя modifier).
`generated/dev` перегенерируется — туда НЕ вносить вручную.
Контракт в `lib.OperationSpec` (алиас в обоих генераторах), значит yaml-generator
должен проставлять флаги при маршале. Расширить реестр со `map[string]string`
до структуры, несущей: `ModifierName`, `DeleteStrategy`, `Idempotency`,
`DeleteParams []struct{Code,Value}`:
```go
type modifierException struct {
ModifierName string
DeleteStrategy string // noop_warn | inverse | error
Idempotency string // none | check_before_run
DeleteParams []deleteParam // только для inverse
}
type deleteParam struct { Code, Value string }
var serviceSpecificModifiers = map[string]modifierException{
"vc_org": {ModifierName: "ip_space", DeleteStrategy: "error", Idempotency: "check_before_run"},
"vc_nsxt": {ModifierName: "network", DeleteStrategy: "inverse",
DeleteParams: []deleteParam{{"needEnableAVI", "false"}}},
}
```
В цикле над 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 + 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 версии`
---
## Открытый вопрос — закрыт
Источник-канон — реестр `serviceSpecificModifiers` в `TOOLS/yaml-generator/main.go`.
Разметка `delete_strategy`/`idempotency` расширяет этот реестр, а НЕ правится вручную
в `generated/dev`.
---
## Решения, нуждающиеся в подтверждении (из ревью)
1. **ID=identity → РЕШЕНО: формат ID НЕ меняем** (оставить `uid:operation:modifier`).
Смена формата форснёт replace у задеплоенных модификаторов и вызовет баг E.
Идемпотентность — через pre-check, не через ID. Шаг 4.3 отменён (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.