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:
@@ -0,0 +1,70 @@
|
||||
# Архитектурный анализ tf_provider — 2026-07-06
|
||||
|
||||
**Кто делал:** Claude Opus (по запросу пользователя)
|
||||
**Метод:** Анализ реальной файловой структуры репозитория (не только промпт)
|
||||
**Выводы:** Без предложений по рефакторингу, без кода
|
||||
|
||||
---
|
||||
|
||||
## 1. Разделение ответственности TOOLS / generated / provider
|
||||
|
||||
Разделение проведено чисто и последовательно. Три вершины треугольника не смешиваются: TOOLS — только инструменты сборки, generated — только продукт (полностью в .gitignore), provider — только исходники универсального ядра. Границы соблюдаются на уровне файловой системы, а не договорённостей, что делает их устойчивыми.
|
||||
|
||||
Ключевое наблюдение: provider **не является самодостаточным Go-модулем**. `internal/resources_core/*.go` на этапе компиляции импортируют пакеты `terraform-provider-nubes/resources_yaml` и `internal/resources_gen`, а `provider.go` вызывает `resources_gen.AllResources()`. Обе директории gitignored и физически отсутствуют в репозитории — они появляются только при сборке. Следствие: provider в чистом виде не собирается и не проходит `go build`/`go test` без предварительного прогона генераторов. Ядро формально отделено от сгенерированного кода, но связано с ним жёсткой компиляционной зависимостью через импорты пакетов, а не через интерфейс.
|
||||
|
||||
---
|
||||
|
||||
## 2. Пайплайн
|
||||
|
||||
Пайплайн линейный и предсказуемый (API → YAML → Go/docs → build → S3). Узкие места:
|
||||
|
||||
- **Двойное копирование через temp-директории.** И `02_...` (resource-generator пишет в temp, потом копирует в `generated/{stand}/go/`), и `03_...` (весь provider копируется в `mktemp -d`, сверху накладываются YAML и Go) используют промежуточные каталоги. Для `03` это оправдано изоляцией по стендам, для `02` — лишний слой.
|
||||
- **Предсобранные бинарники в bin.** Скрипты вызывают готовые `yaml-generator`/`resource-generator`/`docs-generator`, но большинство скриптов их не пересобирают. Это тихий источник рассинхрона: правка `main.go` генератора не влияет на результат, пока бинарник не пересобран вручную.
|
||||
- **Разросшийся набор скриптов.** Помимо основных 01–04 есть `template_v2`, `10`–`13`, стабилизационные прогоны. Нумерация уже не отражает реальный порядок, часть скриптов дублирует функциональность (`02_...v2` vs `02_...template_v2`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Конфигурация стендов
|
||||
|
||||
`profile.env` содержит ~14 переменных, смешивая три разных класса:
|
||||
- (а) параметры окружения (endpoint, namespace, registry host)
|
||||
- (б) версии (`RELEASE_VERSION`=`PROVIDER_VERSION`=`DOCS_VERSION` — три идентичных значения)
|
||||
- (в) пути к секретам (GPG-ключи, S3-конфиг, token-файл)
|
||||
|
||||
Смешение секретов и конфигурации в одном env-файле — главный риск этого узла: легко утечь при копировании, трудно ротировать. Дублирование версии в трёх переменных — источник рассинхрона (плюс версия ещё и захардкожена в main.go = `5.0.75`, при том что `test/profile.env` = `5.0.60`; фактически версия живёт в двух местах и они уже разошлись).
|
||||
|
||||
---
|
||||
|
||||
## 4. Сборка provider через temp-копирование vs go:embed
|
||||
|
||||
Текущая модель — физическое копирование provider в temp + наложение generated сверху. `go:embed` используется только для `operation_timeouts.json`.
|
||||
|
||||
Наблюдения:
|
||||
- Для **сгенерированного Go-кода** `go:embed` в принципе неприменим — это компилируемые пакеты, а не данные; их нельзя «встроить», только скомпилировать. Так что альтернативы копированию здесь по сути нет, вопрос лишь в том, копировать в temp или прямо в `provider/internal/resources_gen/`.
|
||||
- Для **YAML-спеков**, которые читаются в рантайме (`resources_core` подгружает метаданные из пакета `resources_yaml`), уже применяется гибрид: в `generated/{stand}/resources_yaml/` лежит `embed.go`. То есть YAML встраивается в бинарник через embed на стороне генерируемого пакета — это консистентно.
|
||||
- Плата за temp-подход: невозможность запустить IDE/линтер/тесты на «настоящем» дереве, потому что рабочее дерево неполно. Отладка идёт по эфемерной копии.
|
||||
|
||||
---
|
||||
|
||||
## 5. Генераторы — дублирование
|
||||
|
||||
Три генератора — три независимых Go-модуля, каждый со своим `go.mod` и единственной зависимостью `gopkg.in/yaml.v3`. Структуры `ServiceSpec`, `OperationSpec`, `ParamSpec`, `OutputParam` **продублированы** в трёх отдельных `internal/types/`. Общей библиотеки нет.
|
||||
|
||||
Это самый явный архитектурный долг: YAML — это фактический контракт между генераторами, но контракт не выражен единым типом. Добавление поля в спеку требует синхронной правки трёх файлов; расхождение проявится только в рантайме или в кривом выводе, без ошибки компиляции. Функционально генераторы не пересекаются (у каждого своя роль), пересекаются именно модели данных.
|
||||
|
||||
Дополнительно: `docs-generator` (самый крупный, ~316 строк) имеет второй режим `--ops` с отдельным входом `resources_ops_yaml/` — то есть в нём живут фактически два генератора с разными источниками, что размывает его единую ответственность.
|
||||
|
||||
---
|
||||
|
||||
## 6. Архитектурные риски — что сломается первым
|
||||
|
||||
- **Дрейф YAML-контракта между генераторами** — самое вероятное первое место поломки. Дублированные структуры разойдутся при любой эволюции схемы.
|
||||
- **Рассинхрон версии** — уже произошёл (`main.go` 5.0.75 vs `profile.env` 5.0.60). Версия определена в нескольких местах без единого источника.
|
||||
- **Устаревшие бинарники в bin** — правка исходников генератора не даёт эффекта без ручной пересборки; ошибка молчаливая.
|
||||
- **Хрупкость provider как модуля** — нельзя собрать/протестировать без прогона всего пайплайна; CI и локальная разработка вынуждены проходить полный цикл, чтобы получить компилируемое дерево.
|
||||
- **Смена API (`deck-api` → `lk-api-gateway`)** локализована в `yaml-generator` (авто-детект по `index.cfm` в URL) — это хорошо изолировано и риск здесь низкий; но тот же эндпоинт продублирован дефолтом в `docs-generator`, то есть точка знания об API размазана по двум генераторам.
|
||||
- **Секреты в `profile.env`** — операционный риск утечки при копировании конфигов между стендами.
|
||||
|
||||
---
|
||||
|
||||
**Итог:** структура сильна по вертикальному разделению (инструменты/продукт/исходники) и слаба по горизонтальным контрактам между компонентами — модель данных YAML, версия и знание об API-эндпоинте не имеют единого источника и продублированы в нескольких местах. Первым сломается именно то, что продублировано.
|
||||
@@ -0,0 +1,98 @@
|
||||
# 2026-07-07 — Opus answers: duplicate/exist fix strategy
|
||||
|
||||
## Контекст
|
||||
Промпт из `/home/naeel/tf_provider/prompt_for_opus_duplicate.md`
|
||||
После mutex (v5.0.63) pg_user_5 упал: «Операция вернула duplicate/exist, но объект не найден в state_out»
|
||||
|
||||
## Корень всех проблем
|
||||
|
||||
Код путает два разных случая «не-found»:
|
||||
- `known && !found` — список в state_out ЕСТЬ, объекта нет → реальная несогласованность
|
||||
- `!known` — списка в state_out НЕТ вообще (PostgreSQL, Kafka) → проверить невозможно
|
||||
|
||||
Сейчас `!known` трактуется как несогласованность → ошибка.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1 (ПРИОРИТЕТ) — ДА, доверять API при `!known`
|
||||
|
||||
**Где:** templates.go → генерится во все subresource Create
|
||||
|
||||
**Фикс:** усыновлять при `found || !known`, ошибку только для `known && !found`:
|
||||
```go
|
||||
if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) {
|
||||
found, known := false, false
|
||||
if targetValue != "" {
|
||||
var checkErr error
|
||||
found, known, checkErr = resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||||
if checkErr != nil {
|
||||
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
// found → подтверждено в state_out
|
||||
// !known → списка в state_out нет (PG/Kafka) → доверяем API
|
||||
// known&&!found → реальная несогласованность
|
||||
if found || !known {
|
||||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||||
resp.Diagnostics.AddWarning("Подресурс усыновлён", "API вернул already exists; объект принят в state: "+targetValue)
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
return
|
||||
}
|
||||
resp.Diagnostics.AddError("Нарушена консистентность", "API вернул duplicate/exist, но объект подтверждённо отсутствует в state_out: "+targetValue)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2 — ДА, нужен явный маппинг из метаданных
|
||||
|
||||
**Где:** subresource_guard.go — `SubresourceListKey` (эвристика `name+"s"`) и `SubresourceIdentityKey` (special-map на 2 сервиса)
|
||||
|
||||
**Доказательство:** Kafka — реальные ключи `kafkaUsers`/`kafkaTopics`, эвристика даёт `users`/`topics` → `known=false` даже когда данные есть.
|
||||
|
||||
**Фикс:** генератор должен эмитить явные `listKey`/`idKey` как константы из метаданных. Для сервисов без ключа в state_out (PostgreSQL) — эмитить `listKey=""` → `FindSubresourceInStateOut` → `known=false` → работает путь «доверять API» из Вопроса 1.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3 — НЕТ, list-эндпоинтов не существует
|
||||
|
||||
**Проверено по API-метаданным** (svc 90):
|
||||
- Операции: create(19), delete(20), resume(114), suspend(115), recovery(47), modify(48), restart(54)
|
||||
- Subresource: create_user(241), delete_user(244), create_database(245), delete_database(246)
|
||||
- **Ни одной `list_*`/`get_*`/`show_*`**
|
||||
|
||||
Перечислить подресурсы через API невозможно. Единственный источник — state_out, а у PG его нет.
|
||||
|
||||
**Вывод:** стратегия «доверять API» (Вопрос 1) — единственно возможная.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4 — Частично; текст ОК, нужен HTTP 409
|
||||
|
||||
**Где:** subresource_guard.go — `IsSubresourceAlreadyExistsError`
|
||||
|
||||
- `role "pg_user_5" already exists` → ловится ✅
|
||||
- `already exist` (без s), голый `exist` → НЕ ловится
|
||||
- **Главная дыра:** HTTP 409 без текста → НЕ ловится
|
||||
|
||||
**Фикс:**
|
||||
```go
|
||||
markers := []string{"уже существует", "already exists", "already exist", "duplicate", "conflict", " exist"}
|
||||
```
|
||||
Плюс проверка HTTP 409 (требует проброса статус-кода из клиента).
|
||||
|
||||
---
|
||||
|
||||
## Приоритет реализации
|
||||
|
||||
1. **Вопрос 1** — чинит `pg_user_5` сразу
|
||||
2. **Вопрос 2** — чинит Kafka и остальные
|
||||
3. **Вопрос 4** — устойчивость к формулировкам
|
||||
4. **Вопрос 3** — документировать как «невозможно, дизайн-решение»
|
||||
|
||||
Всё правится в шаблоне templates.go и subresource_guard.go. Сгенерированные файлы НЕ править — перегенерировать.
|
||||
|
||||
**Дата:** 2026-07-07
|
||||
**Статус:** анализ завершён, правки НЕ внесены
|
||||
@@ -0,0 +1,73 @@
|
||||
# 2026-07-07 — Opus bug analysis: skip_missing_on_delete и связанные баги
|
||||
|
||||
## Prompt
|
||||
Prompt из `/home/naeel/tf_provider/prompt_for_opus_bugs.md` — анализ 3 файлов на баги, аналогичные `skip_missing_on_delete`.
|
||||
|
||||
## Анализированные файлы
|
||||
- `/home/naeel/tf_provider/TOOLS/resource-generator/internal/templates/templates.go`
|
||||
- `/home/naeel/tf_provider/provider/internal/resources_core/crud.go`
|
||||
- `/home/naeel/tf_provider/generated/test/go/90_postgres_database_resource.go`
|
||||
- `/home/naeel/tf_provider/provider/internal/resources_core/subresource_guard.go`
|
||||
- `/home/naeel/tf_provider/provider/internal/core/client.go`
|
||||
|
||||
---
|
||||
|
||||
## Найденные баги
|
||||
|
||||
### 1. 🔴 КРИТ — Неидемпотентный Delete подресурса
|
||||
**Где:** templates.go / 90_postgres_database_resource.go (сгенерированный код)
|
||||
**Симптом:** если объект удалён вне Terraform, `terraform destroy` падает («Подресурс не найден») вместо успешного удаления из state — ресурс залипает в state навсегда.
|
||||
**Причина:** pre-check `if known && !found { ... AddError }`. Delete отсутствующего объекта по контракту Terraform = успех, а тут ошибка. Дефолт `skip_missing_on_delete=false` лишь усугубляет — это не корень.
|
||||
**Исправление:** при `known && !found` в Delete всегда `return` (успех, объект уже удалён), а не AddError. Флаг `skip_missing_on_delete` тогда становится не нужен.
|
||||
|
||||
### 2. 🔴 — skip_missing_on_delete default false
|
||||
**Где:** templates.go (шаблон генератора)
|
||||
**Симптом:** тот же не-идемпотентный destroy.
|
||||
**Причина:** `booldefault.StaticBool(false)`.
|
||||
**Исправление:** дефолт `true` (либо убрать параметр целиком в пользу фикса №1).
|
||||
|
||||
### 3. 🟡 — IsSubresourceMissingError — хрупкий матчинг
|
||||
**Где:** subresource_guard.go
|
||||
**Симптом:** delete падает как generic-ошибка, если API вернул иную формулировку.
|
||||
**Причина:** матчинг только по 4 подстрокам: `"не найден"`, `"not found"`, `"does not exist"`, `"отсутствует"`. Пропустит `"не существует"`, `"no such database"`, голый `HTTP 404`, `"unknown object"`. Плюс ложные срабатывания — `"not found"` может встретиться в несвязанном тексте.
|
||||
**Исправление:** расширить маркеры (`"не существует"`, `"no such"`, `404`) + учитывать HTTP-код, а не только текст.
|
||||
|
||||
### 4. 🟡 — LockInstance — только в памяти процесса
|
||||
**Где:** client.go
|
||||
**Симптом:** два параллельных `terraform apply` (CI, разные процессы) не сериализуются → orchestrator error / рассинхрон state_out.
|
||||
**Причина:** mutex хранится в `sync.Map` в памяти процесса. Instance-level mutex из v5.0.63 защищает только внутри одного provider-процесса.
|
||||
**Исправление:** документировать ограничение; для кросс-процессной защиты нужен серверный lock/ретрай на 409/«operation in progress».
|
||||
|
||||
### 5. 🔴 КРИТ — adoptExistingInstanceOnCreate: unlock без defer → deadlock
|
||||
**Где:** crud.go, кейсы `StateNotCreated` / `StateSuspended`
|
||||
**Симптом:** при панике в `RunInstanceOperationUniversal` мьютекс инстанса не освобождается → вечный deadlock на этом инстансе.
|
||||
**Причина:** `unlock := client.LockInstance(...)` вызывается вручную, не через `defer`.
|
||||
**Исправление:** обернуть в отдельную функцию с `defer unlock()`.
|
||||
|
||||
### 6. 🟡 — CreateResourceWithTimeout: TOCTOU
|
||||
**Где:** crud.go
|
||||
**Симптом:** между `FindInstanceByDisplayName` и adopt (resume/delete) состояние может измениться.
|
||||
**Причина:** find выполняется вне лока (instanceUid ещё неизвестен).
|
||||
**Исправление:** перепроверять состояние уже под локом внутри adopt перед resume/delete.
|
||||
|
||||
### 7. 🟡 — Read подресурса не проверяет state_out
|
||||
**Где:** 90_postgres_database_resource.go
|
||||
**Симптом:** drift (объект удалён вне Terraform) не обнаруживается; `plan` не покажет пересоздание, а `destroy` затем упадёт (баг №1).
|
||||
**Причина:** `Read` просто переустанавливает state сам в себя, без проверки `FindSubresourceInStateOut`.
|
||||
**Исправление:** в Read проверять наличие в state_out и `resp.State.RemoveResource(ctx)` если не найден.
|
||||
|
||||
---
|
||||
|
||||
## Хардкод-атрибуты instance-шаблона — дефолты ОК
|
||||
- templates.go: `suspend_on_destroy`/`adopt_existing_on_create` — дефолты из конфига (корректны)
|
||||
- `resource_name` (Required), `operation_timeout`/`log_level`/`git_revision` (Optional) — корректны
|
||||
- `skip_missing_on_delete` в instance-шаблоне отсутствует (только у subresource) — правильно
|
||||
|
||||
---
|
||||
|
||||
## Итог
|
||||
Главный корень — не-идемпотентный Delete подресурса (№1).
|
||||
`skip_missing_on_delete=false` (№2) и хрупкий `IsSubresourceMissingError` (№3) — следствия/усилители.
|
||||
|
||||
**Дата:** 2026-07-07
|
||||
**Статус:** анализ завершён, правки НЕ внесены
|
||||
@@ -0,0 +1,35 @@
|
||||
# 2026-07-07 — Opus analysis: duplicate/exist subresource adopt
|
||||
|
||||
## Контекст
|
||||
После фикса mutex (v5.0.63) при параллельном создании subresource'ов pg_user_5 упал:
|
||||
> «Операция вернула duplicate/exist, но объект не найден в state_out»
|
||||
|
||||
## Корневая причина (найдена)
|
||||
|
||||
PostgreSQL (svc 90), `state_out` инстанса НЕ содержит `databases` и `users` вообще.
|
||||
Артефакт: `artifacts/output_inventory/running_suspended_output_fields_for_docs.json`
|
||||
- PG out_paths: `externalConnect, internalConnect, monitoring` — ни баз, ни юзеров.
|
||||
|
||||
Для Kafka (svc 116) ключи есть, но называются `kafkaUsers` и `kafkaTopics` (не `users`/`topics`).
|
||||
|
||||
### Почему adopt subresource'а не работает
|
||||
|
||||
`subresource_guard.go`:
|
||||
- `SubresourceListKey("database")` → эвристика `name+"s"` → `"databases"`
|
||||
- `FindSubresourceInStateOut` → `details.RawOut["databases"]` → ключа нет → `known=false`
|
||||
- Create-обработка duplicate требует `known && found`, но `known=false` всегда
|
||||
- → падает в «Нарушена консистентность» всегда
|
||||
|
||||
Для основных ресурсов adopt работает через `client.FindInstanceByDisplayName` — реальный запрос к API инстансов. Для subresource'ов такого API нет.
|
||||
|
||||
## Дополнительные вопросы для Опуса
|
||||
|
||||
1. Есть ли в API отдельные эндпоинты `list_databases`/`list_users` для PostgreSQL, которые можно дёргать вместо парсинга `state_out`?
|
||||
|
||||
2. `SubresourceListKey`/`SubresourceIdentityKey` — эвристики `name+"s"` и special-map на 2 сервиса. Нужен явный маппинг из YAML-метаданных (реальные ключи: `kafkaUsers`, `kafkaTopics`).
|
||||
|
||||
3. При `known=false` (ключ не найден в state_out) — должен ли adopt доверять ошибке `already exists` от API и усыновлять без подтверждения? Сейчас падает в «Нарушена консистентность».
|
||||
|
||||
4. `IsSubresourceAlreadyExistsError` — достаточно ли маркеров `уже существует/already exists/duplicate/conflict`? Что если API вернёт HTTP 409 без текста или другую формулировку?
|
||||
|
||||
**Дата:** 2026-07-07
|
||||
@@ -0,0 +1,34 @@
|
||||
# 2026-07-07 — v5.0.64: Фикс идемпотентности subresource delete + duplicate adopt
|
||||
|
||||
## План правок
|
||||
|
||||
### 1. templates.go — delete идемпотентность
|
||||
**Баг #1:** pre-check `known && !found → AddError` вместо молчаливого успеха.
|
||||
**Фикс:** `known && !found` → return (успех, объект уже удалён). Только `known && !found` — ошибка.
|
||||
|
||||
### 2. templates.go — create duplicate adopt
|
||||
**Баг:** adopt требует `known && found`, но `!known` (нет данных в state_out) → падает.
|
||||
**Фикс:** `found || !known` — adopt при: найдено ИЛИ негде проверять.
|
||||
|
||||
### 3. templates.go — skip_missing_on_delete default true
|
||||
**Баг:** дефолт `false` заставляет пользователя явно включать костыль.
|
||||
**Фикс:** `StaticBool(true)`. После фикса #1 этот параметр больше не нужен для PG, но сохраняем для обратной совместимости.
|
||||
|
||||
### 4. crud.go — defer unlock
|
||||
**Баг:** `unlock()` без `defer` → паника → deadlock.
|
||||
**Фикс:** обернуть в анонимную функцию с `defer unlock()`.
|
||||
|
||||
### 5. subresource_guard.go — маркер " exist"
|
||||
**Баг:** пропускает формулировки без `s` на конце.
|
||||
**Фикс:** добавить `" exist"` в маркеры.
|
||||
|
||||
## Принципы
|
||||
- Все правки в шаблоне/хелперах — универсальны для всех сервисов
|
||||
- Никаких `if serviceID == 90`
|
||||
- Сгенерированные файлы не трогать — перегенерировать
|
||||
|
||||
## Версия
|
||||
v5.0.64
|
||||
|
||||
**Дата:** 2026-07-07
|
||||
**Статус:** в реализации
|
||||
@@ -0,0 +1,252 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Архитектура отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Opus: архитектура модификаторов (project, полный) — 2026-09-22
|
||||
|
||||
Источник: ответ Opus на `prompt_for_opus_modifier_architecture_full.md`.
|
||||
|
||||
## Ключевая модель
|
||||
|
||||
Модификатор — **декларативная проекция подмножества полей родителя**, а не «действие».
|
||||
Отсюда:
|
||||
- один **reconcile** (Create ≡ Update), не два разных пути;
|
||||
- payload всегда **полный по своим полям** (не дельта);
|
||||
- источник истины — родитель; модификатор в state хранит только read-back.
|
||||
|
||||
---
|
||||
|
||||
## 1. Сравнить и применить — полный payload, не дельта
|
||||
|
||||
Дельта запрещена: бэкенд трактует отсутствующий параметр как reset-to-default (класс A).
|
||||
Reconcile:
|
||||
1. взять все `SchemaParams`;
|
||||
2. заданные пользователем → значение из плана;
|
||||
3. незаданные → live → default (уже в `operation_run_bycode.go`);
|
||||
4. drift в Read — сравнение модели с `state_params` родителя (`state_refresh.go`).
|
||||
|
||||
## 2. Досылка незаданных (заливы A и B) — канон
|
||||
|
||||
`CompactParams` в шаблоне + досылка в клиенте — два конца одного бага.
|
||||
Правило по приоритету (уже в `operation_run_bycode.go:98-118`):
|
||||
|
||||
| Ситуация | Что слать |
|
||||
|---|---|
|
||||
| задан пользователем | значение из плана |
|
||||
| не задан, есть live ParamValue | live |
|
||||
| не задан, нет live, есть DefaultValue | дефолт |
|
||||
| не задан, ничего нет | **пропустить** (не синтезировать) |
|
||||
|
||||
**Дыра:** `CompactParams` в `modifier.go:92` выкидывает пустые ДО клиента (теряется
|
||||
«задал пусто» vs «не задал»). → Убрать `CompactParams` из шаблона модификатора,
|
||||
передавать map напрямую. Единственная точка решения — клиент. `CompactParams`
|
||||
оставить только для instance-ресурсов.
|
||||
|
||||
## 3. Delete / rollback
|
||||
|
||||
No-op Delete = скрытый drift (класс D). Пока обратный payload не подтверждён —
|
||||
допустимы 3 стратегии через флаг YAML `delete_strategy`:
|
||||
1. `noop_warn` — удалить из state + `AddWarning` (дефолт для необратимых: `ip_space`);
|
||||
2. `inverse` — если есть «выключающие» значения в modify (напр. `needEnableAVI:false`);
|
||||
3. `error` — запретить destroy (`AddError`), если откат критичен.
|
||||
|
||||
Обратный payload — та же modify с выключающими значениями. Для `ip_space` его нет → только `noop_warn`.
|
||||
|
||||
## 4. Idempotency + ID
|
||||
|
||||
- ID = **идентичность** (родитель + имя модификатора) = `instanceUID:modifierName`.
|
||||
Это правильно и не должен меняться per-apply. opUid в ID **не класть** (иначе replace).
|
||||
opUid — только в лог/приватный state.
|
||||
- **Двойная аллокация (класс E)** защищается не ID, а **идемпотентностью modify**:
|
||||
pre-check «desired == current» → пропустить run. Для `ip_space` перед modify читать
|
||||
`state_params`; если целевое достигнуто — skip.
|
||||
|
||||
## 5. Связь с родителем
|
||||
|
||||
- `<service>_id` — ссылка на родителя (Required, уже так). `depends_on` не нужен —
|
||||
пользователь передаёт UUID.
|
||||
- Borrow state не нужен: Read тянет `state_params` родителя по UUID.
|
||||
- Родителя нет (`ShouldRemoveFromState`) → модификатор удаляется из state (уже есть).
|
||||
|
||||
## 6. Create vs Update
|
||||
|
||||
Единый `reconcile(ctx, plan)`; Create и Update вызывают его (устраняет дубль веток).
|
||||
|
||||
## 7. Полный перечень кейсов (13 шт)
|
||||
|
||||
| # | Кейс | Поведение |
|
||||
|---|---|---|
|
||||
| 1 | create родителя → create модификатора | reconcile, полный payload |
|
||||
| 2 | изменение одного поля | полный payload, соседние не сбрасываются (A) |
|
||||
| 3 | partial params | досылка live→default→skip (B) |
|
||||
| 4 | `integer > 0` без значения/дефолта | пропустить (не слать `"0"`) |
|
||||
| 5 | `is_modifiable:true` (`needEnableAVI`) | не CreateOnly, менять без replace (C) |
|
||||
| 6 | destroy модификатора | по `delete_strategy` (D) |
|
||||
| 7 | replace/taint | reconcile + idempotency pre-check (E) |
|
||||
| 8 | повторный apply без изменений | desired==current → skip |
|
||||
| 9 | родитель удалён | remove из state |
|
||||
| 10 | API не вернул код в state_params | unknown→null (уже) |
|
||||
| 11 | два модификатора разных типов | разные ID |
|
||||
| 12 | operation in progress | waitForInstanceIdle (уже) |
|
||||
| 13 | drift на платформе | Read → план показывает изменение |
|
||||
|
||||
## Сводка мест правки
|
||||
|
||||
| Место | Правка |
|
||||
|---|---|
|
||||
| `modifier.go:92` | убрать `CompactParams` → прямой map (п.2) |
|
||||
| `modifier.go:77` | единый `reconcile()` (п.6) |
|
||||
| `modifier.go:156` | `delete_strategy` (п.3) |
|
||||
| `modifier.go:100` | ID = `instanceUID:modifierName` (п.4) |
|
||||
| `RunOperationByCodeWithTimeout` / reconcile | idempotency pre-check (п.4,7) |
|
||||
| `params.go:116` | учитывать modifier-канал/`is_modifiable` (класс C, кейс 5) |
|
||||
| loader модификаторов | YAML-поля `delete_strategy`, `idempotency` |
|
||||
| `operation_run_bycode.go` | оставить как есть (guard корректен) |
|
||||
|
||||
## Открытые вопросы к Opus (не закрыты ответом)
|
||||
|
||||
1. **Где брать значения для `inverse`-стратегии Delete?** Для `network` «выключающие»
|
||||
значения — это хардкод per-modifier? Как их задать декларативно в YAML, без хардкода
|
||||
в генераторе?
|
||||
2. **Формат YAML новых полей.** Точная схема `delete_strategy` и `idempotency`:
|
||||
enum-значения, дефолты, валидация (fail-fast на неизвестных).
|
||||
3. **Pre-check «desired == current» — где читать current?** Через
|
||||
`RefreshResourceState`/`state_params` или отдельный GET? Как сериализовать сравнение
|
||||
для map-fixed/array-map-fixed (порядок ключей)?
|
||||
4. **Как пометить модификатор «idempotency: check_before_run» на уровне YAML**
|
||||
(а не хардкодом в коде reconcile)?
|
||||
5. **Что если желаемое == текущее, но была «частичная» ошибка ранее** — пропускать run
|
||||
безопасно всегда, или есть исключения?
|
||||
|
||||
---
|
||||
|
||||
# Ответы Opus №2 (уточнения по 5 вопросам)
|
||||
|
||||
## 1. inverse-Delete — только декларативно в YAML, хардкод запрещён
|
||||
|
||||
Обратный payload зависит от параметров: `needEnableAVI:false` валиден, а
|
||||
`virtualServicesCount` (`integer > 0`) обнулить нечем → `0` невозможен.
|
||||
Значит inverse-значения задаются **явным блоком в YAML**. Если хоть один параметр
|
||||
не имеет валидного inverse — стратегия `inverse` недопустима (fail-fast в загрузчике).
|
||||
Для `ip_space` inverse нет вообще → только `noop_warn`.
|
||||
|
||||
## 2. Точная схема YAML новых полей
|
||||
|
||||
```yaml
|
||||
operations:
|
||||
- kind: modifier
|
||||
modifier: network
|
||||
action: modify
|
||||
delete_strategy: noop_warn # enum: noop_warn | inverse | error
|
||||
idempotency: check_before_run # enum: none | check_before_run
|
||||
delete_params: # обязателен ТОЛЬКО при delete_strategy: inverse
|
||||
- code: needEnableAVI
|
||||
value: "false"
|
||||
params: [...]
|
||||
```
|
||||
|
||||
Go-контракт (`OperationSpec`):
|
||||
```go
|
||||
DeleteStrategy string `yaml:"delete_strategy,omitempty"` // "" → noop_warn
|
||||
Idempotency string `yaml:"idempotency,omitempty"` // "" → none
|
||||
DeleteParams []ParamSpec `yaml:"delete_params,omitempty"`
|
||||
```
|
||||
|
||||
Дефолты: `delete_strategy` → `noop_warn`; `idempotency` → `none`.
|
||||
Fail-fast в `ValidateSpec`: значение вне enum → ошибка; `inverse` с пустым
|
||||
`delete_params` → ошибка; `delete_params.code` нет в `params` → ошибка; inverse-значение
|
||||
нарушает constraint параметра → ошибка на этапе генерации.
|
||||
`GenModifier` получает `DeleteStrategy`, `Idempotency`, `DeleteParams`.
|
||||
|
||||
## 3. Откуда читать current + как сравнивать
|
||||
|
||||
**Читать из `state_params`, отдельный GET не делать** (это уже источник истины для Read;
|
||||
второй источник = риск рассогласования).
|
||||
|
||||
Сравнение по типу:
|
||||
| Тип | Как сравнивать |
|
||||
|---|---|
|
||||
| bool/int/string | равенство после `normalizeUniversalValueV6` |
|
||||
| map-fixed | `JSONStringsEquivalent` (игнор порядка ключей) |
|
||||
| array-map-fixed | deep-equal с сохранением **порядка элементов** (порядок значим) |
|
||||
|
||||
Порядок ключей map-fixed — нормализовать (не значим). Порядок элементов
|
||||
array-map-fixed — НЕ нормализовать (значим).
|
||||
|
||||
## 4. idempotency декларативно
|
||||
|
||||
Поле `idempotency` на modify-операции в YAML → `ValidateSpec` → `GenModifier.Idempotency`
|
||||
→ шаблон `modifier.go` в `reconcile()` эмитит pre-check `{{- if eq .Idempotency "check_before_run" }}`.
|
||||
Для `ip_space` — в YAML; для остальных — дефолт `none`.
|
||||
|
||||
## 5. Когда безопасно skip run при desired == current (НЕ всегда)
|
||||
|
||||
Три условия безопасного skip:
|
||||
1. **Инстанс idle** — если pending/in-progress, сначала `waitForInstanceIdle`, потом
|
||||
перечитать `state_params` (иначе mid-flight аллокация даст ложное «уже равно»).
|
||||
2. **current из живого state_params, НЕ из TF-state** — после частичной ошибки TF-state
|
||||
может врать, а state_params отражает реальную платформу.
|
||||
3. **Сравнение по всем полям, не по одному** — skip только при совпадении ВСЕХ полей.
|
||||
|
||||
Итог:
|
||||
```
|
||||
idle? нет → wait, re-read
|
||||
всё-live == всё-desired? да → skip run
|
||||
иначе → reconcile (полный payload)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Ответы Opus №3 (сверка с фактическим кодом, расхождения + сомнения)
|
||||
|
||||
## Факт №1: контракт — в lib, поведение — в GenModifier
|
||||
|
||||
Подтверждено: `delete_strategy`/`idempotency`/`delete_params` добавляются в
|
||||
**`lib.OperationSpec`** (`TOOLS/lib/types.go`), resource-generator получает через алиас
|
||||
(`types.go:19`). Ссылка «types.go:39» была неточной — канон в lib.
|
||||
|
||||
Граница:
|
||||
| Где | Что |
|
||||
|---|---|
|
||||
| `lib.OperationSpec` / `lib.ParamSpec` | всё из YAML, видно обоим генераторам |
|
||||
| `GenModifier` (локально) | производные для шаблона, флаги `Needs*`, готовый inverse-список |
|
||||
|
||||
Правило: парсится из YAML → lib; вычисляется загрузчиком для шаблона → GenModifier.
|
||||
|
||||
## Факт №2: pre-check — в `core` (вариант A), экспортировать сравнение
|
||||
|
||||
`normalizeUniversalValueV6` приватная и требует `universalCfsParam` — в `resources_core`
|
||||
этих данных нет. Pre-check делать **в `core`**, не в resources_core и не в шаблоне.
|
||||
|
||||
Конкретно — экспортированный метод в `core`, вызывается из `operation_run_bycode.go`
|
||||
сразу после `fetchOperationCfsParams`:
|
||||
```go
|
||||
func (c *UniversalClient) modifierDesiredEqualsCurrent(
|
||||
desired map[string]string, cfsParams []universalCfsParam) bool
|
||||
```
|
||||
Сравнение: нормализовать обе стороны через `normalizeUniversalValueV6`; для
|
||||
map-fixed/array-map-fixed — JSON-эквивалентность. Но `JSONStringsEquivalent` лежит в
|
||||
`resources_core` → импорт в `core` даст **цикл**. Вынести JSON-эквивалентность в
|
||||
нейтральный пакет (`core/jsonutil` или в сам `core`) и переиспользовать в обоих местах.
|
||||
|
||||
Вариант C (только `JSONStringsEquivalent` без нормализации) — **отклонён** (ложный diff
|
||||
`true`/`1`).
|
||||
|
||||
Управление: `RunInstanceOperationUniversalByCode` получает флаг `idempotent` (из
|
||||
`GenModifier.Idempotency` → шаблон → параметр вызова); idle-гейт выше pre-check.
|
||||
|
||||
## Дополнительные сомнения (ответы)
|
||||
|
||||
1. **Idempotency и полный payload НЕ конфликтуют** (разные уровни). Бинарно на весь
|
||||
модификатор: `ALL == ALL` → skip целиком; любое расхождение → полный payload.
|
||||
Полудельты нет.
|
||||
2. **Частичный inverse — допустим и правилен.** `delete_params` покрывает только
|
||||
обратимые поля; необратимые/constraint просто не входят. Fail-fast смягчить:
|
||||
ошибка не «inverse обязан покрыть всё», а «код в delete_params обязан существовать
|
||||
в params и value удовлетворять constraint». Delete при inverse = modify с
|
||||
delete_params + досылка live остальных (полный payload).
|
||||
3. **`noop_warn` дефолт — оставить, но критичные — вручную `error`.** Дефолт мягкий
|
||||
(`noop_warn`, всегда с `AddWarning`), а необратимые (`ip_space`) автор спеки явно
|
||||
помечает `delete_strategy: error` в YAML. Генератор сам не решает обратимо/необратимо.
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Баг отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Opus-разбор: modify-модификатор сбрасывает create-поля в дефолт — 2026-09-22
|
||||
|
||||
Источник: ответ Opus на `prompt_for_opus_modifier_null_bug.md`.
|
||||
|
||||
## Симптом
|
||||
|
||||
`nubes_vc_nsxt_network` (modifier vc_nsxt.network, modify 111) после create Edge с
|
||||
`needEnableAVI=true`, `virtualServicesCount=3` сбрасывал `needEnableAVI` на платформе
|
||||
обратно в `false`.
|
||||
|
||||
## Корень бага (подтверждено cfsParams операций)
|
||||
|
||||
Два пути отправки modify ведут себя по-разному:
|
||||
|
||||
- **Generic modify** (`UpdateResource` → `RunInstanceOperationUniversalWithDefaults`,
|
||||
`client.go:493`) — в цикле дозаполнения шлёт **все** незаданные cfsParams их текущим
|
||||
`ParamValue` (или `DefaultValue`) — безусловно.
|
||||
- **Модификатор** (`RunOperationByCodeWithTimeout` → `RunInstanceOperationUniversalByCode`,
|
||||
`client.go:1518`) — в аналогичном цикле стоял guard `if !param.IsRequired { continue }`,
|
||||
который пропускал опциональные параметры.
|
||||
|
||||
`needEnableAVI` — опциональный параметр modify 111 и не входит в `SchemaParams` модификатора
|
||||
`vc_nsxt.network` (там только SNAT/routedNetConfiguration). Итог:
|
||||
1. модификатор его не шлёт (не его поле);
|
||||
2. back-fill его пропускает (`IsRequired == false`);
|
||||
3. бэкенд видит отсутствующий параметр → трактует как reset-to-default → `false`.
|
||||
|
||||
`CompactParams` тут ни при чём для `needEnableAVI` — параметр вообще не был в payload модификатора.
|
||||
|
||||
## Ответы Opus
|
||||
|
||||
1. **Полный или частичный payload?** Канон — полный: все параметры операции, незаданные
|
||||
дозаполняются текущим live-значением (`ParamValue`). Бэкенд для modify трактует
|
||||
пропущенный/null как reset-to-default, поэтому частичный payload обязан затирать create-поля.
|
||||
2. **Где чинить?** В `RunInstanceOperationUniversalByCode` — убрать `IsRequired`-guard в цикле
|
||||
дозаполнения (стало: слать ВСЕ незаданные params их live-значением, как в `WithDefaults`).
|
||||
- НЕ в `CompactParams` (он не видит полный набор cfsParams, только поля модификатора).
|
||||
- НЕ в шаблоне генератора (шаблон тоже не знает полного набора).
|
||||
3. **Риск для vc_org.ip_space:** основной live-путь безопасен (досылка идёт **текущим** значением,
|
||||
не хардкод-дефолтом). На fallback-пути `/instanceOperations/default/{opId}` `ParamValue` пуст —
|
||||
есть только `DefaultValue`; но тот же риск уже несёт `WithDefaults`, новой регрессии нет.
|
||||
|
||||
## Внесённый фикс
|
||||
|
||||
`provider/internal/core/client.go` — `RunInstanceOperationUniversalByCode`, цикл дозаполнения:
|
||||
убраны `if !param.IsRequired { continue }` и `if !param.IsRequired && val == "" { continue }`.
|
||||
Теперь все незаданные параметры modify досылаются их live-значением (или default).
|
||||
|
||||
Коммит: `c420ea0`.
|
||||
|
||||
## Примечание
|
||||
|
||||
Костыль в `DEV_STAND/FullPipe/edge_network.tf` (явная передача ALB/VS/qos в модификаторе)
|
||||
после фикса ядра становится избыточным, но не вреден. После пересборки провайдера можно
|
||||
убрать эти три поля из `edge_network.tf` — досылка теперь происходит автоматически.
|
||||
@@ -0,0 +1,76 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Ревью плана отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Opus: ревью плана редизайна модификаторов — 2026-09-22
|
||||
|
||||
Источник: ответ на `prompt_for_opus_modifier_plan_review.md` (план `PLAN_modifier_redesign.md`).
|
||||
|
||||
## 1. Порядок шагов — скрытые зависимости
|
||||
|
||||
- Шаг 4 (шаблон) ссылается на API из шагов 6–7 → **сначала 5→6→7, потом 4**.
|
||||
- Шаг 8 (yaml-generator) должен идти ДО регенерации `dev` и до сборки.
|
||||
|
||||
Скорректированный порядок: 1 → 2 → 3 → 5 → 6 → 7 → 4 → 8 → регенерация → 9 → 10.
|
||||
|
||||
## 2. Шаг 5 (вынос JSON-эквивалентности)
|
||||
|
||||
Путь верен. **Оставить реэкспорт-обёртку `JSONStringsEquivalent` в `json_normalize.go`**,
|
||||
не заменять вызовы по всему resources_core (иначе диф на инстансы, вопрос 7).
|
||||
Переносятся самодостаточные 5 функций: `JSONStringsEquivalent`, `normalizeJSONIfPossible`,
|
||||
`encodeCanonicalJSON`, `writeCanonicalJSON`, `normalizeJSONScalarsToStrings`.
|
||||
|
||||
Вариант «готовые строки в core» — отклонить (размазывает нормализацию, не снимает
|
||||
потребность в JSONStringsEquivalent в core).
|
||||
|
||||
## 3. Шаг 6 — сигнатура и сравнение
|
||||
|
||||
- Маппинг code→param по **двум** алиасам: `p.Code` И `p.SvcOperationCfsParam` (как в
|
||||
operation_run_bycode.go:50-58). Один `Code` даст пропуски.
|
||||
- Имя `modifierDesiredEqualsCurrent` — unexported, вызов внутри core. Слово «экспортированный» убрать.
|
||||
- bool/int/string — `normalizeUniversalValueV6` + сравнение. map-fixed — `JSONStringsEquivalent`.
|
||||
- **array-map-fixed — дыра:** `normalizeUniversalValueV6` строит дефолт только для
|
||||
`map-fixed`/`HasPrefix "map"` (params.go:33); `array-map-fixed` туда не попадает →
|
||||
сравнивать сырые значения через `jsonutil.JSONStringsEquivalent`, не через normalize.
|
||||
- desired = только явно заданные коды (до досылки live/default), иначе pre-check всегда «равно».
|
||||
|
||||
## 4. Шаг 4.4 Delete=inverse — подводный камень
|
||||
|
||||
- Delete не имеет `plan` (только `req.State`). `reconcile(ctx, plan *Model)` не подходит.
|
||||
→ `reconcile(ctx, model *Model, override map[string]string)`; для inverse override = delete_params.
|
||||
- `deleteParams` — финальные **wire-строки** (`"false"`, готовый JSON), БЕЗ прогонки через
|
||||
`ParamFormat`/тип. В реестре `deleteParam{Code, Value}` несёт готовую строку.
|
||||
|
||||
## 5. Шаг 8 — расширение реестра
|
||||
|
||||
Верно. Держать в `serviceSpecificModifiers` (main.go:34), не отдельным реестром.
|
||||
Структура `modifierException` корректна. При переходе со `map[string]string` на структуру:
|
||||
`ModifierName` берётся из структуры (сейчас `modName, ok := serviceSpecificModifiers[name]`
|
||||
— строка 95).
|
||||
|
||||
## 6. Пропущенные кейсы
|
||||
|
||||
- **taint/replace + `delete_strategy=error`** — конфликт: replace = Delete→Create, Delete=error
|
||||
блокирует → пользователь не сможет заменить error-модификатор. Решить явно:
|
||||
запретить replace у error (документировать) или отличить «чистый destroy» от replace.
|
||||
- **unknown в pre-check** — при unknown (computed ref) сравнение невозможно; шаг 6 должен
|
||||
skip-ить pre-check при unknown (иначе пустая строка даст ложный diff/панику).
|
||||
- **partial apply** — досылка live для незаданных + pre-check; проверить кейс «часть задана, часть live».
|
||||
|
||||
## 7. Риск сломать инстансы
|
||||
|
||||
Низкий при условиях:
|
||||
- **НЕ удалять `CompactParams`** (helpers.go:68) — убирается только из modifier-шаблона;
|
||||
функция нужна инстанс/action.
|
||||
- **Шаг 7 — новый метод `RunOperationByCodeIdempotent`, НЕ менять сигнатуру**
|
||||
`RunOperationByCodeWithTimeout`/`RunInstanceOperationUniversalByCode` (зовут инстансы).
|
||||
- Шаг 1 (поля OperationSpec) аддитивен — безопасно.
|
||||
|
||||
## Дополнительно (не в вопросах)
|
||||
|
||||
- **Шаг 4.3 (ID=identity) — ломающая миграция state.** Смена формата ID изменит ID уже
|
||||
задеплоенных модификаторов → Terraform форснёт replace. Нужно: либо сохранить старый
|
||||
формат ID, либо явный state-migration plan. В плане не отмечено.
|
||||
- **Шаг 3 — `normalizeDeleteStrategy`/`normalizeIdempotency`** — где живут (в loader.go, рядом
|
||||
с веткой modifier). Не указано.
|
||||
- **ValidateSpec** — проверка `delete_params.code ∈ op.Params` по lower-code; сверить поле `Code`.
|
||||
@@ -0,0 +1,43 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Код-ревью отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Opus-код-ревью: модификаторы (kind: modifier) — 2026-09-22
|
||||
|
||||
Источник: ревью по `prompt_for_opus_modifiers_review.md`.
|
||||
Объекты: `nubes_vc_org_ip_space` (vc_org.ip_space, modify 207) и `nubes_vc_nsxt_network` (vc_nsxt.network, modify 111).
|
||||
Файлы: шаблон `TOOLS/resource-generator/internal/templates/modifier.go`, loader, `generated/dev/go/19_vc_org_ip_space_modifier.go`, `22_vc_nsxt_network_modifier.go`, `registry.go`, `provider/internal/resources_core/crud.go` (RunOperationByCodeWithTimeout), `provider/internal/core/client.go` (RunInstanceOperationUniversalByCode, RefreshResourceState, ShouldRemoveFromState).
|
||||
|
||||
## 1. Жизненный цикл Create/Update/Read/Delete
|
||||
|
||||
- **Create ≡ Update**: оба тела идентичны — гонят `modify` с текущими params. Любое изменение любого атрибута = повторный запуск `modify` целиком, не дельта.
|
||||
- **Идемпотентность на платформе, не в провайдере.** Нет сравнения «до/после», нет проверки, что операция применила именно эти значения (только `validate-cfs` + `run`). Если API-`modify` аккумулирует, а не перезаписывает (особенно `vIPConfigure`) — повтор = дубли/лишний расход квоты.
|
||||
- **Refresh (Read) частичный и потенциально вредный.** `RefreshResourceState` тянет `state_params` и перезаписывает input-поля:
|
||||
- если платформа не эхоит код в `state_params` (типично для операционных modify-параметров) — дрейф не детектируется, Read почти no-op → управление «вслепую»;
|
||||
- если эхоит, но нормализованно (bool как `1/0`, порядок ключей в `routedNetConfiguration`/`map-fixed`) — вечный diff. `JsonNormalize()` стоит только как plan-modifier на Required-строке; ветка `map-fixed` в refresh json-нормализацию не гарантирует.
|
||||
|
||||
## 2. Delete = no-op — ожидаемо? Подводные камни
|
||||
|
||||
No-op ожидаем (обратного payload нет). Но:
|
||||
|
||||
- **destroy убирает ресурс из state, оставляя эффект на платформе** (выделенные IP, включённый AVI/LB). Инфраструктура и state расходятся молча.
|
||||
- **Самый опасный сценарий — taint/replace или destroy→apply**: Create снова гонит `modify` → повторное выделение внешних IP (`nubes_vc_org_ip_space`). Прямой риск двойного выделения и расхода.
|
||||
- `BuildActionID(instanceUID, "modify", "ip_space")` — детерминированный константный ID, не привязан к реальному opUid. State не отражает, какая операция и с какими значениями отработала; два модификатора одного типа на одном инстансе получили бы одинаковый ID.
|
||||
|
||||
## 3. Риски передачи по коду (code → id) в RunInstanceOperationUniversalByCode
|
||||
|
||||
- **Резолв code→id полностью зависит от `GET /instanceOperations/{opUid}?fields=cfsParams`** — того запроса, что даёт 500 на проблемных инстансах. Без fallback модификатор неработоспособен целиком (без словаря `codeToParam` параметры не отправить).
|
||||
- **Коды захардкожены в сгенерированном коде** (`vIPConfigure`, `needEnableAVI`…). Переименование на платформе ломается в рантайме («код параметра X не найден»), а не на компиляции — молчаливая деградация.
|
||||
- **Частичный payload = скрытые сайд-эффекты.** `CompactParams` выкидывает пустые Optional. Для `modify` пропуск параметра платформа может трактовать как «сбросить в дефолт» (не задал `needEnableAVI` → LB может выключиться). Семантика PATCH vs PUT не контролируется провайдером.
|
||||
- opId ищется среди `AvailableOperations`: не то состояние инстанса → жёсткий отказ «операция недоступна». `LockInstance` сериализует операции по инстансу — конкурентность закрыта корректно.
|
||||
|
||||
## ТОП-3 критичных
|
||||
|
||||
1. **Двойное выделение при replace/destroy→apply** (особенно `ip_space`): no-op Delete + повторный `modify` на Create + отсутствие проверки идемпотентности = риск задвоить внешние IP/квоту. Нужен guard перед `modify` (проверка по `state_params`/наличию ресурса), либо явно документировать запрет replace.
|
||||
2. **Refresh либо слепой, либо вечный diff.** Для операционных modify-параметров `state_params` обычно их не возвращает → Read ничего не сверяет; там где возвращает — нормализация (bool/JSON `map-fixed`) ломает план. Решить: честный drift-refresh с нормализацией, либо явно пометить поля как не-refreshable.
|
||||
3. **Жёсткая зависимость от падающего `?fields=cfsParams`.** code→id держится на запросе, который 500-тит на проблемных инстансах — модификатор ложится целиком. Fallback на `/instanceOperations/default/{opId}` — условие работоспособности, а не «приятная опция».
|
||||
|
||||
## Мелочи
|
||||
|
||||
- Константный `BuildActionID` — ID не привязан к реальной операции.
|
||||
- Захардкоженные коды ломаются в рантайме, а не на сборке.
|
||||
@@ -0,0 +1,933 @@
|
||||
# Диалог: Opus 4.8 — анализ архитектуры провайдера и ресурсов-модификаторов (2026-09-30)
|
||||
|
||||
> **Что это.** Полная, без сокращений, запись диалога: постановка задачи пользователем,
|
||||
> ход работы агента (GitHub Copilot) до создания файла-промпта, содержимое промпта,
|
||||
> ответ Opus 4.8 (приведён дословно в том виде, как получен) и открытый вопрос Opus.
|
||||
> **Формат.** Хронология ходов. Реплики пользователя и ответ Opus — дословно, включая опечатки.
|
||||
> **Статус.** Диалог НЕ завершён: Opus задал вопрос, ответ ему ещё не передан.
|
||||
|
||||
---
|
||||
|
||||
## Ход 1 — Задание пользователя (дословно)
|
||||
|
||||
```
|
||||
составь промпт в файл для Опус 4.8
|
||||
пусть проанализирует архитектуру провайдера ... с дополнительными ресурсами-модификаторами
|
||||
ограничь зону его доступносьти - пусть НИЧЕГО не читает из хистори, только файл с архитектурой
|
||||
и точно выдай ему список файлов которые надо изучить, более пусть никуда не лезет
|
||||
если что непончятно - пусть напишет тебе вопрос, я тебе передам
|
||||
взаимодейтвуйте в редиме диалога, НО НЕ НАДО злоупотреблять !
|
||||
ответы от него - МАКСИМАЛЬНО СЖАТО, без лирики и воды, лишь бы ты сам как агент понял
|
||||
```
|
||||
|
||||
Контекст задания: активный файл — `DEV_STAND/FullPipe/shturval.tf1`; активное выделение — строка 117
|
||||
(`suspend_on_destroy`).
|
||||
|
||||
---
|
||||
|
||||
## Ход 2 — Работа агента до создания файла (разведка, только чтение)
|
||||
|
||||
Поиск архитектурных документов и существующих промптов для Opus:
|
||||
|
||||
- `NOTES/20_prompts/` — 20 файлов промптов; среди них `prompt_for_opus_modifier_architecture_full.md`,
|
||||
`prompt_for_opus_modifiers_review.md`, `prompt_for_opus_review_modify_resources_2026-09-24.md`,
|
||||
`prompt_for_opus_modifier_global_architecture.md`. Часть помечена баннером ⛔ «ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО».
|
||||
- `HISTORY/OPUS/` — 16 файлов диалогов с Opus (напр. `2026-09-22_modifier_architecture_project.md`).
|
||||
- Кандидаты на «файл архитектуры»: `TOOLS/ARCHITECTURE.md`, `NOTES/30_analysis/ARCHITECTURE_NEW.md`,
|
||||
`docs/60_strategy/provider_philosophy.md`, `docs/60_strategy/modifier_resources_ideology_and_specification.md`.
|
||||
|
||||
Прочитано:
|
||||
|
||||
- `TOOLS/ARCHITECTURE.md` (246 строк) — в первых строках помечен «PRIMARY SOURCE OF TRUTH»,
|
||||
«THIS FILE IS THE FOUNDATION».
|
||||
- `NOTES/30_analysis/ARCHITECTURE_NEW.md` — «Universal Rebuild — Архитектура и рабочая цепочка»
|
||||
(внутри — ссылки на пути `/home/naeel/terra/universal_rebuild`, т.е. иная раскладка репозитория).
|
||||
- `NOTES/20_prompts/prompt_for_opus_modifier_architecture_full.md`, `…_modifiers_review.md`,
|
||||
`…_review_modify_resources_2026-09-24.md`.
|
||||
- `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md` — карта файлов, разделение «актуально / LEGACY».
|
||||
|
||||
Проверка актуальности через git:
|
||||
|
||||
```
|
||||
git log -3 -- TOOLS/ARCHITECTURE.md
|
||||
cbd559d 2026-09-30 docs(tools): канонический пайплайн + история ужесточения генерации YAML
|
||||
78f9dfb 2026-09-22 refactor(build): эфемерные generated-копии …
|
||||
c14f7de 2026-09-22 docs: раздел «Реестр исключений» + диалог код-ревью opus/astra
|
||||
```
|
||||
|
||||
Установленная фактическая реализация модификаторов (на 2026-09-30):
|
||||
|
||||
- База: YAML на сервис — «чистая выгрузка» из API, без доменных меток. В `TOOLS/yaml-generator/main.go`
|
||||
(комментарий перед `CollectOperations`) прямо сказано: реестр `serviceSpecificModifiers`, ранее
|
||||
помечавший `modify` как `kind=modifier`, удалён при рефакторинге 2026-09-23; модификаторы теперь —
|
||||
«отдельный оверлей (см. `modifiers.yaml` + второй проход)», базовый YAML о них не знает.
|
||||
- Реальные модификаторы — РУЧНЫЕ Go-ресурсы:
|
||||
- `provider/internal/resources_core/org_ip_allocation_resource.go` —
|
||||
`nubes_vc_org_ip_allocation` (сервис 19, `modify`, `vIPConfigure` id 662);
|
||||
- `provider/internal/resources_core/nsxt_snat_resource.go` —
|
||||
`nubes_vc_nsxt_snat` (сервис 22, `modify`, `ipSpaceName` id 372; канон-значение `no-needed`).
|
||||
- Регистрация: `provider/internal/provider/provider.go`, функция `Resources()` (строки 182–190) —
|
||||
`append(resources, resources_core.NewOrgIpAllocationResource, resources_core.NewNsxtSnatResource)`.
|
||||
- Вызываемые методы ядра: `RunInstanceOperationUniversalByCode` (`provider/internal/core/operation_run_bycode.go`),
|
||||
`GetInstanceStateParams` (`provider/internal/core/instance_params.go`),
|
||||
`LockInstance`/`UniversalClient` (`provider/internal/core/client.go`),
|
||||
`ResolveRefSvcParamValue` (`provider/internal/core/refsvc_resolve.go`),
|
||||
`ShouldRemoveFromState` (`provider/internal/resources_core/resource_diagnostics.go`).
|
||||
- Спеки операций/параметров: `generated/dev/resources_yaml/19_vc_org.yaml`, `22_vc_nsxt.yaml`.
|
||||
- Пример применения цепочки: `DEV_STAND/FullPipe/modifiers.tf`.
|
||||
|
||||
Агент задал пользователю 3 уточняющих вопроса (какой файл считать «архитектурой»; что именно должен
|
||||
выдать Opus; имя файла промпта) — **пользователь вызов отклонил (cancelled)**.
|
||||
|
||||
---
|
||||
|
||||
## Ход 3 — Ответ пользователя (дословно)
|
||||
|
||||
```
|
||||
думай сам ! возможно несколько файлов ему дать
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 4 — Решение агента и создание файла
|
||||
|
||||
Агентом принято самостоятельно (без дополнительных вопросов):
|
||||
|
||||
- «Файл с архитектурой» — единственный: `TOOLS/ARCHITECTURE.md`.
|
||||
- Формат выхода Opus — архитектурный разбор: устройство по слоям, спека ↔ код, жизненный цикл
|
||||
модификаторов, риски с указанием `файл:строка`.
|
||||
- Путь промпта — `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md`
|
||||
(соответствует конвенции папки `NOTES/20_prompts/`).
|
||||
|
||||
Проверка существования всех 22 путей из списка (скрипт `test -f` по каждому) — результат:
|
||||
`ALL FILES PRESENT`. Отсутствующих путей нет.
|
||||
|
||||
Коммит: `752244f` —
|
||||
`docs(prompt): промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов`.
|
||||
|
||||
### Содержимое созданного файла `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md` (дословно)
|
||||
|
||||
```markdown
|
||||
# Промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов
|
||||
|
||||
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
|
||||
**Дата:** 2026-09-30. **Стенд-источник фактов:** `dev`.
|
||||
|
||||
---
|
||||
|
||||
## Роль и режим работы
|
||||
|
||||
- Ты — архитектор/ревьюер универсального Terraform-провайдера.
|
||||
- Работаем **в диалоге**: я (агент) передаю твои вопросы пользователю и возвращаю его ответы.
|
||||
- Вопрос задавай ТОЛЬКО если без него ответить нельзя. Максимум 1–2 вопроса за раз, предельно коротко.
|
||||
- Не догадываться. Нет данных — вопрос, а не допущение.
|
||||
- Область не расширять: отвечать ровно на поставленную задачу.
|
||||
|
||||
## Задача
|
||||
|
||||
Проанализировать архитектуру универсального провайдера Nubes и встроенный в неё слой
|
||||
**ресурсов-модификаторов** — отдельных ресурсов, которые вызывают операцию `modify`
|
||||
у родительского инстанса (когда нужного параметра нет в операции `create`).
|
||||
|
||||
Оценить:
|
||||
|
||||
1. Как устроена архитектура по слоям и как течёт поток данных (API → YAML → код → API).
|
||||
2. Соответствие заявленной спеки (`TOOLS/ARCHITECTURE.md`) фактической реализации — все
|
||||
расхождения, с указанием `файл:строка`.
|
||||
3. Корректность жизненного цикла модификаторов: `Create` / `Read` / `Update` / `Delete`,
|
||||
идемпотентность, дрейф (drift), поведение при `replace` / повторном `apply`, импорт.
|
||||
4. Место модификаторов в универсальном ядре: где и как нарушается принцип
|
||||
«ядро универсально, доменные знания — только данные». Насколько оправдано текущее
|
||||
решение (ручные Go-ресурсы, зарегистрированные поверх генерируемых).
|
||||
5. Границы ответственности: что модификатор делает сам, что отдаёт платформе; как
|
||||
выражается обратная операция (откат при `destroy`, значение «выключено»).
|
||||
6. Риски и топ-проблемы — по убыванию критичности, каждое с `файл:строка`.
|
||||
|
||||
## Границы доступа (ЖЁСТКО)
|
||||
|
||||
Читать РАЗРЕШЕНО **только** файлы из списка ниже. Всё остальное — ЗАПРЕЩЕНО, в частности:
|
||||
|
||||
- `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `docs/**`, `site/**`, `site_test/**`,
|
||||
`apps/**`, `charts/**`, `FIYR_MGU/**`, `gateway/**`, `scripts/**`, `secrets/**`,
|
||||
`tfflaskcrud/**`, `tfluceecrud/**`, `tfnodejscrud/**`, `DEV_STAND/**` (кроме одного файла
|
||||
из списка), `TEST_STAND/**`, `PROD_STAND/**`, `provider/artifacts/**`, `provider/bin/**`;
|
||||
- история git (`git log`, `git show`, `git diff` с коммитами), коммиты, теги, ветки;
|
||||
- любой файл репозитория, которого нет в списке ниже.
|
||||
|
||||
Нужен файл вне списка → НЕ читать, а задать мне вопрос.
|
||||
|
||||
## Файлы к изучению (исчерпывающий список)
|
||||
|
||||
### Группа 1. Архитектура (спека)
|
||||
- `TOOLS/ARCHITECTURE.md`
|
||||
|
||||
### Группа 2. Ресурсы-модификаторы и их регистрация
|
||||
- `provider/internal/provider/provider.go`
|
||||
- `provider/internal/resources_core/org_ip_allocation_resource.go`
|
||||
- `provider/internal/resources_core/nsxt_snat_resource.go`
|
||||
- `provider/internal/resources_core/org_ip_allocation_test.go`
|
||||
|
||||
### Группа 3. Рантайм-зависимости модификаторов (ядро)
|
||||
- `provider/internal/core/client.go`
|
||||
- `provider/internal/core/operation_run_bycode.go`
|
||||
- `provider/internal/core/instance_params.go`
|
||||
- `provider/internal/core/refsvc_resolve.go`
|
||||
- `provider/internal/resources_core/resource_diagnostics.go`
|
||||
- `provider/internal/resources_core/crud.go`
|
||||
|
||||
### Группа 4. Генератор (как рождается «универсальная» часть)
|
||||
- `TOOLS/yaml-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/loader/loader.go`
|
||||
|
||||
### Группа 5. Факты API (спеки операций/параметров)
|
||||
- `generated/dev/resources_yaml/19_vc_org.yaml`
|
||||
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`
|
||||
|
||||
### Группа 6. Применение модификаторов (композиция цепочки)
|
||||
- `DEV_STAND/FullPipe/modifiers.tf`
|
||||
|
||||
### Группа 7. Только если без них нельзя ответить (иначе не открывать)
|
||||
- `provider/internal/resources_core/state_refresh.go`
|
||||
- `provider/internal/resources_core/params_compare.go`
|
||||
- `provider/internal/resources_core/helpers.go`
|
||||
- `TOOLS/resource-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/templates/instance.go`
|
||||
|
||||
## Что нужно на выходе
|
||||
|
||||
Структурированный отчёт, разделы строго в этом порядке:
|
||||
|
||||
1. **Устройство архитектуры** — слои и поток данных, 5–10 строк.
|
||||
2. **Спека ↔ код** — список расхождений `ARCHITECTURE.md` с реализацией (`файл:строка`).
|
||||
3. **Дефекты и риски модификаторов** — по убыванию критичности. По каждому:
|
||||
суть → место (`файл:строка`) → последствие → предлагаемое направление (одна строка).
|
||||
4. **Открытые вопросы** — списком, если есть.
|
||||
|
||||
## Формат ответа
|
||||
|
||||
- Максимально сжато: тезисы, без вступлений, воды и «лирики».
|
||||
- Каждое утверждение проверяемо: ссылка `файл:строка`.
|
||||
- Код — только короткие фрагменты, и лишь если без них тезис не понятен.
|
||||
- Никаких «а ещё могу», никаких предложений расширить работу.
|
||||
|
||||
## Правило «стоп»
|
||||
|
||||
Если задание неоднозначно или данных не хватает — **остановиться и задать один короткий
|
||||
вопрос**. Не достраивать смысл и не действовать по догадке.
|
||||
```
|
||||
|
||||
### Итоговое сообщение агента пользователю (дословно)
|
||||
|
||||
```
|
||||
Готово.
|
||||
|
||||
`NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md` (коммит `752244f`).
|
||||
|
||||
Что внутри:
|
||||
- **Задача** — анализ архитектуры провайдера + слоя модификаторов (`Create/Read/Update/Delete`, идемпотентность, drift, спека↔код, место в универсальном ядре, границы/откат, топ-рисков с `файл:строка`).
|
||||
- **Границы (жёстко)** — запрет на `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `docs/**`, стенды, git-историю; нужен файл вне списка → вопрос мне, не чтение.
|
||||
- **Список файлов** — 22 шт., исчерпывающий, 7 групп: спека (`TOOLS/ARCHITECTURE.md`) → модификаторы + регистрация → рантайм-ядро → генератор → YAML-спеки 19/22 → пример (`DEV_STAND/FullPipe/modifiers.tf`) → «только при необходимости».
|
||||
- **Формат ответа** — сжато, тезисы, каждое утверждение с `файл:строка`, без «лирики».
|
||||
- **Режим** — диалог, но без злоупотребления; при неясности один короткий вопрос и стоп.
|
||||
|
||||
Все 22 пути проверены — существуют.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 5 — Ответ Opus 4.8 (дословно, как получен от пользователя)
|
||||
|
||||
Ниже — полный сырой лог сессии Opus (включая служебные строки о чтениях и неудачных поисках),
|
||||
затем его отчёт.
|
||||
|
||||
```
|
||||
Read
|
||||
|
||||
Прочитаю файлы строго по разрешённому списку. Начну с групп 1–3.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю группу 2 (модификаторы) и группу 3 (ядро).
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю группу 3 (ядро) и тест.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю `client.go` (ядро) и группу 4 (генератор).
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю группу 4 (генератор), группу 6 (modifiers.tf) и начну группу 5.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю группу 5 (API YAML). Сначала проверю размер.
|
||||
|
||||
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
|
||||
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
|
||||
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
|
||||
|
||||
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
|
||||
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
|
||||
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
|
||||
|
||||
Searched for files matching `**/generated/dev/resources_yaml/*vc_org*.yaml`, no matches
|
||||
|
||||
Searched for files matching `**/generated/dev/resources_yaml/*nsxt*.yaml`, no matches
|
||||
|
||||
Searched for files matching `**/generated/dev/**/19_*.yaml`, no matches
|
||||
|
||||
Read
|
||||
|
||||
Searched for files matching `**/generated/dev/resources_yaml/*.yaml`, no matches
|
||||
|
||||
Read
|
||||
|
||||
Файлы существуют; grep не сработал по паттерну. Прочитаю их напрямую.
|
||||
|
||||
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
|
||||
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
|
||||
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
|
||||
|
||||
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
|
||||
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
|
||||
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Проанализировал все файлы из разрешённого списка (группы 1–6; группа 7 не понадобилась). Отчёт ниже.
|
||||
```
|
||||
|
||||
### Отчёт Opus (дословно)
|
||||
|
||||
```markdown
|
||||
## 1. Устройство архитектуры
|
||||
|
||||
Поток данных: **API → YAML → Go-код → API**.
|
||||
- Генератор YAML (`main.go`) делает **чистую выгрузку** операций сервиса из API (`create/delete/modify/suspend/...`) без доменных знаний — один файл на сервис (`19_vc_org.yaml`, `22_vc_nsxt.yaml`).
|
||||
- Генератор кода (`loader.go`) читает YAML, классифицирует операции на `instance/subresource/action/modifier`, строит `GenResource/...` и рендерит универсальные ресурсы.
|
||||
- Ядро (`core`) — универсальный HTTP-клиент: запуск операций, ожидание, резолв ref-параметров, досылка дефолтов.
|
||||
- **Модификаторы** — это НЕ генерируемый слой: два ручных Go-ресурса (`org_ip_allocation_resource.go`, `nsxt_snat_resource.go`), зарегистрированных вручную в `provider.go:199-204`. Каждый дёргает `modify` родителя через `RunInstanceOperationUniversalByCode`.
|
||||
|
||||
## 2. Спека ↔ код (расхождения)
|
||||
|
||||
**S1. Реестр `serviceSpecificModifiers` в спеке указан не там, где он есть (его нет).**
|
||||
`ARCHITECTURE.md` (таблица «Exception Registry») утверждает, что `serviceSpecificModifiers` живёт в `main.go`. Фактически этот реестр **удалён** (рефакторинг 2026-09-23) — прямо задокументировано в `main.go:70-90`. Названного реестра нет; модификаторы стали ручными ресурсами. Таблица-спека устарела.
|
||||
|
||||
**S2. «Service-specific Go code is fully generated… No manual edits» / «Core is universal» — нарушено.**
|
||||
`ARCHITECTURE.md` (Core Principles 2, 3; «Provider Model»). Но `org_ip_allocation_resource.go` и `nsxt_snat_resource.go` — ручной сервис-специфичный Go с зашитыми `service_id`, именами параметров и канон-значениями, лежащий в слое `resources_core`. Это доменная логика внутри «ядра».
|
||||
|
||||
**S3. Заявленные «ровно ДВА реестра исключений» — по факту есть третье место.**
|
||||
`ARCHITECTURE.md` («The ONLY allowed deviations… MUST live in exactly two named registries»). Третье место доменных данных/логики — `provider.go:199-204` + два файла модификаторов. Оно не покрыто правилом реестра и не отражается в его диффе.
|
||||
|
||||
**S4. «modify всегда через WithDefaults» — модификаторы идут через ByCode.**
|
||||
`ARCHITECTURE.md` («modify всегда через WithDefaults / `RunInstanceOperationUniversalWithDefaults`»). Модификаторы намеренно используют `RunInstanceOperationUniversalByCode` (`operation_run_bycode.go:10-13`) с причиной в комментарии `nsxt_snat_resource.go:240-250`. Досылка дефолтов там своя (live → paramValue формы → default, `operation_run_bycode.go:105-150`). Буквальное утверждение спеки не выполняется.
|
||||
|
||||
**S5. Полноценный генерируемый слой модификаторов существует, но не задействован.**
|
||||
`loader.go:55-90` полностью поддерживает `kind: modifier` с `delete_strategy` (`noop_warn/inverse/error`), `idempotency` (`none/check_before_run`), `delete_params`, валидацией (validateModifierOperation). Ядро под это имеет `RunInstanceOperationUniversalByIdempotent` (`operation_run_bycode.go:15-19`) и `RunOperationByCodeIdempotent` (`crud.go`). Но оба реальных модификатора — ручные и это всё **не используют**, переизобретая delete-стратегию вручную (`keep_on_destroy` + inverse). Базовые YAML (19, 22) `kind: modifier` не содержат — оверлей `modifiers.yaml`, упомянутый в `main.go:84-90`, в разрешённом списке отсутствует и в базовых спеках не проявлен.
|
||||
|
||||
## 3. Дефекты и риски модификаторов (по убыванию критичности)
|
||||
|
||||
**R1. Двойное владение одним и тем же параметром API.**
|
||||
Суть: `vIPConfigure` (id 662) есть в `modify` генерируемого `nubes_vc_org` (`19_vc_org.yaml`, op modify), а `ipSpaceName` (id 372) — в `modify` генерируемого `nubes_vc_nsxt` (`22_vc_nsxt.yaml`). Те же поля пишет и модификатор.
|
||||
Место: `org_ip_allocation_resource.go:316-340` / `nsxt_snat_resource.go:240-253`.
|
||||
Последствие: если пользователь заводит и инстанс-ресурс, и модификатор — «война дрейфов»: два ресурса по очереди перезаписывают поле каждым apply.
|
||||
Направление: явно исключать пересекающиеся коды из схемы генерируемого ресурса, если поле отдано модификатору (или запретить одновременное использование).
|
||||
|
||||
**R2. Зашитые сервис-специфичные данные обходят страж `check_hardcoded_service_ids.sh`.**
|
||||
Суть: id 19/22, имена `vIPConfigure`/`ipSpaceName`, значение `"no-needed"` зашиты как литералы-аргументы, а не как `svc.ID == N`.
|
||||
Место: `org_ip_allocation_resource.go:296-314` (`ResolveRefSvcParamValue(ctx, 19, …)`), `nsxt_snat_resource.go:43`.
|
||||
Последствие: правило «никаких hardcoded service id вне реестров» формально соблюдено, фактически — нет; страж это не ловит.
|
||||
Направление: вынести id/коды/каноны в один явный реестр-данные, покрытый чекером, либо расширить паттерн чекера.
|
||||
|
||||
**R3. Порядок «edge → аллокация» не гарантируется провайдером.**
|
||||
Суть: платформа требует существующий vDC+Edge до `modify` орги, иначе «Can't cast Complex Object Type Struct to String».
|
||||
Место: описано в `modifiers.tf:6-16`; в коде порядок не выражен — держится только на пользовательском `depends_on`.
|
||||
Последствие: забытый `depends_on` → непонятная ошибка платформы на apply.
|
||||
Направление: либо документировать как жёсткое требование в схеме/описании ресурса, либо проверять готовность edge в `Create` до modify.
|
||||
|
||||
**R4. Нельзя снять аллокацию через атрибут — только `destroy`.**
|
||||
Суть: пустой массив запрещён (`org_ip_allocation_resource.go:328-331` `len==0 → error`), а Delete шлёт `count=0`, но `[]` не отправляется (`org_ip_allocation_resource.go:246-266`).
|
||||
Последствие: «выключено» выражается двумя разными способами (count=0 при destroy vs невозможность `[]` при update) — асимметрия семантики.
|
||||
Направление: определить единый канон «ноль аллокаций» и разрешить его через атрибут, либо явно задокументировать ограничение как намеренное.
|
||||
|
||||
**R5. Модификаторы не идемпотентны на уровне API (modify выполняется всегда).**
|
||||
Суть: `ByCode` без pre-check — `Create`/`Update` всегда POST-ят modify, даже если live уже совпадает. Причина осознанная (комментарий applyAllocation).
|
||||
Последствие: лишние операции modify при повторных apply/после ручных правок state; на больших инстансах — время и нагрузка. Корректности не нарушает (дрейф гасится в `Read`).
|
||||
Направление: если появится надёжное сравнение desired↔live (не с paramValue формы) — переключить на idempotent-путь, уже имеющийся в ядре.
|
||||
|
||||
**R6. Импорт оставляет Required-атрибуты неинициализированными.**
|
||||
Суть: `ImportState` ставит только `id` и `organization`/`nsxt_uid` (`org_ip_allocation_resource.go:288-292`, `nsxt_snat_resource.go:222-226`). `vip_configure`/`ip_space_name` (Required) наполняются лишь в `Read`, и то по условию (nsxt — только если live непустой `nsxt_snat_resource.go:160-166`).
|
||||
Последствие: при импорте эджа с невыставленным SNAT `ip_space_name` останется null → требуется совпадение с конфигом на следующем плане; для пользователя неочевидно.
|
||||
Направление: в `ImportState`/`Read` заполнять атрибут каноном (`no-needed`) при отсутствии значения.
|
||||
|
||||
## 4. Открытые вопросы
|
||||
|
||||
1. Оверлей `modifiers.yaml` и генерируемый слой `kind: modifier` (см. R5/S5) — в разрешённый список не входил. Нужно ли оценивать его как «мёртвый/параллельный» код к удалению, или он планируется как целевая замена ручных модификаторов? (файл вне списка — не читал).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 6 — Указание пользователя (дословно)
|
||||
|
||||
```
|
||||
сначала документируй ВЕСЬ диалог, без сокращений
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Открытый вопрос Opus (ожидает ответа пользователя)
|
||||
|
||||
1. Оверлей `modifiers.yaml` и генерируемый слой `kind: modifier` — «мёртвый/параллельный» код
|
||||
к удалению, или планируемая целевая замена ручных модификаторов?
|
||||
|
||||
---
|
||||
|
||||
## Ход 7 — Запрос пользователя (дословно)
|
||||
|
||||
```
|
||||
твоё критическое мнение ?
|
||||
```
|
||||
|
||||
### Критическое мнение агента о отчёте Opus (дословно)
|
||||
|
||||
```
|
||||
**Сильное**
|
||||
- **S1** — верно. `ARCHITECTURE.md:225` ссылается на реестр `serviceSpecificModifiers`, а в `TOOLS/yaml-generator/main.go` (~стр. 77–90) написано, что он удалён при рефакторинге. Спека устарела — цена находки низкая, но факт точный.
|
||||
- **S5** — верно и важно. `loader.go` умеет `kind: modifier` (+ `delete_strategy`, `idempotency`) и в ядре есть idempotent-путь, но реальные модификаторы ручные. Это не «мёртвый код», а **незавершённая миграция**.
|
||||
- **R1** — самое ценное. `nubes_vc_nsxt` в `Update` шлёт `ip_space_name` (372), и тот же параметр пишет `nubes_vc_nsxt_snat` → два ресурса перезаписывают одно поле. Аналогично `nubes_vc_org` / `v_ip_configure`.
|
||||
- R4, R6 — по делу, но второго порядка.
|
||||
|
||||
**Слабое / спорное**
|
||||
- **S2, S4 — натянуто.** Правила спеки («no manual edits», «core is universal») писаны про **сгенерированный** код; ручной оверлей в них вообще не описан. Это пробел спеки, а не нарушение «по букве». S4 ещё и относится к `Update` инстанс-ресурсов, к модификатору прямого отношения не имеет.
|
||||
- **R2 — нарушение собственного правила.** Opus утверждает, что `check_hardcoded_service_ids.sh` «это не ловит», **не читая сам скрипт** (он вне списка). Надо было оформить вопросом, а не утверждением.
|
||||
- **R5 — завышен по приоритету**: сам же пишет «корректности не нарушает».
|
||||
- **Номера строк не сверены.** У меня из чтения `provider.go` функция `Resources` начинается со строки 182, и регистрация модификаторов идёт сразу за ней, а не 199–204. Одно из двух неверно → «проверяемость» отчёта под вопросом (а она была главным требованием).
|
||||
|
||||
**Упущено**
|
||||
- Файла `modifiers.yaml` в репозитории **нет вообще** — только упоминания-комментарии. Значит это не «параллельный слой», а **задокументированное, но не созданное** наложение; его вопрос №1 надо переформулировать.
|
||||
- Не названа корневая причина: генератор умеет модификаторы, но доменное знание (какая операция, имя, inverse) некуда положить — оверлей не сделан, поэтому написали руками.
|
||||
- Не отмечена рассинхронизация семантики: у генерируемых ресурсов `suspend_on_destroy`, у модификаторов `keep_on_destroy` — два имени для одного смысла.
|
||||
- То, что для Terraform опаснее всего — устойчивость `Read` (вечный diff), — разобрано тоньше всего, хотя именно там был реальный баг с порядком ключей JSON.
|
||||
|
||||
**Итог:** ~60 % полезного (S1, S5, R1, R4 + вопрос про оверлей), но с натяжками в S2/S4/R2 и без разбора `Read`. Главный дефект — поверхностный lifecycle-анализ.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 8 — Указание пользователя (дословно)
|
||||
|
||||
```
|
||||
так сделай ему замечание !!! пусть глубже копнёт ЕСЛИ ЭТО НАДО
|
||||
```
|
||||
|
||||
### Замечания агента (раунд 2) — текст, отправляемый Opus
|
||||
|
||||
Сохранён отдельным файлом: `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers_round2.md`.
|
||||
Содержимое (дословно):
|
||||
|
||||
```markdown
|
||||
# Промпт для Opus 4.8 — РАУНД 2: замечания к отчёту (2026-09-30)
|
||||
|
||||
> Продолжение диалога. Раунд 1 — `prompt_for_opus_provider_architecture_modifiers.md`.
|
||||
> Формат ответа тот же: сжато, тезисы, `файл:строка`, без догадок. Границы доступа — как в раунде 1
|
||||
> (плюс список из §4 ниже). `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, git-история — по-прежнему ЗАПРЕЩЕНЫ.
|
||||
|
||||
---
|
||||
|
||||
## 1. Зачтено (переделывать НЕ надо)
|
||||
|
||||
`S1`, `S5`, `R1`, `R4` — приняты. Не повторяй их в ответе.
|
||||
|
||||
## 2. Замечания — обязательны к отработке
|
||||
|
||||
**M1. Номера строк не сходятся.**
|
||||
Ты дал `provider.go:199-204` для регистрации модификаторов. По моему чтению файла (начиная со
|
||||
строки 180) функция `Resources()` находится примерно на строке 182, и регистрация идёт сразу за ней —
|
||||
твои 199–204 не сходятся. Требование задания — «каждое утверждение проверяемо».
|
||||
Действие: перепроверь **каждую** ссылку `файл:строка` в отчёте и дай точные номера; где не сверял —
|
||||
пометь «не сверено». Без этого отчёт не принимается.
|
||||
|
||||
**M2. `R2` — нарушено правило «без догадок».**
|
||||
Ты утверждаешь, что `check_hardcoded_service_ids.sh` «это не ловит», но этот скрипт **не читал**
|
||||
(его не было в разрешённом списке). Это догадка, а не факт.
|
||||
Действие: скрипт теперь разрешён (см. §4). Либо приведи факт из его кода, либо переформулируй в вопрос.
|
||||
|
||||
**M3. `S2`/`S4` — проверь основание, иначе они натянуты.**
|
||||
Правила спеки («No manual edits to **generated** Go code», «Service-specific Go code is fully
|
||||
**generated** from YAML») писаны про генерируемый код. Ресурсы в `resources_core` — ручные, не
|
||||
генерируемые. Плюс `S4` («modify всегда через WithDefaults») относится к `Update` инстанс-ресурсов,
|
||||
а не к отдельному ресурсу-модификатору.
|
||||
Действие: для каждого из S2/S4 дай **текстуальную опору из спеки** (`TOOLS/ARCHITECTURE.md:строка`)
|
||||
и переформулируй: это **пробел спеки** (нет категории для ручных оверлеев) или **нарушение**? Если
|
||||
опоры нет — пункт снять.
|
||||
|
||||
**M4. `R5` — обоснуй приоритет или понизь.**
|
||||
Ты сам пишешь «корректности не нарушает», но ставишь R5 выше R6.
|
||||
Действие: назови шкалу ранжирования (например: вероятность × последствие × обнаружимость) и
|
||||
пересчитай порядок; либо понизь R5.
|
||||
|
||||
**M5. Главный пробел: устойчивость `Read` и вечный diff.**
|
||||
Для Terraform это опаснее всего, а разобрано тоньше всего (только R6/импорт).
|
||||
Действие: разбери построчно, как `Read` модификатора формирует `vip_configure` / `ip_space_name`
|
||||
из live-состояния и совпадёт ли это с тем, что вернёт `jsonencode` из конфига:
|
||||
- порядок ключей JSON;
|
||||
- тип `count` (строка vs число);
|
||||
- снятие `null` и пустых значений;
|
||||
- что видит `plan` после `Read` для `keep_on_destroy`.
|
||||
Отдельно: может ли `Update` инстанс-ресурса (`nubes_vc_org` / `nubes_vc_nsxt`) вклиниться между
|
||||
apply-ами модификатора и вернуть поле назад (пересечение — это R1, но нужен сценарий во времени).
|
||||
Если считаешь, что глубокий разбор здесь не нужен — обоснуй одной строкой, почему.
|
||||
|
||||
## 3. Что упущено — доработать
|
||||
|
||||
**U1. Файла `modifiers.yaml` в репозитории НЕТ.**
|
||||
Есть только упоминания-комментарии (в `TOOLS/yaml-generator/main.go` и док-заметках). Это не
|
||||
«параллельный слой», а **задокументированное, но не созданное** наложение.
|
||||
Действие: переформулируй свой открытый вопрос №1 с учётом этого факта.
|
||||
|
||||
**U2. Рассинхронизация словаря жизненного цикла.**
|
||||
У генерируемых ресурсов — `suspend_on_destroy`, у модификаторов — `keep_on_destroy`. Два имени
|
||||
для одного смысла, живут в разных ветках кода.
|
||||
Действие: оцени как архитектурный дефект (единый словарь/контракт жизненного цикла), с `файл:строка`.
|
||||
|
||||
**U3. Корневая причина «ручных» модификаторов.**
|
||||
Генератор умеет `kind: modifier`, но доменное знание (какая операция, имя, inverse) некуда положить —
|
||||
оверлей не создан.
|
||||
Действие: сформулируй вывод по вариантам (оверлей-данные vs ручной код vs иное) — одной строкой на вариант.
|
||||
|
||||
## 4. Дополнительно РАЗРЕШЕНО читать (только это, сверх раунда 1)
|
||||
|
||||
- `TOOLS/scripts/check_hardcoded_service_ids.sh`
|
||||
- `TOOLS/resource-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/templates/instance.go`
|
||||
- `TOOLS/resource-generator/internal/templates/modifier.go`
|
||||
- `provider/internal/resources_core/state_refresh.go`
|
||||
- `provider/internal/resources_core/helpers.go`
|
||||
- `provider/internal/resources_core/params_compare.go`
|
||||
|
||||
Всё остальное — по-прежнему нельзя. Нужен файл вне списка → вопрос мне, не чтение.
|
||||
|
||||
## 5. Формат ответа
|
||||
|
||||
- **Только дельта**: что исправляешь, что снимаешь, чем обосновываешь; новые находки по M5/U2.
|
||||
- Полный отчёт заново не переписывать.
|
||||
- Как в раунде 1: тезисы, `файл:строка`, без «лирики» и без «а ещё могу».
|
||||
|
||||
## 6. Разрешение копать глубже
|
||||
|
||||
Если для честного ответа нужно копнуть глубже (в т.ч. по файлам вне §4) — **скажи прямо**, каких
|
||||
файлов не хватает, и задай вопрос. Если глубина не нужна — так и напиши по конкретному пункту.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 9 — Ответ Opus на раунд 2 (дословно, как получен от пользователя)
|
||||
|
||||
### Сырой лог сессии
|
||||
|
||||
```
|
||||
Read
|
||||
|
||||
Раунд 2. Перепроверю номера строк и прочту новые разрешённые файлы.
|
||||
|
||||
Read , lines 193 to 195
|
||||
|
||||
Read
|
||||
|
||||
Read , lines 178 to 195
|
||||
|
||||
Searched for regex `ResolveRefSvcParamValue\(ctx, 19|noNeededIpSpace|func \(r \*|len\(items\) == 0|RunInstanceOperationUniversalByCode|liveItems, parseErr|if raw, ok := live\["ipSpaceName"\]|func parseVipConfigure|func formatVipConfigure|func vipAllocationsEqual` (`**/provider/internal/resources_core/org_ip_allocation_resource.go`), 21 results
|
||||
|
||||
Searched for regex `noNeededIpSpace|func \(r \*NsxtSnat|if raw, ok := live\["ipSpaceName"\]|func \(r \*NsxtSnatResource\) setSnat|RunInstanceOperationUniversalByCode|ImportState` (`**/provider/internal/resources_core/nsxt_snat_resource.go`), 18 results
|
||||
|
||||
Прочитаю новые разрешённые файлы: шаблоны генератора, helpers, params_compare, state_refresh.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю helpers.go, state_refresh.go, params_compare.go для проверки поведения `ParamFormat`/refresh.
|
||||
|
||||
Read
|
||||
|
||||
Проверю точные номера строк в `ARCHITECTURE.md` для опоры S2/S4.
|
||||
|
||||
Searched for regex `must not contain service-specific|fully generated from YAML|No manual edits|no service-specific logic inside the core|modify всегда через WithDefaults|serviceSpecificModifiers|exactly two named registries` (`**/TOOLS/ARCHITECTURE.md`), 7 results
|
||||
|
||||
Searched for regex `for _, param := range cfsParams|func \(c \*UniversalClient\) RunInstanceOperationUniversalByCode|RunInstanceOperationUniversalByIdempotent|val, hasLive := lookupLiveParam` (`**/provider/internal/core/operation_run_bycode.go`), 5 results
|
||||
|
||||
Read , lines 174 to 193
|
||||
|
||||
Дельта по раунду 2.
|
||||
```
|
||||
|
||||
### Дельта (дословно)
|
||||
|
||||
```markdown
|
||||
## M1 — перепроверенные `файл:строка`
|
||||
|
||||
| Ссылка раунда 1 | Было | Факт (сверено) |
|
||||
|---|---|---|
|
||||
| Регистрация модификаторов | `provider.go:199-204` | `provider.go:182` — `func Resources()`; регистрация — `provider.go:186` и `provider.go:187` |
|
||||
| Удалённый реестр в генераторе | `yaml-generator/main.go:70-90` | `main.go:80-90` (комментарий про снятый `serviceSpecificModifiers`) |
|
||||
| hardcoded id 19 | `~300` | `org_ip_allocation_resource.go:308` |
|
||||
| `noNeededIpSpace` | `43` | const — `nsxt_snat_resource.go:44`; `setSnat` — `nsxt_snat_resource.go:234`; inverse-modify — `nsxt_snat_resource.go:199-200` |
|
||||
| Досылка дефолтов ByCode | `105-150` | `operation_run_bycode.go:112-150`; `ByCode` — `operation_run_bycode.go:11`, `ByIdempotent` — `operation_run_bycode.go:18` |
|
||||
| `len(items)==0` → error | `328-331` | `org_ip_allocation_resource.go:333` |
|
||||
| GenModifier-конвейер | `55-90` | `loader.go:57-86` (`op.Kind=="modifier"`) |
|
||||
|
||||
Прочее сверено: org_ip `ImportState` `org_ip_allocation_resource.go:290`; nsxt `ImportState` `nsxt_snat_resource.go:224`; nsxt `Read` overwrite `nsxt_snat_resource.go:152`; org_ip `Read` semantic-compare `org_ip_allocation_resource.go:177-188`.
|
||||
|
||||
## M2 — R2 переформулирован как ФАКТ (скрипт прочитан)
|
||||
|
||||
`check_hardcoded_service_ids.sh:15`: `grep -rnE '\.(ServiceID|ID)[[:space:]]*==[[:space:]]*[1-9][0-9]*' "$ROOT_DIR/TOOLS"`.
|
||||
Два проверяемых факта:
|
||||
1. **Область — только `TOOLS`.** Дерево `provider` не сканируется вовсе → ручные модификаторы в `resources_core` вне охвата стража по определению.
|
||||
2. **Паттерн ловит только `.ServiceID==N` / `.ID==N`.** Литерал-аргумент `org_ip_allocation_resource.go:308` (`ResolveRefSvcParamValue(ctx, 19, …)`) под паттерн не подпадает даже теоретически.
|
||||
|
||||
Итог R2: hardcoded 19/22/`"no-needed"` не покрыты стражем по **двум** причинам (область + паттерн). Догадки убраны.
|
||||
|
||||
## M3 — S2/S4 пересмотрены
|
||||
|
||||
**S4 — СНИМАЮ.** Опора `ARCHITECTURE.md:134` находится в разделе «Generated Code Resilience» и относится к `Update` генерируемых инстанс-ресурсов, а не к ручному модификатору. К модификаторам правило неприменимо → расхождения нет.
|
||||
|
||||
**S2 — переклассифицирую в ПРОБЕЛ СПЕКИ (не «нарушение No manual edits»).**
|
||||
Опоры: `ARCHITECTURE.md:13` и `ARCHITECTURE.md:213` говорят про *generated* Go — к ручным файлам не относятся (замечание верное). Но `ARCHITECTURE.md:12` («core … must not contain service-specific logic») и `ARCHITECTURE.md:110` («no service-specific logic inside the core») сформулированы про «core» без оговорок. Ручные модификаторы с зашитыми id 19/22 лежат в `resources_core` (`org_ip_allocation_resource.go:308`, `nsxt_snat_resource.go:44`). Спека **не содержит категории «ручной сервисный оверлей»** и не говорит, является ли `resources_core` частью «core». Поэтому S2 — пробел контракта (нет разрешённого места для такого кода), а по букве принципа 2 — пограничное противоречие. Не «нарушение No manual edits».
|
||||
|
||||
## M4 — шкала ранжирования и пересчёт
|
||||
|
||||
Шкала: **severity = вероятность × тяжесть_последствия × (1/обнаружимость)**. «Тихие» дефекты (низкая обнаружимость) поднимаются; то, что сразу видно в плане/диффе — опускается.
|
||||
|
||||
- **R2** — вер. высокая (любое добавление сервис-логики), тяжесть средняя (эрозия инварианта «ядро универсально»), обнаружимость низкая (страж молчит) → **верх**.
|
||||
- **R3** — вер. средняя (забытый `depends_on`), тяжесть средняя (непонятная ошибка платформы), обнаружимость средняя → **середина**.
|
||||
- **R6** — вер. средняя (импорт редок, но реален), тяжесть низкая, обнаружимость средняя → **ниже R3**.
|
||||
- **R5** — «корректности не нарушает» (моё же слово), тяжесть минимальная, обнаружимость высокая → **низ**.
|
||||
|
||||
Новый порядок: **R2 > R3 > R6 > R5**. R5 понижен ниже R6 (замечание принято).
|
||||
|
||||
## M5 — устойчивость `Read` и вечный diff (главный разбор)
|
||||
|
||||
**org_ip, путь значения `vip_configure`:**
|
||||
- Конфиг: `jsonencode([{name,count}])` → ключи по алфавиту (`count`,`name`); `count` — тип из `var.ip_count` (в `modifiers.tf:24` не квотирован → зависит от типа переменной).
|
||||
- Канон провайдера `org_ip_allocation_resource.go:385`: `name` первым, `count` — всегда строка.
|
||||
- Эти две формы **байт-различны** (порядок ключей; возможно число vs строка).
|
||||
- Защита от вечного diff — `org_ip_allocation_resource.go:177-188`: сравнение **смысловое** (`org_ip_allocation_resource.go:398`, игнорирует порядок и формат), и при совпадении state **не перезаписывается** → в state остаётся байт-в-байт конфиг-форма → `plan` чист. `count`-число нормализуется в строку в `org_ip_allocation_resource.go:351` до сравнения, поэтому «3» (число) и «"3"» считаются равными. `null`/пустые/`[{}]` отбрасываются там же → не порождают фантомную аллокацию.
|
||||
- Перезапись (реальный дрейф) даёт канон-форму (name-first) — она никогда не совпадёт байтово с `jsonencode`, но diff тогда **легитимен**; ближайший `Update` ставит `state=plan` (`org_ip_allocation_resource.go:119`) и вечного diff не создаёт.
|
||||
|
||||
**nsxt_snat, путь `ip_space_name`:** скаляр-строка. `nsxt_snat_resource.go:152` перезаписывает state только если live-значение непустое; иначе оставляет план (защита Required-атрибута от null). Форматных расхождений нет (простое имя) → вечного diff нет.
|
||||
|
||||
**`keep_on_destroy` в плане:** Optional+Computed, `Default=false` (`org_ip_allocation_resource.go:84`, `nsxt_snat_resource.go:78`). `Read` читает его из state в начале и не трогает (наружу его в API нет) → после первого apply стабилен, `plan` чист. В `modifiers.tf:30` задан `true` явно — diff отсутствует.
|
||||
|
||||
**Вывод M5-основной:** сами модификаторы вечного diff **не дают** — смысловое сравнение в `Read` его гасит.
|
||||
|
||||
**Временной сценарий R1 (инстанс-ресурс возвращает поле назад):**
|
||||
Шаблон instance.go Update строит `params` из **всех** `ModifyParams` безусловно (не пропускает null для не-nested), и вызывает `UpdateResourceWithTimeout` → `RunInstanceOperationUniversalWithDefaults`. Гейт — `hasServiceParamChanges` (тот же шаблон): modify запускается, если изменился **любой** modify-параметр.
|
||||
- `vc_org`: modify содержит **только** `vIPConfigure` (`19_vc_org.yaml`, op modify) → `nubes_vc_org` тронет поле, лишь если пользователь сам задал `vIPConfigure` на инстанс-ресурсе. Риск ниже.
|
||||
- `vc_nsxt`: modify содержит `needEnableAVI`, `virtualServicesCount`, `ipSpaceName`, `qosProfile`, `routedNetConfiguration` (`22_vc_nsxt.yaml`). Сценарий: (t1) модификатор включил SNAT → (t2) пользователь на `nubes_vc_nsxt` меняет `needEnableAVI` → `hasServiceParamChanges=true` → `params[372]=ParamFormat(plan.IpSpaceName)` (запись присутствует всегда) → (t3) следующий `Read` модификатора видит дрейф и на очередном apply восстанавливает. Две сущности «пинают» поле по очереди.
|
||||
|
||||
**Не сверено** (нужны файлы вне §4): пошлёт ли `RunInstanceOperationUniversalWithDefaults` пустой `ipSpaceName` как `""` (затрёт SNAT) или дособерёт из live (как ByCode). Это решает, «затирание» или «no-op». Файлы: `core/operation_*` с реализацией `WithDefaults` и генератор-хелпер `ParamFormat` (funcs.go) для поведения null→"". См. M6.
|
||||
|
||||
## U1 — открытый вопрос №1 переформулирован
|
||||
|
||||
`modifiers.yaml` в репозитории **нет** — только упоминания-комментарии (`main.go:84-90`). Генерируемый слой модификаторов при этом **реален и готов**: шаблон `modifier.go` (полный CRUD + `reconcile` + `delete_strategy` + `idempotency`) и конвейер `loader.go:57-86`. То есть это **задокументированное-но-несозданное наложение**: механизм есть, данных для него нет. Вопрос: создавать `modifiers.yaml`-оверлей (данные) и перевести org_ip/nsxt_snat на генерацию — или узаконить ручные ресурсы отдельной категорией в спеке?
|
||||
|
||||
## U2 — рассинхронизация словаря жизненного цикла (архитектурный дефект)
|
||||
|
||||
Три несогласованных словаря «что делать при destroy», в разных ветках кода:
|
||||
1. Генерируемый инстанс: `suspend_on_destroy` + `keep_on_destroy` (runtime-булевы, приоритет keep) — instance.go schema и `Delete` (ветки `suspend`/`state_only`).
|
||||
2. Генерируемый модификатор: `delete_strategy` = `noop_warn`/`inverse`/`error` — **compile-time из YAML**, без `keep_on_destroy` — modifier.go Delete.
|
||||
3. Ручной модификатор: снова `keep_on_destroy` (runtime) + ручной inverse — `org_ip_allocation_resource.go:84`, `org_ip_allocation_resource.go:195`; `nsxt_snat_resource.go:78`.
|
||||
|
||||
Дефект: один смысл («не трогать эффект при destroy / обратный откат») выражен тремя разными контрактами (runtime-флаг vs compile-time-стратегия), причём генерируемый и ручной модификаторы, решая одну задачу, используют **противоположные** механизмы. Нужен единый словарь жизненного цикла для всех трёх видов ресурсов.
|
||||
|
||||
## U3 — корневая причина «ручных» модификаторов (по вариантам, одной строкой)
|
||||
|
||||
- **Оверлей-данные (`modifiers.yaml` + второй проход):** доменное знание (сервис, операция, имя, inverse, idempotency) — декларативно; org_ip/nsxt_snat становятся генерируемыми; убирает S2/U2/R2 разом; цена — доделать загрузчик оверлея и миграцию.
|
||||
- **Ручной Go-код (статус-кво):** максимум гибкости под нестандартную семантику (replace-массив, `no-needed`); цена — сервис-логика в `resources_core`, обход стража, тройной словарь.
|
||||
- **Иное (гибрид):** генерировать каркас из оверлея, а нетиповые куски (канон vIPConfigure) выносить в маленькие data-хуки; цена — усложнение шаблона ради 1-2 сервисов.
|
||||
|
||||
## M6 — где нужна глубина (прошу доступ)
|
||||
|
||||
Для однозначного вердикта по временному R1 (затирание vs досбор) не хватает **двух** файлов вне §4:
|
||||
1. Реализация `RunInstanceOperationUniversalWithDefaults` (ядро, `core/operation_run*.go` / `operation_cfs.go`) — шлёт ли пустой `ipSpaceName` как `""`.
|
||||
2. Генератор-хелпер `ParamFormat` (`TOOLS/resource-generator/internal/.../funcs.go`) — во что разворачивается null-строка в `params`.
|
||||
|
||||
Разрешить чтение этих двух файлов? Без них пункт помечен «не сверено».
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 10 — Инструкция и пояснение пользователя (дословно)
|
||||
|
||||
```
|
||||
#attachment:Pasted text #1
|
||||
докуменитируй сразу
|
||||
|
||||
пояснияю - модификаторы - модет и слодная, новая, но НЕБОЛЬШАЯ часть всего нашего сервиса
|
||||
не нужно делать на них чрезмерный упор
|
||||
главное - САМ провайдер, его устойчивость и правильность и тд
|
||||
```
|
||||
|
||||
Разбор указания (для дальнейших раундов):
|
||||
|
||||
- **Документировать немедленно** — исполнено этим разделом.
|
||||
- **Приоритет — САМ провайдер**: его устойчивость и корректность. Модификаторы — новая и, возможно,
|
||||
сложная, но **НЕБОЛЬШАЯ** часть сервиса; чрезмерный упор на них делать не нужно.
|
||||
- Следствие: темы `M5`/`M6`/`U2` (глубина по модификаторам) — второстепенны по отношению к вопросам
|
||||
устойчивости/корректности ядра, генерации и всего провайдера в целом.
|
||||
|
||||
---
|
||||
|
||||
## Ход 10a — Запрос пользователя (дословно)
|
||||
|
||||
```
|
||||
твоё мнение ?
|
||||
```
|
||||
|
||||
Мнение агента по раунду 2 приведено ниже, внутри Хода 11 (раздел «Критическое мнение агента
|
||||
о раунде 2»).
|
||||
|
||||
---
|
||||
|
||||
## Ход 11 — Запрос пользователя (дословно)
|
||||
|
||||
```
|
||||
так дополни задание ему, пусть ещё анализирует НО не нало много - токены могути закончиться
|
||||
```
|
||||
|
||||
### Критическое мнение агента о раунде 2 (дословно, отправлено пользователю)
|
||||
|
||||
```
|
||||
**Что стало лучше**
|
||||
- **M1** — Opus честно перепроверил и исправился: `provider.go:182/186/187`. Совпадает с моим чтением → доверие к остальным номерам выросло.
|
||||
- **M2** — догадка устранена: приведена конкретная строка скрипта, область (только `TOOLS/`) и паттерн (`.ServiceID==N`/`.ID==N`). Это уже факт, и факт полезный: дерево `provider/` стражем не покрыто **вообще**.
|
||||
- **M3** — `S4` снят с текстуальной опорой (`ARCHITECTURE.md:134` в разделе про generated-код), `S2` переклассифицирован в пробел спеки с опорой (`:12`, `:110` vs `:13`, `:213`). Честно и корректно.
|
||||
- **U2** — новая ценная находка, которой у меня не было: **третий** словарь жизненного цикла — `delete_strategy` (compile-time в YAML) против runtime-флагов. Настоящий архитектурный дефект.
|
||||
- **M4** — шкала задана, `R5` понижен. Замечание принято по существу.
|
||||
|
||||
**Что остаётся слабым**
|
||||
- **M5 — это чтение кода, а не замер.** Вся защита от вечного diff держится на утверждении «в `Read` смысловое сравнение, и при совпадении state не перезаписывается». Это правдоподобно и проверяемо, но **не проверено прогоном** `terraform apply` ×2 / `plan`. По нашему же правилу «рекомендация без замера — догадка». Opus не предложил верификацию.
|
||||
- **Риск переоценён/недооценён.** Самое опасное (R1: `Update` инстанс-ресурса затирает `ipSpaceName`) осталось **нерешённым** — за M6. При этом Opus сам пишет, что для `vc_nsxt` modify содержит 5 параметров, и запись `params[372]` присутствует **всегда**. Если так — это не «война дрейфов», а **молчаливое затирание SNAT** при любой правке эджа. Это надо было ставить вопросом №1 прошлого раунда, а не откладывать.
|
||||
- **Ничего про сам провайдер.** Раунд целиком про модификаторы — ровно то, что вы просили не переоценивать. Ядро, генератор, устойчивость, корректность — не тронуто.
|
||||
|
||||
**Моё мнение**
|
||||
Раунд 2 — честный и в плюс (правки по номерам, снятие `S4`, находка `U2`). Но по вашему приоритету он **мимо цели**: модификаторы, а не провайдер; и при этом главный вопрос (затирание `ipSpaceName` при `Update` эджа + отсутствие замера) остался открытым. Логичный следующий ход — развернуть раунд на «провайдер целиком», а `M6` **отложить**, а не разворачивать.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 12 — Задание агента (раунд 3) — текст, отправляемый Opus
|
||||
|
||||
Сохранён отдельным файлом: `NOTES/20_prompts/prompt_for_opus_provider_core_round3.md`.
|
||||
Содержимое (дословно):
|
||||
|
||||
```markdown
|
||||
# Промпт для Opus 4.8 — РАУНД 3: сам провайдер (устойчивость и корректность)
|
||||
|
||||
> Продолжение диалога. Раунды 1–2 — `HISTORY/OPUS/2026-09-30_provider_architecture_modifiers_dialog.md`.
|
||||
> Режим и формат ответа — как раньше: сжато, тезисы, `файл:строка`, без догадок.
|
||||
|
||||
---
|
||||
|
||||
## Вводная (смена приоритета)
|
||||
|
||||
Модификаторы (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`) — новая, возможно сложная, но
|
||||
**НЕБОЛЬШАЯ** часть сервиса. Чрезмерный упор на них не нужен.
|
||||
|
||||
**Главное — САМ провайдер: его устойчивость и корректность.**
|
||||
|
||||
- `M6` (доступ к двум файлам для доразбора R1) — **снять**; углубление по модификаторам больше не требуется.
|
||||
- Прошлые находки (`S1`, `S2`, `R2`–`R6`, `U1`–`U3`) **не повторять**.
|
||||
|
||||
## Бюджет (жёстко — экономим токены)
|
||||
|
||||
- Прочитать **не более 10 файлов** суммарно. Ранее прочитанные повторно не открывать.
|
||||
- Ответ — **не более 5 находок**, каждая **не более 3 строк**.
|
||||
- Никаких вступлений, повторения прошлых отчётов, «а ещё могу».
|
||||
|
||||
## Что анализировать (провайдер целиком)
|
||||
|
||||
1. **Жизненный цикл инстанс-ресурса:** `create` / adopt / `suspend` / resume / `modify` / redeploy /
|
||||
`delete` и повторный `apply` — где теряется корректность состояния.
|
||||
2. **Досылка и нормализация параметров** (`WithDefaults`, zero-value fallback, дефолты `map-fixed`,
|
||||
регистр UUID): где риск затереть значение или получить ложный diff.
|
||||
3. **`Read` / refresh:** устойчив ли state у генерируемых ресурсов; где возможен вечный diff.
|
||||
4. **Устойчивость ядра:** ретраи и ошибки, `LockInstance`, идемпотентность, поведение при дубликатах
|
||||
и параллельных `apply`.
|
||||
5. **Генератор → код:** какие классы дефектов порождает шаблон.
|
||||
|
||||
## Границы доступа
|
||||
|
||||
Разрешено читать (только это):
|
||||
|
||||
- `provider/internal/core/**`
|
||||
- `provider/internal/resources_core/**`
|
||||
- `provider/internal/provider/provider.go`
|
||||
- `TOOLS/resource-generator/internal/templates/**`
|
||||
- `TOOLS/resource-generator/internal/params/params.go`
|
||||
- `TOOLS/resource-generator/internal/helpers/helpers.go`
|
||||
- `TOOLS/resource-generator/internal/loader/loader.go`
|
||||
- `TOOLS/resource-generator/internal/writers/writers.go`
|
||||
|
||||
Запрещено: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, `DEV_STAND/**`, `TEST_STAND/**`,
|
||||
`PROD_STAND/**`, git-история. Нужен файл вне списка → вопрос мне, не чтение.
|
||||
|
||||
## Формат ответа
|
||||
|
||||
- **ТОП-5 находок** по убыванию критичности: суть → место (`файл:строка`) → последствие (≤ 3 строк).
|
||||
- Затем **одна строка**: что из этого проверяемо только замером (прогон `apply`/`plan`), а не чтением.
|
||||
- Находок меньше пяти — столько, сколько есть. Водой не добивать.
|
||||
|
||||
## Стоп-правило
|
||||
|
||||
Не хватает файла или данных — один короткий вопрос. Не догадываться.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 12a — Сообщение пользователя и вопрос агента
|
||||
|
||||
Пользователь прислал **только путь к файлу**, без какой-либо инструкции:
|
||||
|
||||
```
|
||||
/home/naeel/TF/tf_provider/NOTES/20_prompts/prompt_for_opus_provider_core_round3.md
|
||||
```
|
||||
|
||||
Агент, не имея однозначной команды, задал уточняющий вопрос с вариантами (проверить пути из
|
||||
списка доступа / поправить текст задания / пересмотреть состав и границы / ничего не делать).
|
||||
**Пользователь вопрос отклонил (cancelled)** — никаких изменений не вносилось, уточнение осталось
|
||||
без ответа.
|
||||
|
||||
---
|
||||
|
||||
## Ход 13 — Ответ Opus на раунд 3 (дословно, как получен от пользователя)
|
||||
|
||||
### Сырой лог сессии
|
||||
|
||||
```
|
||||
Read
|
||||
|
||||
Ограничусь бюджетом. Посмотрю состав `core/` и выберу ключевые непрочитанные файлы.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю ещё 4 ключевых файла ядра: ожидание операций, поиск (дубликаты), нормализацию параметров, cfs.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю ещё 2: состояние инстанса (проверка ретрая 401) и нормализацию параметров.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Бюджет: прочитано 6 файлов. ТОП-5 находок по устойчивости/корректности ядра.
|
||||
```
|
||||
|
||||
### Отчёт (дословно)
|
||||
|
||||
```markdown
|
||||
**1. Транзиентный 401 не ретраится нигде; `getInstanceStateWithRetry` — имя-обман (ретрая нет).**
|
||||
`instance_state.go:39` просто зовёт `doRequest` один раз; `http.go:118` `isRetryable` = {429,502,503,504}, **без 401**.
|
||||
Прямо противоречит ARCHITECTURE.md:105-108 («GetInstanceState retry 401», «doRequest treats 401 as retryable»). Кратковременный 401 Gateway роняет любой read → срыв plan/apply/поллинга.
|
||||
|
||||
**2. Ретраится только GET; любой POST падает намертво на транзиентном 502/503/сети.**
|
||||
`http.go:88` — retry только при `method=="GET"`.
|
||||
Сбой на `/instanceOperations`, `/instanceOperationCfsParams` или `run` рвёт операцию; при create остаётся осиротевший инстанс → следующий apply ловит страж дубликатов `instance_find.go:168`.
|
||||
|
||||
**3. Modify обрывается уже ПОСЛЕ создания операции, если чтение live-параметров сбойнуло.**
|
||||
`operation_run.go:150` (и симметрично bycode `operation_run_bycode.go:108`): `instanceLiveParams` err → `return`, но POST `/instanceOperations` уже прошёл.
|
||||
Любой транзиентный сбой чтения live → повисшая pending-операция, блокирующая инстанс до idle.
|
||||
|
||||
**4. Zero-value fallback угадывает тип по ПОДСТРОКЕ имени параметра.**
|
||||
`params.go:47`: при пустом `dataType` тип берётся из `name/code/label`, содержащих `array/list/map/json`.
|
||||
Строковый параметр с кодом вроде `whitelist`/`...json...` получит `"[]"`/`"{}"` вместо `""` → неверно-типизированное значение уходит в API на незаданном поле.
|
||||
|
||||
**5. Эквивалентность значения в `Read` захардкожена под `jsonEnv`/ref_svc — прочие строки пишутся как вернул API.**
|
||||
`state_refresh.go:143` спец-ветка только для `jsonEnv`; остальное — `ParseString(value)` «как есть» `state_refresh.go:168`. Регистровая нормализация есть лишь в ref_svc-пути `refsvc_resolve.go:29`.
|
||||
Для non-ref_svc строкового поля, где API канонизирует формат (регистр UUID и т.п.), plan≠state → вечный diff.
|
||||
|
||||
Проверяемо только замером (не чтением): №1–3 (поведение при флапающем Gateway, орфан-операции) — прогоном `apply`; №5 (вечный diff) — прогоном `plan`.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 14 — Запрос пользователя (дословно)
|
||||
|
||||
```
|
||||
мнение ? более вопросов в том чате задават не получится - он уже большой и жрёт токены
|
||||
```
|
||||
|
||||
### Мнение агента о раунде 3 (дословно)
|
||||
|
||||
```
|
||||
**Сильное**
|
||||
- **#1** — самое ценное: `ARCHITECTURE.md:105-108` буквально обещает ретрай 401 для GET, а код, по его словам, 401 не ретраит. Если верно — это не «мелочь», а срыв `plan`/`apply`.
|
||||
- **#3** — правдоподобно и практично: обрыв modify уже **после** отправленного POST → висящая операция.
|
||||
- **#5** — бьёт в самое больное место Terraform (вечный diff).
|
||||
|
||||
**Где не верю / нужна аккуратность**
|
||||
- **#1 — слово «нигде» настораживает.** Спека говорит, что ретрай 401 живёт внутри `GetInstanceState`/`GetInstanceStateRaw`, а Opus цитирует `instance_state.go:39` — один вызов `doRequest`. Вполне возможно, что ретрай реализован в другой функции, и абсолютный вывод неверен. Проверять надо именно те функции, а не один call-site.
|
||||
- **#2 — подано как дефект, а это может быть защита.** Слепой ретрай POST `/instanceOperations` **опаснее** обрыва: рискует создать дубликат операции. Настоящий дефект — не «нет ретрая», а «нет идемпотентности / безопасного продолжения». В нынешней формулировке вывод вводит в заблуждение.
|
||||
- **#4 — нет оценки частоты.** Срабатывает только при **пустом** `dataType`. Без ответа «как часто он пуст» это теория, а не риск.
|
||||
- **#5 — возможно, уже покрыто.** По §6.5 `ARCHITECTURE_NEW.md` нормализация UUID при **сравнении** есть в `JSONStringsEquivalent` / `JsonNormalize` / `normalizeCompareValue` / `ParamsMatchForResume`. Прежде чем считать это багом, надо убедиться, что расхождение не гасится там.
|
||||
|
||||
**Общее**
|
||||
- Все пять — чтение кода; ни одна не измерена. Это **гипотезы**, и формально Opus это честно признал.
|
||||
- Раз чат исчерпан по токенам — дальнейшая проверка возможна только **локально**, своими силами: `grep` по `isRetryable`/`GetInstanceState`, сверка `state_refresh.go` с `normalizeCompareValue`.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Статус
|
||||
|
||||
- Раунды 1–3: все три отчёта получены. Раунд 3 уложился в бюджет (прочитано 6 файлов, 5 находок).
|
||||
- **Итог раунда 3 (ядро, устойчивость/корректность):**
|
||||
1. 401 не ретраится нигде; `isRetryable` без 401 — противоречие `ARCHITECTURE.md:105-108`.
|
||||
2. Ретрай только для GET; POST обрывается на 502/503/сети → риск осиротевших операций/инстансов.
|
||||
3. Modify может оборваться **после** создания операции (сбой чтения live) → повисшая pending-операция.
|
||||
4. Zero-value fallback определяет тип по подстроке имени → возможна неверная типизация незаданного поля.
|
||||
5. Нормализация значения в `Read` — только `jsonEnv`/ref_svc → вечный diff на прочих канонизируемых строках.
|
||||
- Находки №1–3 и №5 **не подтверждены замером** (по словам самого Opus) — перенесены как гипотезы.
|
||||
- **Чат с Opus исчерпан по токенам** (сообщено пользователем 2026-09-30): новые вопросы в него
|
||||
задавать нельзя; проверка находок возможна только локально.
|
||||
- **Мнения агента записаны по всем раундам:** раунд 1 — Ход 7; раунд 2 — Ход 11 (и Ход 10a);
|
||||
раунд 3 — Ход 14.
|
||||
- Артефакты: `752244f` — промпт раунда 1; `ea75507` — замечания раунда 2;
|
||||
`e46bc35` — задание раунда 3;
|
||||
`NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers_round2.md`,
|
||||
`NOTES/20_prompts/prompt_for_opus_provider_core_round3.md`.
|
||||
- Следующий шаг (2026-09-30): промпт для **DeepSeek Pro** —
|
||||
`NOTES/20_prompts/prompt_for_deepseek_pro_remediation_plan.md` (план правок кода/документации/архитектуры
|
||||
+ план проверки/тестов; гипотезы групп A/B переданы ему на верификацию; исполнять будет Copilot).
|
||||
- Настоящий документ ведётся по прямому указанию пользователя «документируй ВЕСЬ диалог,
|
||||
без сокращений».
|
||||
@@ -0,0 +1,305 @@
|
||||
Что сказать Opus — кратко
|
||||
Задача: глубокий анализ проекта, найти слабые места, предложить улучшения.
|
||||
|
||||
10 вопросов:
|
||||
Два провайдера (Legacy + Universal) — как и когда выпилить Legacy?
|
||||
Генерационный конвейер — API→YAML→Go→S3. Надёжен ли? Что если API поменяет формат?
|
||||
API Flow V6 — 6 шагов без транзакционности. Что при обрыве посредине?
|
||||
CRUD/Adopt — сложнейшая логика в adoptExistingInstanceOnCreate. Все ли краевые кейзы покрыты?
|
||||
Генератор gen_v2 — шаблоны вшиты в бинарь. Как расширять? Что с новыми kind'ами операций?
|
||||
Баги — 23 файла истории. Какие системные? Фильтрация deleted — главная проблема.
|
||||
Матрица состояний — 16 состояний, все ли обрабатываются?
|
||||
Тесты — crud_test.go, client_test.go. Достаточно ли?
|
||||
Безопасность — токены, GPG-ключ в репозитории, InsecureSkipVerify.
|
||||
Конкурентность — два terraform apply одновременно?
|
||||
Что читать (приоритет):
|
||||
ARCHITECTURE.md
|
||||
client.go
|
||||
crud.go
|
||||
generate_resources_v2.go
|
||||
|
||||
|
||||
Searched for regex `.*` (`tf_provider` · `universal_rebuild/internal/resources_core/crud_test.go`), 56 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#210-210), lines 210 to 350
|
||||
|
||||
Searched for regex `buildSubresourceForceNewCodes|computeCreateOnly|analyzeJsonPlanMod` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 10 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go)
|
||||
|
||||
Searched for regex `func.*IsJson|func.*RefSvc|func.*computeCreateOnly|func.*analyzeJsonPlanMod` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 3 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#2040-2040), lines 2040 to 2150
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/.gitignore)
|
||||
|
||||
Read [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/chat-session-resources/2c7367dd-6d7f-48b4-84d0-25bbb06f7cfb/toolu_01RcjLgQg9SVXsgzGtT1p8Wa__vscode-1782817912278/content.txt)
|
||||
|
||||
Изучил архитектуру, ядро, генератор, историю багов и тесты. Сохраню анализ и представлю его.
|
||||
|
||||
Created memory file [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/MmM3MzY3ZGQtNmQ3Zi00OGI0LTg0ZDAtMjViYmIwNmY3Y2Zi/plan.md)
|
||||
|
||||
# Анализ Terraform Provider для Nubes Cloud
|
||||
|
||||
Изучил архитектуру (ARCHITECTURE.md), ядро (client.go, crud.go), генератор (generate_resources_v2.go), транспорт (provider.go), 24 файла истории и тесты. Ниже — ответы по всем 10 пунктам: текущее состояние → плюсы → минусы → предложения → риски.
|
||||
|
||||
---
|
||||
|
||||
## 1. Два провайдера: Legacy + Universal
|
||||
|
||||
**Состояние.** Legacy (internal, v5.0.52, реестр `registry.terraform.io/nubes/nubes`, 13 ручных ресурсов) и Universal (universal_rebuild, v5.0.51, реестр `terra.k8c.ru/nubes/nubes`, ~50 генерируемых). Оба на `terraform-plugin-framework`, но это **полностью независимые деревья кода** с дублирующимся ядром (core существует в обоих).
|
||||
|
||||
**Хорошо.** Universal — целевая архитектура, чёткие принципы в ARCHITECTURE.md (YAML = source of truth). Разные реестры → можно мигрировать без коллизий.
|
||||
|
||||
**Плохо.** Дублирование ядра (client.go, `instance_lookup.go`) — баги фиксятся в двух местах (см. баг #1: правка в client.go И instance_lookup.go). Версии разъехались (5.0.52 vs 5.0.51) — непонятно, какая «свежее». Нет формального deprecation-плана с датой.
|
||||
|
||||
**Предложения.**
|
||||
- Зафиксировать **матрицу соответствия ресурсов** Legacy→Universal: какие 13 ресурсов уже перекрыты Universal, какие нет.
|
||||
- Объявить Legacy *frozen* (только critical-фиксы), завести `DEPRECATED.md` с целевой версией снятия.
|
||||
- Перенести уникальную логику Legacy (VM/vApp/edge/vdc) в YAML-спеки, проверить паритет, затем archive Legacy в отдельную ветку/тег.
|
||||
|
||||
**Риск.** VM/vApp в Legacy содержат ручную логику (FW-rules, 500-фикс из 04_vm_hang_fix_and_500_error), которую генератор может не воспроизвести. Нужен паритетный прогон на тестовом стенде до снятия Legacy.
|
||||
|
||||
---
|
||||
|
||||
## 2. Генерационный конвейер (API→YAML→Go→S3)
|
||||
|
||||
**Состояние.** 4 шага: `service_spec_gen` (API→YAML, ATTEMPTS=3, REQUEST_DELAY=0.5) → `gen_v2` (YAML→Go) → build+GPG+S3 → mkdocs. Включение сервиса = строка в `services_list.txt`.
|
||||
|
||||
**Хорошо.** Чёткое разделение, профили dev/test/prod изолируют артефакты, retry на шаге сбора YAML.
|
||||
|
||||
**Плохо — главное расхождение с собственными принципами.** ARCHITECTURE.md декларирует «*The generator must enforce these rules and fail fast on drift*», но фактически:
|
||||
- **Неизвестный `kind` операции тихо игнорируется** (generate_resources_v2.go — три `if op.Kind != "..." { continue }`). Если API введёт новый kind — ресурс молча пропадёт из провайдера, без ошибки.
|
||||
- **YAML почти не валидируется**: проверяется только синтаксис (`yaml.Unmarshal`). Отсутствие `op.Kind`/`op.Action` → zero-value → тихое игнорирование. Нет проверки уникальности param ID, наличия required-полей, валидности `RefSvcId`.
|
||||
- При ошибке `format.Source` генератор пишет **неформатированный (возможно битый) код** как fallback вместо остановки.
|
||||
- Исключение сервиса только комментированием в `services_list.txt` → риск рассинхрона (закомментировали в test, забыли в prod).
|
||||
|
||||
**Предложения.**
|
||||
- Добавить фазу `validateSpec()` перед генерацией: required-поля, уникальность ID, известность `kind`/`action`, ссылочная целостность `RefSvcId`. **Fail fast** на неизвестном kind.
|
||||
- Убрать fallback на неформатированный код — при `format.Source` error → паника с понятным сообщением.
|
||||
- CI-шаг «генерация без diff»: прогон генератора → `git diff --exit-code` (детект дрейфа, как требует ARCHITECTURE.md).
|
||||
- Go для генераторов оправдан (один язык со сгенерированным кодом, `text/template`, `format.Source`); bash-обёртки — лишь оркестрация. Менять не нужно.
|
||||
|
||||
**Риск.** Изменение формата ответа API на шаге 1 не обнаружится до runtime у пользователя. Сейчас единственная защита — `ATTEMPTS=3`, что не ловит *семантический* дрейф (поле переименовали, а не пропало).
|
||||
|
||||
---
|
||||
|
||||
## 3. API Flow V6 — отсутствие транзакционности
|
||||
|
||||
**Состояние.** 6 шагов в `CreateGenericInstanceUniversalV6`: `POST /instances` → `POST /instanceOperations` → `GET cfsParams` → resolve refs → `POST` каждого param → `validate-cfs` → `run` → `waitForOperationFinish`.
|
||||
|
||||
**Хорошо.** Завершение по `dtFinish` — надёжный контракт (выстрадан в 02, 05). Нормализация пустых map/json/array есть.
|
||||
|
||||
**Плохо.**
|
||||
- **Orphan при обрыве.** Если процесс упал/таймаут после `POST /instances`, но до `run` — в облаке остаётся инстанс в состоянии `not_created`/`creating`, **не попавший в Terraform state**. Следующий apply найдёт его через `FindInstanceByDisplayName` и упрётся в ошибку «найден в состоянии Not Created; удалите вручную». То есть пользователь обязан чистить руками.
|
||||
- **`doRequest` без retry** (client.go) — нет обработки 429/503/сетевых сбоев. Любой transient-сбой на шаге 4–5 рвёт create.
|
||||
- `req.Close = true` — новое TCP+TLS соединение на каждый запрос (на длинном поллинге дорого).
|
||||
|
||||
**Предложения.**
|
||||
- Retry с экспоненциальным backoff в `doRequest` для идемпотентных GET и для 429/503/сетевых ошибок (с уважением `Retry-After`).
|
||||
- Идемпотентность create: перед `POST /instances` делать `FindInstanceByDisplayName` (уже есть в `CreateResource`) — но также **обрабатывать «недосозданный» инстанс**: предлагать авто-cleanup `not_created`-инстанса при `adopt_existing_on_create=true`, а не только ручное удаление.
|
||||
- Рассмотреть keep-alive (убрать `req.Close=true`) для поллинга — меньше TLS-handshake.
|
||||
|
||||
**Риск.** Авто-cleanup `not_created` — операция удаления, требует явного флага и подтверждения семантики (нельзя удалять то, что пользователь мог создавать вручную параллельно).
|
||||
|
||||
---
|
||||
|
||||
## 4. CRUD / Adopt — `adoptExistingInstanceOnCreate`
|
||||
|
||||
**Состояние.** Покрытые ветки в crud.go:
|
||||
1. `operation_in_progress`/`pending` → ошибка «дождитесь».
|
||||
2. `!adopt_existing_on_create` → ошибка с подсказкой про import.
|
||||
3. `not_created` → ошибка.
|
||||
4. `running` → ref-валидация → adopt (возврат UUID).
|
||||
5. `suspended` → required-params check → resume → проверка статуса после resume → ref-валидация → adopt.
|
||||
6. Иначе (`creating`/`failed`/`error`) → общая ошибка «статус не подходит для авто-усыновления».
|
||||
|
||||
**Хорошо.** Логика соответствует decision-matrix из ARCHITECTURE.md. Ref-валидация при adopt (баг 22) закрыта. Диагностики подробные.
|
||||
|
||||
**Плохо.**
|
||||
- **Конкурентность не покрыта** (см. п.10): между `FindInstanceByDisplayName` и `CreateGenericInstanceUniversalV6` нет блокировки.
|
||||
- `creating`/`failed` падают в *общую* ветку с менее информативным сообщением, чем требует ARCHITECTURE.md (там для `creating`/`pending`/`failed` предписана отдельная диагностика).
|
||||
- Функция ~100 строк, глубокая вложенность, ref-валидация дублируется в двух ветках (running и после resume) — риск рассинхрона при правках.
|
||||
- `state_only`/`detach` destroy → `return nil` без API-вызова: инстанс остаётся в облаке (**это by design** из 20, но «orphaned» с т.з. биллинга — пользователь должен понимать).
|
||||
|
||||
**Предложения.**
|
||||
- Вынести классификацию статуса в один `switch` с явными ветками для каждого из 16 состояний (см. п.7), убрать дублирование ref-валидации в helper.
|
||||
- Для `creating`/`failed` — отдельные сообщения по контракту ARCHITECTURE.md.
|
||||
- Покрыть adopt-матрицу таблично-управляемыми тестами (сейчас 0 тестов на adopt, п.8).
|
||||
|
||||
**Риск.** Рефакторинг самой сложной функции без тестов опасен — сначала тесты, потом рефакторинг.
|
||||
|
||||
---
|
||||
|
||||
## 5. Генератор gen_v2 — шаблоны вшиты в бинарь
|
||||
|
||||
**Состояние.** 3 inline-шаблона `text/template`: `instanceTemplate` (569 строк), `subresourceTemplate` (443), `actionTemplate` (174). `computeCreateOnly` = эвристика (param в create, но не в modify). Identity подресурса: нет modify → все params ForceNew; есть modify → createOnly+delete params ForceNew.
|
||||
|
||||
**Хорошо.** Эвристика createOnly опирается на данные API (не хардкод), `format.Source` гарантирует валидный Go при успехе, восстановление регистра UUID решает «inconsistent result».
|
||||
|
||||
**Плохо.**
|
||||
- Шаблоны как строковые константы внутри `.go` (1186 строк шаблонов) — тяжело поддерживать, нет подсветки/линтинга шаблонов, любая правка = пересборка генератора.
|
||||
- **Новый kind → тихое выпадение ресурса** (см. п.2).
|
||||
- `data_type: json` → `IsJson`+`JsonNormalize` plan-modifier — но **0 тестов** на это, а нормализация JSON исторически проблемная (V2→V6 цикл, баг 13).
|
||||
- Immutable определяется только через отсутствие в modify — если API *временно* не отдаёт modify-параметр (сбой/неполный YAML), параметр ошибочно станет ForceNew → пересоздание ресурса.
|
||||
|
||||
**Предложения.**
|
||||
- Вынести шаблоны в `embed.FS` (`//go:embed templates/*.tmpl`) — поддерживаемость без потери single-binary.
|
||||
- Fail fast на неизвестном kind + лог числа сгенерированных ресурсов на сервис (детект «пропал ресурс»).
|
||||
- Снапшот-тесты генератора: эталонный YAML → ожидаемый `.go` (golden files).
|
||||
- Защита от «исчезнувшего modify-параметра»: предупреждать, если у сервиса есть create-params, но 0 modify-params (подозрительно).
|
||||
|
||||
**Риск.** Если `gen_v2` сломается — ломается **весь** Universal-провайдер. Сейчас единственная страховка — `format.Source`, который при ошибке всё равно пишет битый код.
|
||||
|
||||
---
|
||||
|
||||
## 6. Баги из истории — что системное, что осталось
|
||||
|
||||
**Системные классы (порождали серии багов):**
|
||||
1. **Фильтрация deleted** (23, 22, 21) — API возвращает deleted-инстансы, провайдер их не отсеивал. **Закрыт 5.0.50** (`isDeleted=false` + `isInstanceDeleted()` + ошибка при >1 совпадении).
|
||||
2. **Определение конца операции** (05, 02, 04) — зависание поллинга. **Закрыт** (критерий `dtFinish`).
|
||||
3. **Динамические param ID** (create ID ≠ modify ID, 06) — **закрыт** runtime-discovery.
|
||||
4. **Create-only параметры** (16) — **закрыт 5.0.38**.
|
||||
|
||||
**Не до конца решённые / ограничения:**
|
||||
- **Нормализация map/json/list** — потребовала 5 итераций (V2→V6), помечена как *частично*; новые типы параметров могут снова всплыть.
|
||||
- **Realm-валидация отключена** (21) из-за бага бэкенда `/resourceRealms/available` — валидации realm до деплоя нет.
|
||||
- **FW-rules 500** (04) — **platform-side bug**, воспроизводится и в Cloud Console; провайдер не может починить.
|
||||
- **Disk shrink** (06) — ограничение платформы (только увеличение), провайдер корректно прокидывает ошибку.
|
||||
|
||||
**Главная системная проблема** — именно фильтрация deleted была корнем 3+ багов. Сейчас закрыта, но **отсутствие тестов** означает, что регрессия не будет поймана автоматически.
|
||||
|
||||
**Предложения.** Regression-тесты на deleted-фильтрацию и multi-match; превратить known-limitations в явные диагностики (например, предупреждать про realm «валидация недоступна»).
|
||||
|
||||
---
|
||||
|
||||
## 7. Матрица состояний — 16 состояний
|
||||
|
||||
**Состояние.** `INSTANCE_STATES.md` / `STATE_TRANSITIONS.md` описывают полную матрицу. В коде adopt обрабатывает: `not_created`, `running`, `suspended`, `in_progress`/`pending`; остальные → общая ошибка.
|
||||
|
||||
**Плохо.** Хелперы `isStatusSuspended`/`isStatusNonAdoptable`/`isStatusNotCreated` работают через `strings.Contains` по тексту `explainedStatus` — **хрупко**: изменение формулировки статуса в API сломает классификацию молча. Промежуточные (`creating`, `failed`, `error`) сваливаются в одну ветку без индивидуальных подсказок, хотя ARCHITECTURE.md требует разные диагностики.
|
||||
|
||||
**Предложения.** Завести enum состояний и единую функцию `classifyStatus(raw) → State`, маппинг raw→enum в одном месте, exhaustive `switch` по всем 16 (с `default → явная ошибка «неизвестный статус X»`). Тесты на каждый статус.
|
||||
|
||||
**Риск.** Строковое сопоставление — самое уязвимое место к молчаливому дрейфу API.
|
||||
|
||||
---
|
||||
|
||||
## 8. Тесты — достаточно ли
|
||||
|
||||
**Состояние.** **8 unit-тестов всего**: crud_test.go (3: `isStatusSuspended`, `isStatusNonAdoptable`, delete-default) и client_test.go (5: нормализация значений). Без моков, без integration.
|
||||
|
||||
**Не покрыто (критично):** adopt-логика, ref-валидация, polling/`waitForOperationFinish`, `FindInstanceByDisplayName` (deleted+multi-match), генератор целиком, JSON plan-modifier, suspend/resume, required-params compare.
|
||||
|
||||
**Предложения.**
|
||||
- `httptest.Server` мок Nubes API → тесты Flow V6, поллинга по `dtFinish`, обрыва на шаге N, 429/503.
|
||||
- Табличные тесты adopt-матрицы (все 16 состояний × `adopt_existing_on_create` true/false).
|
||||
- Golden-тесты генератора (YAML→Go).
|
||||
- Контракт-тесты на основе HAR (см. п. ниже).
|
||||
|
||||
**Риск.** Все закрытые системные баги (deleted, polling, param-ID) **не защищены от регрессии**. Любой рефакторинг ядра/генератора — рулетка.
|
||||
|
||||
---
|
||||
|
||||
## 9. Безопасность
|
||||
|
||||
**Состояние.**
|
||||
- Токены: `*.token` в .gitignore ✅; private_key.asc в .gitignore ✅; id_ed25519.txt ✅.
|
||||
- `InsecureSkipVerify` default = **false** ✅, включается только явно/`NUBES_INSECURE=true`. TLS 1.2 min ✅.
|
||||
- `api_token` помечен `Sensitive: true` ✅.
|
||||
|
||||
**Хорошо.** Базовая гигиена соблюдена — ключи и токены не коммитятся, TLS-проверка по умолчанию включена.
|
||||
|
||||
**Плохо / проверить.**
|
||||
- **GPG-ключ физически лежит в secrets** — да, в .gitignore, но стоит проверить `git log --all -- secrets/private_key.asc`, что он не попал в историю ранее. .gitignore не вычищает уже закоммиченное.
|
||||
- public_key.asc, id_ed25519.pub — публичные, ок; но prod.token/`dev.token`/`test.token` существуют локально — убедиться, что покрыты `*.token` (да) и не было коммита до добавления правила.
|
||||
- **Утечка токена в логи**: `formatAPIError` форматирует тело ответа API в ошибку — если API эхает заголовки/токен в body ошибки, он попадёт в диагностику Terraform. `doRequest` сам токен не логирует. Стоит маскировать `Bearer ...` в любых сообщениях.
|
||||
- `ttyOut()` пишет напрямую в tty минуя Terraform — в debug-режиме `StageMsg` может содержать чувствительные данные; они идут в терминал в обход TF-логирования.
|
||||
|
||||
**Предложения.** `git log` аудит секретов; явная маскировка токена в `formatAPIError`/диагностиках; политика ротации `*.token`; вынести секреты из репо в внешний secret-store (для CI).
|
||||
|
||||
**Риск.** Если ключ/токен попал в git-историю до .gitignore — он уже скомпрометирован, .gitignore не поможет. Это надо проверить первым делом.
|
||||
|
||||
---
|
||||
|
||||
## 10. Конкурентность — два `terraform apply`
|
||||
|
||||
**Состояние.** Никаких блокировок. `FindInstanceByDisplayName` + `CreateGenericInstanceUniversalV6` — **не атомарны**. `waitForInstanceIdle` ждёт `operationIsPending/InProgress`, но это не защищает от гонки create.
|
||||
|
||||
**Плохо.**
|
||||
- Два apply с одинаковым `displayName` одновременно: оба проходят `FindInstanceByDisplayName` (никого нет) → оба `POST /instances` → **два инстанса с одним именем**. После этого `FindInstanceByDisplayName` начнёт возвращать ошибку «найдено 2 инстанса» (баг 22 только *детектирует* это, но не предотвращает).
|
||||
- Между modify из двух окружений — гонка на `instanceOperations`; частично гасится `waitForInstanceIdle`, но окно остаётся.
|
||||
|
||||
**Предложения.**
|
||||
- Полагаться на **Terraform state locking** (backend lock) как первичную защиту — это ответственность пользователя, задокументировать.
|
||||
- На стороне API — проверить, есть ли уникальность `displayName` на бэкенде; если нет, провайдер не может гарантировать атомарность.
|
||||
- Минимально: после `POST /instances` сразу повторный `FindInstanceByDisplayName` и, если найдено >1, откатить свой (требует delete — осторожно).
|
||||
|
||||
**Риск.** Полноценная защита возможна только при поддержке со стороны API (уникальность имени или conditional create). Провайдер в одиночку гонку не закрывает.
|
||||
|
||||
---
|
||||
|
||||
## Сводный план улучшений (по приоритету)
|
||||
|
||||
**Steps**
|
||||
|
||||
**P0 — Надёжность runtime (блокеры для прода)**
|
||||
1. Retry + backoff в `doRequest` для 429/503/сетевых сбоев (уважать `Retry-After`).
|
||||
2. Обработка orphan-инстанса `not_created`: авто-cleanup при явном флаге вместо «удалите вручную».
|
||||
3. Документировать и протестировать поведение при конкурентном apply (state-lock + повторная проверка после create).
|
||||
|
||||
**P1 — Защита от дрейфа и регрессий** (*parallel с P0*)
|
||||
4. `validateSpec()` в генераторе + **fail fast на неизвестном kind**; убрать fallback на неформатированный код.
|
||||
5. CI-шаг «генерация без diff» (детект дрейфа per ARCHITECTURE.md).
|
||||
6. Тесты: `httptest` мок API (Flow V6, polling, обрыв, 429), табличные тесты adopt-матрицы, golden-тесты генератора, regression на deleted-фильтрацию.
|
||||
|
||||
**P2 — Снижение тех-долга**
|
||||
7. Единый `classifyStatus → enum` для 16 состояний, exhaustive switch, убрать `strings.Contains`-классификацию.
|
||||
8. Рефакторинг `adoptExistingInstanceOnCreate` (после тестов): вынести ref-валидацию в helper, отдельные диагностики для `creating`/`failed`.
|
||||
9. Шаблоны генератора в `embed.FS`.
|
||||
10. План вывода Legacy: матрица паритета ресурсов, `DEPRECATED.md`, целевая версия снятия.
|
||||
|
||||
**P3 — Безопасность и наблюдаемость**
|
||||
11. `git log` аудит secrets; маскировка `Bearer`-токена в `formatAPIError`/диагностиках.
|
||||
12. Realm-валидация — включить, когда бэкенд починит `/resourceRealms/available`.
|
||||
13. HAR-based контракт-тесты (верификация соответствия провайдера реальному API).
|
||||
|
||||
**Relevant files**
|
||||
- client.go — `doRequest` (retry), `CreateGenericInstanceUniversalV6` (orphan/идемпотентность), `FindInstanceByDisplayName` (конкурентность).
|
||||
- crud.go — `adoptExistingInstanceOnCreate` (рефакторинг + тесты), статус-хелперы.
|
||||
- generate_resources_v2.go — валидация YAML, fail-fast на kind, вынос шаблонов.
|
||||
- provider.go — транспорт (keep-alive, маскировка токена).
|
||||
- Тесты: рядом с crud.go и client.go.
|
||||
|
||||
**Decisions / scope**
|
||||
- Это аналитический отчёт + план; **код не менялся** (режим Plan, и правила проекта запрещают изменения без «делай»).
|
||||
- Большинство критических багов из истории **уже закрыты** (5.0.50). Главный системный риск сейчас — **отсутствие тестов** (регрессии не ловятся) и **дрейф API** (генератор/статусы молча ломаются).
|
||||
|
||||
**Further Considerations**
|
||||
1. С чего начать реализацию? **A)** P0 (надёжность runtime) / **B)** P1 (тесты+защита от дрейфа — фундамент для безопасного рефакторинга) / **C)** аудит безопасности секретов (быстро, но критично, если ключ в истории git).
|
||||
2. Вывод Legacy — делать сейчас параллельно или после стабилизации Universal тестами? Рекомендую **после** (нужен паритет VM/vApp, который без тестов рискован).
|
||||
3. Нужен ли вам этот анализ как отдельный markdown-файл в docs (например `docs/50_history/25_opus_deep_analysis.md`), или достаточно ответа в чате?
|
||||
|
||||
Какой блок развернуть детальнее или с какого приоритета начинать?
|
||||
|
||||
Создано задач: 5
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
## Общее мнение
|
||||
|
||||
**Архитектурно — сильное, зрелое решение.** Идея «YAML как source of truth → генерация Go-кода и доков» правильная и редко встречается в self-made провайдерах. Универсальное ядро без сервис-специфичной логики + декларативные спеки = можно добавить сервис без программиста. Это уровень выше типичного «руками написанного» провайдера.
|
||||
|
||||
**Что реально хорошо:**
|
||||
- Выстраданный контракт с API (`dtFinish` для конца операции, динамические param-ID, фильтрация deleted) — видно, что прошли через боль и закрыли системные баги.
|
||||
- Чистое разделение pipeline (API→YAML→Go→S3), профили dev/test/prod.
|
||||
- Базовая безопасность в порядке (токены/ключи в .gitignore, TLS-проверка по умолчанию).
|
||||
|
||||
**Главные слабости (по убыванию):**
|
||||
1. **Тесты почти отсутствуют** (8 unit-тестов, без моков, без integration). Все закрытые баги не защищены от регрессии — это самый большой риск.
|
||||
2. **Генератор молча ломается** при дрейфе API (неизвестный kind тихо игнорируется, YAML не валидируется) — при этом сам же ARCHITECTURE.md требует «fail fast on drift». Декларация ≠ реализация.
|
||||
3. **Нет транзакционности create и retry** — обрыв посередине оставляет orphan, transient-сбой рвёт apply.
|
||||
4. **Два провайдера с дублированным ядром** — тех-долг, баги фиксятся в двух местах.
|
||||
|
||||
**Вердикт:** концепция и инженерная зрелость ядра — выше среднего, видно опытную руку. Но проект держится на «героике» (ручное тестирование на реальных стендах, комментарии «НЕ МЕНЯТЬ»), а не на автоматических гарантиях. Один слой тестов + fail-fast в генераторе превратили бы его из «работает, потому что мы знаем где не трогать» в по-настоящему надёжный. Сейчас это **крепкий прототип на пути к продакшн-grade**, а не законченный продукт.
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
# Ответы на дополнительные вопросы (сессия 2)
|
||||
|
||||
Read memory [](file:///home/naeel/.vscode-server/data/User/globalStorage/github.copilot-chat/memory-tool/memories/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/MmM3MzY3ZGQtNmQ3Zi00OGI0LTg0ZDAtMjViYmIwNmY3Y2Zi/plan.md)
|
||||
|
||||
Created [](file:///home/naeel/tf_provider/HISTORY/OPUS/3006_1_questions.md)
|
||||
|
||||
Готово: 3006_1_questions.md
|
||||
|
||||
## 8 дополнительных вопросов
|
||||
|
||||
| # | Тема | Что нового (после его анализа) |
|
||||
|---|------|-------------------------------|
|
||||
| 1 | **Расхождение версий** | Регистр отдаёт 5.0.55, код — 5.0.51. Откуда 53-55? |
|
||||
| 2 | **Полный аудит isDeleted** | Все ли GET /instances фильтруют deleted? Особенно `findInstanceUidByDisplayNameRefSvc` |
|
||||
| 3 | **Gitea 413** | Где nginx-конфиг Gitea в репо? Можно ли увеличить лимит через чарты? |
|
||||
| 4 | **Suspend-сервисы** | Полный список кто поддерживает suspend/resume. Есть ли баги (suspend без resume)? |
|
||||
| 5 | **Terraform Operator** | Почему перезапустился 2 дня назад? Хватает ли компонентов? Где GPG/S3 конфиг? |
|
||||
| 6 | **Тестовые стенды** | Какие реально используются? Legacy или Universal? Дубликаты? |
|
||||
| 7 | **auth.k8s.ngcloud.ru** | OIDC-конфиг, обновление токенов, Keycloak HAR |
|
||||
| 8 | **Orphan PVC/STS** | Откуда `storage-check-*` PVC? PostgreSQL 0/0 28 дней — почему? |
|
||||
|
||||
Все вопросы — **только чтение файлов в репо**, никаких правок. Opus может ответить на большинство из них. Вопросы 3 и 8 частично требуют данных из кластера (не из репо) — он это честно отметит.
|
||||
|
||||
Внесены изменения.
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/HISTORY/OPUS/3006_1_questions.md)
|
||||
|
||||
Исследую все 8 вопросов через чтение файлов репозитория. Запущу параллельный сбор данных.
|
||||
|
||||
Searched for regex `/instances\?|/instances"|GET.*instances|page=|isDeleted` (`**/universal_rebuild/internal/core/*.go`), 20 results
|
||||
|
||||
Searched for regex `findInstanceUidByDisplayNameRefSvc|ListRefServiceInstances`, 20 results
|
||||
|
||||
Searched for files matching `**/devops/profiles/**/profile.env`, 3 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/refsvc_resolve.go)
|
||||
|
||||
Нашёл ключевое для вопроса 2 — прочитаю функцию `findInstanceUidByDisplayNameRefSvc` и файлы версий.
|
||||
|
||||
Searched for regex `VERSION|version|5\.0\.` (`**/devops/03_build_and_upload_provider.sh`), 13 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#900-900), lines 900 to 1010
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/profile.env)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/test/profile.env)
|
||||
|
||||
Вопросы 1 и 2 почти закрыты. Соберу данные по остальным: инфраструктура, suspend-аудит, стенды, auth, orphan-ресурсы.
|
||||
|
||||
Searched for regex `auth\.k8s\.ngcloud\.ru|keycloak|shturval|client_max_body_size|gitea|storage-check`, 20 results
|
||||
|
||||
Searched for regex `getInstanceDisplayNameByUidRefSvc|findInstanceDisplayNameByUidRefSvc` (`**/universal_rebuild/internal/core/*.go`), 6 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/charts)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/cloud-dashboard/k8s)
|
||||
|
||||
Searched for regex `storage-check|terraform-operator|registry-server|operator`, 20 results
|
||||
|
||||
Searched for files matching `**/{operator,registry-server,registry-server-build,k8s}/**`, 3 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#1057-1057), lines 1057 to 1140
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/cloud-dashboard/k8s/ingress.yaml)
|
||||
|
||||
Аудит suspend/resume сервисов
|
||||
|
||||
Аудит тестовых стендов
|
||||
|
||||
Read [](file:///home/naeel/tf_provider)
|
||||
|
||||
Searched for regex `kubeconfig|OIDC|oidc|kube-apiserver|kubectl|HOW_TO_SSH|auth\.k8s`, 20 results
|
||||
|
||||
Searched for files matching `**/TEST_STAND/**/*.tf`, 11 matches
|
||||
|
||||
Searched for files matching `**/PROD_STAND/**/*.tf`, 10 matches
|
||||
|
||||
Searched for files matching `**/RABBIT/**/*.tf`, 6 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/RABBIT)
|
||||
|
||||
Searched for files matching `tf_provider` · `universal_rebuild/resources_yaml/**/*.yaml`, no matches
|
||||
|
||||
Searched for files matching `tf_provider` · `devops/profiles/prod/generated/resources_yaml/**/*.yaml`, no matches
|
||||
|
||||
Searched for files matching `tf_provider` · `devops/profiles/test/generated/**/*.yaml`, no matches
|
||||
|
||||
Searched for files matching `tf_provider` · `universal_rebuild/resources_yaml/embed.go`, 1 match
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/resources_yaml)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/test/generated)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/resources_yaml/embed.go)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/test/generated/resources_yaml)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/LUCEE/resources.tf#1-1), lines 1 to 50
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/MARIA_DB/resources.tf#1-1), lines 1 to 50
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/POSTGRES/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/POSTGRES/resources.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/PG1/resources.tf#1-1), lines 1 to 100
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/POSTGRES/resources.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/RABBIT/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/RABBIT/resources.tf#1-1), lines 1 to 100
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/RABBIT/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/RABBIT/resources.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/LUCEE/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/MARIA_DB/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/S3_EVENT_FUNCTION_POC/versions.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/PG1/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/POSTGRES/main.tf)
|
||||
|
||||
Searched for regex `suspend|resume` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/**/*.yaml`), 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/S3_EVENT_FUNCTION_POC/main.tf#1-1), lines 1 to 50
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/POSTGRES/luceUNDnode.tf#1-1), lines 1 to 60
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/POSTGRES/flask_disabled.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/PG1/vm.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/RABBIT/rabbit_worker_disabled.tf)
|
||||
|
||||
Searched for regex `suspend|resume` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/**/*.yaml`), 187 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/90_postgres.yaml#1-1), lines 1 to 20
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/90_postgres.yaml#459-459), lines 459 to 469
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/115_mariadb.yaml#1-1), lines 1 to 20
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/115_mariadb.yaml#238-238), lines 238 to 248
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/91_redis.yaml#1-1), lines 1 to 20
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/117_nifi.yaml#1-1), lines 1 to 20
|
||||
|
||||
Searched for regex `- name:|kind:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/91_redis.yaml`), 6 results
|
||||
|
||||
Searched for regex `- name:|kind:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/117_nifi.yaml`), 9 results
|
||||
|
||||
Searched for regex `- name:|kind:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/112_tenant.yaml`), 9 results
|
||||
|
||||
Searched for regex `- name:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/22_vc_nsxt.yaml`), 8 results
|
||||
|
||||
Searched for regex `- name:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/29_vc_vdc_group.yaml`), 10 results
|
||||
|
||||
Searched for regex `- name:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/100_openwhisk.yaml`), 4 results
|
||||
|
||||
Проверю наличие `operator/`/`k8s/`, прочитаю отчёт по gitea/registry и README про реестр.
|
||||
|
||||
Searched for files matching `**/operator/**`, no matches
|
||||
|
||||
Searched for files matching `**/k8s/**`, 3 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/README.md#40-40), lines 40 to 127
|
||||
|
||||
Read [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/chat-session-resources/2c7367dd-6d7f-48b4-84d0-25bbb06f7cfb/toolu_01RGa2BMVk2W3EiaCRQTaSMg__vscode-1782817912438/content.txt)
|
||||
|
||||
Финальная проверка: наличие `registry-server-build/`, детали оператора/S3/GPG в кластере, keycloak HAR.
|
||||
|
||||
Searched for files matching `**/registry-server-build/**`, no matches
|
||||
|
||||
Searched for files matching `**/HAR/*keycloak*`, no matches
|
||||
|
||||
Searched for regex `S3_ENDPOINT|s3.msk-1|terraform-registry|S3_BUCKET|\.s3cfg`, 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/00_system_mechanics.md)
|
||||
|
||||
Все 8 вопросов исследованы. Ниже ответы в формате **находка → файлы → вывод → рекомендация**.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Откуда версии 5.0.53–5.0.55?
|
||||
|
||||
**Находка.** Версия билда берётся **не из main.go**, а из профиля стенда:
|
||||
- 03_build_and_upload_provider.sh: `VERSION = $1 → иначе $PROVIDER_VERSION/$RELEASE_VERSION (из profile.env) → иначе grep из main.go (fallback)`.
|
||||
- profile.env: `PROVIDER_VERSION="5.0.55"`.
|
||||
- profile.env: `PROVIDER_VERSION="2.1.23"` (!).
|
||||
- main.go хранит `5.0.51` — это лишь **последний fallback**, который почти никогда не срабатывает.
|
||||
|
||||
**Вывод.** 5.0.53/54/55 — это **ручные билды с тестового стенда**: оператор вручную bump-ит `PROVIDER_VERSION` в `profiles/test/profile.env` и запускает `03_build...sh`. main.go при этом не обновляется (README прямо просит обновлять, но на практике это не делается — отсюда рассинхрон). Важно: **prod-профиль застрял на 2.1.23**, а test ушёл на 5.0.55 — это две независимые линейки версий. Мой прошлый анализ (3006_0.md) цитировал версии из кода (5.0.51/5.0.52), а регистр отдаёт версию из profile.env (5.0.55) — поэтому они не совпадают.
|
||||
|
||||
**Рекомендация.** Сделать profile.env единственным источником версии и автоматически прописывать её в main.go при сборке (или вычислять main.go-версию из env через ldflags `-X`). Сейчас 3 источника версии (main.go, test, prod) живут врозь.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: Полный аудит фильтрации `isDeleted`
|
||||
|
||||
**Находка.** Все функции, делающие GET к `/instances`:
|
||||
|
||||
| Функция | Файл | `isDeleted=false` в URL | Проверка в коде | Статус |
|
||||
|---|---|---|---|---|
|
||||
| `FindInstanceByDisplayName` | client.go | ✅ (search) / ❌ (fallback) | ✅ `isInstanceDeleted()` | OK |
|
||||
| `ListRefServiceInstances` | refsvc_resolve.go | ✅ | ✅ `IsDeleted` + только running | OK |
|
||||
| `findInstanceUidByDisplayNameRefSvc` | client.go | ❌ | ✅ `if item.IsDeleted {continue}` + `status=="deleted"` | **OK (исправлен!)** |
|
||||
| `findInstanceDisplayNameByUidRefSvc` | client.go | ❌ | ❌ нет | приемлемо |
|
||||
| `getInstanceDisplayNameByUidRefSvc` | client.go | — (GET по UID) | — | n/a |
|
||||
| `GetInstanceState/Raw/Details` | client.go / instance_outputs.go | — (GET по UID) | ✅/частично | n/a |
|
||||
|
||||
**Вывод.**
|
||||
- **`findInstanceUidByDisplayNameRefSvc` УЖЕ исправлен** — он пропускает deleted в коде (строки client.go) и предпочитает running>suspended. Замечание в 24_ai_analysis_pipeline_architecture_2026_06_30.md («НЕ ИСПРАВЛЕН?») **устарело** — баг класса #23 здесь закрыт. Единственный недочёт — нет `isDeleted=false` в URL (лишний трафик, но не баг корректности).
|
||||
- **`findInstanceDisplayNameByUidRefSvc` (1058)** — единственная функция БЕЗ фильтра deleted ни в URL, ни в коде. Но она ищет по **точному `instanceUid`** (уникальному) и возвращает displayName — это обратный маппинг для чтения state, не выбор «того/не того» инстанса. Класс багов #23 здесь не применим. Риск минимальный: вернёт имя deleted-инстанса, если в state остался его UID.
|
||||
|
||||
**Рекомендация.** Косметика: добавить `&isDeleted=false` в URL `findInstanceUidByDisplayNameRefSvc` (1057) и `findInstanceDisplayNameByUidRefSvc` (1060) для экономии трафика. Корректность уже обеспечена. Обновить вывод в файле 24 (он сеет ложную тревогу).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: Gitea 413 (`client_max_body_size`)
|
||||
|
||||
**Находка.**
|
||||
- charts — **пустая** (list_dir: folder empty).
|
||||
- Манифесты в репо есть только для dashboard: ingress.yaml — ingress для `terra.k8c.ru/dashboard`, аннотаций `client_max_body_size`/`proxy-body-size` нет, и это **не Gitea**.
|
||||
- `gitea-naeel.giteak8s.services.ngcloud.ru` упоминается только как `git_path` в resources.tf и закомментированно в luceUNDnode.tf. Сам Gitea — это **управляемый сервис Nubes** (см. svcs.json: «Gitea», «Комплексная услуга по созданию gitea», service_id 99/114), развёрнутый в кластере `giteak8s`, а не из этого репо.
|
||||
|
||||
**Вывод.** **Конфигурация nginx Gitea в этом репозитории отсутствует.** `client_max_body_size=1MB` задаётся на ingress управляемого Gitea в кластере `giteak8s.services.ngcloud.ru` — **внешняя конфигурация**, недоступная для правки из этого репо.
|
||||
|
||||
**Рекомендация.** Лимит правится за пределами репо — на ingress Gitea-инстанса: аннотация `nginx.ingress.kubernetes.io/proxy-body-size: "0"` (или, например, `512m`). Это требует доступа к namespace Gitea в кластере giteak8s. Из репо проблему не решить. *(Требует данных кластера — отмечаю честно.)*
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: Полный список suspend/resume-сервисов
|
||||
|
||||
**Находка.** YAML-спеки в devops/profiles/prod/generated/resources_yaml/ (43 файла, test идентичен), встроены через `//go:embed *` в embed.go.
|
||||
|
||||
**26 сервисов с suspend И resume** (service_id): `1 dummy, 2 template, 12 s3, 19 vc_org, 20 vc_org_saas, 21 vc_vdc, 23 vc_vm, 26 vapp, 27 vc_vm_v2, 28 vc_vm_v3, 50 nextcloud, 89 flask, 90 postgres, 92 mongodb, 93 rabbitmq, 94 lucee, 95 nodejs, 96 pgadmin, 98 http, 99 gitea, 115 mariadb, 116 kafka, 119 akhq, 120 clickhouse, 149 valo_tenant, 150 k8s_shturval`.
|
||||
|
||||
**17 сервисов без suspend/resume**: `13 s3bucket, 22 vc_nsxt, 25 vcexternalip, 29 vc_vdc_group, 32 vmpostgre, 81 superset, 82 harbor, 88 ziti, 91 redis, 97 nodered, 100 openwhisk, 110 dnszone, 111 dnsrecord, 112 tenant, 113 vc_complex, 114 gitea_complex, 117 nifi`.
|
||||
|
||||
**Вывод.**
|
||||
- **Асимметрии нет**: suspend и resume всегда идут парой (26/26). Сервисов «suspend без resume» — **0**.
|
||||
- `suspend_on_destroy_default` **идеально совпадает** с правилом `hasSuspend → true`: 26 suspend-сервисов = `true`, 17 = `false`. Расхождений нет.
|
||||
- Логика разделения здравая: stateless (DNS, external IP, tenant) — без suspend; stateful (БД, приложения, VM) — с suspend.
|
||||
|
||||
**Рекомендация.** По этому пункту всё чисто. Стоит лишь добавить в генератор `validateSpec()`-проверку «если есть suspend, обязан быть resume» как защиту на будущее (сейчас инвариант соблюдён случайно — генератор его не enforce-ит).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: Terraform Operator — состояние и health
|
||||
|
||||
**Находка — ключевая.** Директорий `operator/`, `registry-server-build/`, корневого `k8s/` **НЕТ в этом checkout** (file_search: «No files found»), хотя они активно упоминаются в README.md, REPO_CONTENTS.md, build-registry-image.yml (`operator/build/Dockerfile.registry`) и deploy-dev.sh (`k8s/overlays/dev`).
|
||||
|
||||
Что есть в репо — описание в 00_system_mechanics.md:
|
||||
- 3 компонента: **Operator** (watch CRD → spawn Job), **Registry API** (Discovery protocol over S3), **Builder Job** (`golang:1.24-alpine`: clone→build→upload S3). Это совпадает с тремя deployment'ами, что вы видели.
|
||||
- **S3**: бакет артефактов `terraform-providers`, схема пути `{hostname}/{namespace}/{provider}/{version}/{file}`, `REGISTRY_HOSTNAME=terra.k8c.ru`. Эндпойнт `s3.msk-1.ngcloud.ru` (upload_provider_s3.py, REPO_CONTENTS.md).
|
||||
- **GPG в кластере — ФЕЙКОВЫЙ**: 00_system_mechanics.md — «GPG Signing: Сейчас фейковое (создаётся пустой `.sig`)». Реальная подпись private_key.asc используется только в **локальном** `03_build...sh`, а не в operator-Job.
|
||||
- **Перезапуск оператора — штатная операция**: в cheat-sheet прямо есть `kubectl rollout restart deploy/terraform-operator -n terra`.
|
||||
|
||||
**Выводы по вопросам:**
|
||||
1. **Перезапуск 2 дня назад** — скорее всего ручной `rollout restart` (документированная команда подхвата изменений `manifests/03-build-script.yaml`), а не краш. Подтвердить можно только по `kubectl describe pod`/`--previous` логам в кластере. *(Требует кластера.)*
|
||||
2. **Трёх deployment'ов достаточно** для registry+CI (operator+registry-server+builder-job). Отдельного docs-server нет и не нужен: доки — статика в S3 (`terraform-registry/docs/...`), publish-docs.sh. cloud-dashboard — отдельный UI, не часть registry.
|
||||
3. **GPG-ключи**: в кластере подпись фейковая (tech debt из system_mechanics); реальный ключ только локально в secrets. **S3-доступ** оператора — через ENV `S3_*`/`REGISTRY_HOSTNAME` в Deployment (должны быть в Secret `terraform-operator`).
|
||||
4. **S3-конфиг бакета в репо** — только переменные и схема путей; манифест Secret/Deployment отсутствует (он в недостающей папке `operator/`/`k8s/`).
|
||||
|
||||
**Рекомендация.** Критично: **внедрить реальную GPG-подпись в operator-Job** (смонтировать private_key.asc как K8s Secret) — сейчас артефакты из кластера подписаны пустышкой, Terraform может ругаться `authentication signature from unknown issuer`. Также — вернуть `operator/`+`k8s/` в этот репо или явно задокументировать, что они в отдельном репозитории (сейчас CI ссылается на отсутствующие пути).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6: Тестовые стенды — что реально используется
|
||||
|
||||
**Находка.** Все `.tf` используют **только Universal** (`source = "terra.k8c.ru/nubes/nubes"`), Legacy (`registry.terraform.io/nubes/nubes`) **не используется нигде**.
|
||||
|
||||
| Стенд | Version | Endpoint | Активные ресурсы |
|
||||
|---|---|---|---|
|
||||
| TEST_STAND/LUCEE | 2.0.6 | test | postgres, lucee |
|
||||
| TEST_STAND/MARIA_DB | 2.0.8 | test | mariadb, lucee, flask |
|
||||
| TEST_STAND/POSTGRES | **5.0.52** | test | postgres + user + database |
|
||||
| TEST_STAND/S3_EVENT_POC | 2.1.23 | — | s3bucket ×2 |
|
||||
| PROD_STAND/PG1 | 2.1.26 | prod | postgres, lucee, nodejs, s3bucket |
|
||||
| PROD_STAND/POSTGRES | 2.1.12 | prod | postgres, lucee, nodejs |
|
||||
| PROD_STAND/RABBIT | 5.0.19 | **test ⚠️** | rabbitmq |
|
||||
| RABBIT/ (корень) | 2.1.10 | prod | rabbitmq, lucee, nodejs, s3bucket |
|
||||
|
||||
**Выводы:**
|
||||
1. Все стенды активны (с реальными ресурсами); часть компонентов закомментирована (VM в PG1, flask в PROD/POSTGRES, http-worker в RABBIT).
|
||||
2. **Сильный разброс версий**: test — 2.0.6…5.0.52, prod — 2.1.10…2.1.26. Каждый стенд пинит свою версию.
|
||||
3. **TEST/POSTGRES не дублирует PROD/POSTGRES**: разные версии (5.0.52 vs 2.1.12), имена (`pg4tf033` vs `pg-tst0`), TEST модульный (только БД+user+database с `var.realm`), PROD связный (БД+lucee+nodejs, хардкод realm). Это разные конфигурации, не дубль.
|
||||
4. **Аномалия**: PROD_STAND/RABBIT/main.tf указывает на **test-endpoint** (`deck-api-test.ngcloud.ru`), хотя лежит в PROD_STAND — вероятно ошибка/недо-миграция.
|
||||
|
||||
**Рекомендация.** Привести версии стендов к двум канонам (одна test, одна prod), исправить endpoint в RABBIT, удалить мёртвые закомментированные `.tf` или вынести в `examples/`.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 7: auth.k8s.ngcloud.ru / Keycloak / shturval
|
||||
|
||||
**Находка.** Поиск `auth.k8s.ngcloud.ru`, `shturval`, `OIDC`, `kubeconfig` по репо: совпадения **только в самом файле вопросов** 3006_1_questions.md. В коде/конфигах — ничего.
|
||||
- HAR `keycloak.nubes.ru.har` существует локально, но **gitignored** (`HAR/*.har` в .gitignore) — и это Keycloak **`keycloak.nubes.ru`** (для управляемых сервисов Nubes), а **не** `auth.k8s.ngcloud.ru`.
|
||||
- Инструкции по доступу — только HOW_TO_SSH.md (SSH, не kubeconfig/OIDC).
|
||||
- `kubectl`-команды в docs (deploy-dev.sh, ops/*.md) предполагают **готовый** kubeconfig, способ его получения не описан.
|
||||
|
||||
**Вывод.** **Конфигурации OIDC кластера, скриптов обновления токена и инструкций по kubeconfig в репо НЕТ.** `auth.k8s.ngcloud.ru` (realm `shturval`) — внешняя система аутентификации кластера, никак не отражённая в репозитории. HAR относится к другому Keycloak (сервисный, `keycloak.nubes.ru`).
|
||||
|
||||
**Рекомендация.** Если доступ к кластеру через `auth.k8s.ngcloud.ru` нужен регулярно — добавить в secrets (gitignored) инструкцию/скрипт `kubelogin`/OIDC token refresh, по аналогии с `HOW_TO_SSH.md`. Сейчас это «племенное знание» вне репо. *(Детали OIDC-эндпойнта — за пределами репо.)*
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 8: Orphan PVC `storage-check-*` и PostgreSQL 0/0
|
||||
|
||||
**Находка.** Поиск `storage-check` по всему репо: совпадения **только в файле вопросов**. Ни одного `.tf`, манифеста или упоминания в docs/HISTORY, создающего `storage-check-local`/`storage-check-vcd`. Никаких упоминаний проблемных PostgreSQL-тенантов `509145c3`/`dc5db45d`.
|
||||
|
||||
**Вывод.**
|
||||
- `storage-check-*` PVC **не создаются Terraform-конфигами из этого репо**. Имена (`storage-check-local`/`storage-check-vcd` = local/vCD storage classes) характерны для **тестов доступности StorageClass** — это типичные probe-PVC, которые создаёт **облачная платформа/CSI или сам terraform-operator-Job** при проверке хранилища, а не пользовательский TF. Подтвердить, что их делает именно operator, по репо нельзя (код оператора отсутствует в checkout, см. Q5).
|
||||
- PostgreSQL `0/0 28 дней` с висящими PVC/Service — это **suspended/недо-удалённые инстансы**: StatefulSet масштабирован в 0 (suspend), но PVC и Service остаются (платформа не чистит при suspend). Это согласуется с семантикой `suspend_on_destroy` (Q4) и багом 20_destroy_detach_semantics: `state_only`/suspend оставляет ресурсы в облаке.
|
||||
|
||||
**Рекомендация.** Источник `storage-check-*` и orphan-StatefulSet'ов нужно искать **в кластере**: `kubectl get pvc storage-check-local -o yaml` → смотреть `ownerReferences`/`annotations` (кто создатель). Если owner — `terraform-operator` SA или CSI healthcheck, чистить через него. Из репозитория эти ресурсы не управляются. *(Требует доступа к кластеру.)*
|
||||
|
||||
---
|
||||
|
||||
## Сводка
|
||||
|
||||
| # | Вопрос | Ответ из репо? | Главный вывод |
|
||||
|---|---|---|---|
|
||||
| 1 | Версии 53-55 | ✅ Полностью | Из `profiles/*/profile.env`, ручной bump; main.go — fallback. prod=2.1.23, test=5.0.55 |
|
||||
| 2 | Аудит isDeleted | ✅ Полностью | Все критичные функции фильтруют; `findInstanceUidByDisplayNameRefSvc` **уже исправлен** (файл 24 врёт) |
|
||||
| 3 | Gitea 413 | ⚠️ Частично | Конфига nginx Gitea в репо нет — внешняя config кластера giteak8s |
|
||||
| 4 | Suspend-сервисы | ✅ Полностью | 26 suspend+resume, 0 асимметрий, default совпадает идеально |
|
||||
| 5 | Operator health | ⚠️ Частично | `operator/`+`k8s/` отсутствуют в checkout; GPG в кластере фейковый; рестарт = штатный redeploy |
|
||||
| 6 | Стенды | ✅ Полностью | Везде Universal, Legacy не используется; разброс версий; RABBIT prod→test endpoint |
|
||||
| 7 | auth.k8s | ❌ Нет в репо | Ни OIDC, ни kubeconfig-скриптов; HAR — другой Keycloak (nubes.ru) |
|
||||
| 8 | Orphan PVC | ❌ Нет в репо | `storage-check-*` не из TF; orphan PG = suspended-остатки; искать в кластере |
|
||||
|
||||
**Требуют данных вне репо** (честно): Q3 (ingress Gitea в giteak8s), Q5 (логи рестарта оператора, Secret S3/GPG), Q7 (OIDC-эндпойнт auth.k8s), Q8 (`ownerReferences` PVC в кластере).
|
||||
|
||||
Сохранить эти ответы в `HISTORY/OPUS/3006_1.md` или оставить в чате?
|
||||
|
||||
Создано задач: 6
|
||||
@@ -0,0 +1,802 @@
|
||||
Что сказать Opus — кратко
|
||||
Задача: глубокий анализ проекта, найти слабые места, предложить улучшения.
|
||||
|
||||
10 вопросов:
|
||||
Два провайдера (Legacy + Universal) — как и когда выпилить Legacy?
|
||||
Генерационный конвейер — API→YAML→Go→S3. Надёжен ли? Что если API поменяет формат?
|
||||
API Flow V6 — 6 шагов без транзакционности. Что при обрыве посредине?
|
||||
CRUD/Adopt — сложнейшая логика в adoptExistingInstanceOnCreate. Все ли краевые кейзы покрыты?
|
||||
Генератор gen_v2 — шаблоны вшиты в бинарь. Как расширять? Что с новыми kind'ами операций?
|
||||
Баги — 23 файла истории. Какие системные? Фильтрация deleted — главная проблема.
|
||||
Матрица состояний — 16 состояний, все ли обрабатываются?
|
||||
Тесты — crud_test.go, client_test.go. Достаточно ли?
|
||||
Безопасность — токены, GPG-ключ в репозитории, InsecureSkipVerify.
|
||||
Конкурентность — два terraform apply одновременно?
|
||||
Что читать (приоритет):
|
||||
ARCHITECTURE.md
|
||||
client.go
|
||||
crud.go
|
||||
generate_resources_v2.go
|
||||
docs/50_history/23_*.md (последние баги)
|
||||
Полный промпт: prompt_for_opus48.md
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Изучу проект. Начну с чтения ключевых файлов и параллельного исследования.
|
||||
|
||||
Searched for files matching `**/ARCHITECTURE.md`, 1 match
|
||||
|
||||
Searched for files matching `**/internal/**/*.go`, 20 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/prompt_for_opus48.md)
|
||||
|
||||
Прочитаю ключевые файлы и запущу параллельное исследование по разным областям.
|
||||
|
||||
Searched for files matching `**/universal_rebuild/internal/core/client.go`, 1 match
|
||||
|
||||
Searched for files matching `**/universal_rebuild/internal/resources_core/crud.go`, 1 match
|
||||
|
||||
Searched for files matching `**/universal_rebuild/tools/gen_v2/generate_resources_v2.go`, 1 match
|
||||
|
||||
Searched for files matching `**/docs/50_history/*.md`, 20 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/ARCHITECTURE.md)
|
||||
|
||||
Прочитаю основные файлы ядра и генератора, параллельно запущу исследование истории багов.
|
||||
|
||||
История багов проекта
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#1-1), lines 1 to 450
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/24_ai_analysis_pipeline_architecture_2026_06_30.md#1-1), lines 1 to 200
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/23_vapp_uid_inconsistency_displayname_resolve_bug.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/22_adopt_ref_validation_duplicate_detection_5_0_50.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/21_plan_validation_ref_svc_filter_5_0_38.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/20_destroy_detach_semantics_5_0_8.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/16_universal_provider_create_only_params.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/13_universal_flow_param_normalization.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/06_postgres_update_and_immutable_params.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/05_postgres_implementation_and_polling_fixes.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/00_system_mechanics.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/01_initial_operator_setup.md#1-1), lines 1 to 100
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/02_registry_protocol_fix.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/03_domain_migration_and_hotfix.md#1-1), lines 1 to 80
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/04_vm_hang_fix_and_500_error.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/07_operation_based_polling_and_negative_testing.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/08_natural_language_infrastructure_ai.md#1-1), lines 1 to 80
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/09_s3_migration_and_branded_docs.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/10_postgres_modify_tests_and_external_ip.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/12_vm_resource_hardening.md#1-1), lines 1 to 80
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/11_postgres_import_implementation.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/15_universal_provider_lifecycle_tests.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/17_subresource_generation_rebuild_and_5_0_1_publish.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/18_docs_publish_pipeline_hardening_5_0_4.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/14_universal_provider_separate.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/19_subresource_update_id_fix_and_5_0_7.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/02_tubulus_stabilization.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#450-450), lines 450 to 900
|
||||
|
||||
Read [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/chat-session-resources/2c7367dd-6d7f-48b4-84d0-25bbb06f7cfb/toolu_01Gk8LXuMrG8gDUMRXKhjs2J__vscode-1782817912242/content.txt)
|
||||
|
||||
Прочитаю crud.go (включая `adoptExistingInstanceOnCreate`) и начало генератора, а также тесты.
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud.go)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#1-1), lines 1 to 120
|
||||
|
||||
Изучу генератор, тесты, транспорт и состояние secrets в gitignore.
|
||||
|
||||
Генератор gen_v2 и тесты
|
||||
|
||||
Searched for files matching `**/.gitignore`, 6 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#120-120), lines 120 to 178
|
||||
|
||||
Searched for files matching `tf_provider` · `**/*_test.go`, 2 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#1-1), lines 1 to 100
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud_test.go#1-1), lines 1 to 50
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go)
|
||||
|
||||
Searched for regex `func \(.*\) main\(\)|func.*Generate` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), no results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#100-100), lines 100 to 300
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud_test.go#50-50), lines 50 to 57
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go#46-46), lines 46 to 46
|
||||
|
||||
Searched for text `template.Must|var.*=|const.*=` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 8 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#300-300), lines 300 to 500
|
||||
|
||||
Searched for regex `func write|func.*Kind|if.*Kind|switch.*op\.Kind` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 7 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#500-500), lines 500 to 700
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#800-800), lines 800 to 900
|
||||
|
||||
Searched for regex `panic|error|yaml\.Unmarshal|unknown|default|fallback` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#850-850), lines 850 to 1200
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud_test.go)
|
||||
|
||||
Searched for regex `` ^const.*Template = |^const.*= ` `` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 3 results
|
||||
|
||||
Searched for regex `^func Test` (`tf_provider` · `universal_rebuild/internal/resources_core/crud_test.go`), 3 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go)
|
||||
|
||||
Searched for regex ``^`$`` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 3 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#1420-1420), lines 1420 to 1430
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#1865-1865), lines 1865 to 1880
|
||||
|
||||
Searched for regex `.*` (`tf_provider` · `universal_rebuild/internal/resources_core/crud_test.go`), 56 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#210-210), lines 210 to 350
|
||||
|
||||
Searched for regex `buildSubresourceForceNewCodes|computeCreateOnly|analyzeJsonPlanMod` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 10 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go)
|
||||
|
||||
Searched for regex `func.*IsJson|func.*RefSvc|func.*computeCreateOnly|func.*analyzeJsonPlanMod` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 3 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#2040-2040), lines 2040 to 2150
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/.gitignore)
|
||||
|
||||
Read [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/chat-session-resources/2c7367dd-6d7f-48b4-84d0-25bbb06f7cfb/toolu_01RcjLgQg9SVXsgzGtT1p8Wa__vscode-1782817912278/content.txt)
|
||||
|
||||
Изучил архитектуру, ядро, генератор, историю багов и тесты. Сохраню анализ и представлю его.
|
||||
|
||||
Created memory file [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/MmM3MzY3ZGQtNmQ3Zi00OGI0LTg0ZDAtMjViYmIwNmY3Y2Zi/plan.md)
|
||||
|
||||
# Анализ Terraform Provider для Nubes Cloud
|
||||
|
||||
Изучил архитектуру (ARCHITECTURE.md), ядро (client.go, crud.go), генератор (generate_resources_v2.go), транспорт (provider.go), 24 файла истории и тесты. Ниже — ответы по всем 10 пунктам: текущее состояние → плюсы → минусы → предложения → риски.
|
||||
|
||||
---
|
||||
|
||||
## 1. Два провайдера: Legacy + Universal
|
||||
|
||||
**Состояние.** Legacy (internal, v5.0.52, реестр `registry.terraform.io/nubes/nubes`, 13 ручных ресурсов) и Universal (universal_rebuild, v5.0.51, реестр `terra.k8c.ru/nubes/nubes`, ~50 генерируемых). Оба на `terraform-plugin-framework`, но это **полностью независимые деревья кода** с дублирующимся ядром (core существует в обоих).
|
||||
|
||||
**Хорошо.** Universal — целевая архитектура, чёткие принципы в ARCHITECTURE.md (YAML = source of truth). Разные реестры → можно мигрировать без коллизий.
|
||||
|
||||
**Плохо.** Дублирование ядра (client.go, `instance_lookup.go`) — баги фиксятся в двух местах (см. баг #1: правка в client.go И instance_lookup.go). Версии разъехались (5.0.52 vs 5.0.51) — непонятно, какая «свежее». Нет формального deprecation-плана с датой.
|
||||
|
||||
**Предложения.**
|
||||
- Зафиксировать **матрицу соответствия ресурсов** Legacy→Universal: какие 13 ресурсов уже перекрыты Universal, какие нет.
|
||||
- Объявить Legacy *frozen* (только critical-фиксы), завести `DEPRECATED.md` с целевой версией снятия.
|
||||
- Перенести уникальную логику Legacy (VM/vApp/edge/vdc) в YAML-спеки, проверить паритет, затем archive Legacy в отдельную ветку/тег.
|
||||
|
||||
**Риск.** VM/vApp в Legacy содержат ручную логику (FW-rules, 500-фикс из 04_vm_hang_fix_and_500_error), которую генератор может не воспроизвести. Нужен паритетный прогон на тестовом стенде до снятия Legacy.
|
||||
|
||||
---
|
||||
|
||||
## 2. Генерационный конвейер (API→YAML→Go→S3)
|
||||
|
||||
**Состояние.** 4 шага: `service_spec_gen` (API→YAML, ATTEMPTS=3, REQUEST_DELAY=0.5) → `gen_v2` (YAML→Go) → build+GPG+S3 → mkdocs. Включение сервиса = строка в `services_list.txt`.
|
||||
|
||||
**Хорошо.** Чёткое разделение, профили dev/test/prod изолируют артефакты, retry на шаге сбора YAML.
|
||||
|
||||
**Плохо — главное расхождение с собственными принципами.** ARCHITECTURE.md декларирует «*The generator must enforce these rules and fail fast on drift*», но фактически:
|
||||
- **Неизвестный `kind` операции тихо игнорируется** (generate_resources_v2.go — три `if op.Kind != "..." { continue }`). Если API введёт новый kind — ресурс молча пропадёт из провайдера, без ошибки.
|
||||
- **YAML почти не валидируется**: проверяется только синтаксис (`yaml.Unmarshal`). Отсутствие `op.Kind`/`op.Action` → zero-value → тихое игнорирование. Нет проверки уникальности param ID, наличия required-полей, валидности `RefSvcId`.
|
||||
- При ошибке `format.Source` генератор пишет **неформатированный (возможно битый) код** как fallback вместо остановки.
|
||||
- Исключение сервиса только комментированием в `services_list.txt` → риск рассинхрона (закомментировали в test, забыли в prod).
|
||||
|
||||
**Предложения.**
|
||||
- Добавить фазу `validateSpec()` перед генерацией: required-поля, уникальность ID, известность `kind`/`action`, ссылочная целостность `RefSvcId`. **Fail fast** на неизвестном kind.
|
||||
- Убрать fallback на неформатированный код — при `format.Source` error → паника с понятным сообщением.
|
||||
- CI-шаг «генерация без diff»: прогон генератора → `git diff --exit-code` (детект дрейфа, как требует ARCHITECTURE.md).
|
||||
- Go для генераторов оправдан (один язык со сгенерированным кодом, `text/template`, `format.Source`); bash-обёртки — лишь оркестрация. Менять не нужно.
|
||||
|
||||
**Риск.** Изменение формата ответа API на шаге 1 не обнаружится до runtime у пользователя. Сейчас единственная защита — `ATTEMPTS=3`, что не ловит *семантический* дрейф (поле переименовали, а не пропало).
|
||||
|
||||
---
|
||||
|
||||
## 3. API Flow V6 — отсутствие транзакционности
|
||||
|
||||
**Состояние.** 6 шагов в `CreateGenericInstanceUniversalV6`: `POST /instances` → `POST /instanceOperations` → `GET cfsParams` → resolve refs → `POST` каждого param → `validate-cfs` → `run` → `waitForOperationFinish`.
|
||||
|
||||
**Хорошо.** Завершение по `dtFinish` — надёжный контракт (выстрадан в 02, 05). Нормализация пустых map/json/array есть.
|
||||
|
||||
**Плохо.**
|
||||
- **Orphan при обрыве.** Если процесс упал/таймаут после `POST /instances`, но до `run` — в облаке остаётся инстанс в состоянии `not_created`/`creating`, **не попавший в Terraform state**. Следующий apply найдёт его через `FindInstanceByDisplayName` и упрётся в ошибку «найден в состоянии Not Created; удалите вручную». То есть пользователь обязан чистить руками.
|
||||
- **`doRequest` без retry** (client.go) — нет обработки 429/503/сетевых сбоев. Любой transient-сбой на шаге 4–5 рвёт create.
|
||||
- `req.Close = true` — новое TCP+TLS соединение на каждый запрос (на длинном поллинге дорого).
|
||||
|
||||
**Предложения.**
|
||||
- Retry с экспоненциальным backoff в `doRequest` для идемпотентных GET и для 429/503/сетевых ошибок (с уважением `Retry-After`).
|
||||
- Идемпотентность create: перед `POST /instances` делать `FindInstanceByDisplayName` (уже есть в `CreateResource`) — но также **обрабатывать «недосозданный» инстанс**: предлагать авто-cleanup `not_created`-инстанса при `adopt_existing_on_create=true`, а не только ручное удаление.
|
||||
- Рассмотреть keep-alive (убрать `req.Close=true`) для поллинга — меньше TLS-handshake.
|
||||
|
||||
**Риск.** Авто-cleanup `not_created` — операция удаления, требует явного флага и подтверждения семантики (нельзя удалять то, что пользователь мог создавать вручную параллельно).
|
||||
|
||||
---
|
||||
|
||||
## 4. CRUD / Adopt — `adoptExistingInstanceOnCreate`
|
||||
|
||||
**Состояние.** Покрытые ветки в crud.go:
|
||||
1. `operation_in_progress`/`pending` → ошибка «дождитесь».
|
||||
2. `!adopt_existing_on_create` → ошибка с подсказкой про import.
|
||||
3. `not_created` → ошибка.
|
||||
4. `running` → ref-валидация → adopt (возврат UUID).
|
||||
5. `suspended` → required-params check → resume → проверка статуса после resume → ref-валидация → adopt.
|
||||
6. Иначе (`creating`/`failed`/`error`) → общая ошибка «статус не подходит для авто-усыновления».
|
||||
|
||||
**Хорошо.** Логика соответствует decision-matrix из ARCHITECTURE.md. Ref-валидация при adopt (баг 22) закрыта. Диагностики подробные.
|
||||
|
||||
**Плохо.**
|
||||
- **Конкурентность не покрыта** (см. п.10): между `FindInstanceByDisplayName` и `CreateGenericInstanceUniversalV6` нет блокировки.
|
||||
- `creating`/`failed` падают в *общую* ветку с менее информативным сообщением, чем требует ARCHITECTURE.md (там для `creating`/`pending`/`failed` предписана отдельная диагностика).
|
||||
- Функция ~100 строк, глубокая вложенность, ref-валидация дублируется в двух ветках (running и после resume) — риск рассинхрона при правках.
|
||||
- `state_only`/`detach` destroy → `return nil` без API-вызова: инстанс остаётся в облаке (**это by design** из 20, но «orphaned» с т.з. биллинга — пользователь должен понимать).
|
||||
|
||||
**Предложения.**
|
||||
- Вынести классификацию статуса в один `switch` с явными ветками для каждого из 16 состояний (см. п.7), убрать дублирование ref-валидации в helper.
|
||||
- Для `creating`/`failed` — отдельные сообщения по контракту ARCHITECTURE.md.
|
||||
- Покрыть adopt-матрицу таблично-управляемыми тестами (сейчас 0 тестов на adopt, п.8).
|
||||
|
||||
**Риск.** Рефакторинг самой сложной функции без тестов опасен — сначала тесты, потом рефакторинг.
|
||||
|
||||
---
|
||||
|
||||
## 5. Генератор gen_v2 — шаблоны вшиты в бинарь
|
||||
|
||||
**Состояние.** 3 inline-шаблона `text/template`: `instanceTemplate` (569 строк), `subresourceTemplate` (443), `actionTemplate` (174). `computeCreateOnly` = эвристика (param в create, но не в modify). Identity подресурса: нет modify → все params ForceNew; есть modify → createOnly+delete params ForceNew.
|
||||
|
||||
**Хорошо.** Эвристика createOnly опирается на данные API (не хардкод), `format.Source` гарантирует валидный Go при успехе, восстановление регистра UUID решает «inconsistent result».
|
||||
|
||||
**Плохо.**
|
||||
- Шаблоны как строковые константы внутри `.go` (1186 строк шаблонов) — тяжело поддерживать, нет подсветки/линтинга шаблонов, любая правка = пересборка генератора.
|
||||
- **Новый kind → тихое выпадение ресурса** (см. п.2).
|
||||
- `data_type: json` → `IsJson`+`JsonNormalize` plan-modifier — но **0 тестов** на это, а нормализация JSON исторически проблемная (V2→V6 цикл, баг 13).
|
||||
- Immutable определяется только через отсутствие в modify — если API *временно* не отдаёт modify-параметр (сбой/неполный YAML), параметр ошибочно станет ForceNew → пересоздание ресурса.
|
||||
|
||||
**Предложения.**
|
||||
- Вынести шаблоны в `embed.FS` (`//go:embed templates/*.tmpl`) — поддерживаемость без потери single-binary.
|
||||
- Fail fast на неизвестном kind + лог числа сгенерированных ресурсов на сервис (детект «пропал ресурс»).
|
||||
- Снапшот-тесты генератора: эталонный YAML → ожидаемый `.go` (golden files).
|
||||
- Защита от «исчезнувшего modify-параметра»: предупреждать, если у сервиса есть create-params, но 0 modify-params (подозрительно).
|
||||
|
||||
**Риск.** Если `gen_v2` сломается — ломается **весь** Universal-провайдер. Сейчас единственная страховка — `format.Source`, который при ошибке всё равно пишет битый код.
|
||||
|
||||
---
|
||||
|
||||
## 6. Баги из истории — что системное, что осталось
|
||||
|
||||
**Системные классы (порождали серии багов):**
|
||||
1. **Фильтрация deleted** (23, 22, 21) — API возвращает deleted-инстансы, провайдер их не отсеивал. **Закрыт 5.0.50** (`isDeleted=false` + `isInstanceDeleted()` + ошибка при >1 совпадении).
|
||||
2. **Определение конца операции** (05, 02, 04) — зависание поллинга. **Закрыт** (критерий `dtFinish`).
|
||||
3. **Динамические param ID** (create ID ≠ modify ID, 06) — **закрыт** runtime-discovery.
|
||||
4. **Create-only параметры** (16) — **закрыт 5.0.38**.
|
||||
|
||||
**Не до конца решённые / ограничения:**
|
||||
- **Нормализация map/json/list** — потребовала 5 итераций (V2→V6), помечена как *частично*; новые типы параметров могут снова всплыть.
|
||||
- **Realm-валидация отключена** (21) из-за бага бэкенда `/resourceRealms/available` — валидации realm до деплоя нет.
|
||||
- **FW-rules 500** (04) — **platform-side bug**, воспроизводится и в Cloud Console; провайдер не может починить.
|
||||
- **Disk shrink** (06) — ограничение платформы (только увеличение), провайдер корректно прокидывает ошибку.
|
||||
|
||||
**Главная системная проблема** — именно фильтрация deleted была корнем 3+ багов. Сейчас закрыта, но **отсутствие тестов** означает, что регрессия не будет поймана автоматически.
|
||||
|
||||
**Предложения.** Regression-тесты на deleted-фильтрацию и multi-match; превратить known-limitations в явные диагностики (например, предупреждать про realm «валидация недоступна»).
|
||||
|
||||
---
|
||||
|
||||
## 7. Матрица состояний — 16 состояний
|
||||
|
||||
**Состояние.** `INSTANCE_STATES.md` / `STATE_TRANSITIONS.md` описывают полную матрицу. В коде adopt обрабатывает: `not_created`, `running`, `suspended`, `in_progress`/`pending`; остальные → общая ошибка.
|
||||
|
||||
**Плохо.** Хелперы `isStatusSuspended`/`isStatusNonAdoptable`/`isStatusNotCreated` работают через `strings.Contains` по тексту `explainedStatus` — **хрупко**: изменение формулировки статуса в API сломает классификацию молча. Промежуточные (`creating`, `failed`, `error`) сваливаются в одну ветку без индивидуальных подсказок, хотя ARCHITECTURE.md требует разные диагностики.
|
||||
|
||||
**Предложения.** Завести enum состояний и единую функцию `classifyStatus(raw) → State`, маппинг raw→enum в одном месте, exhaustive `switch` по всем 16 (с `default → явная ошибка «неизвестный статус X»`). Тесты на каждый статус.
|
||||
|
||||
**Риск.** Строковое сопоставление — самое уязвимое место к молчаливому дрейфу API.
|
||||
|
||||
---
|
||||
|
||||
## 8. Тесты — достаточно ли
|
||||
|
||||
**Состояние.** **8 unit-тестов всего**: crud_test.go (3: `isStatusSuspended`, `isStatusNonAdoptable`, delete-default) и client_test.go (5: нормализация значений). Без моков, без integration.
|
||||
|
||||
**Не покрыто (критично):** adopt-логика, ref-валидация, polling/`waitForOperationFinish`, `FindInstanceByDisplayName` (deleted+multi-match), генератор целиком, JSON plan-modifier, suspend/resume, required-params compare.
|
||||
|
||||
**Предложения.**
|
||||
- `httptest.Server` мок Nubes API → тесты Flow V6, поллинга по `dtFinish`, обрыва на шаге N, 429/503.
|
||||
- Табличные тесты adopt-матрицы (все 16 состояний × `adopt_existing_on_create` true/false).
|
||||
- Golden-тесты генератора (YAML→Go).
|
||||
- Контракт-тесты на основе HAR (см. п. ниже).
|
||||
|
||||
**Риск.** Все закрытые системные баги (deleted, polling, param-ID) **не защищены от регрессии**. Любой рефакторинг ядра/генератора — рулетка.
|
||||
|
||||
---
|
||||
|
||||
## 9. Безопасность
|
||||
|
||||
**Состояние.**
|
||||
- Токены: `*.token` в .gitignore ✅; private_key.asc в .gitignore ✅; id_ed25519.txt ✅.
|
||||
- `InsecureSkipVerify` default = **false** ✅, включается только явно/`NUBES_INSECURE=true`. TLS 1.2 min ✅.
|
||||
- `api_token` помечен `Sensitive: true` ✅.
|
||||
|
||||
**Хорошо.** Базовая гигиена соблюдена — ключи и токены не коммитятся, TLS-проверка по умолчанию включена.
|
||||
|
||||
**Плохо / проверить.**
|
||||
- **GPG-ключ физически лежит в secrets** — да, в .gitignore, но стоит проверить `git log --all -- secrets/private_key.asc`, что он не попал в историю ранее. .gitignore не вычищает уже закоммиченное.
|
||||
- public_key.asc, id_ed25519.pub — публичные, ок; но prod.token/`dev.token`/`test.token` существуют локально — убедиться, что покрыты `*.token` (да) и не было коммита до добавления правила.
|
||||
- **Утечка токена в логи**: `formatAPIError` форматирует тело ответа API в ошибку — если API эхает заголовки/токен в body ошибки, он попадёт в диагностику Terraform. `doRequest` сам токен не логирует. Стоит маскировать `Bearer ...` в любых сообщениях.
|
||||
- `ttyOut()` пишет напрямую в tty минуя Terraform — в debug-режиме `StageMsg` может содержать чувствительные данные; они идут в терминал в обход TF-логирования.
|
||||
|
||||
**Предложения.** `git log` аудит секретов; явная маскировка токена в `formatAPIError`/диагностиках; политика ротации `*.token`; вынести секреты из репо в внешний secret-store (для CI).
|
||||
|
||||
**Риск.** Если ключ/токен попал в git-историю до .gitignore — он уже скомпрометирован, .gitignore не поможет. Это надо проверить первым делом.
|
||||
|
||||
---
|
||||
|
||||
## 10. Конкурентность — два `terraform apply`
|
||||
|
||||
**Состояние.** Никаких блокировок. `FindInstanceByDisplayName` + `CreateGenericInstanceUniversalV6` — **не атомарны**. `waitForInstanceIdle` ждёт `operationIsPending/InProgress`, но это не защищает от гонки create.
|
||||
|
||||
**Плохо.**
|
||||
- Два apply с одинаковым `displayName` одновременно: оба проходят `FindInstanceByDisplayName` (никого нет) → оба `POST /instances` → **два инстанса с одним именем**. После этого `FindInstanceByDisplayName` начнёт возвращать ошибку «найдено 2 инстанса» (баг 22 только *детектирует* это, но не предотвращает).
|
||||
- Между modify из двух окружений — гонка на `instanceOperations`; частично гасится `waitForInstanceIdle`, но окно остаётся.
|
||||
|
||||
**Предложения.**
|
||||
- Полагаться на **Terraform state locking** (backend lock) как первичную защиту — это ответственность пользователя, задокументировать.
|
||||
- На стороне API — проверить, есть ли уникальность `displayName` на бэкенде; если нет, провайдер не может гарантировать атомарность.
|
||||
- Минимально: после `POST /instances` сразу повторный `FindInstanceByDisplayName` и, если найдено >1, откатить свой (требует delete — осторожно).
|
||||
|
||||
**Риск.** Полноценная защита возможна только при поддержке со стороны API (уникальность имени или conditional create). Провайдер в одиночку гонку не закрывает.
|
||||
|
||||
---
|
||||
|
||||
## Сводный план улучшений (по приоритету)
|
||||
|
||||
**Steps**
|
||||
|
||||
**P0 — Надёжность runtime (блокеры для прода)**
|
||||
1. Retry + backoff в `doRequest` для 429/503/сетевых сбоев (уважать `Retry-After`).
|
||||
2. Обработка orphan-инстанса `not_created`: авто-cleanup при явном флаге вместо «удалите вручную».
|
||||
3. Документировать и протестировать поведение при конкурентном apply (state-lock + повторная проверка после create).
|
||||
|
||||
**P1 — Защита от дрейфа и регрессий** (*parallel с P0*)
|
||||
4. `validateSpec()` в генераторе + **fail fast на неизвестном kind**; убрать fallback на неформатированный код.
|
||||
5. CI-шаг «генерация без diff» (детект дрейфа per ARCHITECTURE.md).
|
||||
6. Тесты: `httptest` мок API (Flow V6, polling, обрыв, 429), табличные тесты adopt-матрицы, golden-тесты генератора, regression на deleted-фильтрацию.
|
||||
|
||||
**P2 — Снижение тех-долга**
|
||||
7. Единый `classifyStatus → enum` для 16 состояний, exhaustive switch, убрать `strings.Contains`-классификацию.
|
||||
8. Рефакторинг `adoptExistingInstanceOnCreate` (после тестов): вынести ref-валидацию в helper, отдельные диагностики для `creating`/`failed`.
|
||||
9. Шаблоны генератора в `embed.FS`.
|
||||
10. План вывода Legacy: матрица паритета ресурсов, `DEPRECATED.md`, целевая версия снятия.
|
||||
|
||||
**P3 — Безопасность и наблюдаемость**
|
||||
11. `git log` аудит secrets; маскировка `Bearer`-токена в `formatAPIError`/диагностиках.
|
||||
12. Realm-валидация — включить, когда бэкенд починит `/resourceRealms/available`.
|
||||
13. HAR-based контракт-тесты (верификация соответствия провайдера реальному API).
|
||||
|
||||
**Relevant files**
|
||||
- client.go — `doRequest` (retry), `CreateGenericInstanceUniversalV6` (orphan/идемпотентность), `FindInstanceByDisplayName` (конкурентность).
|
||||
- crud.go — `adoptExistingInstanceOnCreate` (рефакторинг + тесты), статус-хелперы.
|
||||
- generate_resources_v2.go — валидация YAML, fail-fast на kind, вынос шаблонов.
|
||||
- provider.go — транспорт (keep-alive, маскировка токена).
|
||||
- Тесты: рядом с crud.go и client.go.
|
||||
|
||||
**Decisions / scope**
|
||||
- Это аналитический отчёт + план; **код не менялся** (режим Plan, и правила проекта запрещают изменения без «делай»).
|
||||
- Большинство критических багов из истории **уже закрыты** (5.0.50). Главный системный риск сейчас — **отсутствие тестов** (регрессии не ловятся) и **дрейф API** (генератор/статусы молча ломаются).
|
||||
|
||||
**Further Considerations**
|
||||
1. С чего начать реализацию? **A)** P0 (надёжность runtime) / **B)** P1 (тесты+защита от дрейфа — фундамент для безопасного рефакторинга) / **C)** аудит безопасности секретов (быстро, но критично, если ключ в истории git).
|
||||
2. Вывод Legacy — делать сейчас параллельно или после стабилизации Universal тестами? Рекомендую **после** (нужен паритет VM/vApp, который без тестов рискован).
|
||||
3. Нужен ли вам этот анализ как отдельный markdown-файл в docs (например `docs/50_history/25_opus_deep_analysis.md`), или достаточно ответа в чате?
|
||||
|
||||
Какой блок развернуть детальнее или с какого приоритета начинать?
|
||||
|
||||
Создано задач: 5
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
## Общее мнение
|
||||
|
||||
**Архитектурно — сильное, зрелое решение.** Идея «YAML как source of truth → генерация Go-кода и доков» правильная и редко встречается в self-made провайдерах. Универсальное ядро без сервис-специфичной логики + декларативные спеки = можно добавить сервис без программиста. Это уровень выше типичного «руками написанного» провайдера.
|
||||
|
||||
**Что реально хорошо:**
|
||||
- Выстраданный контракт с API (`dtFinish` для конца операции, динамические param-ID, фильтрация deleted) — видно, что прошли через боль и закрыли системные баги.
|
||||
- Чистое разделение pipeline (API→YAML→Go→S3), профили dev/test/prod.
|
||||
- Базовая безопасность в порядке (токены/ключи в .gitignore, TLS-проверка по умолчанию).
|
||||
|
||||
**Главные слабости (по убыванию):**
|
||||
1. **Тесты почти отсутствуют** (8 unit-тестов, без моков, без integration). Все закрытые баги не защищены от регрессии — это самый большой риск.
|
||||
2. **Генератор молча ломается** при дрейфе API (неизвестный kind тихо игнорируется, YAML не валидируется) — при этом сам же ARCHITECTURE.md требует «fail fast on drift». Декларация ≠ реализация.
|
||||
3. **Нет транзакционности create и retry** — обрыв посередине оставляет orphan, transient-сбой рвёт apply.
|
||||
4. **Два провайдера с дублированным ядром** — тех-долг, баги фиксятся в двух местах.
|
||||
|
||||
**Вердикт:** концепция и инженерная зрелость ядра — выше среднего, видно опытную руку. Но проект держится на «героике» (ручное тестирование на реальных стендах, комментарии «НЕ МЕНЯТЬ»), а не на автоматических гарантиях. Один слой тестов + fail-fast в генераторе превратили бы его из «работает, потому что мы знаем где не трогать» в по-настоящему надёжный. Сейчас это **крепкий прототип на пути к продакшн-grade**, а не законченный продукт.
|
||||
|
||||
Read memory [](file:///home/naeel/.vscode-server/data/User/globalStorage/github.copilot-chat/memory-tool/memories/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/MmM3MzY3ZGQtNmQ3Zi00OGI0LTg0ZDAtMjViYmIwNmY3Y2Zi/plan.md)
|
||||
|
||||
Created [](file:///home/naeel/tf_provider/HISTORY/OPUS/3006_1_questions.md)
|
||||
|
||||
Готово: 3006_1_questions.md
|
||||
|
||||
## 8 дополнительных вопросов
|
||||
|
||||
| # | Тема | Что нового (после его анализа) |
|
||||
|---|------|-------------------------------|
|
||||
| 1 | **Расхождение версий** | Регистр отдаёт 5.0.55, код — 5.0.51. Откуда 53-55? |
|
||||
| 2 | **Полный аудит isDeleted** | Все ли GET /instances фильтруют deleted? Особенно `findInstanceUidByDisplayNameRefSvc` |
|
||||
| 3 | **Gitea 413** | Где nginx-конфиг Gitea в репо? Можно ли увеличить лимит через чарты? |
|
||||
| 4 | **Suspend-сервисы** | Полный список кто поддерживает suspend/resume. Есть ли баги (suspend без resume)? |
|
||||
| 5 | **Terraform Operator** | Почему перезапустился 2 дня назад? Хватает ли компонентов? Где GPG/S3 конфиг? |
|
||||
| 6 | **Тестовые стенды** | Какие реально используются? Legacy или Universal? Дубликаты? |
|
||||
| 7 | **auth.k8s.ngcloud.ru** | OIDC-конфиг, обновление токенов, Keycloak HAR |
|
||||
| 8 | **Orphan PVC/STS** | Откуда `storage-check-*` PVC? PostgreSQL 0/0 28 дней — почему? |
|
||||
|
||||
Все вопросы — **только чтение файлов в репо**, никаких правок. Opus может ответить на большинство из них. Вопросы 3 и 8 частично требуют данных из кластера (не из репо) — он это честно отметит.
|
||||
|
||||
Внесены изменения.
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/HISTORY/OPUS/3006_1_questions.md)
|
||||
|
||||
Исследую все 8 вопросов через чтение файлов репозитория. Запущу параллельный сбор данных.
|
||||
|
||||
Searched for regex `/instances\?|/instances"|GET.*instances|page=|isDeleted` (`**/universal_rebuild/internal/core/*.go`), 20 results
|
||||
|
||||
Searched for regex `findInstanceUidByDisplayNameRefSvc|ListRefServiceInstances`, 20 results
|
||||
|
||||
Searched for files matching `**/devops/profiles/**/profile.env`, 3 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/refsvc_resolve.go)
|
||||
|
||||
Нашёл ключевое для вопроса 2 — прочитаю функцию `findInstanceUidByDisplayNameRefSvc` и файлы версий.
|
||||
|
||||
Searched for regex `VERSION|version|5\.0\.` (`**/devops/03_build_and_upload_provider.sh`), 13 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#900-900), lines 900 to 1010
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/profile.env)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/test/profile.env)
|
||||
|
||||
Вопросы 1 и 2 почти закрыты. Соберу данные по остальным: инфраструктура, suspend-аудит, стенды, auth, orphan-ресурсы.
|
||||
|
||||
Searched for regex `auth\.k8s\.ngcloud\.ru|keycloak|shturval|client_max_body_size|gitea|storage-check`, 20 results
|
||||
|
||||
Searched for regex `getInstanceDisplayNameByUidRefSvc|findInstanceDisplayNameByUidRefSvc` (`**/universal_rebuild/internal/core/*.go`), 6 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/charts)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/cloud-dashboard/k8s)
|
||||
|
||||
Searched for regex `storage-check|terraform-operator|registry-server|operator`, 20 results
|
||||
|
||||
Searched for files matching `**/{operator,registry-server,registry-server-build,k8s}/**`, 3 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#1057-1057), lines 1057 to 1140
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/cloud-dashboard/k8s/ingress.yaml)
|
||||
|
||||
Аудит suspend/resume сервисов
|
||||
|
||||
Аудит тестовых стендов
|
||||
|
||||
Read [](file:///home/naeel/tf_provider)
|
||||
|
||||
Searched for regex `kubeconfig|OIDC|oidc|kube-apiserver|kubectl|HOW_TO_SSH|auth\.k8s`, 20 results
|
||||
|
||||
Searched for files matching `**/TEST_STAND/**/*.tf`, 11 matches
|
||||
|
||||
Searched for files matching `**/PROD_STAND/**/*.tf`, 10 matches
|
||||
|
||||
Searched for files matching `**/RABBIT/**/*.tf`, 6 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/RABBIT)
|
||||
|
||||
Searched for files matching `tf_provider` · `universal_rebuild/resources_yaml/**/*.yaml`, no matches
|
||||
|
||||
Searched for files matching `tf_provider` · `devops/profiles/prod/generated/resources_yaml/**/*.yaml`, no matches
|
||||
|
||||
Searched for files matching `tf_provider` · `devops/profiles/test/generated/**/*.yaml`, no matches
|
||||
|
||||
Searched for files matching `tf_provider` · `universal_rebuild/resources_yaml/embed.go`, 1 match
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/resources_yaml)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/test/generated)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/resources_yaml/embed.go)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/test/generated/resources_yaml)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/LUCEE/resources.tf#1-1), lines 1 to 50
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/MARIA_DB/resources.tf#1-1), lines 1 to 50
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/POSTGRES/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/POSTGRES/resources.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/PG1/resources.tf#1-1), lines 1 to 100
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/POSTGRES/resources.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/RABBIT/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/RABBIT/resources.tf#1-1), lines 1 to 100
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/RABBIT/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/RABBIT/resources.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/LUCEE/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/MARIA_DB/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/S3_EVENT_FUNCTION_POC/versions.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/PG1/main.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/POSTGRES/main.tf)
|
||||
|
||||
Searched for regex `suspend|resume` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/**/*.yaml`), 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/S3_EVENT_FUNCTION_POC/main.tf#1-1), lines 1 to 50
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/TEST_STAND/POSTGRES/luceUNDnode.tf#1-1), lines 1 to 60
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/POSTGRES/flask_disabled.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/PG1/vm.tf)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/PROD_STAND/RABBIT/rabbit_worker_disabled.tf)
|
||||
|
||||
Searched for regex `suspend|resume` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/**/*.yaml`), 187 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/90_postgres.yaml#1-1), lines 1 to 20
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/90_postgres.yaml#459-459), lines 459 to 469
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/115_mariadb.yaml#1-1), lines 1 to 20
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/115_mariadb.yaml#238-238), lines 238 to 248
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/91_redis.yaml#1-1), lines 1 to 20
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/profiles/prod/generated/resources_yaml/117_nifi.yaml#1-1), lines 1 to 20
|
||||
|
||||
Searched for regex `- name:|kind:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/91_redis.yaml`), 6 results
|
||||
|
||||
Searched for regex `- name:|kind:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/117_nifi.yaml`), 9 results
|
||||
|
||||
Searched for regex `- name:|kind:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/112_tenant.yaml`), 9 results
|
||||
|
||||
Searched for regex `- name:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/22_vc_nsxt.yaml`), 8 results
|
||||
|
||||
Searched for regex `- name:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/29_vc_vdc_group.yaml`), 10 results
|
||||
|
||||
Searched for regex `- name:|action:` (`tf_provider` · `devops/profiles/prod/generated/resources_yaml/100_openwhisk.yaml`), 4 results
|
||||
|
||||
Проверю наличие `operator/`/`k8s/`, прочитаю отчёт по gitea/registry и README про реестр.
|
||||
|
||||
Searched for files matching `**/operator/**`, no matches
|
||||
|
||||
Searched for files matching `**/k8s/**`, 3 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/README.md#40-40), lines 40 to 127
|
||||
|
||||
Read [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/chat-session-resources/2c7367dd-6d7f-48b4-84d0-25bbb06f7cfb/toolu_01RGa2BMVk2W3EiaCRQTaSMg__vscode-1782817912438/content.txt)
|
||||
|
||||
Финальная проверка: наличие `registry-server-build/`, детали оператора/S3/GPG в кластере, keycloak HAR.
|
||||
|
||||
Searched for files matching `**/registry-server-build/**`, no matches
|
||||
|
||||
Searched for files matching `**/HAR/*keycloak*`, no matches
|
||||
|
||||
Searched for regex `S3_ENDPOINT|s3.msk-1|terraform-registry|S3_BUCKET|\.s3cfg`, 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/00_system_mechanics.md)
|
||||
|
||||
Все 8 вопросов исследованы. Ниже ответы в формате **находка → файлы → вывод → рекомендация**.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Откуда версии 5.0.53–5.0.55?
|
||||
|
||||
**Находка.** Версия билда берётся **не из main.go**, а из профиля стенда:
|
||||
- 03_build_and_upload_provider.sh: `VERSION = $1 → иначе $PROVIDER_VERSION/$RELEASE_VERSION (из profile.env) → иначе grep из main.go (fallback)`.
|
||||
- profile.env: `PROVIDER_VERSION="5.0.55"`.
|
||||
- profile.env: `PROVIDER_VERSION="2.1.23"` (!).
|
||||
- main.go хранит `5.0.51` — это лишь **последний fallback**, который почти никогда не срабатывает.
|
||||
|
||||
**Вывод.** 5.0.53/54/55 — это **ручные билды с тестового стенда**: оператор вручную bump-ит `PROVIDER_VERSION` в `profiles/test/profile.env` и запускает `03_build...sh`. main.go при этом не обновляется (README прямо просит обновлять, но на практике это не делается — отсюда рассинхрон). Важно: **prod-профиль застрял на 2.1.23**, а test ушёл на 5.0.55 — это две независимые линейки версий. Мой прошлый анализ (3006_0.md) цитировал версии из кода (5.0.51/5.0.52), а регистр отдаёт версию из profile.env (5.0.55) — поэтому они не совпадают.
|
||||
|
||||
**Рекомендация.** Сделать profile.env единственным источником версии и автоматически прописывать её в main.go при сборке (или вычислять main.go-версию из env через ldflags `-X`). Сейчас 3 источника версии (main.go, test, prod) живут врозь.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: Полный аудит фильтрации `isDeleted`
|
||||
|
||||
**Находка.** Все функции, делающие GET к `/instances`:
|
||||
|
||||
| Функция | Файл | `isDeleted=false` в URL | Проверка в коде | Статус |
|
||||
|---|---|---|---|---|
|
||||
| `FindInstanceByDisplayName` | client.go | ✅ (search) / ❌ (fallback) | ✅ `isInstanceDeleted()` | OK |
|
||||
| `ListRefServiceInstances` | refsvc_resolve.go | ✅ | ✅ `IsDeleted` + только running | OK |
|
||||
| `findInstanceUidByDisplayNameRefSvc` | client.go | ❌ | ✅ `if item.IsDeleted {continue}` + `status=="deleted"` | **OK (исправлен!)** |
|
||||
| `findInstanceDisplayNameByUidRefSvc` | client.go | ❌ | ❌ нет | приемлемо |
|
||||
| `getInstanceDisplayNameByUidRefSvc` | client.go | — (GET по UID) | — | n/a |
|
||||
| `GetInstanceState/Raw/Details` | client.go / instance_outputs.go | — (GET по UID) | ✅/частично | n/a |
|
||||
|
||||
**Вывод.**
|
||||
- **`findInstanceUidByDisplayNameRefSvc` УЖЕ исправлен** — он пропускает deleted в коде (строки client.go) и предпочитает running>suspended. Замечание в 24_ai_analysis_pipeline_architecture_2026_06_30.md («НЕ ИСПРАВЛЕН?») **устарело** — баг класса #23 здесь закрыт. Единственный недочёт — нет `isDeleted=false` в URL (лишний трафик, но не баг корректности).
|
||||
- **`findInstanceDisplayNameByUidRefSvc` (1058)** — единственная функция БЕЗ фильтра deleted ни в URL, ни в коде. Но она ищет по **точному `instanceUid`** (уникальному) и возвращает displayName — это обратный маппинг для чтения state, не выбор «того/не того» инстанса. Класс багов #23 здесь не применим. Риск минимальный: вернёт имя deleted-инстанса, если в state остался его UID.
|
||||
|
||||
**Рекомендация.** Косметика: добавить `&isDeleted=false` в URL `findInstanceUidByDisplayNameRefSvc` (1057) и `findInstanceDisplayNameByUidRefSvc` (1060) для экономии трафика. Корректность уже обеспечена. Обновить вывод в файле 24 (он сеет ложную тревогу).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: Gitea 413 (`client_max_body_size`)
|
||||
|
||||
**Находка.**
|
||||
- charts — **пустая** (list_dir: folder empty).
|
||||
- Манифесты в репо есть только для dashboard: ingress.yaml — ingress для `terra.k8c.ru/dashboard`, аннотаций `client_max_body_size`/`proxy-body-size` нет, и это **не Gitea**.
|
||||
- `gitea-naeel.giteak8s.services.ngcloud.ru` упоминается только как `git_path` в resources.tf и закомментированно в luceUNDnode.tf. Сам Gitea — это **управляемый сервис Nubes** (см. svcs.json: «Gitea», «Комплексная услуга по созданию gitea», service_id 99/114), развёрнутый в кластере `giteak8s`, а не из этого репо.
|
||||
|
||||
**Вывод.** **Конфигурация nginx Gitea в этом репозитории отсутствует.** `client_max_body_size=1MB` задаётся на ingress управляемого Gitea в кластере `giteak8s.services.ngcloud.ru` — **внешняя конфигурация**, недоступная для правки из этого репо.
|
||||
|
||||
**Рекомендация.** Лимит правится за пределами репо — на ingress Gitea-инстанса: аннотация `nginx.ingress.kubernetes.io/proxy-body-size: "0"` (или, например, `512m`). Это требует доступа к namespace Gitea в кластере giteak8s. Из репо проблему не решить. *(Требует данных кластера — отмечаю честно.)*
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: Полный список suspend/resume-сервисов
|
||||
|
||||
**Находка.** YAML-спеки в devops/profiles/prod/generated/resources_yaml/ (43 файла, test идентичен), встроены через `//go:embed *` в embed.go.
|
||||
|
||||
**26 сервисов с suspend И resume** (service_id): `1 dummy, 2 template, 12 s3, 19 vc_org, 20 vc_org_saas, 21 vc_vdc, 23 vc_vm, 26 vapp, 27 vc_vm_v2, 28 vc_vm_v3, 50 nextcloud, 89 flask, 90 postgres, 92 mongodb, 93 rabbitmq, 94 lucee, 95 nodejs, 96 pgadmin, 98 http, 99 gitea, 115 mariadb, 116 kafka, 119 akhq, 120 clickhouse, 149 valo_tenant, 150 k8s_shturval`.
|
||||
|
||||
**17 сервисов без suspend/resume**: `13 s3bucket, 22 vc_nsxt, 25 vcexternalip, 29 vc_vdc_group, 32 vmpostgre, 81 superset, 82 harbor, 88 ziti, 91 redis, 97 nodered, 100 openwhisk, 110 dnszone, 111 dnsrecord, 112 tenant, 113 vc_complex, 114 gitea_complex, 117 nifi`.
|
||||
|
||||
**Вывод.**
|
||||
- **Асимметрии нет**: suspend и resume всегда идут парой (26/26). Сервисов «suspend без resume» — **0**.
|
||||
- `suspend_on_destroy_default` **идеально совпадает** с правилом `hasSuspend → true`: 26 suspend-сервисов = `true`, 17 = `false`. Расхождений нет.
|
||||
- Логика разделения здравая: stateless (DNS, external IP, tenant) — без suspend; stateful (БД, приложения, VM) — с suspend.
|
||||
|
||||
**Рекомендация.** По этому пункту всё чисто. Стоит лишь добавить в генератор `validateSpec()`-проверку «если есть suspend, обязан быть resume» как защиту на будущее (сейчас инвариант соблюдён случайно — генератор его не enforce-ит).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: Terraform Operator — состояние и health
|
||||
|
||||
**Находка — ключевая.** Директорий `operator/`, `registry-server-build/`, корневого `k8s/` **НЕТ в этом checkout** (file_search: «No files found»), хотя они активно упоминаются в README.md, REPO_CONTENTS.md, build-registry-image.yml (`operator/build/Dockerfile.registry`) и deploy-dev.sh (`k8s/overlays/dev`).
|
||||
|
||||
Что есть в репо — описание в 00_system_mechanics.md:
|
||||
- 3 компонента: **Operator** (watch CRD → spawn Job), **Registry API** (Discovery protocol over S3), **Builder Job** (`golang:1.24-alpine`: clone→build→upload S3). Это совпадает с тремя deployment'ами, что вы видели.
|
||||
- **S3**: бакет артефактов `terraform-providers`, схема пути `{hostname}/{namespace}/{provider}/{version}/{file}`, `REGISTRY_HOSTNAME=terra.k8c.ru`. Эндпойнт `s3.msk-1.ngcloud.ru` (upload_provider_s3.py, REPO_CONTENTS.md).
|
||||
- **GPG в кластере — ФЕЙКОВЫЙ**: 00_system_mechanics.md — «GPG Signing: Сейчас фейковое (создаётся пустой `.sig`)». Реальная подпись private_key.asc используется только в **локальном** `03_build...sh`, а не в operator-Job.
|
||||
- **Перезапуск оператора — штатная операция**: в cheat-sheet прямо есть `kubectl rollout restart deploy/terraform-operator -n terra`.
|
||||
|
||||
**Выводы по вопросам:**
|
||||
1. **Перезапуск 2 дня назад** — скорее всего ручной `rollout restart` (документированная команда подхвата изменений `manifests/03-build-script.yaml`), а не краш. Подтвердить можно только по `kubectl describe pod`/`--previous` логам в кластере. *(Требует кластера.)*
|
||||
2. **Трёх deployment'ов достаточно** для registry+CI (operator+registry-server+builder-job). Отдельного docs-server нет и не нужен: доки — статика в S3 (`terraform-registry/docs/...`), publish-docs.sh. cloud-dashboard — отдельный UI, не часть registry.
|
||||
3. **GPG-ключи**: в кластере подпись фейковая (tech debt из system_mechanics); реальный ключ только локально в secrets. **S3-доступ** оператора — через ENV `S3_*`/`REGISTRY_HOSTNAME` в Deployment (должны быть в Secret `terraform-operator`).
|
||||
4. **S3-конфиг бакета в репо** — только переменные и схема путей; манифест Secret/Deployment отсутствует (он в недостающей папке `operator/`/`k8s/`).
|
||||
|
||||
**Рекомендация.** Критично: **внедрить реальную GPG-подпись в operator-Job** (смонтировать private_key.asc как K8s Secret) — сейчас артефакты из кластера подписаны пустышкой, Terraform может ругаться `authentication signature from unknown issuer`. Также — вернуть `operator/`+`k8s/` в этот репо или явно задокументировать, что они в отдельном репозитории (сейчас CI ссылается на отсутствующие пути).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6: Тестовые стенды — что реально используется
|
||||
|
||||
**Находка.** Все `.tf` используют **только Universal** (`source = "terra.k8c.ru/nubes/nubes"`), Legacy (`registry.terraform.io/nubes/nubes`) **не используется нигде**.
|
||||
|
||||
| Стенд | Version | Endpoint | Активные ресурсы |
|
||||
|---|---|---|---|
|
||||
| TEST_STAND/LUCEE | 2.0.6 | test | postgres, lucee |
|
||||
| TEST_STAND/MARIA_DB | 2.0.8 | test | mariadb, lucee, flask |
|
||||
| TEST_STAND/POSTGRES | **5.0.52** | test | postgres + user + database |
|
||||
| TEST_STAND/S3_EVENT_POC | 2.1.23 | — | s3bucket ×2 |
|
||||
| PROD_STAND/PG1 | 2.1.26 | prod | postgres, lucee, nodejs, s3bucket |
|
||||
| PROD_STAND/POSTGRES | 2.1.12 | prod | postgres, lucee, nodejs |
|
||||
| PROD_STAND/RABBIT | 5.0.19 | **test ⚠️** | rabbitmq |
|
||||
| RABBIT/ (корень) | 2.1.10 | prod | rabbitmq, lucee, nodejs, s3bucket |
|
||||
|
||||
**Выводы:**
|
||||
1. Все стенды активны (с реальными ресурсами); часть компонентов закомментирована (VM в PG1, flask в PROD/POSTGRES, http-worker в RABBIT).
|
||||
2. **Сильный разброс версий**: test — 2.0.6…5.0.52, prod — 2.1.10…2.1.26. Каждый стенд пинит свою версию.
|
||||
3. **TEST/POSTGRES не дублирует PROD/POSTGRES**: разные версии (5.0.52 vs 2.1.12), имена (`pg4tf033` vs `pg-tst0`), TEST модульный (только БД+user+database с `var.realm`), PROD связный (БД+lucee+nodejs, хардкод realm). Это разные конфигурации, не дубль.
|
||||
4. **Аномалия**: PROD_STAND/RABBIT/main.tf указывает на **test-endpoint** (`deck-api-test.ngcloud.ru`), хотя лежит в PROD_STAND — вероятно ошибка/недо-миграция.
|
||||
|
||||
**Рекомендация.** Привести версии стендов к двум канонам (одна test, одна prod), исправить endpoint в RABBIT, удалить мёртвые закомментированные `.tf` или вынести в `examples/`.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 7: auth.k8s.ngcloud.ru / Keycloak / shturval
|
||||
|
||||
**Находка.** Поиск `auth.k8s.ngcloud.ru`, `shturval`, `OIDC`, `kubeconfig` по репо: совпадения **только в самом файле вопросов** 3006_1_questions.md. В коде/конфигах — ничего.
|
||||
- HAR `keycloak.nubes.ru.har` существует локально, но **gitignored** (`HAR/*.har` в .gitignore) — и это Keycloak **`keycloak.nubes.ru`** (для управляемых сервисов Nubes), а **не** `auth.k8s.ngcloud.ru`.
|
||||
- Инструкции по доступу — только HOW_TO_SSH.md (SSH, не kubeconfig/OIDC).
|
||||
- `kubectl`-команды в docs (deploy-dev.sh, ops/*.md) предполагают **готовый** kubeconfig, способ его получения не описан.
|
||||
|
||||
**Вывод.** **Конфигурации OIDC кластера, скриптов обновления токена и инструкций по kubeconfig в репо НЕТ.** `auth.k8s.ngcloud.ru` (realm `shturval`) — внешняя система аутентификации кластера, никак не отражённая в репозитории. HAR относится к другому Keycloak (сервисный, `keycloak.nubes.ru`).
|
||||
|
||||
**Рекомендация.** Если доступ к кластеру через `auth.k8s.ngcloud.ru` нужен регулярно — добавить в secrets (gitignored) инструкцию/скрипт `kubelogin`/OIDC token refresh, по аналогии с `HOW_TO_SSH.md`. Сейчас это «племенное знание» вне репо. *(Детали OIDC-эндпойнта — за пределами репо.)*
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 8: Orphan PVC `storage-check-*` и PostgreSQL 0/0
|
||||
|
||||
**Находка.** Поиск `storage-check` по всему репо: совпадения **только в файле вопросов**. Ни одного `.tf`, манифеста или упоминания в docs/HISTORY, создающего `storage-check-local`/`storage-check-vcd`. Никаких упоминаний проблемных PostgreSQL-тенантов `509145c3`/`dc5db45d`.
|
||||
|
||||
**Вывод.**
|
||||
- `storage-check-*` PVC **не создаются Terraform-конфигами из этого репо**. Имена (`storage-check-local`/`storage-check-vcd` = local/vCD storage classes) характерны для **тестов доступности StorageClass** — это типичные probe-PVC, которые создаёт **облачная платформа/CSI или сам terraform-operator-Job** при проверке хранилища, а не пользовательский TF. Подтвердить, что их делает именно operator, по репо нельзя (код оператора отсутствует в checkout, см. Q5).
|
||||
- PostgreSQL `0/0 28 дней` с висящими PVC/Service — это **suspended/недо-удалённые инстансы**: StatefulSet масштабирован в 0 (suspend), но PVC и Service остаются (платформа не чистит при suspend). Это согласуется с семантикой `suspend_on_destroy` (Q4) и багом 20_destroy_detach_semantics: `state_only`/suspend оставляет ресурсы в облаке.
|
||||
|
||||
**Рекомендация.** Источник `storage-check-*` и orphan-StatefulSet'ов нужно искать **в кластере**: `kubectl get pvc storage-check-local -o yaml` → смотреть `ownerReferences`/`annotations` (кто создатель). Если owner — `terraform-operator` SA или CSI healthcheck, чистить через него. Из репозитория эти ресурсы не управляются. *(Требует доступа к кластеру.)*
|
||||
|
||||
---
|
||||
|
||||
## Сводка
|
||||
|
||||
| # | Вопрос | Ответ из репо? | Главный вывод |
|
||||
|---|---|---|---|
|
||||
| 1 | Версии 53-55 | ✅ Полностью | Из `profiles/*/profile.env`, ручной bump; main.go — fallback. prod=2.1.23, test=5.0.55 |
|
||||
| 2 | Аудит isDeleted | ✅ Полностью | Все критичные функции фильтруют; `findInstanceUidByDisplayNameRefSvc` **уже исправлен** (файл 24 врёт) |
|
||||
| 3 | Gitea 413 | ⚠️ Частично | Конфига nginx Gitea в репо нет — внешняя config кластера giteak8s |
|
||||
| 4 | Suspend-сервисы | ✅ Полностью | 26 suspend+resume, 0 асимметрий, default совпадает идеально |
|
||||
| 5 | Operator health | ⚠️ Частично | `operator/`+`k8s/` отсутствуют в checkout; GPG в кластере фейковый; рестарт = штатный redeploy |
|
||||
| 6 | Стенды | ✅ Полностью | Везде Universal, Legacy не используется; разброс версий; RABBIT prod→test endpoint |
|
||||
| 7 | auth.k8s | ❌ Нет в репо | Ни OIDC, ни kubeconfig-скриптов; HAR — другой Keycloak (nubes.ru) |
|
||||
| 8 | Orphan PVC | ❌ Нет в репо | `storage-check-*` не из TF; orphan PG = suspended-остатки; искать в кластере |
|
||||
|
||||
**Требуют данных вне репо** (честно): Q3 (ingress Gitea в giteak8s), Q5 (логи рестарта оператора, Secret S3/GPG), Q7 (OIDC-эндпойнт auth.k8s), Q8 (`ownerReferences` PVC в кластере).
|
||||
|
||||
Сохранить эти ответы в `HISTORY/OPUS/3006_1.md` или оставить в чате?
|
||||
|
||||
Создано задач: 6
|
||||
@@ -0,0 +1,158 @@
|
||||
Что сказать Opus — кратко
|
||||
Задача: глубокий анализ проекта, найти слабые места, предложить улучшения.
|
||||
|
||||
|
||||
# Трассировка чтения файлов Оpus'ом
|
||||
|
||||
<details><summary>Раскрыть лог чтения</summary>
|
||||
|
||||
docs/50_history/23_*.md (последние баги)
|
||||
Полный промпт: prompt_for_opus48.md
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Изучу проект. Начну с чтения ключевых файлов и параллельного исследования.
|
||||
|
||||
Searched for files matching `**/ARCHITECTURE.md`, 1 match
|
||||
|
||||
Searched for files matching `**/internal/**/*.go`, 20 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/prompt_for_opus48.md)
|
||||
|
||||
Прочитаю ключевые файлы и запущу параллельное исследование по разным областям.
|
||||
|
||||
Searched for files matching `**/universal_rebuild/internal/core/client.go`, 1 match
|
||||
|
||||
Searched for files matching `**/universal_rebuild/internal/resources_core/crud.go`, 1 match
|
||||
|
||||
Searched for files matching `**/universal_rebuild/tools/gen_v2/generate_resources_v2.go`, 1 match
|
||||
|
||||
Searched for files matching `**/docs/50_history/*.md`, 20 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/devops/ARCHITECTURE.md)
|
||||
|
||||
Прочитаю основные файлы ядра и генератора, параллельно запущу исследование истории багов.
|
||||
|
||||
История багов проекта
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#1-1), lines 1 to 450
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/24_ai_analysis_pipeline_architecture_2026_06_30.md#1-1), lines 1 to 200
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/23_vapp_uid_inconsistency_displayname_resolve_bug.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/22_adopt_ref_validation_duplicate_detection_5_0_50.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/21_plan_validation_ref_svc_filter_5_0_38.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/20_destroy_detach_semantics_5_0_8.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/16_universal_provider_create_only_params.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/13_universal_flow_param_normalization.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/06_postgres_update_and_immutable_params.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/05_postgres_implementation_and_polling_fixes.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/00_system_mechanics.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/01_initial_operator_setup.md#1-1), lines 1 to 100
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/02_registry_protocol_fix.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/03_domain_migration_and_hotfix.md#1-1), lines 1 to 80
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/04_vm_hang_fix_and_500_error.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/07_operation_based_polling_and_negative_testing.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/08_natural_language_infrastructure_ai.md#1-1), lines 1 to 80
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/09_s3_migration_and_branded_docs.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/10_postgres_modify_tests_and_external_ip.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/12_vm_resource_hardening.md#1-1), lines 1 to 80
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/11_postgres_import_implementation.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/15_universal_provider_lifecycle_tests.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/17_subresource_generation_rebuild_and_5_0_1_publish.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/18_docs_publish_pipeline_hardening_5_0_4.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/14_universal_provider_separate.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/19_subresource_update_id_fix_and_5_0_7.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/docs/50_history/02_tubulus_stabilization.md)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#450-450), lines 450 to 900
|
||||
|
||||
Read [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/chat-session-resources/2c7367dd-6d7f-48b4-84d0-25bbb06f7cfb/toolu_01Gk8LXuMrG8gDUMRXKhjs2J__vscode-1782817912242/content.txt)
|
||||
|
||||
Прочитаю crud.go (включая `adoptExistingInstanceOnCreate`) и начало генератора, а также тесты.
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud.go)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#1-1), lines 1 to 120
|
||||
|
||||
Изучу генератор, тесты, транспорт и состояние secrets в gitignore.
|
||||
|
||||
Генератор gen_v2 и тесты
|
||||
|
||||
Searched for files matching `**/.gitignore`, 6 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#120-120), lines 120 to 178
|
||||
|
||||
Searched for files matching `tf_provider` · `**/*_test.go`, 2 matches
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#1-1), lines 1 to 100
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud_test.go#1-1), lines 1 to 50
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go)
|
||||
|
||||
Searched for regex `func \(.*\) main\(\)|func.*Generate` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), no results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#100-100), lines 100 to 300
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud_test.go#50-50), lines 50 to 57
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go#46-46), lines 46 to 46
|
||||
|
||||
Searched for text `template.Must|var.*=|const.*=` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 8 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2)
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#300-300), lines 300 to 500
|
||||
|
||||
Searched for regex `func write|func.*Kind|if.*Kind|switch.*op\.Kind` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 7 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#500-500), lines 500 to 700
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#800-800), lines 800 to 900
|
||||
|
||||
Searched for regex `panic|error|yaml\.Unmarshal|unknown|default|fallback` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#850-850), lines 850 to 1200
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/resources_core/crud_test.go)
|
||||
|
||||
Searched for regex `` ^const.*Template = |^const.*= ` `` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 3 results
|
||||
|
||||
Searched for regex `^func Test` (`tf_provider` · `universal_rebuild/internal/resources_core/crud_test.go`), 3 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client_test.go)
|
||||
|
||||
Searched for regex ``^`$`` (`tf_provider` · `universal_rebuild/tools/gen_v2/generate_resources_v2.go`), 3 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#1420-1420), lines 1420 to 1430
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/tools/gen_v2/generate_resources_v2.go#1865-1865), lines 1865 to 1880
|
||||
|
||||
</details>
|
||||
@@ -0,0 +1,122 @@
|
||||
# Дополнительные вопросы к Opus 4.8
|
||||
|
||||
> **Контекст:** ты уже проанализировал проект (3006_0.md). Ниже — уточняющие вопросы на основе новых данных, полученных после твоего анализа. Всё в режиме Plan — только чтение файлов, никаких правок.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Расхождение версий — откуда 5.0.55?
|
||||
|
||||
Ты отметил что кодовая база содержит версии 5.0.51 (universal_rebuild/main.go) и 5.0.52 (main.go Legacy). Но **регистр terra.k8c.ru отдаёт 93 версии до 5.0.55**.
|
||||
|
||||
**Задача:** прочитай `devops/profiles/prod/profile.env`, `devops/profiles/test/profile.env`, `devops/03_build_and_upload_provider.sh`. Проверь — откуда брались версии 5.0.53, 5.0.54, 5.0.55? Это ручные билды? Или автоматические из CI? Где в коде хранится текущая версия universal-провайдера (кроме main.go)?
|
||||
|
||||
Связанный вопрос: `HISTORY/OPUS/3006_0.md` — твой анализ — какая версия в нём указана? Соответствует ли она версии на регистре?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: Полный аудит фильтрации isDeleted
|
||||
|
||||
Ты верно заметил что `FindInstanceByDisplayName` починен в 5.0.50, но **все ли** функции, обращающиеся к `/instances`, фильтруют deleted?
|
||||
|
||||
**Задача:** прочитай `universal_rebuild/internal/core/refsvc_resolve.go` и `universal_rebuild/internal/core/client.go`. Составь полный список ВСЕХ функций, которые делают GET-запросы к `/instances` (любым способом). Для каждой проверь:
|
||||
1. Есть ли `isDeleted=false` в URL
|
||||
2. Есть ли проверка `isDeleted` в коде после ответа
|
||||
3. Если нет ни того ни другого — это потенциальный баг класса #21/22/23
|
||||
|
||||
Особое внимание функции `findInstanceUidByDisplayNameRefSvc` — ты её упомянул в анализе. Проверь её сигнатуру и тело.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: Gitea 413 — проблема с пушем
|
||||
|
||||
Мы сегодня обнаружили что `git push` на `gitea-naeel.giteak8s.services.ngcloud.ru` падает с HTTP 413 (nginx `client_max_body_size` = 1 MB). Даже пакет в 2 MB режется.
|
||||
|
||||
**Задача:** проверь в репозитории:
|
||||
- `charts/` — есть ли там Helm-чарт для Gitea? Где конфигурируется nginx?
|
||||
- `k8s/` — есть ли манифесты Gitea с ingress/nginx аннотациями?
|
||||
- `.github/` — есть ли CI-пайплайны, которые деплоят Gitea?
|
||||
- `devops/ci/` — конфигурация CI
|
||||
|
||||
Можно ли увеличить `client_max_body_size` через существующие манифесты/чарты в репо? Или это внешняя конфигурация?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: Suspend-способные сервисы — полный список
|
||||
|
||||
Твой анализ упоминает что `hasSuspend` определяет `suspendOnDestroyDefault`. Но какие именно сервисы поддерживают suspend?
|
||||
|
||||
**Задача:** прочитай `universal_rebuild/resources_yaml/embed.go` и все YAML-файлы в `universal_rebuild/resources_yaml/` (или в `devops/profiles/prod/generated/resources_yaml/` если есть). Составь:
|
||||
|
||||
1. Полный список сервисов, у которых есть операция `kind: instance, action: suspend`
|
||||
2. Полный список сервисов, у которых есть операция `kind: instance, action: resume`
|
||||
3. Есть ли сервисы, где suspend есть, а resume нет (или наоборот)? Это баг?
|
||||
4. Для каждого suspend-способного сервиса проверь: совпадает ли `suspend_on_destroy_default` в YAML с тем что вычисляется в `service_spec_gen` (hasSuspend → true)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: Terraform Operator — состояние и health
|
||||
|
||||
Мы сегодня проверили кластер `iot-naeel` (через ВМ 5.172.178.213). В namespace `terra` три deployment'а:
|
||||
- `registry-server` — Running 81d
|
||||
- `terraform-operator` — Running 2d (перезапускался!)
|
||||
- `cloud-dashboard` — Running 74d
|
||||
|
||||
**Задача:** прочитай в репозитории:
|
||||
- `k8s/` — манифесты для registry-server и operator'а
|
||||
- `charts/` — Helm-чарты если есть
|
||||
- `operator/` — код оператора
|
||||
- `registry-server-build/` — код registry-сервера
|
||||
|
||||
Ответь:
|
||||
1. Почему `terraform-operator` перезапустился 2 дня назад (30 июня), а остальные 81 день? Он крашился?
|
||||
2. Достаточно ли трёх deployment'ов для полноценного registry? Не хватает ли docs-server'а (судя по `devops/04_build_and_publish_docs.sh`)?
|
||||
3. Где хранятся GPG-ключи для подписи в кластере? Как operator получает доступ к S3?
|
||||
4. Есть ли в репо конфигурация S3-бакета для хранения артефактов?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6: Тестовые стенды — что реально используется?
|
||||
|
||||
В репозитории есть:
|
||||
- `TEST_STAND/` — LUCEE, MARIA_DB, POSTGRES, S3_EVENT_FUNCTION_POC
|
||||
- `PROD_STAND/` — PG1, POSTGRES, RABBIT
|
||||
- `RABBIT/` — отдельный тест RabbitMQ
|
||||
|
||||
**Задача:** прочитай `*.tf` файлы в этих директориях и определи:
|
||||
1. Какие стенды реально используются (судя по датам файлов и содержимому)?
|
||||
2. Какой провайдер используется — Legacy (`registry.terraform.io/nubes/nubes`) или Universal (`terra.k8c.ru/nubes/nubes`)?
|
||||
3. Есть ли расхождения между стендами (разные версии провайдера, разные подходы)?
|
||||
4. `TEST_STAND/POSTGRES/` — не дублирует ли он `PROD_STAND/POSTGRES/`?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 7: auth.k8s.ngcloud.ru — как работает аутентификация?
|
||||
|
||||
Для доступа к кластеру мы используем токен от `auth.k8s.ngcloud.ru` (Keycloak realm `shturval`).
|
||||
|
||||
**Задача:** найди в репозитории любые упоминания `auth.k8s.ngcloud.ru`, `keycloak`, `shturval`. Есть ли:
|
||||
1. Конфигурация OIDC для кластера?
|
||||
2. Скрипты обновления токена?
|
||||
3. Инструкции по получению kubeconfig?
|
||||
4. Связанные HAR-файлы (`HAR/keycloak.nubes.ru.har`)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 8: Неиспользуемые PVC и orphan-ресурсы в кластере
|
||||
|
||||
Мы нашли в кластере `iot-naeel`:
|
||||
- `default/storage-check-local` (100Mi) — тестовый PVC без пода
|
||||
- `default/storage-check-vcd` (10Gi) — тестовый PVC без пода
|
||||
- `509145c3-.../postgresqlk8s` — statefulset 0/0 уже 28 дней, но сервисы и PVC висят
|
||||
- `dc5db45d-.../postgresqlk8s` — statefulset 0/0 уже 28 дней, но сервисы и PVC висят
|
||||
|
||||
**Задача:** проверь в репозитории:
|
||||
- Есть ли Terraform-конфиги, создававшие эти PVC (`storage-check-*`)?
|
||||
- Есть ли в `docs/` или `HISTORY/` упоминания о проблемах с PostgreSQL в этих тенантах?
|
||||
- Может ли `terraform-operator` быть причиной появления `storage-check-*` PVC (тестовые ресурсы оператора)?
|
||||
|
||||
---
|
||||
|
||||
## Формат ответа
|
||||
|
||||
На каждый вопрос: **находка → файлы (конкретные строки) → вывод → рекомендация**. Если вопрос нельзя решить только чтением файлов репозитория — так и напиши, что нужно посмотреть за пределами репо (на кластере, в API).
|
||||
@@ -0,0 +1,137 @@
|
||||
# Анализ генерационных скриптов — 30.06.2026 (сессия 2)
|
||||
|
||||
**Исполнитель:** Opus 4.8
|
||||
**Задача:** аудит 4 bash-скриптов, 6 Go-тулов, профилей, CI — как API-данные превращаются в YAML, Go-код и документацию
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Bash-скрипты — аудит 4 файлов
|
||||
|
||||
**Прочитал:** все 4 скрипта полностью.
|
||||
|
||||
| Аспект | 01_yamls | 02_docs_v2 | 03_build | 04_publish |
|
||||
|--------|---------|-----------|---------|-----------|
|
||||
| `set -euo pipefail` | ✅ | ✅ | ✅ | ✅ |
|
||||
| `trap` cleanup | ❌ | ✅ | ✅ | ✅ |
|
||||
| API error handling | 🔴 НЕТ | — | — | — |
|
||||
| User-Agent в urllib | 🔴 НЕТ | — | — | — |
|
||||
| Проверка зависимостей (`command -v`) | ❌ | ❌ | ❌ | ✅ |
|
||||
| Идемпотентность (`rm` старого) | ✅ | ✅ | ✅ | ✅ |
|
||||
| Lock-файлы (параллельный запуск) | ❌ | ❌ | ❌ | ❌ |
|
||||
| Профиль обязателен | ✅ | ✅ | ✅ | ❌ |
|
||||
|
||||
**Проблемы:**
|
||||
- 🔴 **01_generate_yamls.sh** — python `urllib` ставит только `Authorization`, без User-Agent → дефолтный `Python-urllib/3.x` → DDoS-Guard зарежет. **P0**
|
||||
- 🔴 **01_generate_yamls.sh** — нет обработки 500/битый JSON/пустой ответ → необработанный Python traceback вместо retry. **P0**
|
||||
- 🟠 Пустой `services_list.txt` не ловится — 01_generate_yamls.sh проверяет только существование файла → «успех» без генерации. **P1**
|
||||
- 🟠 Нет `command -v go/python3` ни в 01/02/03 → непонятная ошибка если инструмента нет. **P1**
|
||||
- 🟠 Нет lock-файлов нигде → два параллельных прогона на один `PROFILE_DIR` затрут друг друга. **P1**
|
||||
- 🟡 04_build_and_publish_docs.sh — профиль НЕ обязателен (в отличие от 01-03). **P2**
|
||||
|
||||
**Предложение:** добавить UA в urllib + retry-обёртку (3 попытки, backoff); guard на непустой список; `command -v` проверки; lock через `flock` на `PROFILE_DIR`.
|
||||
|
||||
**Хорошо:** 03_build_and_upload_provider.sh — сильная валидация полноты registry перед сборкой; 04_build_and_publish_docs.sh — лучший fallback (docker → venv → system mkdocs).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: service_spec_gen (API → YAML)
|
||||
|
||||
**Прочитал:** generate_service_spec.go полностью.
|
||||
|
||||
- **HTTP-клиент:** ✅ кастомный клиент с `Timeout: 30s`; ✅ User-Agent уже есть. 🔴 **Retry отсутствует** — любой 429/503/сетевой сбой → ошибка → panic в main. **P1**
|
||||
- **Парсинг API:** ✅ структуры полные — `cfsParam` маппит все 19 полей. Потери только в edge-case. Неожиданный формат → `json.Unmarshal` error → panic, без graceful degradation. **P2**
|
||||
- **classifyOperation:** покрыты `create/modify/delete/suspend/resume` → instance, `create_*/modify_*/delete_*` → subresource, остальное → action. 🟠 **Новые префиксы (`restart_*`, `pause_*`, `enable_*`) попадут в action**, а не subresource. **P1**
|
||||
- **Constraints:** ✅ всё сохраняется — `maxLength/minLength/regex/uniqueScope/dependsOn`. 🟡 `normalizeValueList` делает `fmt.Sprintf("%v")` — для вложенных объектов теряет структуру. **P2**
|
||||
- **YAML:** ✅ `yaml.Marshal` с `omitempty`, вложенные структуры корректны. Имя файла через `normalizeIdentifier` — Unicode схлопывается в `_`. **P2**
|
||||
|
||||
**Предложение:** добавить retry с backoff в `getViaProxy`; расширить `classifyOperation` карту префиксов или сделать её конфигурируемой.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: gen_v2 (YAML → Go)
|
||||
|
||||
**Прочитал:** generate_resources_v2.go.
|
||||
|
||||
- 🟠 **`format.Source` fallback:** при ошибке форматирования пишет `buf.Bytes()` (неотформатированный, возможно невалидный Go) **без warning**. Ошибка всплывёт только на компиляции. **P1**
|
||||
- **computeCreateOnly:** «param в create но не в modify → ForceNew». При отсутствии modify-операции ВСЕ create-параметры → CreateOnly → корректно для immutable subresource. ✅
|
||||
- 🟠 **Subresource identity:** `DeleteParams` помечаются ForceNew, но `computeCreateOnly` их не учитывает → если delete-параметр не входит в create, возможен некорректный план. **P1**
|
||||
- 🟡 **JSON:** `data_type: json → IsJson + JsonNormalize` работает для object/array, но нет валидации что значение реально валидный JSON. **P2**
|
||||
|
||||
**Предложение:** не молчать на ошибке `format.Source` — логировать warning (или fail при невалидном Go); проверить пересечение DeleteParams ∩ CreateParams.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: docs_template_gen_v2
|
||||
|
||||
**Прочитал:** docs_template_gen_v2/main.go.
|
||||
|
||||
- **Генерируется 7 MD-файлов на ресурс:** `.md`, `_example.md`, `_params_create.md`, `_params_modify.md`, `_outputs.md`, `_ops.md`, `_params.md`. Формат — Markdown для mkdocs. ✅
|
||||
- **Покрытие:** ✅ subresources и actions, есть index. Все YAML-поля используются. ✅
|
||||
- **HCL-примеры:** из create-операции, required без default → в HCL, опциональные закомментированы. ✅
|
||||
- 🟠 **Ошибки:** битый YAML → panic, пустые операции — молча пропускаются. **P1**
|
||||
- 🟡 **`-exclude clickhouse`** — дефолт флага + дублирован в 02_..._v2.sh. Хардкод, без объяснения причины. **P2** — вынести в config/комментарий.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: service_ops_gen / service_params_gen — нужны?
|
||||
|
||||
**Прочитал:** оба тула + grep по devops.
|
||||
|
||||
- ❌ **Нигде не вызываются** в bash-скриптах — мёртвый код старой v1-архитектуры.
|
||||
- ✅ **Заменены `service_spec_gen`** (единый YAML на сервис).
|
||||
- **`tools/gen/`** — только placeholder-README, функциональность не реализована.
|
||||
|
||||
**Предложение:** удалить `service_ops_gen`, `service_params_gen`, `tools/gen/` — **но только по явной команде**. **P2**
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6: embed.go и resources_yaml
|
||||
|
||||
- `//go:embed` встраивает все `*.yaml` в бинарник. **Зачем:** провайдер при старте один раз читает YAML и строит lookup-карты, без зависимости от внешних файлов.
|
||||
- **Используется в 4 местах** resources_core: params_mapping.go, params_validation_mapping.go, required_params.go, params_ref_mapping.go.
|
||||
- **Почему генерируется скриптом:** шаблонный файл без логики, генерируется если отсутствует.
|
||||
- ⚠️ **`devops/profiles/*/generated/resources_yaml/embed.go` — не существует** (per-profile embed не реализован).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 7: Профили и CI
|
||||
|
||||
- **Профили test/prod/dev** идентичны по структуре, различаются `profile.env` (API endpoint, token-файл, версии).
|
||||
- **CI = GitLab CI** (pipeline.yaml), 4 стадии: test → build → sign → publish. Запуск `only: tags`.
|
||||
- ❌ **Автогенерации YAML в CI НЕТ** — YAML генерируются локально/вручную и коммитятся; CI только собирает и публикует. **P1**
|
||||
- **Деплой:** Docker-образ `registry-operator:${TAG}` пушится в Harbor → управляется K8s-оператором.
|
||||
- **Скрипты 10-13:** `13 clean` → `12 latest` → `11 alias` → `10 stability` (N раз — тест стабильности).
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 8: Практическая проверка покрытия
|
||||
|
||||
- ✅ **Покрытие 100%:** все 41 активный сервис имеют YAML; исключённый `24 vc_nat` (DEPRECATED) YAML не имеет. Лишних YAML нет.
|
||||
- 🔴 **Ключевая проблема — свежесть содержимого.** 90_postgres.yaml содержит 11 операций, но **`backup`/`reconcile` НЕТ** — API их уже отдаёт, а YAML устарел.
|
||||
|
||||
**Вывод:** проверка «список vs файлы» зелёная, но **не ловит устаревшее содержимое**.
|
||||
|
||||
**Предложение (P1):** добавить в CI шаг diff — перегенерировать YAML и сравнить с закоммиченными; при расхождении — фейлить.
|
||||
|
||||
---
|
||||
|
||||
## Сводка приоритетов
|
||||
|
||||
### P0 (блокеры)
|
||||
1. urllib без User-Agent в 01_generate_yamls.sh → DDoS-Guard
|
||||
2. urllib без обработки ошибок API (500/битый JSON/пусто)
|
||||
|
||||
### P1 (важно)
|
||||
- Нет retry в generate_service_spec.go
|
||||
- `classifyOperation` не знает новые префиксы
|
||||
- `format.Source` молча пишет невалидный Go в gen_v2
|
||||
- DeleteParams ∩ CreateParams в subresource identity
|
||||
- Нет CI-диффа «API vs закоммиченный YAML»
|
||||
- Нет lock-файлов, нет `command -v`, пустой список не ловится
|
||||
|
||||
### P2 (доработки)
|
||||
- Удалить legacy-тулы (по команде)
|
||||
- `-exclude clickhouse` в config
|
||||
- JSON-валидация
|
||||
- Unicode в именах
|
||||
- panic на битом YAML в docs-gen
|
||||
@@ -0,0 +1,174 @@
|
||||
# Вопросы к Opus 4.8: Анализ генерационных скриптов и промежуточного кода
|
||||
|
||||
> **Контекст:** Ты уже проанализировал ПРОВАЙДЕР (client.go, crud.go, gen_v2). Теперь нужен анализ СКРИПТОВ ГЕНЕРАЦИИ — как API-данные превращаются в YAML, Go-код и документацию.
|
||||
> **Режим:** Plan (только чтение). Все пути относительно `/home/naeel/tf_provider/`.
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 1: Bash-скрипты — полный аудит
|
||||
|
||||
**Файлы:**
|
||||
- `devops/01_generate_yamls.sh` — API → YAML
|
||||
- `devops/02_generate_resources_and_docs_v2.sh` — YAML → Go + Docs
|
||||
- `devops/03_build_and_upload_provider.sh` — сборка + S3
|
||||
- `devops/04_build_and_publish_docs.sh` — публикация документации
|
||||
|
||||
**Задача:** прочитай ВСЕ 4 скрипта полностью. Для каждого проверь:
|
||||
|
||||
1. **Обработка ошибок:**
|
||||
- Что происходит при `set -e` — на каком шаге скрипт упадёт?
|
||||
- Есть ли `trap` для очистки временных файлов?
|
||||
- Что если API вернул 500? Пустой ответ? Битый JSON?
|
||||
- Что если `services_list.txt` пустой?
|
||||
|
||||
2. **Зависимости:**
|
||||
- Какие внешние инструменты нужны (go, python3, mc, gpg, mkdocs)?
|
||||
- Проверяются ли они перед запуском?
|
||||
- Что если инструмент отсутствует — понятная ли ошибка?
|
||||
|
||||
3. **Идемпотентность:**
|
||||
- Можно ли перезапустить скрипт при обрыве?
|
||||
- Чистит ли он предыдущий мусор перед генерацией?
|
||||
- Что с параллельным запуском двух экземпляров?
|
||||
|
||||
4. **User-Agent / DDoS-Guard:**
|
||||
- `01_generate_yamls.sh` вызывает python `urllib` для получения имён сервисов (строка ~170). Есть ли там User-Agent?
|
||||
- Как Go-бинари (`service_spec_gen`) получают токен? Через env?
|
||||
|
||||
5. **Профили (test/prod/dev):**
|
||||
- Как скрипты определяют какой профиль использовать?
|
||||
- Что если `--profile` не указан? Есть ли защита от запуска без профиля?
|
||||
- Где хранятся сгенерированные артефакты для каждого профиля?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 2: service_spec_gen — API → YAML (ключевой генератор)
|
||||
|
||||
**Файл:** `universal_rebuild/tools/service_spec_gen/generate_service_spec.go`
|
||||
|
||||
Мы уже добавили User-Agent, но проверь остальное:
|
||||
|
||||
1. **HTTP-клиент:**
|
||||
- Есть ли retry при ошибках API (429, 503, network)?
|
||||
- Какой таймаут? `http.Client{Timeout: 30s}` — достаточно ли?
|
||||
- Используется ли `http.DefaultClient` или свой?
|
||||
|
||||
2. **Парсинг ответа API:**
|
||||
- Структуры `serviceInfo`, `serviceOperationInfo`, `operationInfo` — все ли поля API маппятся?
|
||||
- Есть ли поля которые API возвращает, но генератор игнорирует (потеря данных)?
|
||||
- Что если API вернёт неожиданный формат — будет `panic` или ошибка?
|
||||
|
||||
3. **Классификация операций (`classifyOperation`):**
|
||||
- Как определяется `kind` (instance/subresource/action)?
|
||||
- Правило: `create/modify/delete/suspend/resume` → instance, `create_*/modify_*/delete_*` → subresource, остальное → action. Все ли кейсы покрыты?
|
||||
- Что с новыми префиксами, которые могут появиться (например, `restart_*`)?
|
||||
|
||||
4. **Сбор параметров:**
|
||||
- Поля `ParamSpec` — все ли constraints из API сохраняются (`maxLength`, `minLength`, `regex`, `uniqueScope`, `dependsOn`)?
|
||||
- `normalizeDefault` — правильно ли обрабатывает null/числа/строки?
|
||||
- `normalizeValueList` — преобразует `[]interface{}` в `[]string`. Что если элемент не строка?
|
||||
|
||||
5. **Выходной YAML:**
|
||||
- `yaml.Marshal` — сохраняет ли все поля корректно (omitempty, вложенные структуры)?
|
||||
- Имя файла: `{id}_{name}.yaml`. Что если имя содержит спецсимволы?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 3: gen_v2 — YAML → Go (уже проанализирован, но проверь детали)
|
||||
|
||||
**Файл:** `universal_rebuild/tools/gen_v2/generate_resources_v2.go`
|
||||
|
||||
Мы уже добавили `validateSpec()`. Проверь:
|
||||
|
||||
1. **Обработка ошибок генерации:**
|
||||
- `format.Source` — что при ошибке форматирования? Пишет битый код или паникует?
|
||||
- `writeInstanceResource` / `writeSubresource` / `writeActionResource` — обрабатывают ли ошибки `template.Execute`?
|
||||
|
||||
2. **Вычисление CreateOnly:**
|
||||
- `computeCreateOnly` = параметр в create но не в modify → ForceNew. Что если modify-операция существует но не содержит этот параметр потому что он неизменяем? Это корректно?
|
||||
|
||||
3. **Subresource identity:**
|
||||
- Если нет modify → все параметры ForceNew. Если есть modify → как определяется identity?
|
||||
- `buildSubresourceForceNewCodes` — правильная ли логика?
|
||||
|
||||
4. **JSON-параметры:**
|
||||
- `data_type: json` → `IsJson=true` + `JsonNormalize` plan-modifier. Достаточно ли этого для всех JSON-типов (map, array, object)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 4: docs_template_gen_v2 — генерация документации
|
||||
|
||||
**Файлы:** `universal_rebuild/tools/docs_template_gen_v2/main.go`
|
||||
|
||||
1. **Что именно генерируется?**
|
||||
- Какие страницы/файлы создаются?
|
||||
- Формат выхода — Markdown для mkdocs?
|
||||
- Все ли поля из YAML используются (MAN, params, outputs, lifecycle)?
|
||||
|
||||
2. **Покрытие:**
|
||||
- Генерируется ли документация для subresource и action ресурсов?
|
||||
- Есть ли index/overview страницы?
|
||||
- Примеры (HCL examples) — откуда берутся?
|
||||
|
||||
3. **Ошибки:**
|
||||
- Что при битом YAML? Пустых операциях?
|
||||
- Флаг `-exclude clickhouse` — почему хардкод?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 5: service_ops_gen и service_params_gen — нужны ли они?
|
||||
|
||||
**Файлы:**
|
||||
- `universal_rebuild/tools/service_ops_gen/generate_service_ops.go`
|
||||
- `universal_rebuild/tools/service_params_gen/generate_service_params.go`
|
||||
|
||||
Эти тулы выглядят как **устаревшие** (в коде написано APPEND-ONLY). `service_spec_gen` заменил их (генерирует унифицированный YAML вместо отдельных ops/params YAML).
|
||||
|
||||
1. Используются ли они где-то в скриптах?
|
||||
2. Можно ли их удалить?
|
||||
3. Есть ли `gen/` (v1) — он ещё нужен?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 6: embed.go и resources_yaml
|
||||
|
||||
**Файлы:**
|
||||
- `universal_rebuild/resources_yaml/embed.go`
|
||||
- `devops/profiles/*/generated/resources_yaml/embed.go`
|
||||
|
||||
1. `embed.go` использует `//go:embed *.yaml` — встраивает YAML в бинарник. Зачем?
|
||||
2. Где в коде провайдера используется `resources_yaml.Files`?
|
||||
3. Почему `embed.go` создаётся bash-скриптом, а не лежит в репо?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 7: Профили и CI
|
||||
|
||||
**Файлы:**
|
||||
- `devops/profiles/test/`, `prod/`, `dev/`
|
||||
- `devops/ci/pipeline.yaml`
|
||||
- `.github/workflows/`
|
||||
|
||||
1. Как CI-пайплайн связан с профилями?
|
||||
2. Есть ли автоматический прогон генерации в CI?
|
||||
3. Как происходит деплой — через operator в K8s или вручную?
|
||||
4. `devops/10_yaml_stability_run.sh` — зачем несколько скриптов стабильности (10-13)?
|
||||
|
||||
---
|
||||
|
||||
## Вопрос 8: Практическая проверка
|
||||
|
||||
Мы сегодня попробовали сгенерировать YAML для postgres (service_id=90) и обнаружили что API добавил 2 новые операции (`backup`, `reconcile`) которых нет в старом сгенерированном коде.
|
||||
|
||||
**Задача:** проверь ВСЕ сервисы из `services_list.txt` на наличие YAML в `devops/profiles/test/generated/resources_yaml/`. Какие сервисы есть в списке но отсутствуют в сгенерированных YAML? Какие YAML есть но сервис исключён из списка?
|
||||
|
||||
---
|
||||
|
||||
## Формат ответа
|
||||
|
||||
На каждый вопрос: **что прочитал → проблема → предложение → приоритет (P0/P1/P2)**.
|
||||
|
||||
Особое внимание:
|
||||
- Bash-скриптам (01_generate_yamls.sh — самый критичный)
|
||||
- HTTP-клиентам в Go-генераторах (UA, retry, таймауты)
|
||||
- Целостности данных (не теряем ли поля API при генерации YAML)
|
||||
@@ -0,0 +1,499 @@
|
||||
Вот, отправь Соннету:
|
||||
|
||||
---
|
||||
|
||||
**Контекст**: Пишем Terraform Provider для Nubes Cloud (ColdFusion API). Провайдер на Go, версия 5.0.66, опубликован в registry `terra.k8c.ru/nubes-test/nubes`.
|
||||
|
||||
**Суть проблемы**: `terraform apply` для создания S3-бакета падает с ошибкой. Прошли путь от 403 до EOF.
|
||||
|
||||
**Хронология**:
|
||||
1. `HTTP 403` — DDoS-Guard блокирует Go `crypto/tls` (JA3 fingerprint). curl с того же сервера работает.
|
||||
2. Поменяли URL с `index.cfm/instances` на `index.cfm?endpoint=/instances` — 403 остался.
|
||||
3. Добавили `utls.HelloFirefox_120` — **403 ушёл**, но стал `EOF` (сервер закрывает соединение без ответа).
|
||||
4. Убрали utls, оставили `InsecureSkipVerify: true` — `EOF`.
|
||||
5. Вернули REST-путь (`index.cfm/instances`) + `InsecureSkipVerify: true` — `EOF`.
|
||||
|
||||
**Что работает**: curl с токеном напрямую с того же сервера:
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $TOKEN" -H "User-Agent: Mozilla/5.0" \
|
||||
"https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances&page=1&size=100"
|
||||
# → JSON, 200 OK
|
||||
```
|
||||
|
||||
**Код**: `doRequest()` в client.go (строка 831). Использует `http.DefaultTransport.Clone()` с `TLSClientConfig{InsecureSkipVerify: true, MinVersion: TLS1.2}`.
|
||||
|
||||
**Вопросы**:
|
||||
1. Почему Go HTTP клиент (с идентичными заголовками) получает EOF там, где curl получает JSON?
|
||||
2. Может ли ColdFusion-сервер требовать определённый порядок заголовков или HTTP-метод (POST вместо GET)?
|
||||
3. Как заставить Go `http.Client` отправлять точно такие же запросы как curl? Может, через `net/http/httputil` дампить запрос?
|
||||
|
||||
**Файлы**:
|
||||
- client.go
|
||||
- provider.go
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Searched for files matching `**/client.go`, 2 matches
|
||||
|
||||
Read memory [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/repo/tf_provider_inventory.md)
|
||||
|
||||
Searched for text `doRequest|DoRequest` (`**/universal_rebuild/internal/core/client.go`), 20 results
|
||||
|
||||
Searched for text `doRequest|DoRequest` (`**/internal/core/client.go`), 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#800-800), lines 800 to 900
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/internal/core/client.go#177-177), lines 177 to 260
|
||||
|
||||
Searched for text `HttpClient|NewClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport` (`**/universal_rebuild/internal/core/client.go`), 5 results
|
||||
|
||||
Searched for text `HttpClient|NewClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport` (`**/internal/core/client.go`), 7 results
|
||||
|
||||
Searched for text `HttpClient|NewUniversalClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport|http.Client{` (`**/universal_rebuild/internal/provider/**`), 11 results
|
||||
|
||||
Searched for text `HttpClient|NewUniversalClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport|http.Client{` (`**/internal/provider/**`), 20 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#105-105), lines 105 to 165
|
||||
|
||||
Теперь посмотрю на S3-ресурс и полный URL, чтобы понять что именно запрашивается:
|
||||
|
||||
Searched for text `s3|bucket|S3|Bucket` (`**/universal_rebuild/internal/core/client.go`), 8 results
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#1-1), lines 1 to 50
|
||||
|
||||
Теперь у меня достаточно контекста. Сформирую план.
|
||||
|
||||
Created memory file [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/MjE3NzcyMzEtOGRkYy00ODA4LWI0OGItOGEyZDliNmU5YjFl/plan.md)
|
||||
|
||||
## Анализ и план
|
||||
|
||||
### Корневая причина EOF
|
||||
|
||||
**Главный виновник — HTTP/2.**
|
||||
|
||||
`http.DefaultTransport.Clone()` копирует ссылку на `TLSNextProto` map, в которой зарегистрирован HTTP/2 (`h2`) handler. При TLS-хендшейке Go предлагает `h2` в ALPN-расширении. ColdFusion + DDoS-Guard не поддерживают HTTP/2 — сервер принимает TLS-соединение, но сразу закрывает его без HTTP-ответа → **EOF**.
|
||||
|
||||
Именно поэтому curl работает: он по умолчанию использует HTTP/1.1 и не предлагает `h2` в ALPN.
|
||||
|
||||
`InsecureSkipVerify` здесь роли не играет — он влияет только на проверку сертификата, не на TLS-хендшейк.
|
||||
|
||||
---
|
||||
|
||||
### Ответы на вопросы
|
||||
|
||||
**1. Почему Go получает EOF, а curl — нет?**
|
||||
Go через ALPN предлагает `["h2", "http/1.1"]`. Сервер видит `h2` и закрывает соединение (ColdFusion не понимает HTTP/2). curl предлагает только `["http/1.1"]` — сервер отвечает нормально.
|
||||
|
||||
**2. Может ли ColdFusion требовать особый порядок заголовков?**
|
||||
Маловероятно причина. Порядок заголовков важен для WAAP/DDoS-Guard fingerprinting, но если 403 ушёл, то DDoS-Guard вас пропускает, а EOF — это именно HTTP/2 на уровне приложения.
|
||||
|
||||
**3. Как заставить Go отправлять то же, что curl?**
|
||||
Двумя действиями ниже.
|
||||
|
||||
---
|
||||
|
||||
### Plan: Fix EOF (2 изменения)
|
||||
|
||||
**Файлы к изменению:**
|
||||
- provider.go — строка ~124
|
||||
- client.go — строка ~836 (`doRequest`)
|
||||
|
||||
**Шаг 1 — Отключить HTTP/2** (приоритет #1, наиболее вероятный фикс)
|
||||
|
||||
В provider.go после `transport := http.DefaultTransport.(*http.Transport).Clone()` добавить две строки:
|
||||
|
||||
```go
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.TLSHandshakeTimeout = 60 * time.Second
|
||||
// Отключаем HTTP/2: ColdFusion + DDoS-Guard не поддерживают h2 → EOF
|
||||
transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
|
||||
transport.ForceAttemptHTTP2 = false // запрещаем upgrade
|
||||
```
|
||||
|
||||
> `ForceAttemptHTTP2 = false` одного недостаточно — нужен именно пустой `TLSNextProto`, иначе `h2` всё равно остаётся в ALPN.
|
||||
|
||||
**Шаг 2 — Убрать Content-Type на GET** (приоритет #2, возможная причина WAF-блокировки)
|
||||
|
||||
В `doRequest()` в client.go заголовок `Content-Type` выставлять только если payload != nil:
|
||||
|
||||
```go
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
```
|
||||
|
||||
**Шаг 3 — Debug dump** (если 1+2 не помогают)
|
||||
|
||||
Добавить через env-флаг `NUBES_DEBUG_HTTP=1`:
|
||||
|
||||
```go
|
||||
if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
|
||||
dump, _ := httputil.DumpRequestOut(req, true)
|
||||
fmt.Fprintf(os.Stderr, ">>> REQUEST:\n%s\n", dump)
|
||||
}
|
||||
```
|
||||
|
||||
Потом сравнить вывод с `curl -v --http1.1 -H "Authorization: Bearer ..." ...`.
|
||||
|
||||
---
|
||||
|
||||
**Верификация:**
|
||||
1. `terraform apply` для S3-бакета — нет EOF
|
||||
2. `curl -v --http2 <url>` — проверить что с `--http2` curl тоже получает EOF (подтвердит гипотезу)
|
||||
3. `terraform plan` после успешного apply
|
||||
|
||||
---
|
||||
|
||||
Ждать «делай» для реализации?
|
||||
|
||||
Создано задач: 3
|
||||
|
||||
|
||||
Вопрос Соннету:
|
||||
|
||||
---
|
||||
|
||||
**Контекст**: Terraform Provider на Go. API: ColdFusion за DDoS-Guard.
|
||||
|
||||
**Что сделали**:
|
||||
- `TLSNextProto = make(map[string]...)` — GET работает (200 JSON)
|
||||
- `?endpoint=` формат URL — как у генератора
|
||||
- `User-Agent: Mozilla/5.0`
|
||||
|
||||
**Проблема**: GET → 200. POST → 403. curl (GET и POST) → оба 200.
|
||||
|
||||
**Код POST**:
|
||||
```go
|
||||
func (c *UniversalClient) doRequest(ctx context.Context, method, path string, payload interface{}) ([]byte, http.Header, error) {
|
||||
// payload — json-сериализованная структура
|
||||
req, _ := http.NewRequestWithContext(ctx, method, c.ApiEndpoint, body)
|
||||
// ?endpoint=/instances
|
||||
q := req.URL.Query()
|
||||
q.Set("endpoint", path)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
```
|
||||
|
||||
**Работающий curl**:
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "User-Agent: Mozilla/5.0" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"serviceId":13}' \
|
||||
"https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances"
|
||||
```
|
||||
|
||||
**Вопрос**: Что в Go `http.Client` (c `TLSNextProto = make(...)`) может вызывать 403 на POST, при том что GET работает, и curl POST тоже работает? Может ли `http.NewRequestWithContext` добавлять заголовки/байты, которые триггерят DDoS-Guard на POST но не на GET?
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Прочитаю актуальный код `doRequest` и инициализацию transport, чтобы ответить точно, а не по памяти:
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#804-804), lines 804 to 880
|
||||
|
||||
Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#122-122), lines 122 to 155
|
||||
|
||||
## Диагноз: 3 кандидата, #1 самый вероятный
|
||||
|
||||
Код виден полностью. Вот что реально отличает Go POST от curl POST.
|
||||
|
||||
---
|
||||
|
||||
### #1 (главный подозреваемый): `url.Values.Encode()` кодирует `/` как `%2F`
|
||||
|
||||
```go
|
||||
q.Set("endpoint", endpointPath) // endpointPath = "/instances"
|
||||
req.URL.RawQuery = q.Encode() // → endpoint=%2Finstances
|
||||
```
|
||||
|
||||
curl отправляет: `?endpoint=/instances`
|
||||
Go отправляет: `?endpoint=%2Finstances`
|
||||
|
||||
**Почему GET проходит, а POST нет?** DDoS-Guard WAF для POST-запросов почти всегда имеет **allowlist эндпоинтов** — проверяет, что `endpoint` соответствует разрешённым путям. `/instances` совпадает, `%2Finstances` — нет → 403. GET-запросы проверяются слабее (read-only, меньший риск).
|
||||
|
||||
---
|
||||
|
||||
### #2: Нет заголовка `Accept`
|
||||
|
||||
curl всегда добавляет `Accept: */*` автоматически.
|
||||
Ваш Go-код его не ставит.
|
||||
|
||||
DDoS-Guard fingerprinting видит: POST с телом + нет `Accept` → нет `Accept` = нет браузера = потенциальный бот.
|
||||
|
||||
---
|
||||
|
||||
### #3: `Accept-Encoding: gzip` добавляет transport
|
||||
|
||||
Т.к. `DisableCompression` не выставлен, transport добавляет `Accept-Encoding: gzip` ко всем запросам. Для GET это тоже есть → GET проходит → вероятно, не причина. Но на всякий случай стоит проверить.
|
||||
|
||||
---
|
||||
|
||||
### Как проверить немедленно
|
||||
|
||||
`DumpRequestOut` уже в коде:
|
||||
```bash
|
||||
NUBES_DEBUG_HTTP=1 terraform apply 2>&1 | grep -A 30 ">>> REQ POST"
|
||||
```
|
||||
И сравнить с:
|
||||
```bash
|
||||
curl -v -X POST -H "User-Agent: Mozilla/5.0" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{"serviceId":13}' \
|
||||
"https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances" 2>&1 | grep -E "^>|^<"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Что менять
|
||||
|
||||
**Fix #1 — не использовать `q.Encode()` для endpoint:**
|
||||
|
||||
```go
|
||||
// Вместо q.Set("endpoint",...) + q.Encode()
|
||||
rawQuery := "endpoint=" + endpointPath // слэш не кодируется
|
||||
if extraQuery != "" {
|
||||
rawQuery += "&" + extraQuery
|
||||
}
|
||||
req.URL.RawQuery = rawQuery
|
||||
```
|
||||
|
||||
**Fix #2 — добавить `Accept: */*`** (как curl):
|
||||
|
||||
```go
|
||||
req.Header.Set("Accept", "*/*")
|
||||
```
|
||||
|
||||
**Fix #3 (опционально) — отключить автоматическое gzip в transport:**
|
||||
|
||||
```go
|
||||
transport.DisableCompression = true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Итог
|
||||
|
||||
| Причина | Почему GET ок | Почему POST 403 |
|
||||
|---|---|---|
|
||||
| `%2Finstances` в URL | WAF для GET мягче | WAF для POST проверяет allowlist путей |
|
||||
| Нет `Accept` | То же | Browser fingerprint не проходит |
|
||||
| `Connection: close` | - | (здесь `req.Close = true` нет, уже убрано) |
|
||||
|
||||
Начинать с Fix #1 — вероятность ~70% что это именно оно.
|
||||
|
||||
Создано задач: 4
|
||||
|
||||
|
||||
Вопрос Соннету:
|
||||
|
||||
---
|
||||
|
||||
**Контекст**: Terraform Provider Go. API за DDoS-Guard. GET работает, POST — 403.
|
||||
|
||||
**Проверено**:
|
||||
- `TLSNextProto = make(...)` — ✅ GET 200
|
||||
- `?endpoint=/instances` (не `%2F`) — ✅
|
||||
- `User-Agent: Mozilla/5.0` — ✅
|
||||
- `Accept: */*` — ✅
|
||||
- curl POST (те же URL, тело, заголовки) — ✅ 200/400
|
||||
- Go-тест POST (отдельная программа, тот же TLS-конфиг) — ✅ 201
|
||||
|
||||
**Провайдер**: `doRequest()` → `json.Marshal(payload)` → `bytes.NewBuffer(b)` → `http.NewRequestWithContext(ctx, "POST", url, body)` → `c.HttpClient.Do(req)` → **403**.
|
||||
|
||||
**Код**:
|
||||
```go
|
||||
// provider.go
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.TLSNextProto = make(map[string]func(...))
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}
|
||||
client := &core.UniversalClient{
|
||||
HttpClient: &http.Client{Transport: transport, Timeout: 300 * time.Second},
|
||||
...
|
||||
}
|
||||
|
||||
// client.go doRequest()
|
||||
b, _ := json.Marshal(payload)
|
||||
body := bytes.NewBuffer(b)
|
||||
req, _ := http.NewRequestWithContext(ctx, "POST", c.ApiEndpoint, body)
|
||||
req.URL.RawQuery = "endpoint=/instances"
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
```
|
||||
|
||||
**Вопрос**: Что в Go `http.Client` с кастомным `Transport` (пустой `TLSNextProto` + `TLSClientConfig`) может вызывать 403 только на POST, при том что:
|
||||
1. Отдельная Go-программа с идентичным TLS-конфигом делает POST успешно (201)
|
||||
2. curl делает POST успешно
|
||||
3. GET-запросы через тот же `doRequest` работают
|
||||
|
||||
Есть ли способ дампнуть сырые байты TLS-хендшейка или HTTP-запроса из провайдера (не из тестовой программы), чтобы сравнить с curl?
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Ключевой факт: **standalone Go-программа с тем же кодом → 201, провайдер → 403**. Значит причина не в TLS-конфиге и не в заголовках — она в окружении или состоянии самого провайдера.
|
||||
|
||||
---
|
||||
|
||||
## Четыре кандидата
|
||||
|
||||
### #1 (самый вероятный): `HTTPS_PROXY` в окружении Terraform
|
||||
|
||||
`http.DefaultTransport.Clone()` копирует `Proxy: http.ProxyFromEnvironment`. Если при `terraform apply` выставлена переменная `HTTPS_PROXY`, трафик пойдёт через прокси. Прокси может GET пропускать (кэш), а POST — блокировать или трансформировать.
|
||||
|
||||
Standalone-тест работает? Проверьте, запускался ли он в том же shell с теми же env:
|
||||
|
||||
```bash
|
||||
env | grep -i proxy
|
||||
```
|
||||
|
||||
Фикс — убрать прокси из transport:
|
||||
|
||||
```go
|
||||
transport.Proxy = nil // добавить в provider.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### #2: `DefaultTransport` уже модифицирован до `Clone()`
|
||||
|
||||
Terraform Plugin Framework инициализирует gRPC-транспорт и **может изменить глобальный `http.DefaultTransport`** до того, как ваш `Configure` его клонирует. Standalone-тест стартует с чистым DefaultTransport.
|
||||
|
||||
Фикс — не клонировать DefaultTransport, создать с нуля:
|
||||
|
||||
```go
|
||||
transport := &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 60 * time.Second,
|
||||
ResponseHeaderTimeout: 60 * time.Second,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper),
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecureSkipVerify,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### #3: Повторное использование TCP-соединения (keep-alive)
|
||||
|
||||
Провайдер: клиент создаётся один раз → GET занимает соединение → POST переиспользует то же соединение из пула. DDoS-Guard или ColdFusion может иметь правило: POST на keep-alive соединении после GET = подозрительно.
|
||||
|
||||
Standalone-тест: свежий клиент → каждый запрос на новом соединении.
|
||||
|
||||
Быстрая проверка — добавить `req.Close = true` (форсирует новое соединение на каждый запрос):
|
||||
|
||||
```go
|
||||
req.Close = true
|
||||
```
|
||||
|
||||
Если после этого POST пройдёт — причина именно в connection reuse.
|
||||
|
||||
---
|
||||
|
||||
### #4: Параллельность в Terraform
|
||||
|
||||
Terraform может запускать несколько `Create` параллельно. Rapid-fire POST'ы с одного IP → DDoS-Guard rate limit → 403. Standalone-тест делает один POST.
|
||||
|
||||
---
|
||||
|
||||
## Как дампить реальные байты из провайдера
|
||||
|
||||
### Уровень HTTP (уже есть, но улучшить)
|
||||
|
||||
Добавить дамп и ответа:
|
||||
|
||||
```go
|
||||
if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
|
||||
dump, _ := httputil.DumpRequestOut(req, true)
|
||||
fmt.Fprintf(os.Stderr, "\n>>> REQ %s %s\n%s\n", method, req.URL.String(), dump)
|
||||
|
||||
resp, err := c.HttpClient.Do(req)
|
||||
|
||||
respDump, _ := httputil.DumpResponse(resp, false) // false = без тела
|
||||
fmt.Fprintf(os.Stderr, "\n<<< RESP %d\n%s\n", resp.StatusCode, respDump)
|
||||
return resp, err
|
||||
}
|
||||
```
|
||||
|
||||
### Уровень TLS (сырые байты + расшифровка через Wireshark)
|
||||
|
||||
```go
|
||||
// В provider.go при создании transport:
|
||||
if keylogPath := os.Getenv("SSLKEYLOGFILE"); keylogPath != "" {
|
||||
f, _ := os.OpenFile(keylogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
|
||||
transport.TLSClientConfig.KeyLogWriter = f
|
||||
}
|
||||
```
|
||||
|
||||
Запуск:
|
||||
```bash
|
||||
# Терминал 1: захват трафика
|
||||
sudo tcpdump -i any -w /tmp/cap.pcap 'host deck-api-test.ngcloud.ru'
|
||||
|
||||
# Терминал 2: terraform apply
|
||||
SSLKEYLOGFILE=/tmp/sslkeys.log terraform apply
|
||||
|
||||
# После: открыть cap.pcap в Wireshark
|
||||
# Edit → Preferences → TLS → (Pre)-Master-Secret log: /tmp/sslkeys.log
|
||||
```
|
||||
|
||||
### Уровень TCP (кастомный DialContext — логировать сырые байты)
|
||||
|
||||
```go
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
c, err := (&net.Dialer{Timeout: 30 * time.Second}).DialContext(ctx, network, addr)
|
||||
if err != nil || os.Getenv("NUBES_DEBUG_HTTP") != "1" {
|
||||
return c, err
|
||||
}
|
||||
return &loggingConn{Conn: c, tag: addr}, nil
|
||||
}
|
||||
|
||||
type loggingConn struct {
|
||||
net.Conn
|
||||
tag string
|
||||
}
|
||||
func (lc *loggingConn) Write(b []byte) (int, error) {
|
||||
fmt.Fprintf(os.Stderr, "TCP→%s WRITE %d bytes: %q\n", lc.tag, len(b), b)
|
||||
return lc.Conn.Write(b)
|
||||
}
|
||||
func (lc *loggingConn) Read(b []byte) (int, error) {
|
||||
n, err := lc.Conn.Read(b)
|
||||
fmt.Fprintf(os.Stderr, "TCP←%s READ %d bytes: %q\n", lc.tag, n, b[:n])
|
||||
return n, err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что делать прямо сейчас
|
||||
|
||||
```bash
|
||||
# Шаг 1: проверить прокси
|
||||
env | grep -iE "proxy|https_proxy|http_proxy"
|
||||
|
||||
# Шаг 2: если прокси нет, тест без connection reuse
|
||||
# добавить req.Close = true в doRequest, пересобрать, apply
|
||||
|
||||
# Шаг 3: если не помогло — дамп
|
||||
NUBES_DEBUG_HTTP=1 terraform apply 2>&1 | grep -A 40 ">>> REQ POST"
|
||||
# сравнить с:
|
||||
curl -v -X POST ... 2>&1 | grep -E "^>|^<"
|
||||
```
|
||||
|
||||
Разница в одну строку в дампе покажет причину.
|
||||
@@ -0,0 +1,66 @@
|
||||
# 2026-07-08/09 — FindInstanceByDisplayName: анализ и дебаг
|
||||
|
||||
## Проблема
|
||||
|
||||
`FindInstanceByDisplayName("NaeelOrg", 19)` возвращает nil → adopt не работает → план показывает `+ create`.
|
||||
|
||||
## Три кандидата (Opus, 07-08)
|
||||
|
||||
### 🔴 #1: silent error swallow в GetInstanceStateRaw
|
||||
`client.go:~605` — `GetInstanceStateRaw` может возвращать ошибку, которая проглатывается `continue`. Ни search-путь, ни fallback не логируют ошибку.
|
||||
|
||||
### 🟡 #2: fields с URL-encoded запятыми
|
||||
`url.Values.Encode()` кодирует `,` → `%2C`. Если API не понимает `%2C`, search возвращает пустой массив → fallback → провал.
|
||||
|
||||
### 🟡 #3: API возвращает другое имя поля
|
||||
Если API возвращает `display_name` вместо `displayName` — `item.DisplayName` = "", `EqualFold("", "NaeelOrg")` = false.
|
||||
|
||||
## Дебаг (07-09)
|
||||
|
||||
### Ошибка #1: `req.Close = true` — не причина
|
||||
Правка `req.Close` только для GET не помогла (v5.0.65). План всё ещё `+ create`.
|
||||
|
||||
### Ошибка #2: три сборки под одной версией 5.0.66
|
||||
VM кеширует провайдер. `terraform init -upgrade` не перекачивает ту же версию. Исправлено: новый билд → новая версия.
|
||||
|
||||
### Ошибка #3: stderr не попадает в `terraform plan 2>&1`
|
||||
Terraform запускает плагин как подпроцесс → stderr не ловится. Исправлено: запись в `/tmp/nubes_find_debug.log`.
|
||||
|
||||
### Результат дебага (v5.0.67)
|
||||
|
||||
Лог `/tmp/nubes_find_debug.log`:
|
||||
```
|
||||
[FIND-DEBUG] ModifyPlan entered, client=true
|
||||
[FIND-DEBUG] PlanExistingResourceDiagnostics entered: serviceId=19 name="NaeelOrg" adopt=true client=true
|
||||
[FIND-DEBUG] search returned 1 results
|
||||
[FIND-DEBUG] search item uid=3f0850f2-3506-4efd-b84b-7270b5027ab5 name="NaeelOrg" svcId=19 matchSvc=true matchName=true
|
||||
```
|
||||
|
||||
**FindInstanceByDisplayName РАБОТАЕТ** — инстанс находится, GetInstanceStateRaw проходит.
|
||||
|
||||
### Почему план `+ create` — это НОРМАЛЬНО
|
||||
|
||||
Adopt происходит на **apply**, не на plan. Plan показывает `+ create` потому что ресурса нет в `terraform.tfstate`.
|
||||
|
||||
На apply: `CreateResource()` → `FindInstanceByDisplayName` → найдёт → `adoptExistingInstanceOnCreate` → подхватит в state.
|
||||
|
||||
## Дальше (завтра)
|
||||
|
||||
1. `terraform apply` — adopt должен сработать
|
||||
2. Убрать debug-логи, собрать чистую версию
|
||||
3. Добавить vdc, nsxt, Штурвал
|
||||
|
||||
## Уроки
|
||||
|
||||
1. **Всегда новая версия на каждую сборку** — иначе VM не подхватывает.
|
||||
2. **Писать debug в файл, не в stderr** — в плагинах Terraform stderr не виден.
|
||||
3. **Не гадать** — сначала дебаг-лог, потом правка.
|
||||
4. **Выполнять ТОЛЬКО по «делай»** — без разрешения ничего не трогать.
|
||||
|
||||
## 2026-07-09: фикс resolveRefSvcParamValues — пустые опциональные ref-параметры
|
||||
|
||||
### Баг
|
||||
`resolveRefSvcParamValues()` в `client.go:1169` не проверяет пустые значения. Когда юзер не задаёт опциональный ref-параметр (например `vdcGroupUid`), `FormatString` возвращает `""`, функция пытается резолвить `""` как имя → `findInstanceUidByDisplayNameRefSvc(ctx, 29, "")` → uid="" → ошибка "не найден экземпляр".
|
||||
|
||||
### Фикс (v5.0.68)
|
||||
Добавлена проверка `strings.TrimSpace(value) == ""` перед вызовом `findInstanceUidByDisplayNameRefSvc`. Пустые опциональные ref-параметры пропускаются без ошибки. Универсально для всех сервисов.
|
||||
@@ -0,0 +1,183 @@
|
||||
# Ответ Соннета: Анализ UX документации Nubes Terraform Provider
|
||||
|
||||
**Дата:** 2026-08-10
|
||||
**Вопрос:** см. prompt_for_sonnet_docs_ux.md
|
||||
**Ответчик:** Claude Sonnet
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
Документация функционально корректна, но навигация сломана для нового пользователя — он не может найти нужный ресурс без знания URL. Три приоритета: восстановить sidebar, сделать главную страницу рабочей, поднять размер шрифта MAN.
|
||||
|
||||
---
|
||||
|
||||
## 1. Общая оценка
|
||||
|
||||
**Хорошо:**
|
||||
- YAML → Markdown пайплайн — надёжная основа, параметры актуальны
|
||||
- Cloud snapshot для `state_out_flat` / `vault_secrets` — уникальная ценность, ни у кого нет
|
||||
- Dual example (minimal + full) — правильный выбор
|
||||
|
||||
**Плохо:**
|
||||
- Боковые панели скрыты → юзер попадает на страницу и не знает как вернуться к другим ресурсам
|
||||
- `font-size: 0.62rem` для MAN — нечитаемо, создаёт впечатление "broken UI"
|
||||
- Версия в URL, но не в UI → юзер не уверен смотрит ли он актуальное
|
||||
- Индексная страница — голая таблица из 43 строк без группировки и фильтрации
|
||||
|
||||
---
|
||||
|
||||
## 2. Рекомендации по блокам
|
||||
|
||||
### A. Навигация
|
||||
|
||||
**A1 — Навигация между ресурсами:**
|
||||
Лучший вариант — вернуть левый sidebar с категориями. 43 ресурса легко разбиваются на группы:
|
||||
- **Базы данных**: postgres, mysql, redis, mongodb, ...
|
||||
- **Очереди**: kafka, rabbitmq, activemq, ...
|
||||
- **Хранилище**: s3, swift, ...
|
||||
- **K8s**: kubernetes, helm, ...
|
||||
- **VMware**: vdc, vm, ...
|
||||
- **Приложения**: lucee, nodejs, flask, ...
|
||||
- **Сеть/прочее**: остальное
|
||||
|
||||
Sidebar с категориями даёт ориентацию за 3 секунды. Поиск mkdocs (`search`) — бесплатный бонус.
|
||||
|
||||
**A2 — Вернуть sidebar:**
|
||||
Да. Убрать из `extra.css` строки:
|
||||
```css
|
||||
.md-sidebar--primary { display: none !important; }
|
||||
```
|
||||
Правый sidebar (TOC) убрать только для страниц ресурсов — там он бесполезен. Реализуется через meta-tag `hide: [toc]` в frontmatter генерируемых файлов.
|
||||
|
||||
**A3 — Быстрый поиск:**
|
||||
- Включить встроенный поиск mkdocs-material (`search` plugin)
|
||||
- В `IndexMD()` добавить категории как `## Базы данных`, `## Очереди` — тогда sidebar mkdocs покажет дерево
|
||||
|
||||
---
|
||||
|
||||
### B. Дизайн страницы ресурса
|
||||
|
||||
**B1 — MAN font-size:**
|
||||
Поднять с `0.62rem` до `0.78rem` — достаточно компактно, но читаемо. Заодно обернуть MAN в `<details>` с заголовком "Справка (MAN)" — DevOps обычно не читает MAN, ему нужны параметры.
|
||||
|
||||
**B2 — Структура страницы:**
|
||||
Текущий порядок (MAN в начале) — неоптимален. Предлагаю:
|
||||
```
|
||||
1. Заголовок + Inline nav
|
||||
2. Краткое описание (1-2 строки из ServiceDisplayName + первый абзац MAN)
|
||||
3. Minimal example (СРАЗУ — копируй и пробуй)
|
||||
4. Create params (таблица)
|
||||
5. Outputs (state_out_flat + vault_secrets)
|
||||
6. MAN (в <details> collapsed)
|
||||
```
|
||||
DevOps хочет пример → понял структуру → посмотрел параметры. MAN читает если застрял.
|
||||
|
||||
Это изменение в `buildManualPage()` — перенос `buildExamplePage()` фрагмента вверх. Либо создать новый `buildCombinedLandingPage()`.
|
||||
|
||||
**B3 — Версия в UI:**
|
||||
Добавить в `buildHeader()`:
|
||||
```
|
||||
# Resource nubes_postgres · v5.0.5 · Service ID: 90 · PostgreSQL
|
||||
```
|
||||
`version` уже передаётся в `ResourceDocs()` — просто прокинуть в `buildHeader()`.
|
||||
|
||||
---
|
||||
|
||||
### C. Таблицы параметров
|
||||
|
||||
**C1 — Колонки таблиц:**
|
||||
Текущие колонки: `Code | Type | Description | Constraints`. Добавить `Required` и `Default`:
|
||||
```
|
||||
| Параметр | Тип | Обязательный | По умолчанию | Описание | Ограничения |
|
||||
```
|
||||
`Required` и `Default` уже есть в данных (`SplitParams()` их разделяет), просто не выводятся в единой таблице. Убрать разделение на две таблицы — одна таблица с колонкой Required проще для чтения.
|
||||
|
||||
**C2 — Вложенные параметры (map-fixed):**
|
||||
Текущий вариант (`### clusterConfiguration` → отдельная таблица) — приемлем. Улучшить: добавить ссылку-якорь в основной таблице:
|
||||
```
|
||||
| clusterConfiguration | map-fixed | [Развернуть ↓](#clusterconfiguration) | ... |
|
||||
```
|
||||
Так юзер понимает что кликнуть. Реализуется в `renderParamTable()` + `renderNestedParams()`.
|
||||
|
||||
---
|
||||
|
||||
### D. Примеры
|
||||
|
||||
**D1 — Страница Example:**
|
||||
- Поменять местами: Minimal example → Full example (не в `<details>`)
|
||||
Сейчас Full в раскрывашке — правильно. Но заголовок `Minimal example — only required parameters` на английском среди русского контента — резает глаз. Перевести.
|
||||
- Добавить комментарии в код: `# Выберите из: 1, 3, 5` для параметров с value_list — LLM уже обогащает, но это должно быть в HCL-примере тоже.
|
||||
- Outputs usage: сейчас шаблонные строки с `baza`. Показать реальные ключи из cloud snapshot если есть:
|
||||
```hcl
|
||||
# PostgreSQL connection string:
|
||||
# nubes_postgres.baza.state_out_flat["internalConnect.master"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### E. Общие рекомендации
|
||||
|
||||
**E1 — Чего не хватает:**
|
||||
1. **Lifecycle warning** — блок про `suspend_on_destroy` сейчас внизу create params мелким шрифтом. Это КРИТИЧНАЯ информация (пользователь может случайно "удалить" БД). Поднять выше, оформить как `!!! danger` admonition.
|
||||
2. **Связанные ресурсы** — PostgreSQL → пример связки с Lucee/NodeJS уже есть в `buildOutputsPage()`, но только для service_id=90. Обобщить через теги в YAML.
|
||||
3. **Changelog** — нужен, но это отдельная задача (нужно хранить diff между версиями YAML).
|
||||
|
||||
**E2 — Приоритеты:**
|
||||
Quick wins (высокий эффект, минимум кода):
|
||||
1. Убрать `display:none` с primary sidebar + включить search
|
||||
2. MAN font-size 0.62rem → 0.78rem
|
||||
3. Версия в buildHeader()
|
||||
4. Добавить категории в IndexMD() + _nav_fragment.yml
|
||||
|
||||
---
|
||||
|
||||
## 3. Приоритизированный план действий
|
||||
|
||||
**Phase 1 — Quick wins (1-2 дня, 1 разработчик)**
|
||||
1. `extra.css`: убрать `display:none` для primary sidebar
|
||||
2. `extra.css`: MAN font-size `0.62rem` → `0.78rem`
|
||||
3. writers.go: прокинуть `version` в `buildHeader()`
|
||||
4. `IndexMD()`: добавить категорийные заголовки (требует маппинга service → category)
|
||||
5. `WriteNavFragment()`: группировка по категориям в `_nav_fragment.yml`
|
||||
|
||||
**Phase 2 — Структура страницы (3-5 дней)**
|
||||
6. Новый порядок секций: пример → параметры → MAN в `<details>`
|
||||
7. Объединить required/defaults в одну таблицу с колонкой Required
|
||||
8. Lifecycle warning → admonition `danger`
|
||||
|
||||
**Phase 3 — Таблицы и примеры (1 неделя)**
|
||||
9. Якори для вложенных параметров
|
||||
10. Реальные ключи из cloud snapshot в HCL-примерах
|
||||
11. Перевести заголовки примеров на русский
|
||||
|
||||
**Долгосрок (отдельный эпик)**
|
||||
12. Changelog между версиями
|
||||
13. Теги для связанных ресурсов
|
||||
|
||||
---
|
||||
|
||||
## 4. Конкретные предложения по коду
|
||||
|
||||
| Файл | Изменение |
|
||||
|------|-----------|
|
||||
| extra.css | Убрать строки `display: none !important` для обоих sidebar; MAN `.man-content` `font-size: 0.78rem` |
|
||||
| `writers/writers.go`: `buildHeader()` | Добавить `version` параметр, вывести в заголовок |
|
||||
| `writers/writers.go`: `IndexMD()` | Принять маппинг `categoryMap map[int]string`, выводить заголовки `## Category` |
|
||||
| `writers/writers.go`: `buildCreateParamsPage()` | Единая таблица Required + Default с колонкой Required (bool); lifecycle → admonition |
|
||||
| `writers/writers.go`: `buildManualPage()` | MAN обернуть в `<details><summary>Справка (MAN)</summary>` |
|
||||
| mkdocs.yml | Включить `search` plugin |
|
||||
|
||||
---
|
||||
|
||||
## 5. Антипаттерны — что НЕ делать
|
||||
|
||||
1. **Не делать многоуровневую sidebar по операциям** — `Manual / Create params / Modify params` в sidebar превратит дерево в 43×6=258 пунктов. Только верхний уровень в sidebar, внутри — inline nav.
|
||||
2. **Не трогать LLM prompt ради структуры** — структура страниц это Go-генератор, не LLM. LLM только обогащает тексты.
|
||||
3. **Не делать HTML-таблицы** — Markdown-таблицы уже работают; HTML нужен только для сложных случаев (`lifecycle-note` div — допустимо).
|
||||
4. **Не скрывать пример в `<details>`** — сейчас Full example скрыт, это правильно. Minimal должен быть ОТКРЫТ и первым.
|
||||
5. **Не добавлять JS-фильтрацию на index** — mkdocs search уже умеет фильтровать; второй поиск создаёт путаницу.
|
||||
|
||||
---
|
||||
|
||||
**Ключевой вывод:** Самое больное место — отсутствие sidebar и нечитаемый MAN. Эти два изменения (по 3 строки CSS) дадут 80% UX-эффекта. Остальное — итеративно.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Ответ Соннета: Как смержить nav (статический + динамический)
|
||||
|
||||
**Дата:** 2026-08-10
|
||||
**Вопрос:** Как смержить статический `nav:` (руководства) и динамический (43 ресурса по категориям) в mkdocs?
|
||||
**Ответчик:** Claude Sonnet
|
||||
|
||||
---
|
||||
|
||||
## Нет встроенного `!include` в mkdocs
|
||||
|
||||
mkdocs-material не имеет нативного механизма подключения внешних YAML-фрагментов в `nav:`. Плагины (monorepo, awesome-pages) решают другие задачи и здесь не помогут.
|
||||
|
||||
---
|
||||
|
||||
## Три варианта (по убыванию рекомендованности)
|
||||
|
||||
### Вариант 1 — Рекомендую: расширить существующий Python pre-build шаг
|
||||
|
||||
В `04_build_and_publish_docs.sh` уже есть Python-блок, который пишет `.mkdocs.tmp.yml`. Добавить туда чтение `_nav_fragment.yml` и инъекцию в `nav:`:
|
||||
|
||||
```python
|
||||
import yaml
|
||||
|
||||
nav_fragment_path = Path(docs_dir) / "_nav_fragment.yml" if docs_dir else None
|
||||
if nav_fragment_path and nav_fragment_path.exists():
|
||||
fragment = yaml.safe_load(nav_fragment_path.read_text(encoding="utf-8"))
|
||||
resources_nav = fragment.get("resources_nav", [])
|
||||
|
||||
config = yaml.safe_load(text)
|
||||
for item in config.get("nav", []):
|
||||
if isinstance(item, dict) and "Ресурсы" in item:
|
||||
item["Ресурсы"] = resources_nav
|
||||
break
|
||||
text = yaml.dump(config, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
||||
```
|
||||
|
||||
**Плюсы:** ноль новых зависимостей, PyYAML уже в окружении, merge в одном месте, статические секции ("Руководства") остаются нетронутыми.
|
||||
|
||||
**Предупреждение:** PyYAML при `dump` меняет форматирование (кавычки, отступы) — это нормально для `.mkdocs.tmp.yml`, который никто не читает руками.
|
||||
|
||||
### Вариант 2: docs-generator пишет полный mkdocs.yml
|
||||
|
||||
Сделать отдельный файл `mkdocs_base.yml` (тема, плагины, CSS, статический nav — без ресурсов), Go-генератор его читает, добавляет ресурсный nav, пишет финальный mkdocs.yml.
|
||||
|
||||
**Минус:** Go-генератор становится ответственным за весь mkdocs.yml, сложнее поддерживать структуру темы/плагинов.
|
||||
|
||||
### Вариант 3: mkdocs-awesome-pages
|
||||
|
||||
Плагин создаёт `.pages` файлы в директориях и управляет порядком через них. Но `nav:` в mkdocs.yml при этом должен быть либо полностью убран, либо включать ресурсы явно — проблему merge не решает.
|
||||
|
||||
---
|
||||
|
||||
## Дополнительная проблема: guides при смене `docs_dir`
|
||||
|
||||
Когда `docs_dir` переключается на `generated/{stand}/docs_llm`, пути вида `30_registry/guides/getting-started.md` в `nav:` ломаются — этих файлов там нет.
|
||||
|
||||
**Решение:** в том же Python pre-build шаге скопировать `30_registry` в `$MKDOCS_DOCS_DIR/30_registry/` перед сборкой. Или — убрать guides из профильного nav (оставить только ресурсы).
|
||||
|
||||
---
|
||||
|
||||
**Итого:** Вариант 1 — минимальные изменения, всё уже на месте. Нужно только расширить существующий Python блок в `04_build_and_publish_docs.sh` примерно на 10 строк + скопировать `30_registry` в `docs_dir`.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Sonnet: ответ по улучшению документации
|
||||
|
||||
## Принцип: «User Journey First» — 4 сценария
|
||||
|
||||
A. «Хочу задеплоить» → описание (30s) → минимальный пример (1m) → apply
|
||||
B. «Хочу настроить параметр» → справка с читаемыми constraints
|
||||
C. «Хочу использовать output» → outputs + HCL-примеры
|
||||
D. «Хочу modify/restart/suspend» → страница операций
|
||||
|
||||
## Изменения по 3 слоям
|
||||
|
||||
### Слой 1: LLM-промпт — P1 (макс. польза, 0 компиляции)
|
||||
- Раскрывать `value_list` → «Допустимые значения: 1, 3, 5, 7»
|
||||
- Раскрывать `regex` → «Формат: cron»
|
||||
- Заполнять пустые описания из MAN
|
||||
- Группы (clusterConfiguration) — 1 предложение из MAN
|
||||
- Операции — заполнять «—» из MAN
|
||||
- Заменять TODO в примерах на реальные значения
|
||||
|
||||
### Слой 2: docs-generator (Go) — P2
|
||||
- Убрать колонку ID
|
||||
- value_list → читаемый текст
|
||||
- Двойной пример: минимальный (15 строк) + полный в <details>
|
||||
- Секция «Быстрый старт» перед MAN
|
||||
|
||||
### Слой 3: mkdocs — P3
|
||||
- Sidebar по категориям (Базы данных / K8s / Хранилище / ...)
|
||||
- Хлебные крошки
|
||||
- Убрать inline-навигацию (заменяет sidebar)
|
||||
- Back/Next кнопки
|
||||
|
||||
## Порядок реализации
|
||||
1. LLM промпт — мгновенный эффект на все 34 сервиса
|
||||
2. renderParamTable — убрать ID, раскрыть constraints
|
||||
3. buildExamplePage — двойной пример
|
||||
4. mkdocs nav — категории в sidebar
|
||||
@@ -0,0 +1,130 @@
|
||||
# Sonnet Briefing: анализ и улучшение документации провайдера
|
||||
|
||||
Цель: изучить КАЖДЫЙ шаг генерации документации и предложить конкретные улучшения,
|
||||
чтобы пользователю было понятно и удобно работать с каждым ресурсом.
|
||||
|
||||
---
|
||||
|
||||
## ⛔ Файлы — ПРОЧИТАТЬ ОБЯЗАТЕЛЬНО ВСЕ
|
||||
|
||||
### Генераторы
|
||||
|
||||
| # | Файл | Что смотреть |
|
||||
|---|------|-------------|
|
||||
| 1 | `TOOLS/docs-generator/main.go` | весь main — как вызывается, какие флаги |
|
||||
| 2 | `TOOLS/docs-generator/internal/writers/writers.go` | ВСЕ функции. Особенно: `buildCreateParamsPage`, `buildModifyParamsPage`, `buildOutputsPage`, `buildExamplePage`, `buildManualPage`, `renderParamTable`, `renderNestedParams`, `htmlToMarkdown`, `formatParamOrBlock` |
|
||||
| 3 | `TOOLS/docs-generator/internal/types/` | структуры YAML-спеки |
|
||||
|
||||
### LLM-обработка
|
||||
|
||||
| # | Файл | Что смотреть |
|
||||
|---|------|-------------|
|
||||
| 4 | `TOOLS/scripts/05_generate_docs_llm.py` | весь скрипт — как вызывается LLM, промпт, как парсится ответ |
|
||||
| 5 | `docs/LLM_DOCS_GENERATION.md` | архитектура, правила для LLM |
|
||||
|
||||
### Результаты генерации (примеры)
|
||||
|
||||
| # | Файл | Что смотреть |
|
||||
|---|------|-------------|
|
||||
| 6 | `generated/test/docs/postgres_params_create.md` | RAW-вывод docs-generator (без LLM) |
|
||||
| 7 | `generated/test/docs_llm/90_postgres.md` | ПОСЛЕ LLM-обработки |
|
||||
| 8 | `generated/test/docs/postgres.md` | главная страница (MAN) |
|
||||
| 9 | `generated/test/docs/postgres_example.md` | HCL пример |
|
||||
| 10 | `generated/test/docs/postgres_outputs.md` | выходные параметры |
|
||||
| 11 | `generated/test/docs/postgres_ops.md` | список операций |
|
||||
|
||||
---
|
||||
|
||||
## Текущий пайплайн (3 шага)
|
||||
|
||||
```
|
||||
YAML-спеки
|
||||
→ docs-generator (Go) → raw .md с HTML-таблицами
|
||||
→ LLM (gpt-oss-120b, по одному файлу) → улучшенные .md
|
||||
→ mkdocs-material → статический сайт → S3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что видит пользователь СЕЙЧАС (пример: postgres_params_create.md)
|
||||
|
||||
### До LLM (raw):
|
||||
```html
|
||||
<table><thead><tr><th>ID</th><th>Code</th><th>Type</th><th>Description</th><th>Constraints</th></tr></thead>
|
||||
<tr><td>788</td><td><strong><code>cluster_configuration</code></strong></td><td><code>map-fixed</code></td><td></td><td></td></tr>
|
||||
```
|
||||
- Колонка ID (техническая, пользователю не нужна)
|
||||
- Английские заголовки (Code, Description, Constraints)
|
||||
- Пустые ячейки Description
|
||||
- Raw `value_list=` в Constraints
|
||||
|
||||
### После LLM:
|
||||
```
|
||||
| clusterConfiguration | map-fixed | да | — | — |
|
||||
```
|
||||
- Русские заголовки ✅
|
||||
- ID убран ✅
|
||||
- Но: пустые Description (—), value_list не раскрыт
|
||||
|
||||
---
|
||||
|
||||
## Проблемы (что нужно улучшить)
|
||||
|
||||
### 1. Пустые описания параметров
|
||||
Многие параметры в выводе имеют `—` в колонке «Описание». Если сервис предоставил `descr` или `man` в YAML — он должен быть в документации.
|
||||
|
||||
### 2. Технические колонки
|
||||
- Колонка «Constraints» показывает `value_list=1, 3, 5, 7` вместо читаемого «Допустимые значения: 1, 3, 5, 7»
|
||||
- Колонка «Default» показывает пустую строку вместо «нет» или «—»
|
||||
- ID параметров виден в raw-версии, но нужен ли он вообще?
|
||||
|
||||
### 3. MAN-секция (service_man)
|
||||
Это HTML-строка с полным руководством от облачного провайдера. Она обрабатывается `htmlToMarkdown()` — regex-заменами. Часто результат нечитаемый: сломанные списки, потерянные ссылки, HTML-мусор.
|
||||
|
||||
### 4. HCL-примеры
|
||||
`buildExamplePage` генерирует пример с ВСЕМИ параметрами (required + default). Это гигантский манифест на 100+ строк. Может, показывать сначала минимальный working example, а полный — отдельно?
|
||||
|
||||
### 5. Навигация
|
||||
На каждой странице — строка навигации из 6 ссылок. Занимает место, дублируется. Может, сделать сайдбар или хлебные крошки?
|
||||
|
||||
### 6. LLM-промпт (05_generate_docs_llm.py)
|
||||
Промпт просит «улучшить формулировки», но:
|
||||
- Не просит раскрывать value_list в читаемый вид
|
||||
- Не просит добавлять «почему» и «зачем» к параметрам
|
||||
- Не использует `service_man` как дополнительный контекст для обогащения описаний
|
||||
- Обрабатывает по одному файлу — теряет контекст между страницами
|
||||
|
||||
---
|
||||
|
||||
## Вопросы
|
||||
|
||||
### Q1: Структура страниц
|
||||
Текущая: Manual | Create params | Modify params | Outputs | Ops | Example.
|
||||
Удобно ли это? Что переставить/добавить/убрать? Может, всё на одной странице с якорями?
|
||||
|
||||
### Q2: HTML-таблицы vs Markdown-таблицы
|
||||
Сейчас raw — HTML, LLM конвертирует в Markdown-таблицы. Оставить Markdown? Или HTML-таблицы лучше (CSS, выравнивание)?
|
||||
|
||||
### Q3: LLM-промпт
|
||||
Как улучшить промпт чтобы:
|
||||
- description параметров наполнялся из `service_man` где возможно
|
||||
- value_list показывался читаемо
|
||||
- empty cells говорили «не указано» а не «—»
|
||||
|
||||
### Q4: HCL-примеры
|
||||
Минимальный пример + полный? Или только минимальный? Или только полный?
|
||||
|
||||
### Q5: Постраничная vs одностраничная документация
|
||||
7 .md файлов на сервис. Это норм или перебор? Может, генерировать один README.md на сервис со всем внутри?
|
||||
|
||||
### Q6: Что ещё можно улучшить для UX?
|
||||
Посмотри на любые 2-3 страницы из `generated/test/docs_llm/` и скажи: что непонятно, что раздражает, чего не хватает.
|
||||
|
||||
---
|
||||
|
||||
## Ожидаемый ответ
|
||||
|
||||
1. Анализ текущего состояния: что хорошо, что плохо (с конкретными примерами из файлов)
|
||||
2. Конкретные предложения по каждому из 6 вопросов
|
||||
3. Unified diff предлагаемых изменений в writers.go и 05_generate_docs_llm.py
|
||||
4. Пример одной страницы «как должно быть» для postgres (хотя бы params_create)
|
||||
@@ -0,0 +1,50 @@
|
||||
# Документация — полный диалог и финальный план
|
||||
|
||||
## Ответы на 3 вопроса Соннета
|
||||
|
||||
### 1. Inline-навигацию убирать?
|
||||
**Убирать, но сначала добавить страницы в sidebar.**
|
||||
Сейчас ресурсные страницы не в `mkdocs.yml nav:` — они сироты. Inline nav — единственная навигация.
|
||||
Порядок: сначала P3 (добавить в sidebar через `_nav_fragment.yml`), потом убрать inline из writers.go.
|
||||
|
||||
### 2. Минимальный пример — только required без default?
|
||||
**Да.** Параметр с дефолтом и так сработает без указания. Минимальный пример = required=true И default пустой.
|
||||
15 строк вместо 100.
|
||||
|
||||
### 3. Категории для sidebar
|
||||
Группировка по 7 категориям:
|
||||
|
||||
| Категория | Сервисы |
|
||||
|---|---|
|
||||
| Базы данных | postgres, redis, mongodb, mariadb, clickhouse |
|
||||
| Очереди | rabbitmq, kafka |
|
||||
| Хранилище | s3, s3bucket, nextcloud |
|
||||
| K8s | k8s_velero, k8s_sthutrval_cluster, k8s_openbao, vc_mgmt_sthutrval_cluster |
|
||||
| VMware | vc_org, vc_vdc, vc_nsxt, vcexternalip, vapp, vc_vm_v2, vc_vm_v3, vc_vdc_group |
|
||||
| Приложения | flask, nodejs, lucee, http, gitea, superset, pgadmin, harbor, akhq, llm_ai |
|
||||
| Сеть | zones_v2, dnsrecord |
|
||||
|
||||
---
|
||||
|
||||
## Финальный план (3 слоя)
|
||||
|
||||
### P1: LLM-промпт (05_generate_docs_llm.py) — 0 компиляции, 34 сервиса
|
||||
6 инструкций:
|
||||
- value_list → «Допустимые значения: X, Y, Z»
|
||||
- regex → «Формат: cron / UUID / IP»
|
||||
- Описания групп из MAN
|
||||
- Пустые описания заполнять
|
||||
- Операции без «—»
|
||||
- TODO в примерах → реальные значения
|
||||
|
||||
### P2: docs-generator (writers.go)
|
||||
- renderParamTable: убрать ID, value_list → читаемый текст
|
||||
- buildExamplePage: минимальный пример + полный в <details>
|
||||
|
||||
### P3: mkdocs навигация
|
||||
- docs-generator генерирует _nav_fragment.yml с категориями
|
||||
- 04_build_and_publish_docs.sh вставляет его в mkdocs.yml
|
||||
- writers.go: убрать inline nav
|
||||
- mkdocs.yml: breadcrumbs + prev/next
|
||||
|
||||
### Порядок: P1 → P2 → P3
|
||||
@@ -0,0 +1,32 @@
|
||||
# Документация — финальный план P1 (утверждён)
|
||||
|
||||
Дата: 2026-08-09
|
||||
Источник: Sonnet, после серии брифов и уточнений
|
||||
|
||||
## Что меняется в 05_generate_docs_llm.py
|
||||
|
||||
### 1. SYSTEM_PROMPT — замена
|
||||
Новый промпт с правилами A-E (см. HISTORY/SONNET/docs_prompt_full_response.md)
|
||||
|
||||
### 2. max_tokens: 4096 → 8192
|
||||
|
||||
### 3. Новая функция extract_man(text) → str
|
||||
Вырезает блок ## MAN из Name.md. Используется как контекст для params/ops.
|
||||
|
||||
### 4. Новая функция build_prompt(file_type, filename, content, man) → str
|
||||
Формирует сообщение для LLM: тип файла + MAN-контекст + содержимое.
|
||||
|
||||
### 5. Обработка ВСЕХ типов файлов
|
||||
Было: только Name.md
|
||||
Стало: Name.md, _params_create.md, _params_modify.md, _ops.md, _example.md
|
||||
MAN-контекст: для params и ops, без MAN для главной и примеров.
|
||||
|
||||
### 6. Копирование в docs_llm/
|
||||
- outputs, params-landing, subresource — копировать as-is
|
||||
- 30_registry/, guides/, index.md — копировать из docs/
|
||||
|
||||
### Решения
|
||||
- Subresource: копировать as-is (не через LLM)
|
||||
- max_tokens: 8192
|
||||
- Вывод: docs_llm/
|
||||
- 30_registry копировать в самом скрипте
|
||||
@@ -0,0 +1,35 @@
|
||||
# Sonnet: финальный план P1 — 05_generate_docs_llm.py
|
||||
|
||||
## Пайплайн на один сервис
|
||||
|
||||
```
|
||||
docs/Name.md ─┐
|
||||
docs/Name_params_create.md ─┤ LLM → docs_llm/Name.md
|
||||
docs/Name_params_modify.md ─┤ docs_llm/Name_params_create.md
|
||||
docs/Name_ops.md ─┤ docs_llm/Name_params_modify.md
|
||||
docs/Name_example.md ─┘ docs_llm/Name_ops.md
|
||||
docs_llm/Name_example.md
|
||||
|
||||
docs/Name_outputs.md ─── copy → docs_llm/Name_outputs.md
|
||||
docs/Name_params.md ─── copy → docs_llm/Name_params.md
|
||||
docs/Name_subresource*.md ─── copy → docs_llm/Name_subresource*.md
|
||||
```
|
||||
|
||||
## Ключевые детали
|
||||
|
||||
### MAN-контекст
|
||||
- Извлекается из `docs/Name.md` (raw HTML) функцией `extract_man()`
|
||||
- Передаётся в том же сообщении что и params/ops файлы
|
||||
- Для главной страницы (Name.md) и примеров (_example.md) — без MAN
|
||||
|
||||
### Параметры LLM
|
||||
- `max_tokens`: 4096 → **8192**
|
||||
- `temperature`: 0.15 (без изменений)
|
||||
- `model`: gpt-oss-120b (без изменений)
|
||||
|
||||
### Интеграция с 04_build_and_publish_docs.sh
|
||||
Вариант A: DOCS_GEN_DIR → `docs_llm/`. Скрипт копирует guides/, 30_registry/, index.md в docs_llm/.
|
||||
|
||||
## Вопрос: subresource-страницы обрабатывать LLM?
|
||||
postgres_user.md, postgres_database.md, postgres_backup.md — их структура как у _params_create.md.
|
||||
Пока копировать as-is или тоже через LLM?
|
||||
@@ -0,0 +1,38 @@
|
||||
# Sonnet: ответы — готовый SYSTEM_PROMPT и механизм MAN→группы
|
||||
|
||||
## Вопрос 2: MAN → группы
|
||||
|
||||
Явный маппинг НЕ нужен. LLM делает семантический матч:
|
||||
- Видит группу `clusterConfiguration` с sub-params `cpu, memory, disk, replicas`
|
||||
- Видит в MAN: «Квота (millicore) ядра пода... Квота памяти... Размер диска... Количество узлов»
|
||||
- Сопоставляет по смыслу → «Ресурсы пода кластера: CPU, RAM, диск, реплики»
|
||||
|
||||
Условие: MAN в том же сообщении, что и params-файл.
|
||||
|
||||
## Готовый SYSTEM_PROMPT
|
||||
|
||||
См. полный текст с правилами A-E:
|
||||
- A: удалить колонку ID
|
||||
- B: value_list → «Допустимые значения: X, Y, Z»
|
||||
- C: regex → читаемый формат
|
||||
- D: пустые описания → заполнить из MAN
|
||||
- E: группы (map-fixed) → 1 предложение о содержимом
|
||||
|
||||
+ правила для _ops.md, _example.md, главной страницы (MAN)
|
||||
|
||||
## Новая логика вызова LLM
|
||||
|
||||
MAN передаётся как контекст в том же сообщении что и params-файл:
|
||||
```
|
||||
Тип файла: _params_create
|
||||
Сервис: postgres
|
||||
=== MAN СЕРВИСА ===
|
||||
{текст MAN из Name.md}
|
||||
=== Файл ===
|
||||
{содержимое}
|
||||
```
|
||||
|
||||
## 2 вопроса
|
||||
|
||||
1. max_tokens 4096 → 8192? (params для postgres ~4KB HTML)
|
||||
2. Писать в docs_llm/ или сразу на место?
|
||||
@@ -0,0 +1,162 @@
|
||||
# Sonnet: ПОЛНЫЙ ответ — готовый SYSTEM_PROMPT + механизм MAN→группы
|
||||
|
||||
## Вопрос 2: механизм маппинга MAN → группы
|
||||
|
||||
**Явный маппинг не нужен.** LLM делает его сам через семантику.
|
||||
|
||||
Как это работает для `clusterConfiguration`:
|
||||
|
||||
```
|
||||
LLM видит в _params_create.md:
|
||||
группа: clusterConfiguration (map-fixed)
|
||||
sub-params: cpu, memory, disk, replicas
|
||||
|
||||
LLM видит в MAN (в том же сообщении):
|
||||
«Квота (millicore) ядра пода... Квота (megabyte) памяти...
|
||||
Размер диска... Количество узлов (реплик)...»
|
||||
|
||||
LLM выводит:
|
||||
→ «Ресурсы пода кластера: CPU (milicores), RAM (MB), диск (GB), реплики»
|
||||
```
|
||||
|
||||
**Страховка**: группы без секции в MAN (`autoscaleConfiguration`) — LLM работает только по именам sub-params: `enabled`, `percent`, `quota`, `schedule` → «Автомасштабирование PV: расширяет диск на заданный процент при заполнении».
|
||||
|
||||
**Условие**: MAN передаётся в **том же сообщении**, что и params-файл, а не отдельно.
|
||||
|
||||
---
|
||||
|
||||
## Полный SYSTEM_PROMPT
|
||||
|
||||
```python
|
||||
SYSTEM_PROMPT = """Ты — технический писатель Nubes Terraform Provider.
|
||||
Улучшаешь автогенерированные Markdown-файлы документации.
|
||||
|
||||
═══════════════════════════════════════════════════════
|
||||
АБСОЛЮТНЫЕ ЗАПРЕТЫ
|
||||
═══════════════════════════════════════════════════════
|
||||
- НЕ выдумывай имена параметров, типы, значения по умолчанию
|
||||
- НЕ трогай HCL-блоки (всё внутри ```hcl ... ```)
|
||||
- НЕ трогай имена параметров в таблицах (snake_case / camelCase из API)
|
||||
- НЕ трогай навигационные строки вида [Manual](x.md) · [Create params](y.md) ...
|
||||
- НЕ добавляй и не удаляй строки/колонки в таблицах
|
||||
- Верни ТОЛЬКО готовый текст файла. Без объяснений, без``` вокруг всего текста
|
||||
|
||||
═══════════════════════════════════════════════════════
|
||||
ПРАВИЛА ДЛЯ ТАБЛИЦ ПАРАМЕТРОВ (_params_create.md, _params_modify.md)
|
||||
═══════════════════════════════════════════════════════
|
||||
Таблицы содержат столбцы: ID | Code | Type | Required | Default | Description | Constraints
|
||||
Можно менять ТОЛЬКО текст в <td>Description</td> и <td>Constraints</td>.
|
||||
|
||||
ПРАВИЛО A — удали колонку ID:
|
||||
Удали <th>ID</th> из заголовка и соответствующий первый <td>число</td> из каждой строки.
|
||||
|
||||
ПРАВИЛО B — value_list в Constraints:
|
||||
value_list=1, 3, 5, 7 → очисти ячейку Constraints до пустой.
|
||||
В ячейку Description добавь строку: «Допустимые значения: **1, 3, 5, 7**»
|
||||
Если в Description уже был текст — добавь после него, через пробел или перевод строки (<br/>).
|
||||
|
||||
ПРАВИЛО C — regex в Constraints:
|
||||
Замени regex-строку на читаемое описание формата:
|
||||
- cron-подобный regex → «Формат: cron-выражение. Пример: `0 0 * * *`»
|
||||
- UUID regex → «Формат: UUID»
|
||||
- IP-адрес regex → «Формат: IP-адрес»
|
||||
- Прочее → кратко опиши формат своими словами
|
||||
|
||||
ПРАВИЛО D — пустое Description (пустая ячейка или —):
|
||||
Напиши краткое описание параметра (1–2 предложения). Приоритет источников:
|
||||
1. MAN — ищи текст, связанный с параметром по смыслу и по именам sub-params
|
||||
2. Имя параметра snake_case → понятный русский
|
||||
3. Тип и контекст соседних параметров в группе
|
||||
|
||||
ПРАВИЛО E — верхнеуровневые группы (строки с map-fixed или array-map-fixed):
|
||||
Эти строки — контейнеры, в них вложены sub-params.
|
||||
Если Description пустое — напиши 1 предложение: что содержит группа и зачем.
|
||||
Смотри на имена sub-params (они идут в следующих строках) + MAN.
|
||||
Пример: clusterConfiguration с sub-params cpu/memory/disk/replicas
|
||||
→ «Ресурсы пода кластера: CPU (milicores), RAM (MB), диск (GB) и количество реплик»
|
||||
Пример: backupConfiguration с sub-params s3_uid/retain/schedule
|
||||
→ «Параметры резервного копирования: S3-хранилище, расписание и глубина хранения»
|
||||
|
||||
═══════════════════════════════════════════════════════
|
||||
ПРАВИЛА ДЛЯ ОПЕРАЦИЙ (_ops.md)
|
||||
═══════════════════════════════════════════════════════
|
||||
Операции без описания (пустая строка, нет текста после —):
|
||||
Напиши 1 предложение о том, что делает операция с ресурсом.
|
||||
Используй MAN если передан. Не придумывай параметров.
|
||||
|
||||
Универсальные шаблоны (если MAN не помогает):
|
||||
suspend → «Приостановка ресурса (поды остановлены, данные сохранены)»
|
||||
resume → «Запуск ранее остановленного ресурса»
|
||||
restart → «Перезапуск подов ресурса. ⚠️ Возможна кратковременная недоступность»
|
||||
reconcile → «Принудительная синхронизация состояния с API»
|
||||
recovery → «Восстановление из резервной копии»
|
||||
|
||||
═══════════════════════════════════════════════════════
|
||||
ПРАВИЛА ДЛЯ ГЛАВНОЙ СТРАНИЦЫ (Name.md — секция ## MAN)
|
||||
═══════════════════════════════════════════════════════
|
||||
Блок ## MAN содержит HTML внутри <div class="man-content">.
|
||||
Преобразуй HTML → читаемый Markdown:
|
||||
<h2>/<h3> → ## / ###
|
||||
<ul><li> → - элемент списка
|
||||
<strong> → **текст**
|
||||
<code> → `текст`
|
||||
<a href="url">текст</a> → [текст](url)
|
||||
<br/>, <p>, <div> → удали тег, замени переносами строк где нужно
|
||||
Лишние пустые строки подряд → одна пустая строка
|
||||
|
||||
Сохраняй всё смысловое содержание. Не перефразируй, не сокращай.
|
||||
|
||||
═══════════════════════════════════════════════════════
|
||||
ПРАВИЛА ДЛЯ ПРИМЕРОВ (_example.md)
|
||||
═══════════════════════════════════════════════════════
|
||||
Строки с TODO — замени на типичный реальный пример если он предсказуем:
|
||||
resource_name = "TODO" → "my-postgres"
|
||||
resource_realm = "TODO" → "k8s-3-sandbox-nubes-ru" # укажите ваш кластер
|
||||
master_ip_space = "TODO" → "internet-no-antiddos-v1" # из вашей организации
|
||||
slave_ip_space = "TODO" → "internet-no-antiddos-v1" # из вашей организации
|
||||
|
||||
Оставь TODO если значение непредсказуемо (UUID чужого ресурса):
|
||||
s3_uid = "TODO" → s3_uid = "TODO" # UUID ресурса nubes_s3 из state: nubes_s3.backup_store.id
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Новая логика USER-сообщения
|
||||
|
||||
```python
|
||||
# При обработке _params_create / _params_modify / _ops / _example:
|
||||
man_text = extract_man_section(service_main_md) # берём ## MAN из Name.md
|
||||
|
||||
prompt = f"""Тип файла: {file_type}
|
||||
Сервис: {service_name}
|
||||
|
||||
=== MAN СЕРВИСА (контекст для описаний групп и параметров) ===
|
||||
{man_text}
|
||||
|
||||
=== Файл для улучшения: {filename} ===
|
||||
{file_content}
|
||||
"""
|
||||
|
||||
# При обработке Name.md (главная):
|
||||
prompt = f"""Тип файла: ГЛАВНАЯ СТРАНИЦА
|
||||
Сервис: {service_name}
|
||||
|
||||
=== Файл для улучшения: {filename} ===
|
||||
{file_content}
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2 вопроса от Соннета
|
||||
|
||||
1. `max_tokens` сейчас `4096` — но `_params_create.md` для postgres ~4KB HTML. Поднять до `8192`?
|
||||
2. Писать в `docs_llm/` или сразу на место?
|
||||
|
||||
---
|
||||
|
||||
## Ответы
|
||||
|
||||
1. **max_tokens = 8192** — да. После обогащения описаниями файл станет больше.
|
||||
2. **Писать в `docs_llm/`** — не затирать сырой вывод docs-generator, нужен для отладки.
|
||||
@@ -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='<div style="font-size:11px;...">Этапы</div>';
|
||||
stages.forEach(s=>{
|
||||
const done=!!s.dtFinish; // этап завершён?
|
||||
const icon=done?(s.isSuccessful?'✅':'❌'):'⏳'; // ⏳ = текущий
|
||||
html+=`<div>${icon} ${s.stage} — ${(s.duration||0).toFixed(1)}s</div>`;
|
||||
});
|
||||
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`. ЭТО НЕ ТРОГАТЬ НИ ПРИ КАКИХ УСЛОВИЯХ.
|
||||
- ⛔ Логика поллинга (частота, таймауты) — не менять без согласования.
|
||||
- ⛔ Существующие сигнатуры функций — не менять без согласования.
|
||||
- ✅ Новая логика — новые функции/блоки, не ломать существующее.
|
||||
@@ -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 сек)
|
||||
- Таймауты
|
||||
- Сигнатуры функций
|
||||
@@ -0,0 +1,65 @@
|
||||
# Sonnet: ответы по тестированию stages без реальных инстансов
|
||||
|
||||
## 1. Мок API
|
||||
|
||||
Достаточно мокать только `/instanceOperations/{uid}?fields=...`.
|
||||
|
||||
`waitForOperationFinish` делает ровно один тип запроса — `doRequest GET /instanceOperations/{uid}?fields=...`. Создаёшь `httptest.NewServer`, настраиваешь нужные ответы, конструируешь клиент:
|
||||
|
||||
```go
|
||||
c := &UniversalClient{
|
||||
HttpClient: &http.Client{},
|
||||
ApiEndpoint: testServer.URL,
|
||||
ApiToken: "test-token",
|
||||
}
|
||||
```
|
||||
|
||||
Мокать create → params → run **не нужно** — вызываешь `waitForOperationFinish` напрямую с любым `opUid`.
|
||||
|
||||
## 2. Время
|
||||
|
||||
Проблема: `time.NewTicker(5 * time.Second)` захардкожен, таймаут 30 минут.
|
||||
|
||||
**Решение: добавить поле `PollInterval time.Duration` в `UniversalClient`.**
|
||||
Нулевое значение → default 5s. В тесте ставить 1ms.
|
||||
|
||||
`time.Now()` мокать **не нужно** — `timeout` уже параметр функции. В тесте передаёшь `200ms`, дедлайн считается `time.Now().Add(200ms)` — реальное время, работает нормально.
|
||||
|
||||
## 3. Перехват вывода tty
|
||||
|
||||
**Рекомендуемый подход: добавить поле `Writer io.Writer` в `UniversalClient`.**
|
||||
В `waitForOperationFinish` заменить `ttyOut()` на: `if c.Writer != nil { w = c.Writer } else { w = ttyOut() }`.
|
||||
В тесте: `c.Writer = &bytes.Buffer{}` → проверяешь что именно напечатано. В проде: оставляешь nil → поведение не меняется.
|
||||
|
||||
`os.Pipe()` + подмена `os.Stderr` **не рекомендует** — глобальное состояние, ломается при параллельных тестах.
|
||||
|
||||
## 4. Конкретные тест-кейсы
|
||||
|
||||
| # | Сценарий | Mock отдаёт | Ожидаем |
|
||||
|---|----------|-------------|---------|
|
||||
| 1 | Stages по одному | poll 1: 1 stage без dtFinish; poll 2: тот же stage с dtFinish; poll 3: операция с dtFinish+isSuccessful=true | `err == nil` |
|
||||
| 2 | Все stages сразу | poll 1: dtFinish + isSuccessful=true + все stages завершены | `err == nil`, Writer содержит `[OK]` для каждого |
|
||||
| 3 | Fail на этапе 3 | stages[0,1] OK, stages[2] isSuccessful=false, операция dtFinish + isSuccessful=false + errorLog="disk error" | `err` содержит `"disk error"` |
|
||||
| 4 | API 500 | всегда 500 (doRequest уже делает 3 retry внутри) | `err != nil` |
|
||||
| 5 | Таймаут | никогда не отдаёт dtFinish | `err` содержит `"не завершилась за установленный таймаут"` |
|
||||
| 6 | Отмена ctx | poll завис | `err` содержит `"отменена"` |
|
||||
|
||||
## 5. Рефакторинг
|
||||
|
||||
**Да, минимально — два новых поля в `UniversalClient`:**
|
||||
|
||||
```go
|
||||
type UniversalClient struct {
|
||||
// ... существующие поля ...
|
||||
PollInterval time.Duration // 0 = default 5s
|
||||
Writer io.Writer // nil = ttyOut()
|
||||
}
|
||||
```
|
||||
|
||||
В `waitForOperationFinish` (~3 строки правки):
|
||||
- `interval := c.PollInterval; if interval == 0 { interval = 5*time.Second }`
|
||||
- `w := c.Writer; if w == nil { w = ttyOut(); defer w.(*os.File).Close() }`
|
||||
|
||||
Не ломает существующие вызовы (оба поля zero-value совместимы с текущим поведением).
|
||||
|
||||
**Что НЕ нужно менять:** сигнатуру функции, логику дедлайна через `time.Now()`, retry в `doRequest`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# DeepSeek Flash: ответы по тестированию stages
|
||||
|
||||
См. HISTORY/SONNET/stages_testing_question.md — исходные вопросы.
|
||||
|
||||
## 1. Мок API
|
||||
|
||||
**Да, достаточно httptest.NewServer.** Только `/instanceOperations/{uid}?fields=...`. Цепочку create→params→run не мокать.
|
||||
|
||||
```go
|
||||
client := &UniversalClient{
|
||||
HttpClient: srv.Client(),
|
||||
ApiEndpoint: srv.URL,
|
||||
ApiToken: "test",
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Время
|
||||
|
||||
**Поле PollInterval time.Duration** в UniversalClient. 0 → default 5s. В тесте 1ms.
|
||||
|
||||
Таймаут: передать маленький timeout параметром (например 30ms). time.Now мокать не нужно.
|
||||
|
||||
## 3. Перехват tty
|
||||
|
||||
Вариант B — os.Pipe + подмена os.Stderr. Минус: не потокобезопасно (без t.Parallel() ок).
|
||||
|
||||
Вариант C (чище) — вынести печать в метод printStages(w io.Writer, ...), тогда ttyOut для продешкна, bytes.Buffer для теста.
|
||||
|
||||
## 4. Тест-кейсы (5 шт)
|
||||
|
||||
| # | Кейс | Выдача сервера |
|
||||
|---|------|----------------|
|
||||
| 1 | stages по одному | счётчик вызовов: stage1 без dtFinish → stage1 с dtFinish, stage2 без → всё dtFinish |
|
||||
| 2 | все сразу | первый ответ: всё dtFinish, isSuccessful=true |
|
||||
| 3 | fail на этапе 3 | stage3: isSuccessful=false, errorLog="boom" |
|
||||
| 4 | API 500 → retry | 500 → 200. ⚠️ doRequest retry 2s→4s→8s, нужен RetryBaseDelay |
|
||||
| 5 | таймаут | всегда isInProgress=true, dtFinish пустой |
|
||||
|
||||
## 5. Рефакторинг
|
||||
|
||||
**Да, минимально. НО:** в коде стоит ⛔ запрет на правку без оператора.
|
||||
|
||||
Безопасный минимум:
|
||||
- PollInterval (0 → 5s default)
|
||||
- printStages(w io.Writer) — вынос вывода
|
||||
- RetryBaseDelay (0 → 2s default) — для кейса 500
|
||||
|
||||
Альтернатива без рефакторинга: тестировать с реальными 5s тиками — медленно.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Sonnet v2: ответы по тестированию stages (после исправленного брифа)
|
||||
|
||||
## Q1. Мок API
|
||||
Только `/instanceOperations/{uid}?fields=...`. `httptest.NewServer` + `HttpClient`/`ApiEndpoint`.
|
||||
|
||||
## Q2. Время
|
||||
Два hardcoded таймера: `time.NewTicker(5s)` и `doRequest` delays (2s→4s→8s).
|
||||
- Без рефакторинга: тесты медленные (~5-19s)
|
||||
- С рефакторингом: поле `tickerInterval` — но ⛔ запрещает менять частоту опроса
|
||||
|
||||
## Q3. Перехват tty
|
||||
- Без рефакторинга: вывод в stderr, не ломает тесты
|
||||
- С рефакторингом: поле `ttyWriter io.Writer`
|
||||
|
||||
## Q4. Тест-кейсы (5 шт)
|
||||
1. stages по одному — `atomic.Int32` счётчик
|
||||
2. все сразу
|
||||
3. фейл — `IsSuccessful=false`, `ErrorLog="disk error"`
|
||||
4. **503 (не 500!)** — 500 не retryable. 503 — retryable. Тест ~19s без рефакторинга
|
||||
5. таймаут — `timeout=6s`
|
||||
|
||||
## Q5. Рефакторинг
|
||||
Без рефакторинга — всё тестируемо, но медленно.
|
||||
С рефакторингом — 6 строк, не в protected zone, но `tickerInterval` требует согласования (⛔).
|
||||
|
||||
## Итог
|
||||
| Тест | Без рефакторинга | С рефакторингом |
|
||||
|---|---|---|
|
||||
| sequential | 15s | 150ms |
|
||||
| all_at_once | 5s | 50ms |
|
||||
| fail | 5s | 50ms |
|
||||
| 503_retry | 19s | быстро |
|
||||
| timeout | 5s | 100ms |
|
||||
| tty capture | ❌ | ✅ |
|
||||
@@ -0,0 +1,53 @@
|
||||
# Sonnet: тестирование stages без реальных инстансов
|
||||
|
||||
Реализовали вывод этапов в `waitForOperationFinish()`. Код в репе.
|
||||
Нужно протестировать БЕЗ создания реальных инстансов в облаке.
|
||||
|
||||
---
|
||||
|
||||
## ⛔ Файлы — ПРОЧИТАТЬ ОБЯЗАТЕЛЬНО (иначе ответ будет неполным)
|
||||
|
||||
| # | Файл | Строки | Что искать |
|
||||
|---|------|--------|------------|
|
||||
| 1 | `provider/internal/core/client.go` | 29–55 | `UniversalClient` struct: поля `HttpClient`, `ApiEndpoint`, `ApiToken` |
|
||||
| 2 | `provider/internal/core/client.go` | 895–1010 | `waitForOperationFinish()` — ВСЯ функция от сигнатуры до закрывающей `}` |
|
||||
| 3 | `provider/internal/core/client.go` | 895–910 | ⛔ «ЗАПРЕЩЕНО ПРАВИТЬ ЭТОТ КОД БЕЗ ЯВНОГО СОГЛАСОВАНИЯ» |
|
||||
| 4 | `provider/internal/core/client.go` | 900–930 | `ttyOut()`, `opStage` struct, `operationStatusResponse` struct |
|
||||
| 5 | `provider/internal/core/client.go` | 1030–1140 | `doRequest()` — retry-логика, паузы **2s → 4s → 8s**, `maxRetries=3` |
|
||||
| 6 | `provider/internal/core/client_test.go` | весь файл | как создаётся `UniversalClient`, структура существующих тестов |
|
||||
|
||||
---
|
||||
|
||||
## Вопросы
|
||||
|
||||
### 1. Мок API
|
||||
Достаточно ли `httptest.NewServer` ТОЛЬКО для `/instanceOperations/{uid}?fields=...`?
|
||||
Или нужно мокать всю цепочку create → params → run → poll?
|
||||
|
||||
### 2. Время
|
||||
`time.NewTicker(5*time.Second)`, таймаут 30 мин. Как ускорить тест?
|
||||
Dependency injection? Mock `time.Now`?
|
||||
|
||||
### 3. Перехват tty
|
||||
`ttyOut()` → `/dev/tty` с fallback на `os.Stderr`. Как перехватить вывод?
|
||||
|
||||
### 4. Тест-кейсы
|
||||
Какие тесты написать:
|
||||
- stages по одному (прогресс)
|
||||
- stages все сразу
|
||||
- фейл на этапе N
|
||||
- API 500 → retry (**учти задержки в `doRequest`: 2s → 4s → 8s**)
|
||||
- таймаут операции
|
||||
|
||||
### 5. Рефакторинг
|
||||
Стоит ли рефакторить `waitForOperationFinish`?
|
||||
**Учти ⛔ запрет на правку (стр. 895–910).**
|
||||
|
||||
---
|
||||
|
||||
## Ожидаемый ответ
|
||||
|
||||
1. Конкретный ответ на каждый вопрос
|
||||
2. Unified diff минимальных изменений (если нужен рефакторинг)
|
||||
3. Псевдокод каждого тест-кейса
|
||||
4. Оценка: что можно без рефакторинга, что потребует правок
|
||||
@@ -0,0 +1,108 @@
|
||||
# Prompt для Gemini: диаграмма пайплайна IoT Demo
|
||||
|
||||
## Задача
|
||||
|
||||
Создай красивую диаграмму (flowchart/архитектурную схему) пайплайна данных для IoT-демонстрации «Умный дом». Нужна **одна картинка** с 6 блоками сервисов и стрелками между ними.
|
||||
|
||||
---
|
||||
|
||||
## Сервисы (блоки)
|
||||
|
||||
### 1. Producer (верхний левый угол)
|
||||
- **Название:** Producer
|
||||
- **Подпись:** Node.js · Генератор IoT-событий
|
||||
- **Форма:** прямоугольник, синий/голубой (#3B82F6 или похожий)
|
||||
- **Иконка:** ⚙️ или 📡
|
||||
- **Что делает:** Эмулирует 8 IoT-датчиков (температура °C, влажность %, энергопотребление kW), каждые 3 секунды генерирует случайное JSON-событие
|
||||
|
||||
### 2. RabbitMQ (центр-верх)
|
||||
- **Название:** RabbitMQ
|
||||
- **Подпись:** Очередь сообщений · iot-events
|
||||
- **Форма:** прямоугольник, оранжевый (#FF6600 — брендовый цвет RabbitMQ)
|
||||
- **Иконка:** 🐰 или 📬
|
||||
- **Что делает:** Буфер сообщений AMQP. Принимает от Producer, отдаёт Consumer. Durable — не теряет при рестарте.
|
||||
|
||||
### 3. Consumer (правый верхний)
|
||||
- **Название:** Consumer
|
||||
- **Подпись:** Node.js · Обработчик
|
||||
- **Форма:** прямоугольник, зелёный (#10B981)
|
||||
- **Иконка:** 📥 или 🔄
|
||||
- **Что делает:** Читает очередь RabbitMQ, параллельно пишет в Redis и MongoDB
|
||||
|
||||
### 4. Redis (левый-центр, под Producer)
|
||||
- **Название:** Redis
|
||||
- **Подпись:** Кэш · latest + counters + recent
|
||||
- **Форма:** прямоугольник, красный (#DC2626 — брендовый цвет Redis)
|
||||
- **Иконка:** ⚡ или 🗄️
|
||||
- **Что делает:** Оперативный кэш для Dashboard. Хранит: последние значения датчиков (hash), счётчики по типам (hash), ленту 1000 событий (zset).
|
||||
|
||||
### 5. MongoDB (центр-низ)
|
||||
- **Название:** MongoDB
|
||||
- **Подпись:** Архив · TTL 7 дней
|
||||
- **Форма:** прямоугольник, тёмно-зелёный (#00684A — брендовый цвет MongoDB)
|
||||
- **Иконка:** 🍃 или 💾
|
||||
- **Что делает:** Постоянный архив всех событий в коллекции iot_events. Автоудаление через 7 дней (TTL-индекс).
|
||||
|
||||
### 6. Dashboard (правый-низ)
|
||||
- **Название:** Dashboard
|
||||
- **Подпись:** Node.js + Chart.js · Веб-интерфейс
|
||||
- **Форма:** прямоугольник, фиолетовый (#8B5CF6)
|
||||
- **Иконка:** 📊 или 🖥️
|
||||
- **Что делает:** Читает Redis, отдаёт JSON API и HTML с графиками. Bar chart (значения), Doughnut (счётчики), таблица событий. Автообновление каждые 3 сек.
|
||||
|
||||
---
|
||||
|
||||
## Стрелки (потоки данных)
|
||||
|
||||
| От | К | Протокол | Подпись на стрелке |
|
||||
|---|---|---|---|
|
||||
| Producer | RabbitMQ | AMQP (порт 5672) | JSON-события → очередь iot-events |
|
||||
| RabbitMQ | Consumer | AMQP (порт 5672) | JSON-события ← очередь iot-events |
|
||||
| Consumer | Redis (latest) | TCP (порт 6379) | HSET sensor_id → значение |
|
||||
| Consumer | Redis (counters) | TCP (порт 6379) | HINCRBY device_type |
|
||||
| Consumer | Redis (recent) | TCP (порт 6379) | ZADD лента событий |
|
||||
| Consumer | MongoDB | TCP (порт 27017) | insertOne → коллекция iot_events |
|
||||
| Redis | Dashboard | TCP (порт 6379) | HGETALL + ZRANGE (только чтение) |
|
||||
|
||||
---
|
||||
|
||||
## Расположение на схеме
|
||||
|
||||
```
|
||||
Producer ──→ RabbitMQ ──→ Consumer
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
↓ ↓ ↓
|
||||
Redis MongoDB Redis
|
||||
(latest, (архив) (counters,
|
||||
recent) ...)
|
||||
│ │ │
|
||||
└──────────────┼──────────────┘
|
||||
↓
|
||||
Dashboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Стиль
|
||||
|
||||
- **Тёмный фон** (#0F172A или похожий) — как у самого дашборда
|
||||
- **Блоки** с закруглёнными углами (border-radius 8–12px)
|
||||
- **Цвета блоков** — брендовые цвета каждого сервиса (см. выше)
|
||||
- **Текст в блоках** — белый или светлый
|
||||
- **Стрелки** — с подписями протоколов (AMQP, TCP) и названиями данных
|
||||
- **Иконки** в блоках приветствуются
|
||||
- **Легенда** внизу: протоколы (AMQP — оранжевая стрелка, TCP — серая стрелка)
|
||||
- Заголовок схемы: «IoT Demo — Умный дом» и подзаголовок «6 сервисов Nubes Cloud»
|
||||
|
||||
---
|
||||
|
||||
## Дополнительно
|
||||
|
||||
Можно добавить разделительную линию (или фон) показывающую, что все сервисы внутри Kubernetes-кластера, а наружу торчит только Dashboard (HTTP/HTTPS) и Producer/Consumer (для health-check).
|
||||
|
||||
---
|
||||
|
||||
## Формат
|
||||
|
||||
Сгенерируй **SVG** или **PNG**, который можно вставить в README.md.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Промпт для Gemini — ФИНАЛЬНАЯ ПРАВКА
|
||||
|
||||
У тебя уже есть схема. В ней **две ошибки**, исправь только их:
|
||||
|
||||
## Ошибка 1: Dashboard соединён с MongoDB — УБРАТЬ
|
||||
|
||||
Никакой связи между Dashboard и MongoDB нет. Dashboard читает ТОЛЬКО Redis. Убери эту стрелку.
|
||||
|
||||
## Ошибка 2: Нет связи Consumer → MongoDB
|
||||
|
||||
Consumer пишет в MongoDB (insertOne). Добавь стрелку Consumer → MongoDB с подписью "TCP · insertOne · архив 7 дн".
|
||||
|
||||
---
|
||||
|
||||
## Правильные связи (ВСЕ, ничего лишнего):
|
||||
|
||||
```
|
||||
Producer ──AMQP──→ RabbitMQ ──AMQP──→ Consumer
|
||||
│
|
||||
┌───────────┼───────────┐
|
||||
│ TCP │ TCP │ TCP
|
||||
▼ ▼ ▼
|
||||
Redis MongoDB Redis
|
||||
(HSET/ZADD) (insertOne) (HINCRBY)
|
||||
│
|
||||
│ TCP (только чтение)
|
||||
▼
|
||||
Dashboard ──HTTP──→ Браузер
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что МЕНЯТЬ в имеющейся схеме:
|
||||
|
||||
1. **Удали** стрелку между Dashboard и MongoDB (в любую сторону)
|
||||
2. **Добавь** стрелку Consumer → MongoDB (TCP, insertOne)
|
||||
3. **Проверь** что Consumer → Redis (TCP, три команды: HSET, HINCRBY, ZADD)
|
||||
4. **Проверь** что Redis → Dashboard (TCP, только чтение)
|
||||
5. Всё. Больше ничего не трогай.
|
||||
|
||||
Никаких других изменений. Только эти две правки.
|
||||
Reference in New Issue
Block a user