diff --git a/HISTORY/2026-09-30_uuid_case_normalization_on_create.md b/HISTORY/2026-09-30_uuid_case_normalization_on_create.md new file mode 100644 index 0000000..3d5cb59 --- /dev/null +++ b/HISTORY/2026-09-30_uuid_case_normalization_on_create.md @@ -0,0 +1,87 @@ +# 2026-09-30 — Регистр UUID: нормализация на ОТПРАВКЕ в API (create/modify/redeploy) + +> Разбор: `docs/60_strategy/terraform_case_sensitivity_fix.md` §11 (главный документ по теме), +> `NOTES/30_analysis/ARCHITECTURE_NEW.md` §6.5. + +## Что обнаружилось + +Костыль `lower(...)` в конфиге стенда Штурвала — **не «просто проще», а обязателен**. +Без него `terraform apply` (create кластера) падает: платформа отвечает +«Edge не развёрнут в указанном vDC». + +Обнаружено при запуске Terraform **из-под Windows**, на провайдере **2.0.23** +(то есть после всех «фиксов регистра», выпущенных 24.09). + +Костыль живёт в примере (и в gitea `Nail/tf_examples`): + +```hcl +# tf_examples/fullpipe_chain/shturval.tf:139-140 +vdc_uid = lower(nubes_vc_vdc.vdc.id) +nsxt_uid = lower(nubes_vc_nsxt.edge.id) +``` + +## Почему прошлые фиксы не помогли (главная мысль) + +Провайдер `2.0.23` нормализует регистр UUID **только при СРАВНЕНИИ**: +план vs state, adopt/suspend/resume, modifier-compare, диагностика +(`jsonutil.LowercaseUUIDsInText` → `JSONStringsEquivalent`, `JsonNormalize()`, +`ParamsMatchForResume`, `normalizeCompareValue`). + +**Путь ОТПРАВКИ в API остался без нормализации.** Все map-fixed JSON-параметры +собираются одной функцией `resources_core.BuildJSON` +(`provider/internal/resources_core/helpers.go`), а её вызывает сгенерированный код +(`NestedJSONExpr`, шаблон `TOOLS/resource-generator/internal/templates/instance.go`, +ветки Create / Modify / Redeploy). `BuildJSON` берёт `ValueString()` подполей **как есть**. + +Ресурс `nubes_vc_nsxt` отдаёт `id` в UPPERCASE (`2C37FED1-…`), платформа хранит +UUID в lowercase и **сравнивает регистр при create** → `startupConfiguration.nsxtUid` +в верхнем регистре отвергается. + +Почему не спас `resolveRefSvcParamValues` (`core/refsvc.go`, +`core/refsvc_resolve.go`): он нормализует только **top-level** refSvc-параметры и +`s3.*uid` **внутри** map-fixed. `vdcUid`/`nsxtUid` — обычные строковые подполя +JSON, refSvcId у них нет, под шаблон `s3.*uid` они не подпадают. + +## Что сделано + +| Файл | Изменение | +|---|---| +| `provider/internal/resources_core/helpers.go` | `BuildJSON` оборачивает результат в `jsonutil.LowercaseUUIDsInText(...)` (+ импорт `core/jsonutil`, комментарий-обоснование) | + +Одна точка → покрыты **все** map-fixed-параметры всех ресурсов на +create / modify / redeploy (19 сгенерированных ресурсов, `resources_gen/`). +Регенерация не требуется (логика сериализации одна). + +## Оценка риска (почему это безопасно) + +- `BuildJSON` используется **только для отправки** в API, не для построения state. +- Regex `uuidAnywhereRegex` = `[0-9a-f]{8}-xxxx-xxxx-xxxx-xxxxxxxxxxxx` — совпадает + только с UUID; пароли/имена/произвольные строки не задевает. +- Проверено по спекам: внутри map-fixed **нет** строковых секретных полей + (password/secret/token) — только `*Uid`-ссылки на ресурсы. +- Это **выравнивание** с уже принятым в провайдере правилом «регистр UUID незначим» + (то же приведение уже делается на сравнении), а не новое поведение. + +Остаточный риск: если в map-fixed когда-нибудь появится строковое поле, где +пользователь хранит **свой** UUID, и регистр там семантически важен (не ссылка на +ресурс) — он будет приведён к lowercase. Сейчас таких полей нет. + +## Следствия + +- `lower(...)` в HCL становится **не нужен** — убирать в конфигах и в примере + (отдельной командой, после релиза провайдера). +- **Не выпущено**: версия не поднималась, релиз/заливка в реестр не выполнялись, + регенерация не запускалась. +- В локальных стендах костыля нет: `DEV_STAND/FPipeGmail/shturval.tf:125-126` и + `DEV_STAND/FullPipe/shturval.tf1:123` передают `nubes_vc_vdc.vdc.id` / + `nubes_vc_nsxt.edge.id` напрямую → на create у них тот же риск. + +## Открытые вопросы (не закрыты) + +1. Проверить на живом стенде: create кластера Штурвала **без** `lower(...)` на сборке + с этим фиксом — `apply` запускает только пользователь. +2. `core/params.go` → `normalizeUniversalValueV6`: скалярные UUID, попадающие в + дефолты create (`instance_create.go`) и в досылку modify (`operation_run.go`), + к lowercase не приводятся (вторично, нужен замер). +3. `resources_core/ref_validation.go` (`ValidateRefParamsOnAdopt`): ref-параметр + внутри JSON не валидируется при adopt (открыто с 24.09). diff --git a/NOTES/30_analysis/ARCHITECTURE_NEW.md b/NOTES/30_analysis/ARCHITECTURE_NEW.md index 0a4d558..f48c22a 100644 --- a/NOTES/30_analysis/ARCHITECTURE_NEW.md +++ b/NOTES/30_analysis/ARCHITECTURE_NEW.md @@ -145,6 +145,27 @@ go build -o terraform-provider-nubes - Генератор может создавать «разбитые» snake_case для CamelCase (например `resource_c_p_u`). - Это ожидаемо, но если критично — нужен отдельный маппинг (по согласованию). +### 6.5 Регистр UUID в JSON-параметрах (map-fixed) — ОБЯЗАТЕЛЬНО ЗНАТЬ + +- Платформа хранит UUID в lowercase, но **сравнивает регистр при create**. Ресурс + `nubes_vc_nsxt` отдаёт `id` в UPPERCASE → `startupConfiguration.vdcUid/nsxtUid` + в верхнем регистре → ошибка «Edge не развёрнут в указанном vDC». +- **Нормализация нужна в ДВУХ разных местах, и они не взаимозаменяемы:** + 1. **Сравнение** (план vs state, adopt/suspend/resume, modifier-compare, диагностика) — + `jsonutil.LowercaseUUIDsInText` внутри `JSONStringsEquivalent`, `JsonNormalize()`, + `ParamsMatchForResume`, `normalizeCompareValue`. + 2. **Отправка в API** — единственная точка: `resources_core.BuildJSON` + (`provider/internal/resources_core/helpers.go`), её вызывает генератор + (`NestedJSONExpr` в `templates/instance.go`: Create / Modify / Redeploy). + С 30.09 результат оборачивается в `LowercaseUUIDsInText(...)`. +- ⛔ **Не «лечить» это в HCL** (`lower(...)` в конфиге стенда) — это костыль, который + существовал только потому, что путь отправки не нормализовал UUID. Он ломался при + работе из-под Windows на провайдере `2.0.23`. +- Детали, аудит всех мест и границы применимости: `docs/60_strategy/terraform_case_sensitivity_fix.md` + (§10 — регистр при сравнении, §11 — регистр при отправке). +- Не покрыто: скалярные UUID в `normalizeUniversalValueV6` (дефолты create / досылка modify) + и валидация ref-параметров внутри JSON при adopt — см. §11 и HISTORY/2026-09-30. + --- ## 7) Проверенная цепочка (dummy) diff --git a/docs/60_strategy/terraform_case_sensitivity_fix.md b/docs/60_strategy/terraform_case_sensitivity_fix.md index 14c0cc5..d9e1add 100644 --- a/docs/60_strategy/terraform_case_sensitivity_fix.md +++ b/docs/60_strategy/terraform_case_sensitivity_fix.md @@ -280,6 +280,8 @@ Error: required params mismatch for resource_name shturval-dev: startupConfigura | 7 | `resources_core/ref_validation.go` (`ValidateRefParamsOnAdopt`) | ref-параметр, зашитый внутрь JSON, **не проверяется вообще** (открыто) | | 8 | `core/operation_run*.go` (`lookupLiveParam`) | подстановка live-значений по ключам: при другом регистре ключа может молча не сработать (открыто, требует живой проверки) | +> **Дополнение 2026-09-30**: пункты 9–10 (регистр UUID на **отправке** в API) — см. §11. + **Фикс (провайдер `2.0.23`, dev).** - `internal/core/jsonutil/jsonutil.go`: добавлен `LowercaseUUIDsInText` (UUID-подстрока → lowercase), и строковые @@ -295,3 +297,57 @@ Error: required params mismatch for resource_name shturval-dev: startupConfigura значения); смотрите `jsonutil.LowercaseUUIDsInText`. 8. Не «лечите» это нормализацией плана целиком (скаляры → строки, сортировка ключей): для атрибутов из config допустимо менять только регистр UUID-подстрок, иначе Terraform ругнётся на несоответствие плана конфигу. +9. Разделяйте **сравнение** и **отправку**: фиксы §10 закрывали только сравнение (план/state/adopt). Если ресурс + падает на create без видимых расхождений плана — смотрите путь отправки, §11. + +--- + +## 11. Обновление 2026-09-30: UUID внутри JSON на ОТПРАВКЕ в API (create/modify/redeploy) + +**Что уточнилось.** Все фиксы §10 закрывали **сравнение** (план vs state, adopt-suspended, +modifier-compare, диагностика) и **не** трогали то, что реально уходит в API при создании. +На провайдере `2.0.23` (то есть уже после §10) `create` кластера Штурвала **без** `lower(...)` +в HCL падал с «Edge не развёрнут в указанном vDC». Обнаружено при запуске Terraform +**из-под Windows**. + +**Где именно.** + +- Все map-fixed JSON-параметры (`startupConfiguration`, `clusterConfiguration`, …) собираются + одной функцией `resources_core.BuildJSON` (`provider/internal/resources_core/helpers.go`) — + она берёт значения подполей (`ValueString()`) **как есть**. +- Вызывает её сгенерированный код через `NestedJSONExpr` (`TOOLS/resource-generator/internal/templates/instance.go`, + ветки **Create** ~строка 302, **Modify** ~488, **Redeploy** ~505). +- `nubes_vc_nsxt` отдаёт `id` в UPPERCASE (`2C37FED1-…`), платформа хранит UUID в lowercase + и **сравнивает регистр при create** → UPPERCASE `nsxtUid`/`vdcUid` внутри JSON отвергается. +- `resolveRefSvcParamValues` (`core/refsvc.go`, `core/refsvc_resolve.go`) тут не спасает: он + нормализует только **top-level** refSvc-параметры и `s3.*uid` **внутри** map-fixed; + `vdcUid`/`nsxtUid` — обычные строковые подполя JSON, refSvcId у них нет. + +**Аудит (продолжение таблицы §10, проверено 30.09).** + +| # | Место | Что ломает | +|---|---|---| +| 9 | `resources_core/helpers.go` (`BuildJSON`) — **отправка** map-fixed JSON в API (create/modify/redeploy) | create падает: «Edge не развёрнут в указанном vDC» — регистр `vdcUid`/`nsxtUid` внутри JSON | +| 10 | `core/params.go` (`normalizeUniversalValueV6`) — дефолты create (`instance_create.go`) и досылка modify (`operation_run.go`) | скалярный UUID к lowercase не приводится (открыто, вторично) | + +**Фикс (не выпущен, только код).** + +- `provider/internal/resources_core/helpers.go`: `BuildJSON` оборачивает результат в + `jsonutil.LowercaseUUIDsInText(...)` (+ импорт `core/jsonutil`). Одна точка → покрыты **все** + map-fixed-параметры всех ресурсов на create / modify / redeploy. Регенерация не нужна. + +**Почему это безопасно.** + +- `BuildJSON` используется **только для отправки**, не для построения state (проверено: вызовы + только в `resources_gen/*.go`). +- Regex `uuidAnywhereRegex` (`[0-9a-f]{8}-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) совпадает только с UUID; + пароли/имена/иные строки не задевает. Внутри map-fixed строковых секретных полей + (password/secret/token) в спеках нет — только `*Uid`-ссылки на ресурсы. +- Это **выравнивание** с правилом «регистр UUID незначим» (то же приведение уже делается на + сравнении), а не новое поведение. + +**Костыль в HCL.** `tf_examples/fullpipe_chain/shturval.tf:139-140` +(`vdc_uid = lower(...)`, `nsxt_uid = lower(...)`) после релиза этого фикса станет не нужен. +В локальных стендах `lower()` не используется: `DEV_STAND/FPipeGmail/shturval.tf:125-126` и +`DEV_STAND/FullPipe/shturval.tf1:123` передают `nubes_vc_vdc.vdc.id` / `nubes_vc_nsxt.edge.id` +напрямую → на create у них тот же риск. diff --git a/docs/help/architecture-and-methods.md b/docs/help/architecture-and-methods.md index 8b7256e..642f4c6 100644 --- a/docs/help/architecture-and-methods.md +++ b/docs/help/architecture-and-methods.md @@ -48,6 +48,11 @@ - map/json → "{}" - list/array → "[]" - Пустые строки в JSON-параметрах запрещены (валидаторы на plan). +- UUID-подстроки в JSON (map-fixed) приводятся к lowercase **и при сравнении, и при отправке** + в API (`resources_core.BuildJSON` → `jsonutil.LowercaseUUIDsInText`). Платформа сравнивает + регистр UUID при create, а ресурсы могут отдавать `id` в UPPERCASE (например `nsxtUid`). + Поэтому `lower(...)` в конфигах стендов не нужен — см. + `docs/60_strategy/terraform_case_sensitivity_fix.md` §10–§11. ## 8. Soft Delete и карантин - Для тяжёлых ресурсов delete часто заменён на suspend с периодом удержания. diff --git a/provider/internal/resources_core/helpers.go b/provider/internal/resources_core/helpers.go index 9b7975e..73aec1f 100644 --- a/provider/internal/resources_core/helpers.go +++ b/provider/internal/resources_core/helpers.go @@ -6,6 +6,8 @@ import ( "strings" "github.com/hashicorp/terraform-plugin-framework/types" + + "terraform-provider-nubes/internal/core/jsonutil" ) func FormatBool(v types.Bool) string { @@ -81,6 +83,12 @@ func CompactParams(params map[string]string) map[string]string { // BuildJSON строит JSON-строку из map[string]string значений. // Ключи не кавычатся если они уже в кавычках. +// +// FIX(uuid-case): после сборки приводим UUID-подстроки к lowercase. Облако хранит +// UUID в lowercase, но ресурсы могут отдавать id в UPPERCASE (например nsxtUid у +// кластера Штурвала), а платформа сравнивает регистр при create и падает с +// «Edge не развёрнут в указанном vDC». Без этой нормализации приходилось писать +// lower(...) в HCL у каждого такого поля. func BuildJSON(parts map[string]string) string { if len(parts) == 0 { return "{}" @@ -89,5 +97,5 @@ func BuildJSON(parts map[string]string) string { for k, v := range parts { pairs = append(pairs, fmt.Sprintf(`"%s":%s`, k, v)) } - return "{" + strings.Join(pairs, ",") + "}" + return jsonutil.LowercaseUUIDsInText("{" + strings.Join(pairs, ",") + "}") }