8.6 KiB
ПЛАН реализации: редизайн ресурсов-модификаторов (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):
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.
Добавить:
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).
Добавить экспортированный:
func (c *UniversalClient) modifierDesiredEqualsCurrent(
desired map[string]string, cfsParams []universalCfsParam) bool
Логика:
- для каждого desired-кода → найти
universalCfsParam→ liveParamValue; - нормализовать обе стороны
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}:
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.
Порядок коммитов (по смыслу)
feat(lib): delete_strategy/idempotency/delete_params в OperationSpecfeat(gen): GenModifier расширение + LoadSpecs + ValidateSpecrefactor(gen): шаблон modifier — reconcile, без CompactParams, ID identity, Delete стратегияrefactor(core): вынести JSON-эквивалентность в jsonutilfeat(core): modifierDesiredEqualsCurrent + флаг idempotentfeat(yaml): реестр исключений модификаторов (delete_strategy/idempotency)test(core,gen): unit-кейсыchore(dev): bump версии
Открытый вопрос — закрыт
Источник-канон — реестр serviceSpecificModifiers в TOOLS/yaml-generator/main.go.
Разметка delete_strategy/idempotency расширяет этот реестр, а НЕ правится вручную
в generated/dev.