docs(history): раскладка HISTORY по тематическим папкам (75 файлов)

Было: 41 файл в корне HISTORY/ + авторские папки OPUS/ и SONNET/ (34 файла).
Стало — тематическая нумерация в стиле NOTES/ (10_, 20_, …):

  10_reviews/    ревью кода и разборы от LLM (2)
  20_releases/   заливки версий в реестр, чистки реестра, нумерация версий (8)
  30_provider/   ядро провайдера: архитектура, модификаторы, UUID, nested (6)
  40_generator/  генератор YAML/спеки, формат MAN (3)
  50_docs/       пайплайн документации, навигация, публикация, хостинг S3 (9)
  60_stands/     стенды и примеры: CRUD, FullPipe, Штурвал, TEST_STAND (7)
  70_infra/      реестр, API Gateway, DDoS-Guard, VPN/213, зеркала (4)
  90_llm/        диалоги и промпты с LLM вне тематики: OPUS/, SONNET/, gemini/ (34)

OPUS/ и SONNET/ перенесены как есть в 90_llm/ — чтобы не рвать пары
«бриф → ответ» внутри диалогов. Все переносы — через git mv (история сохранена).
Перед правкой: TMP/backup_2026-10-02/HISTORY_before_restructure.tar.gz.
Перекрёстные ссылки обновляются следующим коммитом.
This commit is contained in:
Repinoid
2026-10-02 07:35:32 +03:00
parent 9cc7b3f260
commit 2c196e8cc8
75 changed files with 0 additions and 0 deletions
@@ -0,0 +1,178 @@
# 2026-07-06 — Архитектурный рефакторинг tf_provider
## Контекст
После анализа Claude Opus (`HISTORY/OPUS/2026-07-06_architectural_analysis.md`) выявлены архитектурные проблемы и выполнены исправления.
## Выполненные изменения
### 1. Реструктуризация проекта
```
Было: Стало:
devops/ TOOLS/scripts/ (все .sh)
devops/profiles/ TOOLS/config/ (profile.env, services_list, timeouts)
devops/ARCHITECTURE.md TOOLS/ARCHITECTURE.md
devops/config/ УДАЛЕНО (дубликат profiles)
provider/resources_yaml/ УДАЛЕНО (сгенерированное → generated/)
provider/internal/resources_gen/ УДАЛЕНО (сгенерированное → generated/)
generated/{test,prod,dev}/ (вывод пайплайна)
```
### 2. Независимость генераторов
- `yaml-generator` — требует `NUBES_OUTPUT_DIR`, без default в `provider/`
- `resource-generator` — требует `NUBES_RESOURCES_DIR` + `NUBES_RESOURCES_GEN_DIR`
- `docs-generator` — требует `--resources`, `--docs`, `--version` флаги
- Удалён `detectVersion()` — больше не читает `provider/main.go`
- Удалён `detectRoot()`, `pickPath()` — нет хардкод-путей
### 3. Общая библиотека типов
Создан `TOOLS/lib/` — единый YAML-контракт. Модуль: `tf-tools/lib`.
Подключены:
- `yaml-generator` → type alias `ParamSpec = lib.ParamSpec` и др.
- `resource-generator` → type alias `OutputParam`, `OperationSpec`, `ParamSpec`
НЕ подключён:
- `docs-generator` — будет заменён на LLM-генератор, не трогаем
При добавлении поля в YAML — править `lib/types.go`, несогласованность ловится компилятором.
### 4. Синхронизация версий
- `provider/main.go`: 5.0.75 → 5.0.60 (соответствует последней сборке)
- `profile.env`: три переменные → одна `VERSION`
- `03_build`, `04_publish`: `${PROVIDER_VERSION:-${RELEASE_VERSION:-}}` → `${VERSION}`
### 5. Автосборка бинарников
`01_generate_yamls.sh`: если исходники новее бинарника — пересборка.
### 6. Удалённый мусор
- `devops/config/` — дубликат profiles
- `02_generate_resources_and_docs.sh` + `_template.sh` — legacy, заменены v2
- `cloud-dashboard/`, `tools/` (root), `internal/` (root), `universal_rebuild/`
- 18 одноразовых файлов (check_ops.py, s.sh, ...)
### 7. Слияние ops-generator → docs-generator
`--ops` флаг. Три генератора вместо четырёх.
### 8. Документация LLM
`docs/LLM_DOCS_GENERATION.md` — подход, промпт, тест на Postgres (gpt-oss-120b, 9/10).
## Текущее состояние
```
tf_provider/
├── TOOLS/ — всё для генерации (код + скрипты + настройки)
│ ├── yaml-generator/
│ ├── resource-generator/
│ ├── docs-generator/
│ ├── lib/ — общие YAML-типы
│ ├── scripts/
│ ├── config/{test,prod,dev}/
│ └── ARCHITECTURE.md
├── provider/ — только исходники
├── generated/ — вывод пайплайна (gitignored)
└── docs/ — документация
```
## Осталось
- [x] yaml-generator → lib
- [ ] resource-generator → lib
- [ ] docs-generator → lib
---
## ⚠️ BUG: modify падает с 500 — GET ?fields=cfsParams вызывает getResourceRealmConfig
**Симптом:** `terraform apply` при modify возвращает:
```
не удалось получить детали операции: ошибка API 500: Invalid call of the function [getResourceRealmConfig], first Argument [resourceRealm] is of invalid type, Cannot cast Object type [Struct] to a value of type [string]
```
**Причина:** `RunInstanceOperationUniversalWithDefaults` (client.go:384) делает GET `/instanceOperations/{opUid}?fields=cfsParams`. Бэкенд (ColdFusion) при вычислении cfsParams вызывает `getResourceRealmConfig` через `expression_parser.cfc:184`. Если `resourceRealm` в контексте инстанса — Struct (объект), а не string, функция падает.
**Где проявляется:**
- `client.go:420` — `RunInstanceOperationUniversalWithDefaults` (modify/create/delete с дефолтами)
- `client.go:1399` — `RunInstanceOperationUniversalByCode` (операции по code)
- `client.go:224` — `CreateGenericInstanceUniversalV6` (CREATE flow) — работает на новых инстансах
**Почему на одних инстансах падает, на других нет:**
- Новые инстансы: `resourceRealm` = string → OK
- Старые/модифицированные: `resourceRealm` = Struct → 500
**Исправление (v5.0.62):**
- `crud.go:58`: `UpdateResourceWithTimeout` → `RunInstanceOperationUniversal` (без GET)
- GET не нужен для modify — все параметры уже известны из конфига
**Статус:** ✅ FIXED v5.0.62
**Файлы:**
- `/home/naeel/tf_provider/provider/internal/resources_core/crud.go:58`
- `/home/naeel/tf_provider/provider/internal/core/client.go:384` (WithDefaults — не используется в modify)
**Тесты (v5.0.62):**
- ✅ CREATE postgres — OK
- ✅ MODIFY postgres (replicas, backup_config, access_config) — OK
- ✅ CREATE/DELETE user — OK
- ✅ CREATE/DELETE database — OK
- ✅ Invalid role → 400 c понятным сообщением — OK
- ✅ No-change plan (идемпотентность) — OK
**Остаётся риск:**
- `RunInstanceOperationUniversalByCode` (client.go:1399) всё ещё делает GET — может упасть для service_operation_resource на проблемных инстансах
- `CreateGenericInstanceUniversalV6` (client.go:224) — CREATE на инстансах где resourceRealm = Struct может упасть
---
## ⚠️ BUG: Параллельные операции на одном инстансе ломаются
**Симптом:** При создании нескольких subresource'ов (user, database) на одном postgres-инстансе:
- `ошибка API 500: Cannot connect to the orchestrator` — бэкенд не справляется с параллельными запросами
- `операция завершилась успешно, но объект не найден в state_out` — user создался, но RefreshResourceState не видит его
- `экземпляр не готов: операция в ожидании` — инстанс занят предыдущей операцией
**Причина:** Terraform по умолчанию параллелит до 10 ресурсов (`-parallelism=10`). Все операции на одном инстансе (5 user'ов + 7 database'ов) стартуют одновременно → гонка на бэкенде.
**Пример:** 2026-07-07 на инстансе `5111108D`:
- 5 user'ов создавались параллельно: user_2 ok, user_1+user_3 — orchestrator error, user_4+user_5 — state_out не найден
- 7 database'ов упали: инстанс не готов
**План исправления:** mutex map в провайдере (стандартный подход как в AWS/GCP провайдерах):
```go
// client.go — глобальная карта мьютексов
var instanceMutexes sync.Map // key: instanceUid
func (c *UniversalClient) lockInstance(instanceUid string) func() {
mu, _ := c.instanceMutexes.LoadOrStore(instanceUid, &sync.Mutex{})
mu.(*sync.Mutex).Lock()
return func() { mu.(*sync.Mutex).Unlock() }
}
```
Вызывать в каждом CRUD:
```go
unlock := client.lockInstance(instanceID)
defer unlock()
```
**Альтернативы (хуже):**
- `depends_on` цепочкой в конфиге — неудобно, требует ручной правки .tf
- `-parallelism=1` — замедляет ВСЕ ресурсы, не только subresource'ы
**Затрагивает:**
- Все subresource-операции (postgres_user, postgres_database, mariadb_user, clickhouse_user, etc.)
- Modify + delete на одном инстансе тоже могут столкнуться
**Статус:** ⚠️ OPEN — не исправлено
**Файлы для правки:**
- `/home/naeel/tf_provider/provider/internal/core/client.go` — добавить mutex map + lockInstance
- `/home/naeel/tf_provider/provider/internal/resources_core/crud.go` — добавить lock/unlock в Create/Update/Delete
- `/home/naeel/tf_provider/provider/internal/resources_core/service_operation_resource.go` — добавить lock/unlock
@@ -0,0 +1,69 @@
# 2026-07-16 — Успешный тест nested-провайдера (5.1.2)
## Результат
PostgreSQL создан на TEST-стенде с nested HCL-блоками. Провайдер 5.1.2, registry `registry.kube5s.ru <!-- ⛔ LEGACY: registry.kube5s.ru ЗАКРЫТ. Актуальный хост: tf-registry.containerk8s.services.ngcloud.ru --> <!-- ⛔ LEGACY: registry.kube5s.ru <!-- ⛔ LEGACY: registry.kube5s.ru ЗАКРЫТ. Актуальный хост: tf-registry.containerk8s.services.ngcloud.ru --> ЗАКРЫТ. Актуальный хост: tf-registry.containerk8s.services.ngcloud.ru -->`.
## Конфигурация (nubes_postgres.tf)
```hcl
resource "nubes_postgres" "npg" {
resource_name = "pgtst01"
startup_configuration = {
resource_realm = var.realm
}
cluster_configuration = {
cpu = 500
memory = 512
replicas = 1
disk = 10
}
access_configuration = {
master_ip_space = "no-needed"
master_access_list = jsonencode(["10.0.0.0/8"])
slave_ip_space = "no-needed"
slave_access_list = jsonencode([])
}
postgres_configuration = {
version = "17"
ssl_required = true
pooler_master = false
pooler_slave = false
}
postgres_conf = jsonencode([{ param_name = "log_connections", param_value = "" }])
backup_configuration = {
s3_uid = var.s3_uid
retain = 14
schedule = "0 0 * * *"
}
autoscale_configuration = {
enabled = false
schedule = 0
percent = 10
quota = 100
}
}
```
## State после apply
```
nubes_postgres.npg
nubes_postgres_database.pg_db_2
nubes_postgres_user.pg_user_0
nubes_s3bucket.bukka0
```
## Операции (все 201 OK)
- create (#18) — 2 мин 16 сек
- resume (#6) — восстановление после suspend
- create_user × 3
- create_database × 3
- delete_user × 2
- delete_database × 3
- suspend
## Версия
5.1.2 (test), 2.1.0 (prod), 3.1.0 (dev)
## Registry
`registry.kube5s.ru <!-- ⛔ LEGACY: registry.kube5s.ru ЗАКРЫТ. Актуальный хост: tf-registry.containerk8s.services.ngcloud.ru -->` (kube5s.ru DNS → ingress 185.247.187.151)
Сертификат: `registry-kube5s-tls` (Let's Encrypt)
@@ -0,0 +1,89 @@
# 2026-07-16 — SubParams: map-fixed → nested Terraform attributes
## Контекст
Генератор ресурсов не обрабатывал `map-fixed`/`array-map-fixed` параметры —
они шли как `types.String`, пользователь был вынужден писать JSON руками.
Теперь YAML содержит `sub_params` (получены через метод Виталия —
`/instanceOperations/default/{id}` с `dataDescriptor`), и генератор
раскрывает их во вложенные Terraform-блоки.
## Изменения (2026-07-16, ветка svc-api)
### Подготовка
- Переход на Gateway API (`lk-api-gateway`) для всех стендов
- Метод Виталия: `/instanceOperations/default/{id}` вместо `/serviceOperation/{id}`
- YAML сгенерированы заново: dev=50, test=48, prod=46
- Списки сервисов обновлены из Gateway
- DDoS-Guard: User-Agent + Referer для всех запросов
- LEGACY-пометки на всех старых `deck-api`
### Размоноличивание templates.go
- `templates.go` (1224 строки) → 3 файла: `instance.go`, `subresource.go`, `action.go`
- Имена констант (`Instance`, `Subresource`, `Action`) не менялись
### Багфиксы lifecycle (Соннет)
- **Баг A**: нет `HasError()` guard после диагностик в Create — сайд-эффект выполнялся вопреки ошибкам
- **Баг B**: `not created` → авто-delete+create в crud.go противоречил философии (должен быть hard error)
### SubParams — вложенные Terraform-блоки
**Изменённые файлы:**
| Файл | Что |
|------|-----|
| `TOOLS/resource-generator/internal/types/types.go` | `SubParams []Param`, `IsNested bool` в `Param` |
| `TOOLS/resource-generator/internal/loader/loader.go` | `ConvertParams` — рекурсивная конвертация SubParams; `NormalizeParamType` → `map-fixed`/`array-map-fixed` |
| `TOOLS/resource-generator/internal/helpers/helpers.go` | 8 новых функций: `IsNested`, `IsNestedList`, `NestedModelName`, `NestedTfType`, `NestedSchemaType`, `NestedSchemaBlock`, `NestedSchemaEnd`, `NestedJSONExpr`, `SubSchemaType`, `SubDefaultExpr` |
| `TOOLS/resource-generator/internal/templates/instance.go` | nested struct'ы перед Model, `SingleNestedAttribute`/`ListNestedAttribute` в Schema, `BuildJSON` в Create/Modify |
| `TOOLS/resource-generator/internal/writers/writers.go` | 10 новых template-функций зарегистрировано |
| `provider/internal/resources_core/helpers.go` | `BuildJSON(map[string]string) string` — строит JSON из map |
**Что генерируется (пример postgres):**
```go
// Вложенный struct
type PostgresClusterConfigurationModel struct {
Cpu types.Int64 `tfsdk:"cpu" json:"cpu"`
Memory types.Int64 `tfsdk:"memory" json:"memory"`
Replicas types.Int64 `tfsdk:"replicas" json:"replicas"`
Disk types.Int64 `tfsdk:"disk" json:"disk"`
}
// В Model — указатель на nested struct
ClusterConfiguration *PostgresClusterConfigurationModel `tfsdk:"cluster_configuration"`
// В Schema — SingleNestedAttribute
"cluster_configuration": schema.SingleNestedAttribute{Required: true,
Attributes: map[string]schema.Attribute{
"cpu": schema.Int64Attribute{Optional: true, Computed: true, Default: int64default.StaticInt64(500)},
...
},
},
// В Create/Modify — JSON через BuildJSON
params[788] = resources_core.BuildJSON(map[string]string{
"cpu": fmt.Sprintf("%d", data.ClusterConfiguration.Cpu.ValueInt64()),
...
})
```
**Для array-map-fixed** (postgresConf) — `ListNestedAttribute` + `[]Model`.
### Результаты генерации
| Стенд | Go-файлов |
|-------|-----------|
| dev | 73 |
| test | 70 |
| prod | 65 |
### Подводные камни (учтены)
- **Required + Default**: подполя с default → `Optional + Computed + Default`
- **JSON-ключи**: `json:"cpu"` теги = оригинальный code (camelCase)
- **types.* в JSON**: `BuildJSON` вместо `json.Marshal` (types.Int64 не маршалится как число)
- **value_list**: enum-валидаторы — out of scope
- **SubParams только для Instance**: subresource/action используют плоские параметры
### Версия
5.0.68 → 5.0.73
@@ -0,0 +1,219 @@
# 2026-09-21 — FullPipe (vDC + Edge): серия фиксов генератора и провайдера
## Контекст
Поднимался полный стенд `DEV_STAND/FullPipe` (целевой пайплайн: Организация → vDC → Edge),
заливался провайдер в реестр (`nubes-dev/nubes`). По ходу вылезла цепочка багов —
в генераторе ресурсов, в сгенерированном коде и в docs-генераторе.
Версия на выходе: **2.0.5** (DEV, namespace `nubes-dev`).
---
## Баг 1. Непересобираемый генератор (stale binary) — устранён ранее в этот же день
**Симптом:** `kind: modifier` в YAML не поддерживался; генерация YAML падала.
**Причина:** `02_generate_resources_and_docs_v2.sh` пересобирал `resource-generator`
только по `mtime`. Лежавший в `TOOLS/resource-generator/bin/resource-generator`
устаревший бинарь затенял исходники.
**Фикс:**
- генераторы (`resource-generator`, `docs-generator`) пересобираются **всегда** из исходников;
- устаревший бинарник удалён; `TOOLS/resource-generator/bin/` добавлен в `.gitignore`;
- обновлены `README.md`, `TOOLS/README.md`.
- Коммит: `7ecd2aa Fix generator rebuild and release pipeline`.
---
## Баг 2. `declared and not used: resolvedKafkaUid` — сборка падала
**Симптомы (сборка из сгенерированного кода):**
```
internal/resources_gen/119_akhq_resource.go:246:2: declared and not used: resolvedKafkaUid
internal/resources_gen/111_dnsrecord_resource.go:248:2: declared and not used: resolvedZoneUid
internal/resources_gen/21_vc_vdc_resource.go:244:2: declared and not used: resolvedOrganizationUid
... (и ещё по всем ресурсам с refSvc в create)
```
**Причина:** в шаблоне `TOOLS/resource-generator/internal/templates/instance.go`:
- блок объявления резолва шёл по `{{range .SchemaParams}}` — т.е. объявлял `resolvedX`
для **всех** refSvc-полей;
- а мапа `params` в `Create` НЕ содержала refSvc-условия и писала сырое `data.X`.
Итог: `resolvedX` объявлен, но нигде не использован → ошибка компиляции.
**Фикс (шаблон `instance.go`, `subresource.go`):**
- циклы резолва переведены на `{{range .CreateParams}}` / `{{range .ModifyParams}}`;
- в мапу `params` в `Create` добавлено refSvc-условие:
```
{{.ID}}: resolved{{ToCamel .Code}}, // в API уходит UUID
{{else}} data.X // сырое значение
```
- Коммит: `2286d34`.
---
## Баг 3. `terraform destroy` падал: «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)»
**Симптом:**
```
terraform destroy
nubes_vc_vdc.vdc: Refreshing state... [id=...]
╷ Error: РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)
```
**Причина:** `ModifyPlan` сгенерированного ресурса на **destroy-плане** запускал
create-time проверку существования/adopt (`PlanExistingResourceDiagnostics...` →
`FindInstanceByDisplayName`). Гварды `config == nil` и
`State.Raw.IsNull() && Plan.Raw.IsNull()` destroy не отсекали (config ненулевой —
блок ресурса ещё в `.tf`; а в destroy-плане state есть, plan = null).
Debug-подтверждение: `/tmp/nubes_find_debug.log` →
`PlanExistingResourceDiagnostics entered: serviceId=21 name="fullpipe-vdc" adopt=false`.
**Обходной путь (временный):** `adopt_existing_on_create=true` — но это «телега впереди
лошади»: destroy не должен зависеть от adopt.
**Фикс (шаблон `instance.go`, `ModifyPlan`):** добавить destroy-guard
```
if req.Plan.Raw.IsNull() { return }
```
Теперь destroy-план не запускает create-time проверку и доходит до `Delete`,
который по `suspend_on_destroy=true` отправляет `suspend`.
Логика suspend уже была в `Delete`: `deleteMode := "state_only"` → `"suspend"`.
- Коммит: `2286d34`.
---
## Баг 4. `Provider produced inconsistent result after apply`: `.organization_uid` было `"kontora"`, стало UUID
**Симптом:**
```
.provider produced an unexpected new value: .organization_uid:
was cty.StringVal("kontora"), but now cty.StringVal("ec4d3a6a-...")
```
**Причина:** refSvc-поле резолвилось и **записывалось обратно в state**, из-за чего
state (UUID) не совпадал с plan (user input).
**Фикс (универсальный, все сервисы):**
- резолв идёт только в локальную переменную `resolvedX`; в state остаётся ровно то,
что ввёл пользователь (имя ИЛИ UUID);
- refresh исключает refSvc-поля (`{{if eq .RefSvcId 0}}`) — не перезаписывает ввод;
- `ResolveRefSvcParamValue` принимает имя (→ UUID) и UUID (→ lowercase);
обратный маппинг `ResolveRefSvcParamDisplayName` для refresh.
- Коммит: `2286d34`.
---
## Баг 5. `Provider returned invalid result object after apply`: `vdc_group_uid` остался unknown
**Симптом (создание Edge):**
```
Error: Provider returned invalid result object after apply
After the apply operation, the provider still indicated an unknown value for
nubes_vc_nsxt.edge.vdc_group_uid.
```
**Причина:** в схеме refSvc-поля были `Optional: true, Computed: true` **без дефолта**
(строка шаблона: `{{- else if or .IsJson (gt .RefSvcId 0) }}Computed: true,{{- end }}`).
Если пользователь поле не задавал (например, `vdc_group_uid` при `vdc_type="vdc"`),
Terraform планировал его как **unknown** и требовал от провайдера известное значение.
Провайдер его не вычисляет (по дизайну хранит ввод юзера) → остаётся unknown → ошибка.
`Computed: true` — рудимент **старого** дизайна (когда провайдер писал резолвленный UUID
в state). После перехода на «храним ввод юзера» он стал вредным.
**Фикс (оба шаблона: `instance.go`, `subresource.go`):**
```
- {{- else if or .IsJson (gt .RefSvcId 0) }}Computed: true,{{- end }}
+ {{- else if .IsJson }}Computed: true,{{- end }}
```
Теперь незаданный refSvc = `null` (известное значение). `IsJson` оставлен Computed
намеренно (нужно для нормализации JSON из API).
Проверено: в сгенерированном `22_vc_nsxt_resource.go` →
`"vdc_group_uid": schema.StringAttribute{Optional: true, ...}` (без `Computed`).
- Коммит: `bffe3d9`.
---
## Баг 6. docs-generator: вложенный `map-fixed` рендерится как блок (НЕ исправлено → TODO)
Пример в сгенерированной доке (`generated/dev/docs/vc_nsxt_example.md`) рисует
`routed_net_configuration` **блоком**, но схема — `SingleNestedAttribute`, значит нужен
аргумент `= { ... }`. Копирование примера → `terraform validate` падает:
`Unsupported block type`.
Виноват `TOOLS/docs-generator/internal/writers/writers.go` → `formatParamOrBlock`
(~стр. 715). Подробности — `docs/TODO/docs_generator_nested_attr_syntax.md`.
Коммит: `92e04da`.
---
## Баг 7. FullPipe: дефолт `vdc_storage_config = "fast"`
**Симптом:** дефолт в `variables.tf` — `[{"name":"fast","size":200}]`.
Имя политики берётся из ресурсного пула (`getKeyListFromStruct(...providerVdcs[...].storage)`),
и `fast` в окружении не существует.
**История (по git):** `fast` появился в первом коммите стенда `7ff98f8` — причём их было
**два**: `vdc_provider_vdc = "fast-2.8"` и `vdc_storage_config = "fast"`. Коммит
`7d44697` («Fix FullPipe VDC example placeholders») поправил только `provider_vdc`
(`"fast-2.8"` → `null`), а `storage_config` не тронул. Так что «опять fast» — это
незакрытый второй хвост, а не откат.
**Фикс:** дефолт → `[{"name":"SATA","size":"200"}]` (совпадает с рабочим `terraform.tfvars`).
Коммит: `d608fba`.
---
## Добавлено в стенд FullPipe
- `DEV_STAND/FullPipe/edge.tf` — ресурс `nubes_vc_nsxt.edge` (create),
`vdc_uid = nubes_vc_vdc.vdc.id` (Edge создаётся после vDC),
`routed_net_configuration = { ... }` (аргумент, не блок — см. Баг 6).
- переменные `nsxt_*` в `variables.tf`, outputs `nsxt_*` в `outputs.tf`,
пример в `terraform.tfvars.example`.
- `versions.tf` → провайдер `2.0.4` (затем `2.0.5`).
- Коммит: `d608fba`.
---
## Изменённые файлы (генератор)
| Файл | Что |
|------|-----|
| `TOOLS/resource-generator/internal/templates/instance.go` | destroy-guard в `ModifyPlan`; резолв refSvc по `.CreateParams`; refSvc-условие в мапе `params`; refSvc без `Computed` |
| `TOOLS/resource-generator/internal/templates/subresource.go` | резолв по `.CreateParams`/`.ModifyParams`; refSvc без `Computed` |
| `TOOLS/scripts/02_generate_resources_and_docs_v2.sh` | детерминированная пересборка генераторов |
| `.gitignore`, `README.md`, `TOOLS/README.md` | игнор бинарника, доки |
## Версии
| Стенд | Namespace | Версия |
|---|---|---|
| DEV | `nubes-dev` | `2.0.5` |
## Коммиты сессии (master)
```
bffe3d9 fix(generator): refSvc-поля без Computed (unset = null, а не unknown)
92e04da docs(TODO): баг docs-generator - вложенный map-fixed как блок вместо = {}
d608fba stand(FullPipe): vc_nsxt (edge.tf), storage_config fast->SATA, provider 2.0.4
1401003 release(dev): 2.0.4
2286d34 fix(generator): destroy-guard в ModifyPlan + универсальный refSvc (имя или UUID)
7ecd2aa Fix generator rebuild and release pipeline
```
## Открытые вопросы
- [ ] docs-generator: `map-fixed` → `= { ... }`, `array-map-fixed` → JSON/jsonencode
(см. `docs/TODO/docs_generator_nested_attr_syntax.md`).
- [ ] Проверить `IsJson`-поля без дефолта: тот же класс unknown-after-apply? (не воспроизводилось).
- [ ] `fast` в тест-фикстуре `provider/internal/core/client_test.go:287` и спек-доке
`docs/60_strategy/...:158` — не трогали.
@@ -0,0 +1,144 @@
# Правки ядра и модификаторов по итогам анализа 2026-09-30 (раунд Flash)
**Репо:** `/home/naeel/TF/tf_provider`. **Дата:** 2026-09-30.
**Источник заданий:** `NOTES/20_prompts/prompt_for_deepseek_pro_remediation_plan.md`
(гипотезы A1–A5, B1–B9) + диалог с Opus `HISTORY/OPUS/2026-09-30_provider_architecture_modifiers_dialog.md`.
---
## Что сделано (6 коммитов)
| Коммит | Пункт | Файлы | Суть |
|---|---|---|---|
| `047d53a` | C | `TOOLS/ARCHITECTURE.md` | Спека приведена к коду: ручные ресурсы, 401, удалённый `serviceSpecificModifiers`, раздел «Lifecycle Vocabulary». |
| `c5a4499` | B3 | `TOOLS/scripts/check_hardcoded_service_ids.sh`, `org_ip_allocation_resource.go` | Страж сканирует `TOOLS/` + `provider/internal/` (кроме `resources_gen/`), второй паттерн — литеральный ref-svc id. `ResolveRefSvcParamValue(ctx, 19, …)` → константа `svcIDVcOrg`. |
| `383f8ea` | A1 | `core/http.go`, `core/client_test.go`, `ARCHITECTURE.md` | 401 добавлен в `isRetryable` (действует для GET). Тест `TestIsRetryable`. |
| `ea75cac` | B6 | `core/modifier_compare.go`, `operation_run_bycode.go`, `nsxt_snat_resource.go` | Idempotency pre-check сравнивает с **live** (`state.params`), а не с `paramValue` формы. `setSnat` → `ByIdempotent`. Тест `TestModifierDesiredEqualsLive`. |
| `8519ba0` | B7 | `org_ip_allocation_resource.go`, `nsxt_snat_resource.go`, `org_ip_allocation_test.go` | `ImportState` заполняет Required (`vip_configure` из live/`[]`; `ip_space_name` из live/`no-needed`). Тесты на чистые хелперы. |
| `0e26e98` | — | `core/refsvc.go`, `docs/60_strategy/terraform_case_sensitivity_fix.md` | Убран устаревший комментарий про несуществующий блок «Restore user-provided casing»; §4 помечен как исторический. |
**Проверка после каждого коммита:** `go build ./...` OK, `go test ./internal/... -short` PASS,
`bash TOOLS/scripts/check_hardcoded_service_ids.sh` → OK.
**Бэкапы:** `TMP/backup_2026-09-30/` (исходные версии всех правимых файлов).
---
## Что ОТКЛОНЕНО после проверки по коду (важно)
- **A5 (нормализация регистра в `Read`) — был бы РЕГРЕССОМ.**
Принятое решение (проверено): state хранит регистр **пользователя**; ref_svc-атрибуты **исключены
из read-back** (шаблон `instance.go` добавляет `InputField` только при `eq .RefSvcId 0`); UUID
внутри JSON нормализуются при **отправке** (`resources_core.BuildJSON` →
`jsonutil.LowercaseUUIDsInText`). См. `docs/60_strategy/terraform_case_sensitivity_fix.md` §4 (пометка),
§10–§11. Нормализация state к lowercase сломала бы соответствие plan=config.
- **A3 (не обрывать modify при сбое live) — осознанная защита, а не дефект.**
`instanceLiveParams` намеренно возвращает ошибку: тихий fallback на `paramValue` (дефолт ФОРМЫ)
возвращает reset-баг (затирание параметров инстанса, HAR/edge_.har: `needEnableAVI`). Требуется
отдельное решение (см. Q2 промпта раунда 4).
- **A4 (угадывание типа по подстроке имени) — нужен замер.**
Fallback применяется только к required-параметру без `paramValue`/`defaultValue`
(`instance_create.go:105-113`) и при досылке modify. Гарантированного улучшения нет, риск сломать
больше, чем починить. Оставлено как есть.
---
## Отложено
- **A2 — retry POST.** Слепой ретрай создающего `POST /instanceOperations` опаснее обрыва
(дубликат операции). Решение — за владельцем (варианты в промпте раунда 4, Q1).
- **B8** — создавать ли оверлей `modifiers.yaml` или узаконить ручные модификаторы категорией в спеке.
- **B9** — единый словарь жизненного цикла (в спеку внесён как незакрытый вопрос; решение — Q4 промпта).
---
## Артефакты
- Промпт раунда 4 для Opus: `NOTES/20_prompts/prompt_for_opus_remediation_round4.md`
(5 коротких вопросов, лимит ответа ≤ 25 строк).
- Ограничение сессии: чат с Opus по раундам 1–3 исчерпан по токенам → раунд 4 в новом чате.
---
## Замер Q3 (2026-09-30): безопасно ли угадывание типа по имени?
**Источник:** `generated/dev/resources_yaml/*.yaml` (40 файлов), поля `data_type` / `required`.
**Метод:** подсчёт + эмуляция `normalizeUniversalValueV6` (ветка `nameHint`). Только чтение.
| Метрика | Значение |
|---|---|
| required-параметров всего | 852 |
| из них с пустым `data_type` | 5 |
| всего параметров с пустым `data_type` | 12 (~1.2 %) |
| из них угадывание по имени даёт ≠ `""` | **1** — `1_dummy.yaml` (`jsonExample` → `{}`), тестовый сервис |
Required с пустым `data_type` (все получают `""`; угадывание не срабатывает):
`120_clickhouse/delete:username`, `12_s3/create:resourceRealm`, `13_s3bucket/create:maxSize`,
`151_k8s_openbao/create:policyName`, `28_vc_vm_v3/create:userLogin`.
**Вывод.** Гипотеза A4 («риск неверной типизации» из-за подстроки имени) на dev-спеках
**не подтверждается**: для всех реальных сервисов угадывание по имени не срабатывает (итог `""`);
единственный эффект — тестовый `1_dummy.jsonExample`. То есть правка косметическая (упрощение),
а не исправление дефекта. Решение «снимать/оставлять» — за владельцем.
---
## Раунд 4 (Opus, новый чат) — решения и правки
Промпт: `NOTES/20_prompts/prompt_for_opus_remediation_round4.md` (5 вопросов, ответ ≤ 25 строк).
Ответ получен; ниже — что принято и что сделано.
| Q | Решение | Статус |
|---|---|---|
| Q1 retry POST | Не ретраить. `POST /instanceOperations` не идемпотентен, `Idempotency-Key` у API нет. | Зафиксировано в `TOOLS/ARCHITECTURE.md` (коммит `7a6f665`) |
| Q2 черновик операции | Отмены нет: `DELETE /instanceOperations/{uid}` отсутствует и в коде, и в HAR (проверено: `grep '"method": "DELETE"'` по `HAR/*.har` — ноль совпадений). Оставляем как есть, задокументировано. | `7a6f665` |
| Q3 zero-value по имени | **Закрыт замером**: угадывание не срабатывает (см. выше) — не дефект. | замер `54f036e` |
| Q4 словарь жизненного цикла | Единый контракт: `keep_on_destroy` + `suspend_on_destroy`; `delete_strategy` = маппинг (`noop_warn`→keep, `inverse`→destroy, `error`→валидация). | `7a6f665` |
| Q5 осиротевший инстанс | **Исправлено** (критичный). Ядро возвращает `instanceUid` вместе с ошибкой после создания; шаблон пишет partial state. | `9da9766` |
**Q5 детали:** `core/instance_create.go` — все ошибки ПОСЛЕ получения `instanceUid` возвращают
`instanceUid` (до создания — `""`); `templates/instance.go` — при `err != nil && id != ""` пишет
`data.ID` + `resp.State.Set` перед `AddError`. Регенерация dev (`02` + `dev-materialize`) → фикс в
40 файлах `resources_gen` (эфемерные, не в git). Тесты: `TestCreateGenericInstance_KeepsUIDWhenOperationCreateFails`,
`TestCreateGenericInstance_EmptyUIDWhenInstanceCreateFails`.
**Коммиты раунда 4:** `54f036e` (замер), `9da9766` (Q5), `7a6f665` (Q1/Q2/Q4).
**Осталось:** замер владельцем (`terraform plan` ×2 на `DEV_STAND/FullPipe`); B8 (`modifiers.yaml`).
---
## Раунд 5 — код-ревью (Opus) и его фиксы
Промпт: `NOTES/20_prompts/prompt_for_opus_code_review_round5.md`. Opus нашёл 6 пунктов; критичные — 2.
| # | Находка | Решение |
|---|---|---|
| 1 | **Регрессия B6:** pre-check стоял ПОСЛЕ `POST /instanceOperations` → при совпадении оставался «черновик» операции (pending). | Исправлено: pre-check до создания операции. |
| 2 | Partial state: `resp.State.Set(&data)` мог записать unknown/computed → «invalid new value … unknown». | Исправлено: пишется только `id` (`SetAttribute`). |
| 3 | 401 ретраится, но токен между попытками не обновляется. | Принято как есть; пояснено в комментарии `http.go`. |
| 4 | `ImportState` vs `Read` — конфликта нет. | ок. |
| 5 | `modifierDesiredEqualsCurrent` — мёртвый код (только тесты). | Удалено; тесты переведены на живые функции. |
| 6 | Сравнение по `default`-схеме рискует ложным пропуском (ревизия п.1). | Исправлено: pre-check **без схемы** — только live-коды. |
**Итоговый контракт pre-check (см. `TOOLS/ARCHITECTURE.md` → «Modifier Idempotency»):**
1. pre-check выполняется **до** `POST /instanceOperations`;
2. единственный источник — live (`state.params`), схема операции не запрашивается
(`default/{opId}` может расходиться с живой; живая доступна только после создания);
3. поиск значения — по самому коду (`live[lower(code)]`);
4. fail-safe: пусто/нет кода/ошибка live → modify выполняется;
5. сравнение: похоже на JSON — смысловое (порядок ключей не значим), иначе — скалярное
с нормализацией (`null`/`""` → `""`; `true`/`false` без учёта регистра).
Файлы: `core/modifier_compare.go` (новые `modifierRawValuesEqual`, `looksLikeJSON`,
`normalizeRawScalar`, `modifierDesiredEqualsLive` без схемы), `core/operation_run_bycode.go`,
`core/operation_cfs.go` (удалена `fetchOperationSchemaByID`), `core/client_test.go`,
`core/modifier_compare_test.go`, `TOOLS/ARCHITECTURE.md`.
Тесты: `TestRunInstanceOperationByIdempotent_SkipsWithoutCreatingOperation` (POST операции не
вызывается при совпадении), `TestModifierRawValuesEqual_Scalars/JSON`,
`TestModifierDesiredEqualsLive` (совпало / отличается / нет кода / live недоступен).
Проверено: `go build ./...` OK, `go test ./internal/... -short` PASS.
@@ -0,0 +1,130 @@
# 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 становится **не нужен** — убирать в конфигах и в примере
(отдельной командой, после релиза провайдера).
- **Выпущено 30.09.2026: dev `2.0.24`** — собрано (linux/windows/darwin amd64),
подписано GPG и залито в реестр (`nubes-dev/nubes/2.0.24/`), версия видна
в `/v1/providers/nubes-dev/nubes/versions`. `VERSIONS.md` обновлён.
Доки в реестр (шаг `04_build_and_publish_docs.sh`) **не публиковались**.
- В локальных стендах костыля нет: `DEV_STAND/FPipeGmail/shturval.tf:125-126` и
`DEV_STAND/FullPipe/shturval.tf1:123` передают `nubes_vc_vdc.vdc.id` /
`nubes_vc_nsxt.edge.id` напрямую → на create у них тот же риск.
## Процедура релиза (dev) — воспроизводимо (проверено 30.09.2026)
1. Поднять `VERSION` в `TOOLS/config/dev/profile.env` и закоммитить
(иначе доки генерируются со старой версией).
2. Собрать и залить:
```bash
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.24
```
Скрипт сам выполняет шаги `01` + `02`, собирает 3 платформы
(linux/windows/darwin amd64), подписывает GPG и заливает в S3.
3. Проверить публикацию:
```bash
curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-dev/nubes/versions
```
4. Обновить `VERSIONS.md` и закоммитить.
5. Для локального `go build`/`go test`: `02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev`
и `TOOLS/scripts/dev-materialize.sh dev` (эфемерная копия в `provider/`).
⚠️ Доки в реестр (шаг `04_build_and_publish_docs.sh`) в этом релизе **не публиковались**.
## Где нормализация нужна (карта, чтобы не потерять)
Нормализация регистра UUID нужна в **двух независимых местах**:
1. **Сравнение** (план ↔ state, adopt, suspend/resume, modifier-compare, диагностика):
`jsonutil.LowercaseUUIDsInText` → `JSONStringsEquivalent`, `JsonNormalize()`,
`ParamsMatchForResume`, `normalizeCompareValue`.
2. **Отправка в API** — единственная точка `resources_core.BuildJSON`
(`provider/internal/resources_core/helpers.go`), вызывается сгенерированным кодом
через `NestedJSONExpr` (`TOOLS/resource-generator/internal/templates/instance.go`:
Create ~302, Modify ~488, Redeploy ~505).
Правила:
- ⛔ `lower(...)` в HCL — костыль, а не решение (был нужен только из-за ненормализованной отправки).
- ⛔ Не нормализовать план целиком (скаляры→строки, сортировка ключей) — вечный diff;
менять только регистр UUID-подстрок.
- `resolveRefSvcParamValues` (`core/refsvc.go`) покрывает только top-level `refSvcId`
и `s3.*uid` внутри map-fixed; `vdcUid`/`nsxtUid` — нет.
- Спеки map-fixed без строковых секретов (только `*Uid`) → regex `uuidAnywhereRegex` безопасен.
## Открытые вопросы (не закрыты)
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).