fix(provider): нормализация регистра UUID при отправке map-fixed JSON в API

- resources_core.BuildJSON оборачивает результат в jsonutil.LowercaseUUIDsInText:
  платформа сравнивает регистр UUID при create, а ресурсы отдают id в UPPERCASE
  (nsxtUid/vdcUid) -> без нормализации create Штурвала падал 'Edge не развёрнут
  в указанном vDC' (обнаружено на провайдере 2.0.23 из-под Windows).
- Одна точка покрывает все map-fixed-параметры (create/modify/redeploy),
  регенерация не требуется.
- Документация: HISTORY/2026-09-30, docs/60_strategy/terraform_case_sensitivity_fix.md §11,
  NOTES/30_analysis/ARCHITECTURE_NEW.md §6.5, docs/help/architecture-and-methods.md §7.

Не выпущено: версия не поднималась, релиз/регенерация не выполнялись.
This commit is contained in:
Repinoid
2026-09-30 08:34:04 +03:00
parent 09e38928d0
commit 9aed2dcdd9
5 changed files with 178 additions and 1 deletions
@@ -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).
+21
View File
@@ -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)
@@ -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 у них тот же риск.
+5
View File
@@ -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 с периодом удержания.
+9 -1
View File
@@ -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, ",") + "}")
}