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