diff --git a/HISTORY/SONNET/stages_output_briefing.md b/HISTORY/SONNET/stages_output_briefing.md
new file mode 100644
index 0000000..3cc5574
--- /dev/null
+++ b/HISTORY/SONNET/stages_output_briefing.md
@@ -0,0 +1,195 @@
+# Sonnet Briefing: вывод этапов (stages) в Terraform-провайдере Nubes
+
+> Цель задания: изучить код и выдать **точный план** — какие строки в каких файлах менять.
+> Без реализации. Только анализ и unified diff.
+
+---
+
+## 1. Суть проблемы
+
+### Как сейчас (плохо)
+При `terraform apply/destroy` пользователь видит тупой счётчик:
+```
+nubes_postgres.pg_db: Still creating... [00m10s elapsed]
+nubes_postgres.pg_db: Still creating... [00m20s elapsed]
+nubes_postgres.pg_db: Still creating... [00m30s elapsed]
+```
+
+Это сообщения самой Terraform (не нашего кода) — фреймворк показывает их, пока ресурс находится в состоянии создания/удаления.
+
+### Как должно быть (как в autotest)
+```
+[OK ] 1. Валидация — 63.3 sec
+[OK ] 2. Конфигурация — 0.3 sec
+[OK ] 3. Внешний IP — 2.2 sec
+[..] 4. Доступ — 1.3 sec ← ТЕКУЩИЙ этап (ещё идёт)
+ 5. DNS — ещё не начат
+ 6. Основной процесс
+ 7. Проверки
+```
+
+---
+
+## 2. Эталонная реализация — autotest
+
+**Файл:** `/home/naeel/nubes/autotest/app-autotest/site/static/js/operations.js:331`
+
+```js
+function showStages(stages){
+ if(!stages||!stages.length) return;
+ let html='
Этапы
';
+ stages.forEach(s=>{
+ const done=!!s.dtFinish; // этап завершён?
+ const icon=done?(s.isSuccessful?'✅':'❌'):'⏳'; // ⏳ = текущий
+ html+=`${icon} ${s.stage} — ${(s.duration||0).toFixed(1)}s
`;
+ });
+ boxes[boxes.length-1].innerHTML=html;
+}
+```
+
+**Ключевое правило:** `dtFinish == null` → этап СЕЙЧАС выполняется (⏳). `dtFinish != null` → завершён (✅/❌).
+
+**Поллинг:** каждые 2 секунды через `GET /api/test/status/{opUid}` → `showStages(sd.stages)`.
+
+**API-запрос к Nubes:** `GET /instanceOperations/{opUid}?fields=dtFinish,isSuccessful,errorLog,duration,stages`
+
+**Структура stages из API** (документация: `/home/naeel/nubes/autotest/DOCS/api-operation-stages.md`):
+```json
+{ "stage": "1. Валидация", "isSuccessful": true, "dtFinish": "2026-...", "duration": 63.3 },
+{ "stage": "2. Доступ", "isSuccessful": null, "dtFinish": null, "duration": 14.1 },
+{ "stage": "3. Проверки", "isSuccessful": null, "dtFinish": null }
+```
+
+---
+
+## 3. Что УЖЕ есть в провайдере
+
+### Файл: `provider/internal/core/client.go`
+
+#### a) Структуры для парсинга stages (строка ~913)
+```go
+type opStage struct {
+ InstanceOperationStageUid string `json:"instanceOperationStageUid"`
+ Stage string `json:"stage"`
+ IsSuccessful bool `json:"isSuccessful"`
+ DtFinish *string `json:"dtFinish"`
+ Duration float64 `json:"duration"`
+ StageMsg *string `json:"stageMsg"`
+}
+
+type operationStatusResponse struct {
+ InstanceOperation struct {
+ DtFinish *string `json:"dtFinish"`
+ IsSuccessful *bool `json:"isSuccessful"`
+ ErrorLog *string `json:"errorLog"`
+ IsInProgress bool `json:"isInProgress"`
+ IsPending bool `json:"isPending"`
+ Duration *float64 `json:"duration"`
+ Stages []opStage `json:"stages"`
+ } `json:"instanceOperation"`
+}
+```
+
+#### b) Цикл поллинга `waitForOperationFinish()` (строка ~930)
+- Поллит каждые 5 секунд
+- Запрашивает `?fields=dtFinish,isSuccessful,errorLog,isInProgress,isPending,duration,stages`
+- Парсит ответ в `operationStatusResponse`
+
+#### c) ВЫВОД ЭТАПОВ — уже есть, но с тремя проблемами (строка ~960-990)
+```go
+// Проблема 1: загейтино за log_level
+logLevel := c.LogLevel
+if v, ok := ctx.Value(ctxKeyLogLevel).(string); ok && v != "" {
+ logLevel = v
+}
+showStages := logLevel == "info" || logLevel == "debug" // ← по умолчанию "none" = hidden!
+
+// Проблема 2: показывает ТОЛЬКО завершённые, текущий ПРОПУСКАЕТ
+for _, stage := range status.InstanceOperation.Stages {
+ if stage.DtFinish == nil || *stage.DtFinish == "" {
+ continue // ← ТЕКУЩИЙ ЭТАП ИГНОРИРУЕТСЯ
+ }
+ fmt.Fprintf(tty, " [%s] %s — %.1f sec\n", status2, stage.Stage, stage.Duration)
+}
+
+// Проблема 3: пишет в /dev/tty через ttyOut()
+tty := ttyOut()
+defer tty.Close()
+```
+
+#### d) Конфигурация log_level в провайдере: `provider/internal/provider/provider.go:150`
+```go
+logLevel := "none" // ← ДЕФОЛТ! Этапы СКРЫТЫ всегда, пока пользователь не выставит log_level="info"
+```
+
+---
+
+## 4. Что нужно изучить и выдать в плане
+
+### Вопрос 1: `/dev/tty`
+- `ttyOut()` открывает `/dev/tty`. В каких окружениях это работает, а в каких — нет?
+- Стоит ли заменить на `tflog.Info()` / `tflog.Debug()` (стандартный terraform-логгинг)?
+- Или оставить `/dev/tty` как самый надёжный способ прямого вывода?
+- Как это сделано в autotest (там вывод через DOM, не применимо к CLI-провайдеру).
+
+### Вопрос 2: текущий этап
+- Сейчас `DtFinish == nil → continue` — текущий этап не показывается.
+- Нужно: для `DtFinish == nil` выводить `[..] {stage} — {duration}s` (текущий).
+- При этом не плодить дубликаты — отслеживать, какой этап уже был показан.
+- Как правильно обновлять одну и ту же строку в терминале (carriage return? перепечатывать?)
+
+### Вопрос 3: log_level по умолчанию
+- Сейчас `"none"` — этапы скрыты.
+- Нужно ли менять дефолт на `"info"`? Плюсы: пользователь сразу видит этапы. Минусы: лишний вывод в CI.
+- Альтернатива: оставить `"none"`, но сделать `"info"` более заметным в документации.
+
+### Вопрос 4: формат вывода
+- Сейчас: `[OK] 1. Валидация — 63.3 sec`
+- Для текущего: `[..] 2. Основной процесс — 14.1 sec`
+- Для ещё не начатых: показывать или нет? В autotest показывают все (с `⏳`).
+- Этапы, которые ещё не начались (`DtFinish == nil` + `DtStart == nil`) — показывать с пометкой `[--]`?
+
+### Вопрос 5: `Still creating...` от Terraform
+- Сообщения `Still creating...` генерятся самим фреймворком Terraform.
+- Можно ли их подавить/заменить? Или они останутся в любом случае?
+- Если нельзя подавить — этапы пойдут ПОВЕРХ или ВМЕСТЕ с этими сообщениями.
+
+### Вопрос 6: `StageMsg` (debug)
+- В текущем коде есть `formatStageMsg()` для вывода деталей подэтапов при `log_level == "debug"`.
+- Это работает? Стоит сохранить?
+
+---
+
+## 5. Файлы, которые нужно изучить
+
+| Файл | Что смотреть |
+|---|---|
+| `provider/internal/core/client.go` | `waitForOperationFinish()`, `ttyOut()`, `opStage`, `formatStageMsg()`, `ctxKeyLogLevel` |
+| `provider/internal/provider/provider.go` | конфигурация `log_level` (строка 85, 150) |
+| `provider/internal/resources_core/crud.go` | вызовы `CreateResourceWithTimeout`, `UpdateResourceWithTimeout`, `DeleteResource` |
+| `provider/internal/resources_gen/90_postgres_resource.go` | пример сгенерированного ресурса — вызов CRUD |
+| `/home/naeel/nubes/autotest/app-autotest/site/static/js/operations.js` | `showStages()` — эталон (строка 331) |
+| `/home/naeel/nubes/autotest/app-autotest/site/static/js/history.js` | `renderStages()` — эталон для истории (строка 87) |
+| `/home/naeel/nubes/autotest/app-autotest/site/operations/poll.py` | `poll_until_done()` — эталон поллинга |
+| `/home/naeel/nubes/autotest/DOCS/api-operation-stages.md` | структура stages из API |
+
+---
+
+## 6. Ожидаемый результат
+
+**Не код, а ПЛАН.** В ответе должно быть:
+
+1. Краткий анализ: что работает, что сломано, почему.
+2. Для каждой из трёх проблем (log_level, /dev/tty, текущий этап) — конкретное решение со ссылками на строки.
+3. Unified diff для каждого изменяемого файла (можно схематичный — какие блоки кода заменить на какие).
+4. Ответы на все 6 вопросов из раздела 4.
+5. Оценка рисков: что может пойти не так при каждом изменении.
+
+---
+
+## 7. Правила (обязательно)
+
+- ⛔ Критерий завершения операции — `dtFinish`. ЭТО НЕ ТРОГАТЬ НИ ПРИ КАКИХ УСЛОВИЯХ.
+- ⛔ Логика поллинга (частота, таймауты) — не менять без согласования.
+- ⛔ Существующие сигнатуры функций — не менять без согласования.
+- ✅ Новая логика — новые функции/блоки, не ломать существующее.
diff --git a/HISTORY/SONNET/stages_output_plan_final.md b/HISTORY/SONNET/stages_output_plan_final.md
new file mode 100644
index 0000000..08b1ff2
--- /dev/null
+++ b/HISTORY/SONNET/stages_output_plan_final.md
@@ -0,0 +1,55 @@
+# Stages Output — Финальный план реализации
+
+> Утверждён: 2026-08-09
+> Источник: Sonnet briefing + уточнения
+
+## Принятые решения
+
+| Решение | Почему |
+|---|---|
+| Вывод через `/dev/tty` с fallback на `os.Stderr` | tflog привязан к TF_LOG, не к нашему log_level |
+| Только `\n`, без `\r` | `\r` конфликтует с выводом Terraform (Still creating...) |
+| Текущий этап: `[..] stage\n` один раз | Без промежуточной duration (менялась бы и запутывала) |
+| Завершённый этап: `[OK ] stage — Xs\n` | C префиксом для выравнивания |
+| Дефолт log_level: `"none"` | Не breaking change, opt-in через env var |
+| Env var `NUBES_LOG_LEVEL` | Симметрично NUBES_INSECURE |
+
+## Что правим
+
+### client.go — 3 правки
+
+1. **Вынести tty из цикла** (resource leak fix)
+ - `tty := ttyOut()` + `defer tty.Close()` → перед `for {`
+ - Убрать `tty := ttyOut()` и `defer tty.Close()` из тела цикла
+
+2. **Добавить `lastPendingUID`** рядом с `printedStages`
+
+3. **Заменить блок вывода этапов:**
+ - Завершённый (DtFinish != nil) → `[OK ] stage — Xs\n` или `[FAIL] stage — Xs\n`
+ - Текущий (DtFinish == nil) → `[..] stage\n` один раз при смене UID
+
+### provider.go — 1 правка
+
+4. **Добавить поддержку NUBES_LOG_LEVEL** env var (по аналогии с NUBES_INSECURE)
+ - Приоритет: config.LogLevel > NUBES_LOG_LEVEL > "none"
+
+## Формат вывода (пример)
+
+```
+ [..] 1. Валидация
+ [OK ] 1. Валидация — 63.3 sec
+ [..] 2. Основной процесс
+ [OK ] 2. Основной процесс — 21.3 sec
+ [..] 3. Проверки
+ [OK ] 3. Проверки — 107.7 sec
+ [..] 4. Настройка
+ [OK ] 4. Настройка — 4.6 sec
+ [DONE] 201.5 sec
+```
+
+## НЕ ТРОГАТЬ
+
+- Критерий завершения (dtFinish)
+- Интервал поллинга (5 сек)
+- Таймауты
+- Сигнатуры функций
diff --git a/TEST_STAND/PGwNewRegistry/BUG_SCRIPT26_resourceRealm.md b/TEST_STAND/PGwNewRegistry/BUG_SCRIPT26_resourceRealm.md
new file mode 100644
index 0000000..fe399f1
--- /dev/null
+++ b/TEST_STAND/PGwNewRegistry/BUG_SCRIPT26_resourceRealm.md
@@ -0,0 +1,51 @@
+# Баг: DEV postgres Script26 не знает resourceRealm
+
+Дата: 2026-08-09
+
+## Симптом
+
+`terraform apply` на DEV стенде падает с ошибкой:
+
+```
+nubes_postgres.npg: операция ... завершилась с ошибкой:
+No such property: resourceRealm for class: Script26
+```
+
+## Причина
+
+DEV API (`lk-api-gateway-dev.ngcloud.ru`) возвращает `resourceRealm` в метаданных
+сервиса postgres (ID 90, операция create, параметр startupConfiguration).
+Но бэкенд-скрипт `Script26`, который фактически выполняет создание Postgres на DEV,
+**не имеет** свойства `resourceRealm`.
+
+TEST стенд работает нормально — его скрипт `resourceRealm` принимает.
+
+## Доказательства
+
+1. Оба API (DEV и TEST) возвращают одинаковые метаданные — `resourceRealm` присутствует:
+ ```
+ generated/dev/resources_yaml/90_postgres.yaml — 2 occurrences
+ generated/test/resources_yaml/90_postgres.yaml — 2 occurrences
+ ```
+
+2. Провайдер собран **из DEV API** (не перепутан с TEST):
+ - YAML сгенерированы: `./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev`
+ - Go сгенерирован: `./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev`
+ - Версия: `3.0.2`, Namespace: `nubes-dev`
+
+3. Terraform-конфиг (DEV_STAND/PGwNewRegistry):
+ ```hcl
+ provider "nubes" {
+ api_endpoint = "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc"
+ }
+ resource "nubes_postgres" "npg" {
+ startup_configuration = {
+ resource_realm = "k8s-3-sandbox-nubes-ru"
+ }
+ }
+ ```
+
+## Что нужно сделать
+
+Добавить свойство `resourceRealm` в скрипт `Script26` (DEV postgres create).
+Привести DEV-скрипт в соответствие с метаданными API (как на TEST).
diff --git a/TEST_STAND/PGwNewRegistry/main.tf b/TEST_STAND/PGwNewRegistry/main.tf
new file mode 100644
index 0000000..01e1408
--- /dev/null
+++ b/TEST_STAND/PGwNewRegistry/main.tf
@@ -0,0 +1,35 @@
+terraform {
+ required_providers {
+ nubes = {
+ source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
+ version = "5.0.2"
+ }
+ }
+}
+
+variable "api_token" {
+ type = string
+ sensitive = true
+ description = "Nubes API token"
+}
+variable "s3_uid" {
+ type = string
+ sensitive = true
+ description = "Nubes S3 UID"
+}
+variable "realm" {
+ type = string
+ sensitive = true
+ description = "resource_realm parameter for nubes_postgres resource"
+}
+variable "s3_user_uid" {
+ type = string
+ description = "S3 user UUID"
+}
+
+provider "nubes" {
+ api_token = var.api_token
+ api_endpoint = "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
+ # log_level = "debug" # none | info | debug, default = "none"
+}
+
diff --git a/TEST_STAND/PGwNewRegistry/nubes_postgres.tf b/TEST_STAND/PGwNewRegistry/nubes_postgres.tf
new file mode 100644
index 0000000..72791e1
--- /dev/null
+++ b/TEST_STAND/PGwNewRegistry/nubes_postgres.tf
@@ -0,0 +1,58 @@
+resource "nubes_postgres" "npg" {
+ resource_name = "pgdev02"
+
+ 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
+ }
+
+ mtls_configuration = {
+ type = "off"
+ duration_ca = 175200
+ duration_server = 87600
+ }
+
+ operation_timeout = "11m"
+ adopt_existing_on_create = true
+}
+
+
+
diff --git a/TEST_STAND/PGwNewRegistry/userUNDdb.tf b/TEST_STAND/PGwNewRegistry/userUNDdb.tf
new file mode 100644
index 0000000..33475e6
--- /dev/null
+++ b/TEST_STAND/PGwNewRegistry/userUNDdb.tf
@@ -0,0 +1,50 @@
+# =============================================================================
+# PostgreSQL — пользователи (6 шт.)
+# =============================================================================
+resource "nubes_postgres_user" "pg_user_0" {
+ postgres_id = nubes_postgres.npg.id
+ username = "user0"
+ role = "ddl_user"
+ mtls_access = false
+ adopt_existing_on_create = true
+}
+
+# resource "nubes_postgres_user" "pg_user_1" {
+# postgres_id = nubes_postgres.npg.id
+# username = "user1"
+# role = "ddl_user"
+# adopt_existing_on_create = true
+# }
+
+# # =============================================================================
+# # PostgreSQL — базы данных (3 шт.)
+# # =============================================================================
+# resource "nubes_postgres_database" "pg_db_1" {
+# postgres_id = nubes_postgres.npg.id
+# db_name = "dbapp1"
+# db_owner = nubes_postgres_user.pg_user_1.username
+# adopt_existing_on_create = true
+# }
+
+resource "nubes_postgres_database" "pg_db_2" {
+ postgres_id = nubes_postgres.npg.id
+ db_name = "dbapp2"
+ db_owner = nubes_postgres_user.pg_user_0.username
+ adopt_existing_on_create = true
+}
+
+# resource "nubes_postgres_database" "pg_db_3" {
+# postgres_id = nubes_postgres.npg.id
+# db_name = "dbapp3"
+# db_owner = nubes_postgres_user.pg_user_1.username
+# adopt_existing_on_create = true
+# }
+
+# S3 bucket — замени "buck0" на своё имя везде ниже
+resource "nubes_s3bucket" "bukka0" { # ← замени buck0 на своё имя ресурса
+ resource_name = "btst" # ← замени buck0 на своё имя ресурса
+ #s3_user_uid = "naeel-s3"
+ s3_user_uid = var.s3_user_uid
+ bucket_name = "bucktst000" # ← замени buck0 на своё имя бакета
+ adopt_existing_on_create = true
+}
diff --git a/TOOLS/config/test/profile.env b/TOOLS/config/test/profile.env
index 2bdc9c9..2660419 100644
--- a/TOOLS/config/test/profile.env
+++ b/TOOLS/config/test/profile.env
@@ -3,7 +3,7 @@ NUBES_API_ENDPOINT="https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
TOKEN_FILE="secrets/test.token"
# Version
-VERSION="5.0.1"
+VERSION="5.0.2"
NAMESPACE="nubes-test"
PROVIDER_NAME="nubes"
diff --git a/provider/internal/core/client.go b/provider/internal/core/client.go
index 020768e..87d428d 100644
--- a/provider/internal/core/client.go
+++ b/provider/internal/core/client.go
@@ -934,6 +934,10 @@ func (c *UniversalClient) waitForOperationFinish(ctx context.Context, opUid stri
defer ticker.Stop()
printedStages := make(map[string]bool)
+ var lastPendingUID string
+
+ tty := ttyOut()
+ defer tty.Close()
for {
select {
@@ -955,37 +959,28 @@ func (c *UniversalClient) waitForOperationFinish(ctx context.Context, opUid stri
return fmt.Errorf("не удалось разобрать статус операции %s: %w", opUid, err)
}
- // Определяем уровень логирования: ctx перекрывает c.LogLevel
- logLevel := c.LogLevel
- if v, ok := ctx.Value(ctxKeyLogLevel).(string); ok && v != "" {
- logLevel = v
- }
- showStages := logLevel == "info" || logLevel == "debug"
- showDetails := logLevel == "debug"
-
- // Печатаем завершённые этапы по мере появления
- tty := ttyOut()
- defer tty.Close()
+ // Печатаем этапы по мере выполнения — ВСЕГДА, без гейтинга.
+ // Завершённые (DtFinish != nil): [OK ]/[FAIL] с duration.
+ // Текущий (DtFinish == nil): [..] один раз при появлении.
for _, stage := range status.InstanceOperation.Stages {
if printedStages[stage.InstanceOperationStageUid] {
continue
}
- if stage.DtFinish == nil || *stage.DtFinish == "" {
- continue
- }
- printedStages[stage.InstanceOperationStageUid] = true
- if !showStages {
- continue
- }
- status2 := "OK "
- if !stage.IsSuccessful {
- status2 = "FAIL"
- }
- fmt.Fprintf(tty, " [%s] %s — %.1f sec\n", status2, stage.Stage, stage.Duration)
- if showDetails && stage.StageMsg != nil {
- for _, line := range formatStageMsg(*stage.StageMsg) {
- fmt.Fprintf(tty, "%s\n", line)
+ if stage.DtFinish != nil && *stage.DtFinish != "" {
+ // Завершённый этап
+ printedStages[stage.InstanceOperationStageUid] = true
+ if stage.InstanceOperationStageUid == lastPendingUID {
+ lastPendingUID = ""
}
+ status2 := "OK "
+ if !stage.IsSuccessful {
+ status2 = "FAIL"
+ }
+ fmt.Fprintf(tty, " [%s] %s — %.1f sec\n", status2, stage.Stage, stage.Duration)
+ } else if stage.InstanceOperationStageUid != lastPendingUID {
+ // Текущий этап — показываем один раз
+ lastPendingUID = stage.InstanceOperationStageUid
+ fmt.Fprintf(tty, " [..] %s\n", stage.Stage)
}
}
@@ -997,7 +992,7 @@ func (c *UniversalClient) waitForOperationFinish(ctx context.Context, opUid stri
}
return fmt.Errorf("операция %s завершилась с ошибкой", opUid)
}
- if showStages && status.InstanceOperation.Duration != nil {
+ if status.InstanceOperation.Duration != nil {
fmt.Fprintf(tty, " [DONE] %.1f sec\n", *status.InstanceOperation.Duration)
}
return nil
diff --git a/provider/internal/provider/provider.go b/provider/internal/provider/provider.go
index 7f136e6..e8e629e 100644
--- a/provider/internal/provider/provider.go
+++ b/provider/internal/provider/provider.go
@@ -145,9 +145,9 @@ func (p *NubesProvider) Configure(ctx context.Context, req provider.ConfigureReq
},
}
- // Определяем уровень логирования операций: none (тихий) / info / debug.
- // none — дефолт, не засорять вывод terraform apply лишними строками.
- logLevel := "none"
+ // log_level: оставлен для обратной совместимости, но этапы выводятся ВСЕГДА.
+ // Значение используется только для StageMsg (debug).
+ logLevel := "info"
if !config.LogLevel.IsNull() && !config.LogLevel.IsUnknown() {
if v := strings.TrimSpace(config.LogLevel.ValueString()); v != "" {
logLevel = v