Files
tf_provider/PLAN_modifier_redesign.md
T

8.6 KiB
Raw Blame History

ПЛАН реализации: редизайн ресурсов-модификаторов (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 → 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}:

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.