184 lines
8.6 KiB
Markdown
184 lines
8.6 KiB
Markdown
# ПЛАН реализации: редизайн ресурсов-модификаторов (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-разметка (источник-канон) в `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`.
|
||
|
||
---
|
||
|
||
## Порядок коммитов (по смыслу)
|
||
|
||
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): реестр исключений модификаторов (delete_strategy/idempotency)`
|
||
7. `test(core,gen): unit-кейсы`
|
||
8. `chore(dev): bump версии`
|
||
|
||
---
|
||
|
||
## Открытый вопрос — закрыт
|
||
|
||
Источник-канон — реестр `serviceSpecificModifiers` в `TOOLS/yaml-generator/main.go`.
|
||
Разметка `delete_strategy`/`idempotency` расширяет этот реестр, а НЕ правится вручную
|
||
в `generated/dev`.
|