Files
tf_provider/PLAN_modifier_redesign.md
T

9.0 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-разметка в generated/dev/resources_yaml/

19_vc_org.yaml (modify 207, modifier ip_space):

    - name: modify
      id: 207
      kind: modifier
      modifier: ip_space
      delete_strategy: error
      idempotency: check_before_run

22_vc_nsxt.yaml (modify 111, modifier network):

    - name: modify
      id: 111
      kind: modifier
      modifier: network
      delete_strategy: inverse
      delete_params:
        - code: needEnableAVI
          value: "false"

ВАЖНО: generated/dev — gitignored/генерируемый. Канон YAML — в yaml-generator (источник из API), поэтому разметку delete_strategy/idempotency надо вносить в источник (механизм исключений yaml-generator, а не в generated), иначе перегенерация её сотрёт. Уточнить: где живёт источник правил модификаторов (registry исключений).


Шаг 9. Тесты

provider/internal/core/ — unit:

  • modifierDesiredEqualsCurrent: совпадение/расхождение по bool/int/map-fixed/array-map-fixed;
  • досылка: live→default→skip (integer>0 без дефолта не шлёт "0").

TOOLS/resource-generator — unit:

  • ValidateSpec: неизвестный enum, inverse без delete_params, delete_params.code вне params.

Шаг 10. Сборка и заливка

  • go build ./... + go vet ./... по всем 5 модулям;
  • подать сборку TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev (bump версии отдельным коммитом).

Порядок коммитов (по смыслу)

  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): разметка ip_space/network
  7. test(core,gen): unit-кейсы
  8. chore(dev): bump версии

Открытый вопрос (блокирует шаг 8)

Где живёт источник-канон YAML модификаторов (не generated/dev)? Разметку delete_strategy/idempotency надо вносить в источник, а не в сгенерированный файл, иначе перегенерация (01_generate_yamls.sh) сотрёт. Нужно найти механизм (yaml-generator exceptions / registry), где держать эти флаги.