docs: пометить отменённый заход модификаторов как LEGACY + исправить ложные факты
- баннеры «ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО» на 4 файла HISTORY/OPUS/2026-09-22_modifier_* и docs/60_strategy/modifier_resources_ideology_and_specification.md - vIPConfigure: replace-семантика, НЕ накопительная (по тесту docs/ORG_IP_MODIFIER_TEST_2026-09-22.md) - обновлены ссылки на перенесённые материалы (docs/... -> NOTES/..., HOW_TO/...)
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
<!-- ⛔ LEGACY: deck-api.ngcloud.ru ЗАКРЫВАЕТСЯ. Актуальный API: lk-api-gateway.ngcloud.ru/api/v1/svc -->
|
||||
# Universal Rebuild — Архитектура и рабочая цепочка (актуально)
|
||||
|
||||
Документ для нового чата: описывает, как устроено «универсальное ядро», как формируются YAML‑спеки сервисов и как генерируются Go‑ресурсы. Учитывает ошибки/уроки из текущего чата.
|
||||
|
||||
---
|
||||
|
||||
## 0) Базовые правила работы
|
||||
- Не менять существующий Go‑код без явного согласования.
|
||||
- Новая логика — только новые функции/файлы (если не было явного разрешения на правку).
|
||||
- Все операции с облаком — read‑only, если отдельно не разрешено создание/удаление.
|
||||
- Для токенов: новый `access_token` сохранять в `/home/naeel/terra/HH-MM-SS.token`.
|
||||
|
||||
---
|
||||
|
||||
## 1) Архитектура (слои)
|
||||
|
||||
### 1.1 Универсальное ядро (core)
|
||||
**Папка:** `universal_rebuild/internal/core`
|
||||
- `client.go` — универсальный клиент API и общий flow операций.
|
||||
- **Критерий завершения операции:** `dtFinish` (см. комментарии в коде). Логику запрещено менять без согласования.
|
||||
|
||||
### 1.2 Провайдер (provider)
|
||||
**Папка:** `universal_rebuild/internal/provider`
|
||||
- Конфигурация провайдера, получение токена, подключение ресурсов через реестр.
|
||||
|
||||
### 1.3 Сгенерированные ресурсы
|
||||
**Папка:** `universal_rebuild/internal/resources_gen`
|
||||
- Автогенерируемые ресурсы Terraform по YAML‑спецификациям.
|
||||
- `registry.go` (генерируется) — регистрирует все ресурсы.
|
||||
- `crud.go` — общие CRUD‑хелперы (создание/modify/delete). Этот файл **не генерируется**, его нужно сохранять.
|
||||
|
||||
### 1.4 YAML‑спеки ресурсов
|
||||
**Папка:** `universal_rebuild/resources_yaml`
|
||||
- YAML для каждого сервиса. Источник истины для генератора ресурсов.
|
||||
- Формат включает `create.params`, `modify.params`, `lifecycle`.
|
||||
|
||||
### 1.5 Генераторы
|
||||
- **YAML‑генератор без instanceUid**
|
||||
- `universal_rebuild/tools/service_params_gen/main.go`
|
||||
- Получает параметры сервиса напрямую через `/index.cfm?endpoint=...`.
|
||||
- **Go‑генератор**
|
||||
- `universal_rebuild/tools/gen/main.go`
|
||||
- Читает YAML и генерирует ресурсы + `registry.go`.
|
||||
|
||||
### 1.6 Отдельные modifier-ресурсы
|
||||
Для parent-level операций `modify`, которые должны выполняться отдельным шагом Terraform-цепочки, используется `kind: modifier`.
|
||||
|
||||
Пример:
|
||||
|
||||
```yaml
|
||||
- name: modify
|
||||
kind: modifier
|
||||
action: modify
|
||||
modifier: ip_space
|
||||
params: []
|
||||
```
|
||||
|
||||
Такой блок не попадает в обычный instance CRUD. Go-генератор создаёт отдельный ресурс с именем `nubes_<service>_<modifier>`. Ресурс принимает ID родительского инстанса и параметры операции, выполняет parent `modify` при Create/Update и читает актуальные значения из `state_params` при Read.
|
||||
|
||||
Для `vcOrg` используется modifier `ip_space` с параметром `vIPConfigure`; для `vcNsxt` используется modifier `network` с параметрами операции сетевой настройки. Nested API-параметры modifier-ресурсов передаются как JSON-строки, поэтому их Terraform-значения должны быть валидным JSON.
|
||||
|
||||
Удаление modifier пока является no-op: подтверждённого обратного payload для отмены выделенных IP или SNAT нет. Операции удаления родительского сервиса не являются rollback и намеренно не вызываются.
|
||||
|
||||
---
|
||||
|
||||
## 2) Как получить параметры сервиса (без instanceUid)
|
||||
|
||||
Источник описан в:
|
||||
- `docs/40_analysis/har/discovery/service_parameters_fetch.md`
|
||||
|
||||
API‑цепочка:
|
||||
1) `GET /api/v1/index.cfm?endpoint=/services/{svcId}`
|
||||
- даёт список операций сервиса (`operations`)
|
||||
2) `GET /api/v1/index.cfm?endpoint=/serviceOperation/{svcOperationId}`
|
||||
- даёт `cfsParams` (id, code, type, required, default, valueList, refSvcId, func)
|
||||
3) (опц.) `GET /api/v1/index.cfm?endpoint=/param-value-list/{svcOperationCfsParamId}`
|
||||
|
||||
**Почему так:** прямых эндпойнтов на список параметров по `service_id` нет. Параметры извлекаются из описаний операций.
|
||||
|
||||
---
|
||||
|
||||
## 3) YAML‑генерация (service_params_gen)
|
||||
|
||||
**Файл:** `universal_rebuild/tools/service_params_gen/main.go`
|
||||
|
||||
Входные переменные:
|
||||
- `NUBES_API_TOKEN` (если не задан — берётся из `test_universal/terraform.tfvars`)
|
||||
- `NUBES_API_ENDPOINT` (по умолчанию `https://deck-api.ngcloud.ru/api/v1/index.cfm`)
|
||||
- `NUBES_SERVICE_ID` (обязателен)
|
||||
- `NUBES_SERVICE_NAME` (опц.)
|
||||
- `NUBES_OUTPUT` (опц.)
|
||||
|
||||
Пример:
|
||||
```bash
|
||||
cd /home/naeel/terra/universal_rebuild
|
||||
NUBES_SERVICE_ID=1 NUBES_SERVICE_NAME=dummy go run ./tools/service_params_gen/main.go
|
||||
```
|
||||
|
||||
Результат:
|
||||
- `/home/naeel/terra/universal_rebuild/resources_yaml/dummy.yaml`
|
||||
|
||||
---
|
||||
|
||||
## 4) Go‑генерация (tools/gen)
|
||||
|
||||
**Файл:** `universal_rebuild/tools/gen/main.go`
|
||||
|
||||
Что делает:
|
||||
- читает все YAML из `resources_yaml/`
|
||||
- генерирует ресурсы в `internal/resources_gen/`
|
||||
- генерирует `registry.go`
|
||||
|
||||
Команда:
|
||||
```bash
|
||||
cd /home/naeel/terra/universal_rebuild
|
||||
go run ./tools/gen/main.go
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5) Build
|
||||
|
||||
```bash
|
||||
cd /home/naeel/terra/universal_rebuild
|
||||
go build -o terraform-provider-nubes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6) Важные нюансы и ошибки (из опыта чата)
|
||||
|
||||
### 6.1 resourceRealm
|
||||
- `resourceRealm` **должен задаваться пользователем в .tf**, если параметр required.
|
||||
- Нельзя автоподставлять `serviceName` как realm: API отклонит (пример с Postgres).
|
||||
|
||||
### 6.2 Warnings о дубликатах
|
||||
- Предупреждение о `RESOURCE WITH SAME NAME EXISTS` должно появляться только на create (когда state.ID отсутствует).
|
||||
- Для managed ресурсов (ID уже есть) предупреждения быть не должно.
|
||||
|
||||
### 6.3 Deleted ресурсы
|
||||
- Если `explainedStatus=deleted` или `isDeleted=true` → ресурс считается отсутствующим, state должен очищаться.
|
||||
|
||||
### 6.4 Имена параметров
|
||||
- Генератор может создавать «разбитые» snake_case для CamelCase (например `resource_c_p_u`).
|
||||
- Это ожидаемо, но если критично — нужен отдельный маппинг (по согласованию).
|
||||
|
||||
---
|
||||
|
||||
## 7) Проверенная цепочка (dummy)
|
||||
|
||||
1) YAML:
|
||||
```bash
|
||||
NUBES_SERVICE_ID=1 NUBES_SERVICE_NAME=dummy go run ./tools/service_params_gen/main.go
|
||||
```
|
||||
2) Go‑код:
|
||||
```bash
|
||||
go run ./tools/gen/main.go
|
||||
```
|
||||
3) Build:
|
||||
```bash
|
||||
go build -o terraform-provider-nubes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8) Что делать дальше
|
||||
- Повторять пункты 3–5 для каждого сервиса:
|
||||
- `NUBES_SERVICE_ID=13 NUBES_SERVICE_NAME=bucket`
|
||||
- `NUBES_SERVICE_ID=90 NUBES_SERVICE_NAME=postgres`
|
||||
- Проверять YAML на корректность required‑параметров, `resourceRealm` и defaults.
|
||||
- Поднимать ресурсы в `.tf` и тестировать: create / modify / suspend / delete.
|
||||
|
||||
---
|
||||
|
||||
## 9) Где искать доп. материалы
|
||||
- Архитектура (старые, но полезные):
|
||||
- `docs/ai_universal_provider_gen.md`
|
||||
- `docs/00_overview/ai_universal_provider_gen.md`
|
||||
- API discovery:
|
||||
- `docs/40_analysis/har/discovery/service_parameters_fetch.md`
|
||||
|
||||
---
|
||||
|
||||
**Короткая формула:**
|
||||
`API (services → serviceOperation)` → `YAML` → `Go resources + registry` → `build` → `tests`.
|
||||
|
||||
---
|
||||
|
||||
## 10) Политика удаления (карантин)
|
||||
- Многие инстансы **нельзя удалять сразу**: из-за возможных данных действует **2‑недельный карантин**.
|
||||
- **Dummy** подпадает под эту политику.
|
||||
- Для `nubes_dummy` при **destroy** или удалении из манифеста выполняется **`suspend`**, а не `delete`.
|
||||
- При `apply`, если инстанс dummy в состоянии **suspend**, выполняется **`resume`**.
|
||||
@@ -0,0 +1,760 @@
|
||||
<!-- ⛔ LEGACY: deck-api.ngcloud.ru ЗАКРЫВАЕТСЯ. Актуальный API: lk-api-gateway.ngcloud.ru/api/v1/svc -->
|
||||
# Полный анализ кодовой базы и план развития
|
||||
|
||||
**Дата:** 2026-03-13
|
||||
**Проект:** Terraform Provider for Nubes Cloud
|
||||
**Версия провайдера:** dev (legacy) / 5.0.18 (universal_rebuild)
|
||||
**Go:** 1.24 / Terraform Plugin Framework: v1.16 (legacy), v1.8 (rebuild)
|
||||
|
||||
---
|
||||
|
||||
## Содержание
|
||||
|
||||
1. [Общая архитектура](#1-общая-архитектура)
|
||||
2. [Инвентаризация кода](#2-инвентаризация-кода)
|
||||
3. [Критические проблемы (P0)](#3-критические-проблемы-p0)
|
||||
4. [Серьёзные проблемы (P1)](#4-серьёзные-проблемы-p1)
|
||||
5. [Средний приоритет (P2)](#5-средний-приоритет-p2)
|
||||
6. [Анализ по слоям](#6-анализ-по-слоям)
|
||||
7. [Эволюция API-клиента](#7-эволюция-api-клиента)
|
||||
8. [Генератор кода v2](#8-генератор-кода-v2)
|
||||
9. [Соответствие provider_philosophy.md](#9-соответствие-provider_philosophymd)
|
||||
10. [Тестирование](#10-тестирование)
|
||||
11. [Безопасность](#11-безопасность)
|
||||
12. [Дорожная карта (Roadmap)](#12-дорожная-карта-roadmap)
|
||||
13. [Рекомендации по агенту/модели](#13-рекомендации-по-агентумодели)
|
||||
|
||||
---
|
||||
|
||||
## 1. Общая архитектура
|
||||
|
||||
### Два провайдера в одном репозитории
|
||||
|
||||
| Компонент | Каталог | Версия | Registry Address | Статус |
|
||||
|-----------|---------|--------|------------------|--------|
|
||||
| **Legacy Provider** | `/internal/`, `/main.go` | dev | `registry.terraform.io/nubes/nubes` | Ручной код, 13 ресурсов |
|
||||
| **Universal Provider** | `/universal_rebuild/` | 5.0.18 | `registry.kube5s.ru <!-- ⛔ LEGACY: registry.kube5s.ru ЗАКРЫТ. Актуальный хост: tf-registry.containerk8s.services.ngcloud.ru --> <!-- ⛔ LEGACY: registry.kube5s.ru <!-- ⛔ LEGACY: registry.kube5s.ru ЗАКРЫТ. Актуальный хост: tf-registry.containerk8s.services.ngcloud.ru --> ЗАКРЫТ. Актуальный хост: tf-registry.containerk8s.services.ngcloud.ru -->/nubes/nubes` | Генерируемый, ~50 ресурсов |
|
||||
|
||||
### Архитектурные слои
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ TERRAFORM CLI / HCL │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Provider Layer (provider.go) │
|
||||
│ ├── Resource Registration │
|
||||
│ ├── Auth (token / env / file) │
|
||||
│ └── HTTP Client Init │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Resource Layer │
|
||||
│ ├── Manual (vm, edge, vdc, vapp, postgres…) │ ← legacy, internal/provider/
|
||||
│ └── Generated (50+ services) │ ← universal_rebuild/internal/resources_gen/
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Core Layer │
|
||||
│ ├── UniversalClient (API V6 flow) │
|
||||
│ ├── Instance Lookup / State │
|
||||
│ ├── Operation Runner / Polling │
|
||||
│ └── Timeout Management │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ CRUD Layer (resources_core/) │
|
||||
│ ├── CreateResource / UpdateResource / Delete │
|
||||
│ ├── adoptExistingInstanceOnCreate() │
|
||||
│ ├── State Refresh / Output Mapping │
|
||||
│ └── Params Compare / Ref Resolution │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Nubes Cloud API (deck-api.ngcloud.ru/api/v1) │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Конвейер генерации
|
||||
|
||||
```
|
||||
API (live) YAML specs Go code + Docs
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
01_generate_yamls.sh → resources_yaml/*.yaml → 02_generate_*.sh
|
||||
(service_ops_gen) (gen_v2 + docs_template_gen_v2)
|
||||
│
|
||||
▼
|
||||
03_build_and_upload.sh → S3
|
||||
04_build_and_publish_docs.sh → S3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Инвентаризация кода
|
||||
|
||||
### Legacy Provider (`internal/`)
|
||||
|
||||
| Файл | LOC | Назначение | Качество |
|
||||
|------|-----|------------|----------|
|
||||
| `provider/provider.go` | 110 | Регистрация, auth, HTTP клиент | ⚠️ InsecureSkipVerify |
|
||||
| `provider/client_impl.go` | ~350 | HTTP-клиент (NubesClient) | Без retry, без polling |
|
||||
| `provider/vm_resource.go` | ~500 | VM lifecycle (лучший ресурс) | ✅ Полный CRUD + import |
|
||||
| `provider/edge_resource.go` | ~400 | Edge gateway | ✅ Полный flow |
|
||||
| `provider/vdc_resource.go` | ~300 | VDC с triggers | ✅ Multi-stage create |
|
||||
| `provider/vapp_resource.go` | ~250 | vApp | ✅ Базовый CRUD |
|
||||
| `provider/postgres_resource.go` | ~200 | Postgres | ⚠️ Только create/delete |
|
||||
| `provider/s3bucket_resource.go` | ~200 | S3 bucket | ✅ CRUD |
|
||||
| `provider/pgadmin_resource.go` | ~150 | PgAdmin | ⚠️ Ограниченный |
|
||||
| `provider/org_resource.go` | ~100 | Organization | ⚠️ Минимальный |
|
||||
| `provider/quickstart_resource.go` | ~300 | Full-stack helper | Специальный |
|
||||
| `provider/tubulus_resource.go` | ~200 | AI/Gemini интеграция | Экспериментальный |
|
||||
| `provider/validators.go` | ~150 | Валидаторы | ✅ Хорошо |
|
||||
| `core/client.go` | 1065 | UniversalClient (V1–V6) | 🔴 Раздут, 6 версий |
|
||||
| `core/instance_lookup.go` | 87 | Поиск инстансов | ⚠️ Нет exact match |
|
||||
| `core/instance_ops.go` | 159 | Операции + polling | ⚠️ Fixed 5s interval |
|
||||
| **Итого** | **~4500** | | |
|
||||
|
||||
### Universal Rebuild (`universal_rebuild/`)
|
||||
|
||||
| Каталог | Файлов | LOC | Назначение |
|
||||
|---------|--------|-----|------------|
|
||||
| `internal/core/` | 5 | ~1400 | UniversalClient V6, timeouts, refSvc |
|
||||
| `internal/provider/` | 2 | ~300 | Provider setup, timeout embed |
|
||||
| `internal/resources_core/` | 16 | ~2500 | CRUD, state refresh, diagnostics, params |
|
||||
| `internal/resources_gen/` | ~100+ | ~15000+ | Сгенерированные ресурсы (50 сервисов) |
|
||||
| `resources_yaml/` | ~50 | — | YAML-спецификации сервисов |
|
||||
| `tools/gen_v2/` | 1 | ~2500 | Генератор Go-кода |
|
||||
| `tools/docs_template_gen_v2/` | 1 | ~800 | Генератор документации |
|
||||
| `tools/service_ops_gen/` | 1 | ~800 | API → YAML генератор |
|
||||
| `tools/service_params_gen/` | 1 | ~600 | Параметрический генератор |
|
||||
| **Итого** | **~180** | **~24000+** | |
|
||||
|
||||
### Generated Code Summary (`resources_gen/`)
|
||||
|
||||
| Тип ресурса | Кол-во | Примеры |
|
||||
|-------------|--------|---------|
|
||||
| Instance (CRUD) | ~50 | postgres, rabbitmq, kafka, k8s, vc_vm |
|
||||
| Subresource (user/db) | ~20 | postgres_user, postgres_database |
|
||||
| Action (restart/etc.) | ~10 | postgres_restart, postgres_recovery |
|
||||
| Registry | 1 | registry.go (auto-сгенерированный список) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Критические проблемы (P0)
|
||||
|
||||
### P0-1: InsecureSkipVerify=true в production
|
||||
|
||||
**Где:** `internal/provider/provider.go:103`
|
||||
|
||||
```go
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true, // ← MITM уязвимость
|
||||
}
|
||||
```
|
||||
|
||||
**Риск:** Атака "человек посередине" (MITM) — перехват API-токенов и данных.
|
||||
|
||||
**Решение:**
|
||||
```go
|
||||
// Новый атрибут провайдера:
|
||||
"insecure": schema.BoolAttribute{
|
||||
Optional: true,
|
||||
Description: "Skip TLS certificate verification (dev only)",
|
||||
},
|
||||
// + env var NUBES_INSECURE
|
||||
```
|
||||
|
||||
**Также проверить:** `universal_rebuild/internal/provider/provider.go` — аналогичная проблема.
|
||||
|
||||
---
|
||||
|
||||
### P0-2: Захардкоженный путь debug-лога
|
||||
|
||||
**Где:** `internal/core/client.go:18`
|
||||
|
||||
```go
|
||||
f, err := os.OpenFile("/home/naeel/terra/debug_nubes.log", ...)
|
||||
```
|
||||
|
||||
**Риск:** Сбой на любой другой машине. Потенциальная утечка данных в файл вне проекта.
|
||||
|
||||
**Решение:**
|
||||
- Использовать `tflog` (terraform plugin logging) вместо файлового лога
|
||||
- Или env var `NUBES_DEBUG_LOG` с fallback на `/tmp/nubes_debug.log`
|
||||
|
||||
---
|
||||
|
||||
### P0-3: Ноль автотестов
|
||||
|
||||
**Факт:** В репозитории не найдено ни одного `*_test.go` файла.
|
||||
|
||||
**Риск:**
|
||||
- Регрессии при правках генератора
|
||||
- Невозможно валидировать lifecycle-логику без ручной проверки
|
||||
- Нет CI/CD confidence
|
||||
|
||||
**Решение:** См. раздел [10. Тестирование](#10-тестирование).
|
||||
|
||||
---
|
||||
|
||||
### P0-4: 6 версий CreateGenericInstance в одном файле
|
||||
|
||||
**Где:** `internal/core/client.go` — 1065 строк, 6 методов.
|
||||
|
||||
| Версия | Строки | Статус |
|
||||
|--------|--------|--------|
|
||||
| V1 `CreateGenericInstance` | 52-168 | Legacy, не используется |
|
||||
| V2 `...Universal` | 195-334 | Legacy |
|
||||
| V3 `...UniversalV2` | 361-504 | Legacy |
|
||||
| V4 `...UniversalV3` | 531-676 | Legacy |
|
||||
| V5 `...UniversalV4` | 703-840 | Legacy |
|
||||
| V6 `...UniversalV5` | 867-1000 | Production |
|
||||
|
||||
**Риск:** Путаница — какой метод вызывать? Разная нормализация. Разные баги.
|
||||
|
||||
**Решение:**
|
||||
- V1–V5 — пометить `// Deprecated: use CreateGenericInstanceUniversalV5`
|
||||
- Убедиться, что все ресурсы используют V6/V5
|
||||
- В перспективе — удалить мёртвый код (после аудита вызовов)
|
||||
|
||||
---
|
||||
|
||||
## 4. Серьёзные проблемы (P1)
|
||||
|
||||
### P1-1: Lifecycle-флаги — legacy vs. canonical
|
||||
|
||||
**Требование (provider_philosophy.md §7-9):**
|
||||
- `adopt_existing_on_create` (default: `false`)
|
||||
- `suspend_on_destroy` (default: `true`)
|
||||
|
||||
**Реальность в legacy:**
|
||||
- `internal/generated/bolvan_resource_universal_lifecycle.go` использует `delete_mode` и `resume_if_exists`
|
||||
- Это прямо запрещено в стратегии
|
||||
|
||||
**Реальность в universal_rebuild:**
|
||||
- `resources_core/crud.go` использует `resumeIfExists` bool параметр
|
||||
- Генератор `gen_v2` генерирует канонические флаги `suspend_on_destroy`, `adopt_existing_on_create`
|
||||
- **Разрыв:** CRUD-слой принимает bool, но не полностью реализует decision matrix из §7
|
||||
|
||||
**Решение:**
|
||||
1. Обновить `crud.go` — полная реализация status-matrix:
|
||||
- `not created` → hard error
|
||||
- `creating/pending/failed` → hard error
|
||||
- `suspend` + `adopt=false` → hard error с диагностикой
|
||||
- `running` + `adopt=true` → adopt (import)
|
||||
- `running` + `adopt=false` → hard error "already exists"
|
||||
2. Legacy bolvanka — отдельная задача, не трогать
|
||||
|
||||
---
|
||||
|
||||
### P1-2: Read() — стабы в сгенерированных ресурсах
|
||||
|
||||
**Проблема:** Многие сгенерированные ресурсы имеют пустой `Read()`.
|
||||
|
||||
**Последствия:**
|
||||
- Terraform не видит state drift (облако изменилось, TF state устарел)
|
||||
- `terraform plan` после `apply` показывает расхождения
|
||||
- `terraform import` бесполезен без Read
|
||||
|
||||
**Уже решено в universal_rebuild?**
|
||||
Да, `state_refresh.go:RefreshResourceState()` обеспечивает полный read-back. Но нужно убедиться, что все сгенерированные ресурсы этот метод ВЫЗЫВАЮТ в своём Read().
|
||||
|
||||
---
|
||||
|
||||
### P1-3: Нет retry/backoff для API-вызовов
|
||||
|
||||
**Где:** Все HTTP-вызовы через `doRequest()` — один попытка, без retry.
|
||||
|
||||
**Реальный сценарий:**
|
||||
- API вернул 503 (maintenance) → terraform apply упал
|
||||
- Сетевой timeout → terraform apply упал
|
||||
- Rate limit (429) → terraform apply упал
|
||||
|
||||
**Решение:**
|
||||
```go
|
||||
// Добавить в core/client.go
|
||||
func (c *UniversalClient) doRequestWithRetry(ctx context.Context, ...) (*http.Response, error) {
|
||||
maxRetries := 3
|
||||
backoff := 2 * time.Second
|
||||
for attempt := 0; attempt <= maxRetries; attempt++ {
|
||||
resp, err := c.doRequest(ctx, ...)
|
||||
if err == nil && resp.StatusCode < 500 && resp.StatusCode != 429 {
|
||||
return resp, nil
|
||||
}
|
||||
if attempt < maxRetries {
|
||||
time.Sleep(backoff * time.Duration(1<<attempt)) // exponential
|
||||
}
|
||||
}
|
||||
return lastResp, lastErr
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P1-4: Polling с фиксированным интервалом 5с
|
||||
|
||||
**Где:** `instance_ops.go:81` — `time.NewTicker(5 * time.Second)`
|
||||
|
||||
**Проблема:**
|
||||
- Для быстрых операций (suspend ~10с) — 5с интервал нормально
|
||||
- Для долгих (create VM ~5мин) — 5с создаёт лишние API-запросы
|
||||
- Нет adaptive polling (увеличение интервала со временем)
|
||||
|
||||
**Решение:**
|
||||
```go
|
||||
// Adaptive polling: 3s → 5s → 10s → 15s → 30s (max)
|
||||
intervals := []time.Duration{3*time.Second, 5*time.Second, 10*time.Second, 15*time.Second, 30*time.Second}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P1-5: FindInstanceByDisplayName — неточный поиск
|
||||
|
||||
**Где:** `instance_lookup.go:29`
|
||||
|
||||
**Проблема:** Ищет подстрокой по `displayName`, нет exact match. Если есть "mydb" и "mydb-test", может вернуть неверный инстанс.
|
||||
|
||||
**Решение:** Добавить exact match фильтр после получения результатов:
|
||||
```go
|
||||
if instance.DisplayName == displayName { // exact match
|
||||
return instance, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Средний приоритет (P2)
|
||||
|
||||
### P2-1: Дублирование client.go между legacy и universal_rebuild
|
||||
|
||||
Два файла `client.go`:
|
||||
- `internal/core/client.go` (1065 строк, V1–V6)
|
||||
- `universal_rebuild/internal/core/client.go` (1034 строк, V6 + refSvc)
|
||||
|
||||
**Решение:** Legacy provider постепенно заменяется universal_rebuild. Не рефакторить — просто фиксировать (freeze) legacy.
|
||||
|
||||
---
|
||||
|
||||
### P2-2: Update() не реализован в большинстве ресурсов
|
||||
|
||||
Сгенерированные ресурсы через `gen_v2` уже генерируют Update() если у сервиса есть `modify` операция. Ручные ресурсы (edge, vdc, org) — нет.
|
||||
|
||||
**Решение:** Для ручных ресурсов — оставить как есть (ForceNew), если modify не критичен. Для generated — уже работает.
|
||||
|
||||
---
|
||||
|
||||
### P2-3: Нет валидации YAML-спецификаций
|
||||
|
||||
Генератор `01_generate_yamls.sh` → YAML → `gen_v2` — нет промежуточной валидации YAML на корректность/полноту.
|
||||
|
||||
**Решение:** Добавить JSON Schema для YAML-спецификаций и валидировать перед генерацией.
|
||||
|
||||
---
|
||||
|
||||
### P2-4: Нормализация параметров — разные стратегии
|
||||
|
||||
| Версия | Стратегия normalization |
|
||||
|--------|------------------------|
|
||||
| V1-V2 | empty → empty |
|
||||
| V3-V4 | empty → `{}` (map) / `[]` (array) |
|
||||
| V5-V6 | TrimSpace + null-string + `\"\"` → empty |
|
||||
|
||||
**Решение:** Зафиксировать V6 behavior как единственный стандарт. Задокументировать.
|
||||
|
||||
---
|
||||
|
||||
### P2-5: Отсутствие structured logging
|
||||
|
||||
- `tflog` используется, но нет единого формата
|
||||
- Нет trace ID / correlation ID для chain запросов
|
||||
- Debug-лог в файл вместо terraform framework
|
||||
|
||||
**Решение:** Стандартизировать tflog с трейсами:
|
||||
```go
|
||||
tflog.Debug(ctx, "API request", map[string]interface{}{
|
||||
"method": "POST",
|
||||
"url": url,
|
||||
"instance_uid": uid,
|
||||
"operation": opName,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Анализ по слоям
|
||||
|
||||
### 6.1 Provider Layer
|
||||
|
||||
| Аспект | Legacy | Universal Rebuild |
|
||||
|--------|--------|-------------------|
|
||||
| Auth | token/env/file ✅ | token/env/file ✅ |
|
||||
| TLS | InsecureSkipVerify 🔴 | InsecureSkipVerify 🔴 |
|
||||
| HTTP Timeout | 300s | Configurable ✅ |
|
||||
| Resources | 13 (ручные) | 50+ (генерируемые) |
|
||||
| Schema | Hand-written | Generated from YAML ✅ |
|
||||
|
||||
### 6.2 Resource Layer
|
||||
|
||||
**Legacy ресурсы (ручные):**
|
||||
- VM — лучший: полный CRUD, import, status polling, рабочие timeout'ы
|
||||
- Edge — хороший: multi-step create, operation discovery
|
||||
- VDC — хороший: triggers для пересоздания
|
||||
- Postgres, PgAdmin, Org — минимальные, часть Methods не реализованы
|
||||
|
||||
**Universal ресурсы (generated):**
|
||||
- Полный CRUD для instance если есть create/modify/suspend operations
|
||||
- Subresource (user, database) с ForceNew если нет modify
|
||||
- Action (restart, recovery) с trigger field `run_id`
|
||||
- Lifecycle flags: `suspend_on_destroy`, `adopt_existing_on_create` ✅
|
||||
- State refresh через `RefreshResourceState` ✅
|
||||
|
||||
### 6.3 Core Layer
|
||||
|
||||
**Сильные стороны:**
|
||||
- Единый API flow (V6): create → init op → send params → validate → run → wait
|
||||
- Timeout management с override per-service
|
||||
- RefSvc resolution (linked services)
|
||||
- Operation polling с dtFinish проверкой
|
||||
|
||||
**Слабые стороны:**
|
||||
- Нет retry / circuit breaker
|
||||
- Фиксированный polling interval
|
||||
- 6 версий Create в legacy (мусор)
|
||||
- Debug log в файл (hardcoded path)
|
||||
|
||||
### 6.4 CRUD Layer (`resources_core/`)
|
||||
|
||||
**Сильные стороны:**
|
||||
- `adoptExistingInstanceOnCreate()` — базовая adopt-логика
|
||||
- `RefreshResourceState` — generic state sync с type conversion
|
||||
- `RequiredParamsMismatch()` — сравнение params при adopt
|
||||
- `resource_diagnostics.go` — форматированные диагностики
|
||||
|
||||
**Слабые стороны:**
|
||||
- Decision matrix из philosophy § 7 не полностью реализована
|
||||
- Delete behavior: "state_only" по умолчанию если пустая строка — нарушает `suspend_on_destroy=true` default
|
||||
- Нет обработки статусов `creating`, `pending`, `failed`
|
||||
|
||||
---
|
||||
|
||||
## 7. Эволюция API-клиента
|
||||
|
||||
```
|
||||
V1 (simple)
|
||||
│ + server defaults
|
||||
V2 (Universal)
|
||||
│ + name/code hints для normalization
|
||||
V3 (UniversalV2)
|
||||
│ + label field
|
||||
V4 (UniversalV3)
|
||||
│ + TrimSpace, null → empty
|
||||
V5 (UniversalV4)
|
||||
│ + debug file logging, `\"\"` handling
|
||||
V6 (UniversalV5) ← PRODUCTION (legacy provider)
|
||||
│
|
||||
└─→ V6 Rebuilt ← PRODUCTION (universal_rebuild)
|
||||
+ refSvc resolution
|
||||
+ configurable timeouts
|
||||
+ ensureInstanceCreated()
|
||||
+ operation timeout per-service
|
||||
```
|
||||
|
||||
**Рекомендация:**
|
||||
- Legacy V1-V5 → пометить deprecated, не удалять (immutability policy)
|
||||
- В перспективе: legacy provider замораживается, весь development идёт в `universal_rebuild`
|
||||
|
||||
---
|
||||
|
||||
## 8. Генератор кода v2
|
||||
|
||||
### Архитектура генератора
|
||||
|
||||
```
|
||||
tools/gen_v2/generate_resources_v2.go (~2500 LOC)
|
||||
│
|
||||
├── loadSpecs() → Парсинг YAML per-service
|
||||
│ ├── Instance operations → GenResource
|
||||
│ ├── Subresource operations → GenSubresource
|
||||
│ └── Action operations → GenAction
|
||||
│
|
||||
├── writeInstanceResource() → Go template → *_resource.go
|
||||
├── writeSubresource() → Go template → *_subresource.go
|
||||
├── writeActionResource() → Go template → *_action.go
|
||||
└── writeRegistry() → registry.go (список всех ресурсов)
|
||||
```
|
||||
|
||||
### Что генерирует
|
||||
|
||||
Для каждого сервиса от 1 до 3 файлов:
|
||||
|
||||
1. **Instance Resource** (`90_postgres_resource.go`):
|
||||
- `Schema()` — из YAML params (create + modify)
|
||||
- `Create()` → `resources_core.CreateResourceWithTimeout()`
|
||||
- `Read()` → `resources_core.RefreshResourceState()`
|
||||
- `Update()` → `resources_core.UpdateResourceWithTimeout()` (если есть modify)
|
||||
- `Delete()` → `resources_core.DeleteResourceWithTimeout()`
|
||||
- `ImportState()` → passthrough ID
|
||||
|
||||
2. **Subresource** (`90_postgres_user_resource.go`):
|
||||
- ForceNew для всех params (если нет modify_user)
|
||||
- Create → `RunOperationByCode("create_user")`
|
||||
- Delete → `RunOperationByCode("delete_user")`
|
||||
|
||||
3. **Action** (`90_postgres_restart.go`):
|
||||
- Trigger field `run_id` (PlanModifier: UseStateForUnknown)
|
||||
- Create → `RunOperationByCode("restart")`
|
||||
|
||||
### Сильные стороны генератора
|
||||
|
||||
- ✅ Единый YAML → Go pipeline
|
||||
- ✅ Canonical lifecycle flags (`suspend_on_destroy`, `adopt_existing_on_create`)
|
||||
- ✅ RefSvc resolution для linked services
|
||||
- ✅ ForceNew detection для create-only params
|
||||
- ✅ Type mapping (string/bool/int64)
|
||||
- ✅ Auto-registry generation
|
||||
|
||||
### Слабые стороны
|
||||
|
||||
- ❌ Не генерирует `*_test.go`
|
||||
- ❌ Нет go fmt / go vet на generated output
|
||||
- ❌ Нет YAML schema validation перед генерацией
|
||||
- ❌ Нет diff-отчёта (что изменилось при регенерации)
|
||||
- ⚠️ Template embedded inline (не отдельные `.tmpl` файлы) — сложно поддерживать при росте
|
||||
|
||||
---
|
||||
|
||||
## 9. Соответствие provider_philosophy.md
|
||||
|
||||
### Матрица статусов (§7)
|
||||
|
||||
| Статус инстанса | adopt=false | adopt=true | Реализовано? |
|
||||
|-----------------|-------------|------------|--------------|
|
||||
| Not found / Deleted | Create | Create | ✅ |
|
||||
| Suspended | **Hard error** | Resume + Adopt | ⚠️ Частично |
|
||||
| Running | **Hard error** | Adopt (import) | ⚠️ Частично |
|
||||
| Not Created | **Hard error** | **Hard error** | ❌ Нет проверки |
|
||||
| Creating/Pending | **Hard error** | **Hard error** | ❌ Нет проверки |
|
||||
| Failed | **Hard error** | **Hard error** | ❌ Нет проверки |
|
||||
|
||||
### Destroy behavior (§7)
|
||||
|
||||
| Флаг | Действие | Реализовано? |
|
||||
|------|----------|--------------|
|
||||
| `suspend_on_destroy=true` (default) | Suspend | ✅ |
|
||||
| `suspend_on_destroy=false` | State-only | ✅ |
|
||||
|
||||
### Diagnostics format (§8)
|
||||
|
||||
**Требуется:**
|
||||
```
|
||||
resource_name: mydb
|
||||
service_id: 90
|
||||
instance_uid: abc-123
|
||||
status: running
|
||||
status_raw: Running
|
||||
flag: adopt_existing_on_create = false
|
||||
decision: Error — instance already exists
|
||||
action: Set adopt_existing_on_create = true or rename resource
|
||||
```
|
||||
|
||||
**Реализовано:** `resource_diagnostics.go` реализует multi-line format, но не все поля и не все кейсы.
|
||||
|
||||
### GAP Analysis
|
||||
|
||||
| Требование | Статус | Файл |
|
||||
|------------|--------|------|
|
||||
| Canonical flags in schema | ✅ | gen_v2 templates |
|
||||
| Decision matrix on Create | ⚠️ 60% | crud.go |
|
||||
| Hard error on "Not Created" | ❌ | crud.go |
|
||||
| Hard error on "Creating/Pending/Failed" | ❌ | crud.go |
|
||||
| Multi-line diagnostics | ⚠️ 70% | resource_diagnostics.go |
|
||||
| Param mismatch check on adopt | ✅ | required_params_compare.go |
|
||||
| Plan messages with cloud status | ❌ | Not implemented |
|
||||
|
||||
---
|
||||
|
||||
## 10. Тестирование
|
||||
|
||||
### Текущее состояние
|
||||
|
||||
**Тестовых файлов:** 0
|
||||
**Unit tests:** 0
|
||||
**Integration tests:** 0
|
||||
**Acceptance tests:** 0
|
||||
|
||||
### План тестирования
|
||||
|
||||
#### Фаза 1: Unit Tests для Core (приоритет — P0)
|
||||
|
||||
| Тест | Файл | Покрытие |
|
||||
|------|------|----------|
|
||||
| `TestNormalizeUniversalValue` | `core/client_test.go` | Нормализация всех типов |
|
||||
| `TestIsInstanceDeleted` | `core/instance_lookup_test.go` | Все статусы |
|
||||
| `TestIsStatusSuspended` | `core/crud_test.go` | Edge cases |
|
||||
| `TestAdoptLogicMatrix` | `core/crud_test.go` | Все комбинации status × adopt flag |
|
||||
| `TestParamsMismatch` | `core/params_compare_test.go` | Сравнение params |
|
||||
| `TestRefreshResourceState` | `core/state_refresh_test.go` | Type conversion |
|
||||
| `TestOperationTimeoutParsing` | `core/timeouts_test.go` | Config loading |
|
||||
| `TestResolveRefSvcParam` | `core/refsvc_test.go` | UUID ↔ display_name |
|
||||
|
||||
**Оценка:** ~40 test cases, ~800 LOC
|
||||
|
||||
#### Фаза 2: Integration Tests (приоритет — P1)
|
||||
|
||||
| Тест | Описание |
|
||||
|------|----------|
|
||||
| `TestCreateAndDeleteInstance` | Full lifecycle на test stand |
|
||||
| `TestAdoptExistingInstance` | Create → suspend → re-create with adopt=true |
|
||||
| `TestModifyInstance` | Create → modify → verify params changed |
|
||||
| `TestSubresourceLifecycle` | User create → delete |
|
||||
| `TestActionExecution` | Restart trigger |
|
||||
|
||||
**Требуется:** Test stand (dev profile) + test service (dummy/bolvanka)
|
||||
|
||||
#### Фаза 3: Acceptance Tests (приоритет — P2)
|
||||
|
||||
```bash
|
||||
TF_ACC=1 go test ./internal/... -v -run TestAcc
|
||||
```
|
||||
|
||||
С реальным Terraform CLI: plan → apply → verify → destroy.
|
||||
|
||||
---
|
||||
|
||||
## 11. Безопасность
|
||||
|
||||
### Текущие уязвимости
|
||||
|
||||
| # | Уязвимость | OWASP | Severity | Где |
|
||||
|---|-----------|-------|----------|-----|
|
||||
| S1 | InsecureSkipVerify=true | A07:Crypto Failures | CRITICAL | provider.go:103 |
|
||||
| S2 | Hardcoded debug log path | A05:Security Misconfig | HIGH | core/client.go:18 |
|
||||
| S3 | Нет input validation на API responses | A03:Injection | MEDIUM | core/client.go |
|
||||
| S4 | Token в памяти без rotation | A07:Auth Failures | MEDIUM | provider.go |
|
||||
| S5 | Нет rate limiting | A04:Insecure Design | LOW | core/client.go |
|
||||
| S6 | Secrets в репозитории (secrets/) | A05:Security Misconfig | HIGH | secrets/ |
|
||||
|
||||
### Рекомендации по безопасности
|
||||
|
||||
1. **S1:** `insecure` flag в provider schema (default: false), env var `NUBES_INSECURE`
|
||||
2. **S2:** Удалить файловый debug log, использовать только `tflog`
|
||||
3. **S3:** Валидировать JSON responses на ожидаемые поля
|
||||
4. **S6:** Перенести secrets в vault / CI variables, добавить в `.gitignore`
|
||||
|
||||
---
|
||||
|
||||
## 12. Дорожная карта (Roadmap)
|
||||
|
||||
### Фаза 0: Стабилизация (текущая — P0 fixes)
|
||||
|
||||
| # | Задача | Объём | Зависимости |
|
||||
|---|--------|-------|-------------|
|
||||
| 0.1 | Сделать InsecureSkipVerify конфигурируемым | 25 LOC | — |
|
||||
| 0.2 | Заменить hardcoded debug log на tflog/env var | 15 LOC | — |
|
||||
| 0.3 | Пометить V1–V5 CreateGenericInstance deprecated | Комментарии | — |
|
||||
| 0.4 | Unit tests для core layer (Фаза 1) | ~800 LOC | — |
|
||||
| 0.5 | Добавить secrets/ в .gitignore | 1 строка | — |
|
||||
|
||||
### Фаза 1: Compliance с provider_philosophy.md
|
||||
|
||||
| # | Задача | Объём | Зависимости |
|
||||
|---|--------|-------|-------------|
|
||||
| 1.1 | Полная decision matrix в crud.go | ~100 LOC | 0.4 |
|
||||
| 1.2 | Hard error для "Not Created", "Creating", "Failed" | ~50 LOC | 1.1 |
|
||||
| 1.3 | Multi-line diagnostics для всех кейсов | ~100 LOC | 1.1 |
|
||||
| 1.4 | Plan messages с cloud status | ~80 LOC | 1.1 |
|
||||
| 1.5 | Integration test для adopt matrix | ~200 LOC | 1.1 |
|
||||
|
||||
### Фаза 2: Надёжность
|
||||
|
||||
| # | Задача | Объём | Зависимости |
|
||||
|---|--------|-------|-------------|
|
||||
| 2.1 | Retry с exponential backoff для API-вызовов | ~80 LOC | — |
|
||||
| 2.2 | Adaptive polling intervals | ~40 LOC | — |
|
||||
| 2.3 | Exact match в FindInstanceByDisplayName | ~10 LOC | — |
|
||||
| 2.4 | YAML schema validation перед генерацией | ~200 LOC | — |
|
||||
| 2.5 | go fmt + go vet в pipeline генерации | ~10 LOC | — |
|
||||
|
||||
### Фаза 3: Масштабирование
|
||||
|
||||
| # | Задача | Объём | Зависимости |
|
||||
|---|--------|-------|-------------|
|
||||
| 3.1 | Генерация *_test.go в gen_v2 | ~500 LOC | 2.4 |
|
||||
| 3.2 | Diff-отчёт при регенерации | ~200 LOC | — |
|
||||
| 3.3 | Acceptance tests (TF_ACC) | ~500 LOC | 1.5 |
|
||||
| 3.4 | Structured logging с trace ID | ~150 LOC | — |
|
||||
| 3.5 | CI pipeline (build → test → publish) | Config | 3.1, 3.3 |
|
||||
|
||||
### Фаза 4: Production Hardening
|
||||
|
||||
| # | Задача | Объём | Зависимости |
|
||||
|---|--------|-------|-------------|
|
||||
| 4.1 | Заморозить legacy provider (internal/) | Процесс | 3.5 |
|
||||
| 4.2 | Миграция ручных ресурсов в universal_rebuild | Большая | 4.1 |
|
||||
| 4.3 | Мониторинг generation stability | Infra | 3.5 |
|
||||
| 4.4 | Нагрузочное тестирование (параллельный apply) | ~200 LOC | 3.3 |
|
||||
|
||||
---
|
||||
|
||||
## 13. Рекомендации по агенту/модели
|
||||
|
||||
### Выбор модели для разных задач
|
||||
|
||||
| Тип задачи | Рекомендуемый агент | Почему |
|
||||
|-------------|---------------------|--------|
|
||||
| **Архитектурные решения** | Claude Opus 4.6 (текущий) | Глубокий контекст, сложная логика |
|
||||
| **Отладка сложных багов** | Claude Opus 4.6 | Лучше держит контекст, видит неочевидные связи |
|
||||
| **Анализ codebase, code review** | Claude Opus 4.6 | Качество анализа выше |
|
||||
| **Генерация Go-кода по шаблонам** | Claude Sonnet 4.6 | Достаточно для шаблонного кода, экономичнее |
|
||||
| **Написание тестов** | Claude Sonnet 4.6 | Шаблонная работа |
|
||||
| **Массовые правки в генераторе** | Claude Sonnet 4.6 | Достаточно контекста в одном файле |
|
||||
| **Правки shell-скриптов** | Claude Sonnet 4.6 | Простые правки |
|
||||
| **Документация** | Claude Sonnet 4.6 | Текстовая генерация |
|
||||
| **Lifecycle state machine** | Claude Opus 4.6 | Сложные state transitions |
|
||||
| **API reverse-engineering** | Claude Opus 4.6 | Нужен глубокий анализ HAR/JSON |
|
||||
|
||||
### Стратегия переключения
|
||||
|
||||
1. **Планирование и design review** → Opus 4.6
|
||||
2. **Имплементация запланированного** → Sonnet 4.6
|
||||
3. **Баг не воспроизводится / непонятная причина** → Opus 4.6
|
||||
4. **Регенерация и routine CI** → Sonnet 4.6
|
||||
|
||||
### Практический совет
|
||||
|
||||
Для текущей фазы развития (стабилизация + compliance):
|
||||
- **Opus 4.6** для задач 1.1–1.4 (lifecycle decision matrix — сложная логика)
|
||||
- **Sonnet 4.6** для задач 0.1–0.5, 2.1–2.5, 3.1 (шаблонные правки, тесты)
|
||||
|
||||
---
|
||||
|
||||
## Приложение A: Ключевые файлы для изучения
|
||||
|
||||
| Приоритет | Файл | Зачем |
|
||||
|-----------|------|-------|
|
||||
| ★★★ | `docs/60_strategy/provider_philosophy.md` | Канон lifecycle-логики |
|
||||
| ★★★ | `universal_rebuild/internal/resources_core/crud.go` | CRUD + adopt-логика |
|
||||
| ★★★ | `universal_rebuild/internal/core/client.go` | API V6 flow |
|
||||
| ★★☆ | `universal_rebuild/tools/gen_v2/generate_resources_v2.go` | Генератор |
|
||||
| ★★☆ | `universal_rebuild/internal/resources_core/state_refresh.go` | State sync |
|
||||
| ★★☆ | `universal_rebuild/internal/resources_core/resource_diagnostics.go` | Диагностики |
|
||||
| ★☆☆ | `devops/ARCHITECTURE.md` | Build pipeline design |
|
||||
| ★☆☆ | `internal/core/client.go` | Legacy reference (V1–V6) |
|
||||
|
||||
## Приложение B: Команды для быстрого старта
|
||||
|
||||
```bash
|
||||
# Проверка компиляции (universal_rebuild)
|
||||
cd universal_rebuild && go build ./...
|
||||
|
||||
# Проверка компиляции (legacy)
|
||||
cd /home/naeel/remote_dev/terraform && go build ./...
|
||||
|
||||
# Генерация YAML из API
|
||||
cd devops && bash 01_generate_yamls.sh
|
||||
|
||||
# Генерация Go-кода из YAML
|
||||
cd devops && bash 02_generate_resources_and_docs_template_v2.sh
|
||||
|
||||
# Сборка провайдера
|
||||
cd devops && bash 03_build_and_upload_provider.sh
|
||||
|
||||
# Публикация документации
|
||||
cd devops && bash 04_build_and_publish_docs.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Документ сформирован автоматически на основе полного анализа кодовой базы. Обновлять при существенных архитектурных изменениях.*
|
||||
@@ -0,0 +1,63 @@
|
||||
# Отчет о проблеме: Ошибка 500 при получении cfsParams для vc_vdc и план исправления
|
||||
|
||||
## 1. Проблема
|
||||
При создании виртуального дата-центра `nubes_vc_vdc` (сервис 21 `vc_vdc`, операция создания 9) Terraform завершается с ошибкой клиента:
|
||||
```text
|
||||
не удалось получить детали операции: ошибка API 500: Invalid call of the function [getResourceRealmConfig], first Argument [resourceRealm] is of invalid type, Cannot cast Object type [Struct] to a value of type [string]: the function is located at [/app/api/v1/resources/instance_operation_cfs_param.cfc]
|
||||
```
|
||||
|
||||
## 2. Анализ причины
|
||||
1. **Место падения в провайдере**: `provider/internal/core/client.go` (строка 272 в `createInstanceWithContext`):
|
||||
```go
|
||||
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("не удалось получить детали операции: %w", err)
|
||||
}
|
||||
```
|
||||
2. **Поведение бэкенда Nubes (Lucee/ColdFusion)**:
|
||||
- В файле `/app/api/v1/resources/instance_operation_cfs_param.cfc` при обработке запроса `?fields=cfsParams` для операции 9 вызывается функция `getResourceRealmConfig(resourceRealm)`.
|
||||
- Для сервиса `vc_vdc` поле `resourceRealm` в БД DEV-окружения хранится как комплексный объект (`Struct`), а не скалярная строка (`string`).
|
||||
- При попытке приведения типа `Struct -> string` бэкенд падает с HTTP 500.
|
||||
|
||||
3. **Сравнение с веб-интерфейсом (HAR/vdc.har)**:
|
||||
- В официальном веб-интерфейсе ЛК запрос `GET /instanceOperations/{opUid}?fields=cfsParams` **вообще не выполняется**.
|
||||
- Браузер выполняет строго следующий флоу:
|
||||
1. `POST /instanceOperations` -> получает `instanceOperationUid`
|
||||
2. `POST /instanceOperationCfsParams` -> отправляет каждое значение CFS-параметра
|
||||
3. `GET /instanceOperations/{opUid}/validate-cfs` -> валидация бэкендом
|
||||
4. `POST /instanceOperations/{opUid}/run` -> запуск операции в оркестраторе
|
||||
5. Polling `GET /instanceOperations/{opUid}` -> ожидание статуса завершения
|
||||
|
||||
4. **Зачем провайдер делает шаг 2**:
|
||||
- Шаг 2 в провайдере использовался исключительно для вызова `resolveRefSvcParamValues(ctx, opDetails.InstanceOperation.CfsParams, params)` — чтобы узнать `refSvcId` параметров и попробовать отрезолвить имена в UUID.
|
||||
- Для ресурса `nubes_vc_vdc` параметр `organization_uid` (CFS param 30, refSvc 19) **уже гарантированно отрезолвлен в UUID** до создания операции (на этапе `ModifyPlan` и в начале `Create`).
|
||||
- Остальные параметры `vc_vdc` (провайдер сети, профиль Provider VDC, CPU, RAM, резервирование, storage_config) являются скалярами/числами/JSON-строками и не содержат `refSvcId`.
|
||||
- Соответственно, данные запроса `?fields=cfsParams` для `vc_vdc` фактически не требуются.
|
||||
|
||||
## 3. План изменений в провайдере (что будем менять)
|
||||
|
||||
### Целевой файл: `provider/internal/core/client.go`
|
||||
В функции `createInstanceWithContext` (и при необходимости в `runInstanceOperationUniversalByCodeWithTimeout` / `updateResourceWithTimeout`) заменяется жёсткое падение на условный graceful fallback:
|
||||
|
||||
```go
|
||||
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
|
||||
if err != nil {
|
||||
// Проверяем, есть ли среди переданных строковых параметров не-UUID значения,
|
||||
// требующие резолвинга через refSvcId.
|
||||
// Если все строковые параметры уже UUID или числа/литералы — логируем предупреждение и продолжаем.
|
||||
c.logWarn(ctx, "не удалось получить cfsParams для операции %s (%v), продолжаем отправку параметров", opUid, err)
|
||||
} else {
|
||||
var opDetails universalOpResponse
|
||||
if err := json.Unmarshal(opDetailsResp, &opDetails); err == nil {
|
||||
params, err = c.resolveRefSvcParamValues(ctx, opDetails.InstanceOperation.CfsParams, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Критерии безопасности:
|
||||
1. Fallback условный: если бэкенд упал, но параметры уже валидны / являются UUID — выполнение продолжается.
|
||||
2. Логируется явный warning с идентификатором операции `opUid` и телом ошибки.
|
||||
3. Валидация значений параметров не теряется — её по-прежнему выполняет бэкенд на этапе `GET /validate-cfs`.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Отчет о работе: Исправление ошибки 500 при модификации VM
|
||||
|
||||
## 1. Проблема
|
||||
При попытке изменить ресурс `nubes_vm_instance` (например, изменить `vm_cpu`), Terraform получает ошибку 500 от API:
|
||||
```
|
||||
Status 500: {"ERROR":"Invalid call of the function [checkParam], 4th Argument [instanceOperationCfsParamUid] is of invalid type, Cannot cast String [] to a value of type [guid]","DETAIL":"the function is located at [/app/api/v1/resources/instance_operation_cfs_param_ls.cfc]"}
|
||||
```
|
||||
|
||||
## 2. Анализ причины
|
||||
Сообщение `Cannot cast String [] to a value of type [guid]` указывает на то, что бэкенд получает массив строк там, где ожидает одиночный GUID. Обычно это происходит в REST API, когда клиент отправляет несколько значений для одного ключа, или когда отправляется `POST` запрос для создания сущности, которая уже существует, и система дублирует параметр в список.
|
||||
|
||||
В отличие от создания (Create), операция модификации (Modify/Update) в Nubes API при инициализации (`GET /instanceOperations/...`) уже содержит текущие значения параметров (`CfsParams`).
|
||||
|
||||
Если мы безусловно используем метод `POST` для отправки параметров (как это было сделано в ресурсе Postgres), мы создаем дубликат параметра. По всей видимости, движок API (ColdFusion?) объединяет старое и новое значение в массив, что ломает валидацию `checkParam`.
|
||||
|
||||
## 3. Выполненные действия (Solution Attempt)
|
||||
|
||||
Я модифицировал файл `internal/provider/vm_resource.go`, полностью переписав функцию `submitVMOperationParams`.
|
||||
|
||||
**Суть изменений:**
|
||||
1. **Динамический маппинг**: Перед отправкой параметров провайдер теперь запрашивает детали операции (`GetInstanceOperation`).
|
||||
2. **Определение UID**: Мы строим карту существующих параметров: `ParamName -> { ID, UID, CurrentValue }`.
|
||||
3. **Гибридная логика PUT/POST**:
|
||||
* Если параметр **уже существует** в операции (есть `instanceOperationCfsParamUid`) -> Мы используем метод **`PUT`**.
|
||||
* URL: `/instanceOperationCfsParams`
|
||||
* Payload: включает `instanceOperationCfsParamUid`.
|
||||
* Если параметр **новый** (нет UID) -> Мы используем метод **`POST`**.
|
||||
* URL: `/instanceOperationCfsParams`
|
||||
* Payload: включает только `instanceOperationUid` и `svcOperationCfsParamId`.
|
||||
|
||||
### Фрагмент кода (internal/provider/vm_resource.go):
|
||||
```go
|
||||
if info.Uid != "" {
|
||||
// Update existing parameter -> PUT
|
||||
method = "PUT"
|
||||
payload = map[string]interface{}{
|
||||
"instanceOperationCfsParamUid": info.Uid,
|
||||
"svcOperationCfsParamId": info.Id,
|
||||
"instanceOperationUid": operationUid,
|
||||
"paramValue": np.Value,
|
||||
}
|
||||
} else {
|
||||
// Create new parameter -> POST
|
||||
method = "POST"
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Текущий статус
|
||||
Код компилируется и выполняется. Был обновлен провайдер (`terraform-provider-nubes`).
|
||||
Однако, при последнем запуске (`terraform apply`) ошибка 500 воспроизвелась снова с тем же текстом.
|
||||
|
||||
Это означает, что либо:
|
||||
1. Логика `PUT` тоже вызывает дублирование (возможно, API не поддерживает PUT для этого эндпоинта так, как мы ожидаем).
|
||||
2. Или ошибка `instanceOperationCfsParamUid is String[]` возникает из-за того, что я передаю этот UID в теле JSON, но API может ожидать его в URL или query params (хотя для PUT принято в теле или URL).
|
||||
|
||||
## 5. Планируемые дальнейшие шаги (остановлены)
|
||||
Для полного решения требовалось бы:
|
||||
1. Проверить работу `PUT` через curl с разными форматами payload.
|
||||
2. Попробовать передавать UID параметра в рамках URL (REST-style): `/instanceOperationCfsParams/{uid}`.
|
||||
|
||||
**Статус:** Работа остановлена по требованию пользователя. Код зафиксирован в текущем состоянии (Гибридный PUT/POST).
|
||||
@@ -0,0 +1,217 @@
|
||||
# Forensic Analysis: API Model to Ordinary YAML
|
||||
|
||||
## Цель
|
||||
|
||||
Исследовать существующую цепочку:
|
||||
|
||||
```text
|
||||
API model
|
||||
↓
|
||||
ordinary generator
|
||||
↓
|
||||
API YAML
|
||||
```
|
||||
|
||||
Цель этапа — получить подтверждённую картину движения данных и установить, что сохраняется, преобразуется или теряется до формирования API YAML.
|
||||
|
||||
Этот документ предназначен для передачи Opus перед анализом.
|
||||
|
||||
## Строгие ограничения
|
||||
|
||||
На этом этапе запрещено:
|
||||
|
||||
- изменять файлы;
|
||||
- писать код;
|
||||
- менять ordinary generator;
|
||||
- проектировать `ModifierSpec`;
|
||||
- проектировать `modifiers.yaml`;
|
||||
- проектировать `delete_rule`;
|
||||
- проектировать output layout или orchestration;
|
||||
- обсуждать inverse и dependency ordering;
|
||||
- придумывать API identifiers;
|
||||
- придумывать `parameter_path`;
|
||||
- придумывать HTTP method или payload structure;
|
||||
- считать API YAML полным источником данных без доказательства.
|
||||
|
||||
Если факт невозможно установить, его нужно обозначить как `unknown` или как требующий проверки фактическим API. Нельзя закрывать неизвестность архитектурным предположением.
|
||||
|
||||
## Что исследовать
|
||||
|
||||
### 1. Исходная API-модель
|
||||
|
||||
Установить:
|
||||
|
||||
- где находится canonical API model;
|
||||
- в каком формате она представлена;
|
||||
- как представлены services и operations;
|
||||
- как представлены параметры;
|
||||
- как представлены nested objects и arrays;
|
||||
- как представлены типы параметров;
|
||||
- существует ли стабильный operation ID;
|
||||
- существует ли стабильный parameter ID;
|
||||
- какие данные доступны до запуска ordinary generator.
|
||||
|
||||
### 2. Ordinary generator
|
||||
|
||||
Установить:
|
||||
|
||||
- какой input получает generator;
|
||||
- где он читает API-модель;
|
||||
- какие внутренние структуры строит;
|
||||
- какие преобразования выполняет;
|
||||
- какие поля нормализует или переименовывает;
|
||||
- какие поля вычисляет;
|
||||
- какие поля отбрасывает;
|
||||
- где формируется API YAML;
|
||||
- где именно могут происходить потери данных.
|
||||
|
||||
Обязательно различать:
|
||||
|
||||
```text
|
||||
данные отсутствуют уже в API
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
данные присутствуют в API, но теряются ordinary generator
|
||||
```
|
||||
|
||||
### 3. API YAML
|
||||
|
||||
Установить:
|
||||
|
||||
- какие поля сохраняются;
|
||||
- какие поля представлены иначе, чем в API-модели;
|
||||
- сохраняются ли nested objects и arrays;
|
||||
- сохраняются ли типы;
|
||||
- сохраняются ли operation identity и parameter identity;
|
||||
- сохраняются ли HTTP method и path, если они есть в исходной модели;
|
||||
- какие данные доступны будущему modifier layer;
|
||||
- какие данные потенциально доступны только в исходной API-модели.
|
||||
|
||||
## Обязательные трассировки
|
||||
|
||||
### `vc_org`
|
||||
|
||||
Отдельно проследить `vIPConfigure` и `count`:
|
||||
|
||||
```text
|
||||
API model
|
||||
→ generator input
|
||||
→ internal generator representation
|
||||
→ generator transformation
|
||||
→ API YAML
|
||||
```
|
||||
|
||||
Для каждого этапа указать:
|
||||
|
||||
- присутствует ли `vIPConfigure`;
|
||||
- присутствует ли `count`;
|
||||
- в каком типе они представлены;
|
||||
- в какой структуре находятся;
|
||||
- изменяются ли их имена или типы;
|
||||
- теряются ли они;
|
||||
- если теряются, в какой точке.
|
||||
|
||||
Не считать заранее известной структуру `vIPConfigure` или `count`.
|
||||
|
||||
### `vc_nsxt`
|
||||
|
||||
Отдельно проследить `ipSpaceName` и связанные параметры по той же цепочке:
|
||||
|
||||
```text
|
||||
API model
|
||||
→ generator input
|
||||
→ internal generator representation
|
||||
→ generator transformation
|
||||
→ API YAML
|
||||
```
|
||||
|
||||
Установить:
|
||||
|
||||
- где появляется `ipSpaceName`;
|
||||
- к какой operation или структуре он относится;
|
||||
- в каком типе представлен;
|
||||
- является ли обычным полем, nested field или частью массива;
|
||||
- какие связанные параметры находятся рядом;
|
||||
- сохраняется ли он в API YAML;
|
||||
- изменяются ли его значение или тип;
|
||||
- теряются ли связанные поля.
|
||||
|
||||
## Требования к доказательности
|
||||
|
||||
Каждый вывод разделять на:
|
||||
|
||||
- **Подтверждённый факт** — непосредственно виден из кода, структуры данных, фактического API input, generator input или API YAML.
|
||||
- **Неизвестное** — информация отсутствует или неоднозначна.
|
||||
- **Предположение** — не использовать как основание для архитектуры; только явно перечислять как неподтверждённое.
|
||||
|
||||
Для каждого важного вывода указывать конкретное основание: файл, функцию, структуру, входной документ или фрагмент YAML/JSON. Если точное место невозможно назвать, это указать как ограничение анализа.
|
||||
|
||||
## Формат итогового отчёта
|
||||
|
||||
Отчёт должен содержать только следующие разделы:
|
||||
|
||||
1. **Подтверждённые факты**
|
||||
2. **Исходная API-модель**
|
||||
3. **Как ordinary generator читает модель**
|
||||
4. **Как формируется API YAML**
|
||||
5. **Цепочка данных для `vc_org.vIPConfigure.count`**
|
||||
6. **Цепочка данных для `vc_nsxt.ipSpaceName` и связанных параметров**
|
||||
7. **Что сохраняется**
|
||||
8. **Что преобразуется или нормализуется**
|
||||
9. **Что теряется**
|
||||
10. **Где происходят потери**
|
||||
11. **Какие данные доступны в API YAML**
|
||||
12. **Какие данные доступны только в API-модели**
|
||||
13. **Неизвестные места**
|
||||
14. **Что необходимо проверить фактическим API**
|
||||
|
||||
## Критерий завершения
|
||||
|
||||
Forensic analysis завершён только тогда, когда для каждого потенциально необходимого modifier-элемента можно проследить происхождение:
|
||||
|
||||
```text
|
||||
API model
|
||||
→ generator input
|
||||
→ internal representation
|
||||
→ transformation
|
||||
→ API YAML
|
||||
```
|
||||
|
||||
без неизвестных промежуточных преобразований.
|
||||
|
||||
Для `vc_org` и `vc_nsxt` должны быть подтверждены:
|
||||
|
||||
```text
|
||||
operation identity
|
||||
parameter identity
|
||||
parameter structure
|
||||
parameter type
|
||||
API model representation
|
||||
generator representation
|
||||
YAML representation
|
||||
transformation
|
||||
loss or absence
|
||||
```
|
||||
|
||||
Если на существенном этапе остаётся `???`, исследование не завершено. Этот пункт нужно зафиксировать как `unknown`, а не проектировать решение.
|
||||
|
||||
## Главный принцип
|
||||
|
||||
Сначала:
|
||||
|
||||
```text
|
||||
исследовать
|
||||
↓
|
||||
зафиксировать факты
|
||||
↓
|
||||
зафиксировать неизвестное
|
||||
↓
|
||||
отличить отсутствие данных от потери данных
|
||||
```
|
||||
|
||||
Только после отдельного согласования forensic report можно переходить к проектированию `ModifierSpec`, `modifiers.yaml`, `delete_rule`, modifier generator, output boundary и orchestration.
|
||||
|
||||
На текущем этапе никаких решений по этим компонентам принимать нельзя.
|
||||
@@ -0,0 +1,234 @@
|
||||
# Forensic Analysis: API Model to Ordinary YAML
|
||||
|
||||
**Анализ от Gemini 3.8 flash.**
|
||||
|
||||
Исследование проведено строго в границах требований [docs/FORENSIC_ANALYSIS_BRIEF_2026-09-23.md](FORENSIC_ANALYSIS_BRIEF_2026-09-23.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Подтверждённые факты
|
||||
|
||||
- **Цепочка генерации YAML:** Инструмент `yaml-generator` ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go)) опрашивает live HTTP REST Gateway API Nubes по сети и сериализует результат в YAML-файлы спецификаций (`generated/{stand}/resources_yaml/{service_id}_{name}.yaml`), используя структуры контракта [TOOLS/lib/types.go](../TOOLS/lib/types.go).
|
||||
- **Спецификации сервисов в репозитории:** Канонические сгенерированные спеки сервисов физически размещены в [generated/dev/resources_yaml/](../generated/dev/resources_yaml/). В частности, [19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml) и [22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml).
|
||||
- **В `vc_org.yaml`:**
|
||||
- Операция `modify` имеет числовой ID `207`, `kind: instance`, `action: modify`.
|
||||
- Параметр `vIPConfigure` присутствует как параметр операции `modify` с числовым ID `662`, типом `data_type: array-map-fixed`, `required: true`.
|
||||
- Поле `count` присутствует внутри `sub_params` параметра `vIPConfigure` с числовым ID `40`, `data_type: integer > 0`, `required: true`, `is_modifiable: false`.
|
||||
- Поле `name` присутствует внутри `sub_params` параметра `vIPConfigure` с числовым ID `39`, `data_type: string`, `required: true`, `is_modifiable: false`.
|
||||
- **В `vc_nsxt.yaml`:**
|
||||
- Операция `modify` имеет числовой ID `111`, `kind: instance`, `action: modify`.
|
||||
- Параметр `ipSpaceName` присутствует в операции `modify` с числовым ID `372`, `data_type: string`, `required: false`, `sort: 50`.
|
||||
- С ним рядом в операции `modify` присутствуют:
|
||||
- `needEnableAVI` (ID `368`, `data_type: boolean`, `required: false`, `value_list: ["false", "true"]`);
|
||||
- `virtualServicesCount` (ID `369`, `data_type: integer > 0`, `required: false`, `minvalue: 1`, `maxvalue: 4`);
|
||||
- `qosProfile` (ID `856`, `data_type: string`, `required: false`);
|
||||
- `routedNetConfiguration` (ID `1112`, `data_type: map-fixed`, `required: true`, с вложенными подполями `ipAddrPool`, `mainDns`, `secondDns`).
|
||||
- **Генератор ресурсов (Ordinary Resource Generator):**
|
||||
- Расположен в [TOOLS/resource-generator/main.go](../TOOLS/resource-generator/main.go).
|
||||
- При `op.Kind == "instance"` (что установлено для `modify` в [generated/dev/resources_yaml/19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml) и [generated/dev/resources_yaml/22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml)) генератор объединяет параметры `create` и `modify` в схему одного общего ресурса `nubes_vc_org` / `nubes_vc_nsxt`.
|
||||
- Параметры, отсутствующие в операции `create`, но присутствующие в операции `modify` (как `vIPConfigure` в `vc_org`), не включаются в жизненный цикл `create`, а при отсутствии отдельной разметки `kind: modifier` в YAML генератор не создаёт под них отдельного ресурса модификатора.
|
||||
|
||||
---
|
||||
|
||||
## 2. Исходная API-модель
|
||||
|
||||
- **Источник и формат:** Модель получается HTTP-клиентом ([TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L44-L62)) по протоколу HTTP GET в формате JSON.
|
||||
- **Эндпоинты API:**
|
||||
- Метаданные сервиса и список операций: `GET /services/{svcId}` (возвращает `types.ServiceResponse`).
|
||||
- Метаданные конкретной операции: `GET /instanceOperations/default/{svcOperationId}` (возвращает `types.ServiceOperationResponse`).
|
||||
- **Идентификаторы операций и параметров:**
|
||||
- Операция идентифицируется стабильным числовым `svcOperationId` (int) и строковым именем `operation` (например, `"modify"`, `"create"`).
|
||||
- Параметры идентифицируются стабильным числовым `svcOperationCfsParamId` (int) и строковым кодом `svcOperationCfsParam` (например, `"vIPConfigure"`, `"ipSpaceName"`).
|
||||
- **Вложенные объекты и массивы (`dataDescriptor`):**
|
||||
- В ответе эндпоинта `/instanceOperations/default/{id}` сложная структура передаётся в поле `dataDescriptor: map[string]CfsSubParam`.
|
||||
- Каждое подполе имеет свой числовой `svcOperationCfsSubparamId`, строковый ключ (код подполя), `dataType`, `isRequired`, `isModifiableDefinition`, `defaultValue`, `valueList` (строка через запятую или массив).
|
||||
|
||||
---
|
||||
|
||||
## 3. Как ordinary generator читает модель
|
||||
|
||||
- **Входной поток:**
|
||||
`yaml-generator` вызывает `cli.GetService(svc.ID)` и перебирает список `info.Operations`.
|
||||
- **Чтение операций и параметров:**
|
||||
Для каждой операции вызывается `cli.GetServiceOperation(op.SvcOperationID)` ([TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L182-L240)).
|
||||
- **Преобразования и нормализация:**
|
||||
- Имена сервисов и операций нормализуются в snake_case функцией `normalize.Identifier` ([TOOLS/yaml-generator/internal/normalize/normalize.go](../TOOLS/yaml-generator/internal/normalize/normalize.go#L17-L50)).
|
||||
- Классификация операции: функция `classifyOperation` делит операции на `instance` (для `create`, `modify`, `delete`, `suspend`, `resume`), `subresource` (если есть символ подчеркивания) или `action`.
|
||||
- Разворачивание `dataDescriptor`: генератор обходит `map[string]CfsSubParam`, преобразует подполя в срез `types.ParamSpec` и детерминированно сортирует по `subParams[i].ID`.
|
||||
- Сортировка верхнеуровневых параметров по `params[i].ID`.
|
||||
- Сортировка операций по имени и ID.
|
||||
- **Что отбрасывается / не сохраняется в YAML:**
|
||||
- Конкретные HTTP method и URL-пути эндпоинтов API платформы (они зашиты в код клиента, в спек YAML не пишутся).
|
||||
- Вспомогательные поля `CfsParam`, не имеющие тега `yaml:` в [TOOLS/lib/types.go](../TOOLS/lib/types.go#L70-L101), если они не сериализуются или приходят пустыми: `IsModifiable`, `IsSensitive` (указаны с `omitempty`). Поле `dataDescriptor` как мапа отбрасывается — сохраняется преобразованный срез `sub_params`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Как формируется API YAML
|
||||
|
||||
- Сериализация структуры `types.ServiceSpec` в YAML выполняется через библиотеку `gopkg.in/yaml.v3` ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go#L107-L114)).
|
||||
- В результирующий файл пишутся:
|
||||
- Метаданные сервиса (`name`, `service_id`, `service_display_name`, `service_short_name`, `service_man`).
|
||||
- Стандартная секция `lifecycle` и `outputs`.
|
||||
- Секция `operations` со списком операций и всеми их параметрами (`id`, `code`, `data_type`, `required`, `default`, `value_list`, `descr`, `man`, `sort`, `sub_params`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Цепочка данных для `vc_org.vIPConfigure.count`
|
||||
|
||||
1. **API Model:**
|
||||
- Эндпоинт `/instanceOperations/default/207` возвращает параметр с `svcOperationCfsParamId: 662`, `svcOperationCfsParam: "vIPConfigure"`, `dataType: "array-map-fixed"`.
|
||||
- Внутри него поле `dataDescriptor` содержит ключ `"count"`:
|
||||
- `svcOperationCfsSubparamId: 40`,
|
||||
- `dataType: "integer > 0"`,
|
||||
- `isRequired: true`,
|
||||
- `defaultValue: ""`,
|
||||
- `descr: "Пример: \`1\`"`.
|
||||
2. **Generator Input:**
|
||||
- Читается в структуру `types.CfsParam` с мапой `DataDescriptor map[string]CfsSubParam` ([TOOLS/yaml-generator/internal/types/types.go](../TOOLS/yaml-generator/internal/types/types.go#L49-L72)).
|
||||
3. **Internal Generator Representation:**
|
||||
- В [TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L210-L232) мапа `dataDescriptor` разворачивается в `types.ParamSpec.SubParams`. Поле `count` становится элементом среза `SubParams` с `ID: 40`, `Code: "count"`, `DataType: "integer > 0"`.
|
||||
4. **Generator Transformation:**
|
||||
- Сортируется по ID (`sort.Slice(subParams, ...)`).
|
||||
5. **API YAML:**
|
||||
- Записывается в [generated/dev/resources_yaml/19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml#L131-L150):
|
||||
```yaml
|
||||
- id: 662
|
||||
code: vIPConfigure
|
||||
data_type: array-map-fixed
|
||||
required: true
|
||||
sort: 10
|
||||
sub_params:
|
||||
- id: 39
|
||||
code: name
|
||||
data_type: string
|
||||
required: true
|
||||
default: ""
|
||||
is_modifiable: false
|
||||
- id: 40
|
||||
code: count
|
||||
data_type: integer > 0
|
||||
required: true
|
||||
default: ""
|
||||
descr: 'Пример: `1`'
|
||||
is_modifiable: false
|
||||
```
|
||||
- **Потерь в YAML нет:** `vIPConfigure` и `count` полностью и без искажений сохранены в каноническом YAML спецификации.
|
||||
|
||||
---
|
||||
|
||||
## 6. Цепочка данных для `vc_nsxt.ipSpaceName` и связанных параметров
|
||||
|
||||
1. **API Model:**
|
||||
- Эндпоинт `/instanceOperations/default/111` возвращает операцию `modify` сервиса 22.
|
||||
- Параметр `ipSpaceName` возвращается с:
|
||||
- `svcOperationCfsParamId: 372`,
|
||||
- `svcOperationCfsParam: "ipSpaceName"`,
|
||||
- `dataType: "string"`,
|
||||
- `isRequired: false`,
|
||||
- `descr: "Имя ip Space для внешнего IP"`,
|
||||
- `man: "Необходимо указывать, если включён параметр \`Выделить VIP для SNAT\`"`,
|
||||
- `sort: 50`.
|
||||
- В HAR-дампах ([HAR/edge_.har](../HAR/edge_.har#L7985)) на живом инстансе в рантайме возвращается `valueList: ["no-needed", ...]`.
|
||||
2. **Generator Input:**
|
||||
- Читается в структуру `types.CfsParam` ([TOOLS/yaml-generator/internal/types/types.go](../TOOLS/yaml-generator/internal/types/types.go#L49-L72)).
|
||||
3. **Internal Generator Representation:**
|
||||
- Преобразуется в `types.ParamSpec` со значениями `ID: 372`, `Code: "ipSpaceName"`, `DataType: "string"`.
|
||||
4. **Generator Transformation:**
|
||||
- Нормализуются defaults и value_list через `normalizeDefault` и `normalizeValueList`.
|
||||
5. **API YAML:**
|
||||
- Записывается в [generated/dev/resources_yaml/22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml#L149-L155):
|
||||
```yaml
|
||||
- id: 372
|
||||
code: ipSpaceName
|
||||
data_type: string
|
||||
required: false
|
||||
descr: Имя ip Space для внешнего IP
|
||||
man: Необходимо указывать, если включён параметр `Выделить VIP для SNAT`
|
||||
sort: 50
|
||||
```
|
||||
- Рядом в той же операции сохранены: `needEnableAVI` (ID 368), `virtualServicesCount` (ID 369), `qosProfile` (ID 856), `routedNetConfiguration` (ID 1112 с sub_params).
|
||||
- **Особенность по `value_list`:** в статическом `22_vc_nsxt.yaml` поле `value_list` для `ipSpaceName` отсутствует (`null` в ответе static-дефолтов эндпоинта `/instanceOperations/default/111`), хотя в рантайме на конкретном инстансе `valueList` динамически содержит `["no-needed", ...]`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Что сохраняется
|
||||
|
||||
- Полная идентичность сущностей: `service_id`, `svcOperationId` (как `id` операции), `svcOperationCfsParamId` (как `id` параметра), `svcOperationCfsSubparamId` (как `id` в `sub_params`).
|
||||
- Строковые коды: `operation`, коды параметров (`code`).
|
||||
- Исходные типы платформы: `dataType` (`string`, `boolean`, `integer > 0`, `array-map-fixed`, `map-fixed`).
|
||||
- Вся структура вложенности (`dataDescriptor` → `sub_params`).
|
||||
- Флаги `required`, валидационные regex, min/max, описания (`descr`, `man`), порядок (`sort`).
|
||||
|
||||
---
|
||||
|
||||
## 8. Что преобразуется или нормализуется
|
||||
|
||||
- Имена операций и сервисов приводятся к ASCII snake_case через `normalize.Identifier`.
|
||||
- `dataDescriptor` из мапы ключей преобразуется в упорядоченный срез `sub_params` с сортировкой по числовому `id`.
|
||||
- Значения `valueList` и `default` приводятся к строковым представлениям (убираются пробелы, пустые значения приводятся к `nil`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Что теряется
|
||||
|
||||
- HTTP-метод и путь обращения к API (в YAML отсутствуют; генератор считает их внешним знанием рантайма).
|
||||
- Динамические значения списков выбора (`valueList`): эндпоинт дефолтов `/instanceOperations/default/{id}` возвращает пустой `valueList` для полей, зависящих от конкретного тенанта/инстанса (например, доступные `ipSpaceName` для конкретного VDC/Org).
|
||||
|
||||
---
|
||||
|
||||
## 10. Где происходят потери
|
||||
|
||||
- Потери HTTP-метаданных (метод, URL) происходят на этапе маршалинга структуры `types.ServiceSpec` в YAML ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go#L94-L105)), так как они изначально отсутствуют в контракте [TOOLS/lib/types.go](../TOOLS/lib/types.go).
|
||||
- Отсутствие runtime `valueList` обусловлено вызовом шаблонного эндпоинта `/instanceOperations/default/{id}` вместо запроса контекста живого инстанса.
|
||||
|
||||
---
|
||||
|
||||
## 11. Какие данные доступны в API YAML
|
||||
|
||||
- Полный перечень всех сервисов, операций (`create`, `modify`, `delete`, `suspend`, `resume`, сабресурсов) и их параметров.
|
||||
- Точные типы платформы (`data_type`) и иерархия подполей (`sub_params`).
|
||||
- Идентификаторы `id` (CFS param IDs) и символические коды (`code`).
|
||||
- Метаданные валидации (обязательность, регулярные выражения, ограничения диапазонов).
|
||||
|
||||
---
|
||||
|
||||
## 12. Какие данные доступны только в API-модели
|
||||
|
||||
- Динамические списки допустимых значений (`valueList`), вычисляемые бэкендом для конкретного состояния конкретного инстанса (например, список реально существующих ipSpaces организации при вызове modify на Edge).
|
||||
- Внутренние служебные поля платформы CFS, отфильтрованные моделью генератора (`contractId`, `contragentId`, `nestedRefData`, `config`, `statePath`, `expression`).
|
||||
|
||||
---
|
||||
|
||||
## 13. Неизвестные места
|
||||
|
||||
- Неизвестно, возвращает ли платформа Nubes какую-либо схему валидации для эндпоинтов отката/деаллокации (например, принимает ли `vc_org.modify` пустой массив `vIPConfigure: []` для полного снятия или требует только уменьшения `count: 0`), так как в дефолтной модели операции 207 описан только общий формат `vIPConfigure`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Что необходимо проверить фактическим API
|
||||
|
||||
- Поведение `vc_org` (операция 207) при передаче `vIPConfigure: []` против `vIPConfigure: [{"name": "...", "count": 0}]` при попытке полной деаллокации IP-пространства.
|
||||
- Поведение `vc_nsxt` (операция 111) при передаче `ipSpaceName: "no-needed"` на различных окружениях (Dev/Test/Prod).
|
||||
|
||||
---
|
||||
|
||||
## Мнение Opus: финальное ревью отчёта
|
||||
|
||||
Отчёт признан годным и принят как вход для дальнейшего архитектурного этапа.
|
||||
|
||||
### Достаточно для дальнейшей работы
|
||||
|
||||
- Цепочка `API model → yaml-generator → API YAML` прослежена по коду, а не по догадкам: `classifyOperation`, `params.Merge`, разворачивание `dataDescriptor` в `sub_params`.
|
||||
- Обе обязательные трассировки (`vc_org.vIPConfigure.count`, `vc_nsxt.ipSpaceName`) доведены до YAML с подтверждением «потерь нет».
|
||||
- Зафиксирован ключевой факт: динамический `valueList` есть только в рантайме живого инстанса, а в API YAML его нет. Это прямое ограничение для будущего modifier-слоя.
|
||||
- Неизвестное поведение payload деаллокации (`vIPConfigure: []` против `count: 0`) помечено как `unknown`, а не закрыто предположением.
|
||||
|
||||
### Следствия перед архитектурным этапом
|
||||
|
||||
- Под ярлыком «Ordinary Resource Generator» в отчёте упоминаются два разных инструмента: `yaml-generator` пишет спеки, а `resource-generator` создаёт Go-ресурсы. Для проектирования `ModifierSpec` это две разные точки вмешательства.
|
||||
- `vIPConfigure` и `ipSpaceName` присутствуют только в `modify` и отсутствуют в `create`. В текущей схеме они сливаются в общий ресурс и не имеют отдельного жизненного цикла. Это корень задачи модификаторов.
|
||||
- Runtime-`valueList` придётся получать не из дефолтного эндпоинта, а из контекста инстанса. Это вопрос рантайма провайдера, а не генератора.
|
||||
|
||||
### Итоговая оценка
|
||||
|
||||
Ошибок, которые ломали бы выводы отчёта, не выявлено. Отчёт можно использовать как подтверждённую основу для дальнейшего проектирования.
|
||||
@@ -0,0 +1,69 @@
|
||||
# HAR-разбор: SNAT / ipSpace / модификации (dev)
|
||||
|
||||
Дата: 2026-09-22. Источник: `/home/naeel/TF/tf_provider/HAR/*.har` (записи UI на dev-стенде, 2026-09-20).
|
||||
Релевантные файлы: `edge_.har` (SNAT/Edge), `ipSpace0.har`, `org_enough_.har`, `org_not_enough_.har`, `org0.har`.
|
||||
|
||||
## Поток modify в реальном API
|
||||
|
||||
1. `POST /api/v1/svc/instanceOperations` — `{"instanceUid":"...","operation":"modify"}` → возвращает `instanceOperationUid`.
|
||||
2. `POST /api/v1/svc/instanceOperationCfsParams` — по одному запросу на параметр:
|
||||
`{"paramValue":"...","instanceOperationUid":"...","svcOperationCfsParamId":NNN}`.
|
||||
3. `GET /svc/instanceOperations/{id}/validate-cfs`
|
||||
4. `POST /svc/instanceOperations/{id}/run`
|
||||
5. Поллинг `GET /svc/instanceOperations/{id}`.
|
||||
|
||||
## Найденные payload-и
|
||||
|
||||
| Операция | param id | код | значение из HAR |
|
||||
|---|---|---|---|
|
||||
| edge modify | 368 | `needEnableAVI` | `false` / `true` |
|
||||
| edge modify | 369 | `virtualServicesCount` | `1` / `2` |
|
||||
| edge modify | 856 | `qosProfile` | `QoS-100Mbit` |
|
||||
| edge modify | **372** | **`ipSpaceName`** | **`no-needed`** / `""` |
|
||||
| edge modify | 1112 | `routedNetConfiguration` | `{"mainDns":"81.22.46.22","secondDns":"185.247.187.77","ipAddrPool":"10.10.102.0/24"}` |
|
||||
| org modify | **662** | **`vIPConfigure`** | `[{"name":"internet-ipv4-v1","count":"3"}]` |
|
||||
|
||||
## Ответы на открытые вопросы
|
||||
|
||||
1. **Тумблера «Выделить VIP для SNAT» в API НЕТ.** SNAT управляется целиком через `ipSpaceName` (param 372).
|
||||
Его `valueList` (из метаданных в HAR): `no-needed, internet-antiddos-v1, internet-no-antiddos-v1, ...` — то есть `no-needed` это легальное значение «SNAT не нужен».
|
||||
- Включить SNAT: `ipSpaceName = <имя ipSpace из org>`.
|
||||
- Выключить: `ipSpaceName = "no-needed"`.
|
||||
2. ✅ **Каноническое «SNAT выключен» = `no-needed`.** Подтверждено: в UI (Edge → Modify → поле «ip Space для VIP», параметр `ipSpaceName`) текущее значение показывается как `no-needed`. Reverse для SNAT = `modify` с `ipSpaceName="no-needed"` → delete SNAT-модификатора можно реализовать не как no-op. (`""` из `ipSpace0.har` — не каноническое, а промежуточное состояние.)
|
||||
3. 🟡 **Де-аллокация IP в org — попытка зафиксирована (`org2.har`, 2026-09-22):** UI отправил `modify` с
|
||||
`vIPConfigure=[{"name":"internet-ipv4-v1","count":"2"}]` (count уменьшен с 3 до 2).
|
||||
HTTP-ошибки НЕТ, но операция осталась в `isPending:true` — не выполнилась (согласуется с ограничением ниже).
|
||||
**Вывод:** payload де-аллокации = ТА ЖЕ структура `vIPConfigure`, только меньше `count` (не отдельная операция).
|
||||
Точная семантика «удалить совсем» (`count=0` или опустить элемент) не подтверждена.
|
||||
🔴 **Ограничение (подтверждено):** уменьшить/удалить ipSpace в `vcOrg` **нельзя, пока существуют дочерние инстансы** (VDC/Edge/кластер).
|
||||
Следствие: reverse возможен только ПОСЛЕ уничтожения детей → порядок destroy критичен:
|
||||
`кластер → SNAT-модификатор (no-needed) → org IP de-alloc → edge → vdc → org`.
|
||||
Чтобы снять payload «удалить совсем», нужен чистый org без детей (или плановый teardown).
|
||||
|
||||
## Побочные факты
|
||||
|
||||
- У `ipSpaceName` (372) в API есть `valueList`, но в нашем YAML его **нет** → проверить, тянет ли генератор `valueList` (возможно, он динамический: имена ipSpace конкретной org).
|
||||
- Имя ipSpace в живом примере — `internet-ipv4-v1` (не произвольное).
|
||||
- `qosProfile` (856) UI всегда шлёт как `QoS-100Mbit`.
|
||||
- `routedNetConfiguration` передаётся JSON-строкой.
|
||||
- В состоянии org: `"vip":{"no-needed":{},"internet-ipv4-v1":{"count":4}}` — `no-needed` фигурирует и в стейте.
|
||||
|
||||
## Наблюдения на возможно сломанном Edge (2026-09-22, nsx_WZ03709-saas-wmfop5be)
|
||||
|
||||
⚠️ ВАЖНО: этот Edge, судя по всему, в сломанном состоянии (devops-проблема).
|
||||
Ошибки ниже **НЕ считать универсальными правилами API** — перепроверить на здоровом Edge.
|
||||
|
||||
1. `modify` 14:40:52 → «ipSpace '' не найден на https://sandbox.nubes.ru» — при пустом `ipSpaceName` бэкенд отклонил запрос. ❓ Возможно, следствие сломанного Edge, не правило.
|
||||
2. `modify` 14:43:44 → «Insufficient rule blocks» при попытке снять «Включить ALB». ❓ Возможно, застрявшие VS/SE Group, не правило.
|
||||
3. `delete` (2 раза) → FORBIDDEN «Cannot delete SE Group assignment … since there are Virtual Services». ❓ Возможно, застрявшие VS, не правило.
|
||||
|
||||
**Что остаётся надёжным (из API-метаданных, НЕ из этих ошибок):**
|
||||
- `valueList` у `ipSpaceName` содержит `no-needed` (+ имена ipSpace) — из описания параметра.
|
||||
- UI показывает `no-needed` как текущее значение при выключенном SNAT.
|
||||
|
||||
## Что ещё нужно выяснить из UI (открытые вопросы)
|
||||
|
||||
1. 🔴 **Де-аллокация IP в org — пока НЕ снять:** UI/бэкенд не даёт удалить ipSpace, пока есть дочерние инстансы (подтверждено 2026-09-22). Нужен чистый org или плановый teardown. Гипотеза payload — `vIPConfigure=[]` (unverified).
|
||||
2. ✅ **Имя ipSpace — выбор ИЗ СПИСКА** (подтверждено UI). Свободного ввода нет → список динамический (текущие ipSpace org + `no-needed`).
|
||||
Следствие для провайдера: `ip_space_name` в SNAT-модификаторе должен браться из **computed-вывода org-модификатора**, а не быть свободной строкой.
|
||||
3. 🟡 Полное удаление ipSpace и поведение при destroy Edge с включённым SNAT — на будущее (блокировано п.1).
|
||||
@@ -0,0 +1,122 @@
|
||||
# Ответ Opus: анализ решения IaC-развёртывания Штурвала (модификаторы + скрытые зависимости)
|
||||
|
||||
**Дата:** 2026-09-23
|
||||
**Связанный промпт:** `docs/prompts/prompt_for_opus_iac_shturval_modify.md`
|
||||
**Связанный анализ:** `docs/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md`
|
||||
**Статус:** документирование ответа. Конкретный план НЕ составляется.
|
||||
|
||||
> ⚠️ **ВАЖНАЯ ПОПРАВКА (2026-09-23, позже).** Ответ Опуса ниже строился на НЕВЕРНОЙ посылке «`vIPConfigure` — накопительный API». Это опровергнуто тестом `docs/ORG_IP_MODIFIER_TEST_2026-09-22.md`: `vIPConfigure` ведёт себя как **replace-состояние** — идемпотентно (1→1), работает в обе стороны (вверх/вниз/до 0), `count` читается из `state.params`. Соответственно «блокеры» (a) Read счётчика и (b) адресное освобождение **сняты как ложные**. Остаётся только (c) Read цепочки `providerVdc → providerGateway → ipSpace`. НЕ использовать прежнюю формулировку «накопительный API несовместим с декларативной моделью» как источник истины.
|
||||
|
||||
---
|
||||
|
||||
## 1. Суть ответа (главный вывод)
|
||||
|
||||
Форма IaC-ресурсов фиксируется **уже сейчас**, потому что она диктуется моделью Terraform (декларативность, идемпотентность, inverse), а не спеками платформы.
|
||||
|
||||
НО есть **три блокера от платформы**, без которых идемпотентность и Delete принципиально недостижимы на стороне провайдера.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ответ Opus по пунктам
|
||||
|
||||
### Пункт 1 — накопительный `vIPConfigure` → идемпотентный ресурс
|
||||
|
||||
- Ресурс отдельный (`nubes_org_vip_allocation`) с `depends_on` на оргу, НЕ операция внутри орги.
|
||||
- **Ключ идемпотентности — желаемое состояние, а не дельта.** Юзер задаёт целевой `count` на `name`; провайдер сам считает `target − current` и модифицирует только разницу.
|
||||
- **Create:** Read текущего числа vIP → выделить `target − current`. Если API не отдаёт «сколько уже есть» — нужен серверный счётчик/тег, иначе идемпотентность недостижима.
|
||||
- **Read:** читать родителя (оргу), извлекать фактическое число IP по `name` в state. Если API не различает «кем/зачем выделено» — Read вернёт общий пул, drift неизбежен.
|
||||
- **Update:** та же дельта-логика (target изменился → доначислить/освободить).
|
||||
- **Delete (inverse):** `modify` с обратным знаком до `count=0` по этому `name`. Требует адресного освобождения конкретных IP. Если освобождение — тоже накопительный modify без адресации, inverse корректно сделать нельзя.
|
||||
|
||||
**Риск (ОПРОВЕРГНУТ позже):** это утверждение строилось на ложной посылке «накопительный API». Факт: `vIPConfigure` — replace-состояние, дельта `target − current` по факту не нужна — достаточно слать целевой `count`, платформа сама выставляет его (идемпотентно). См. `ORG_IP_MODIFIER_TEST_2026-09-22.md`.
|
||||
|
||||
### Пункт 2 — `ipSpaceName` (цепочка providerVdc → providerGateway → ipSpace)
|
||||
|
||||
- Это **выводимое значение из инфраструктуры, НЕ пользовательский ввод** → data-source, а не аргумент ресурса.
|
||||
- Правильно: `data "nubes_ip_space" { org/vdc = ... }`, который проходит цепочку providerVdc → providerGateway → ipSpace и возвращает `name`. Ресурс берёт значение по ссылке.
|
||||
- **Граница «данные vs логика»:** в реестре хранить **тип поля и его источник** (что это computed-from-parent, а не user-input). Сама цепочка обхода — логика data-source, не данные реестра.
|
||||
- НЕ вычислять из state родителя вручную в ресурсе (скрытая связанность, ломается при >1 T0). Data-source явно выражает зависимость в графе tf.
|
||||
- Пока платформа «подкладывает» значение сама — data-source должен уметь то же читать. Если API этой цепочки нет на чтение — **блокер**.
|
||||
|
||||
### Пункт 3 — не завязываться на «один T0»
|
||||
|
||||
- Закладывать **явный селектор шлюза** уже сейчас: `provider_gateway` / `t0_id` как аргумент (или ключ data-source), даже если сегодня один и выводится автоматически (optional + computed default).
|
||||
- vIP-аллокация и SNAT привязывать к **конкретному gateway id**, а не к «дефолтному в орге».
|
||||
- **Что сломается при >1 T0, если не заложить:** `ipSpaceName` станет неоднозначным (несколько ipSpace), vIP-аллокация не будет знать, к какому шлюзу. Придётся менять схему (добавлять обязательный селектор) → breaking change.
|
||||
- **Заложив optional-селектор сейчас:** при росте T0 меняется только default-резолвинг, схема остаётся совместимой.
|
||||
|
||||
### Пункт 4 — ждать спеки или фиксировать форму сейчас
|
||||
|
||||
- **Форму ресурсов можно и нужно фиксировать сейчас** — она диктуется моделью Terraform, а не спеками.
|
||||
- **Не блокер (делаем сейчас):** раздельные ресурсы + `depends_on`; целевое состояние вместо дельты; селектор шлюза; data-source для `ipSpaceName`; inverse через обратный modify.
|
||||
- **Блокер (нужно от платформы) — только один подтверждённый:**
|
||||
- c) API чтения цепочки providerVdc → providerGateway → ipSpace (иначе data-source невозможен).
|
||||
- **Сняты как ложные (опровергнуты тестом 2026-09-22):**
|
||||
- a) чтение текущего числа vIP — УЖЕ работает через `state.params.vIPConfigure`;
|
||||
- b) адресное освобождение IP — УЖЕ работает: `count` меньше/`0` задаётся тем же `modify`, в обе стороны.
|
||||
- **Вывод:** проектируем форму сейчас, блокер только (c). Новые спеки повлияют на **резолвинг значений**, не на форму ресурсов — если форма построена на «целевое состояние + селектор + data-source».
|
||||
|
||||
### Пункт 5 — минимально-инвазивный порядок внедрения
|
||||
|
||||
**От платформы (до кодинга ресурсов) — обязательно:**
|
||||
- Read цепочки → `ipSpaceName` (для data-source).
|
||||
- Подтверждение, что генератор умеет строить схему из объединения `create`+`modify` полей (иначе `vIPConfigure`/`ipSpaceName` вообще не попадут в схему).
|
||||
|
||||
> ⚠️ Read счётчика vIP и адресное освобождение — НЕ блокеры (уже подтверждено тестом). Исключены.
|
||||
|
||||
**На стороне провайдера — можно сейчас, не дожидаясь:**
|
||||
- Раздельные ресурсы vip-allocation / nsxt-snat с `depends_on`.
|
||||
- Логика «target − current = дельта» (заглушка current, пока нет Read).
|
||||
- Data-source-скелет для `ipSpaceName` (с TODO на реальный обход цепочки).
|
||||
- Optional+computed селектор шлюза.
|
||||
- Inverse-контракт (Delete = обратный modify до нуля).
|
||||
|
||||
**Главный неустранимый на нашей стороне блокер:** ❌ СНЯТ — строился на ложной посылке «накопительный API». Реальный остаточный блокер — только (c) чтение цепочки providerVdc → providerGateway → ipSpace (для data-source `ipSpaceName`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Что нового vs то, что уже собирались делать
|
||||
|
||||
### Совпадает со старыми планами (НЕ новое)
|
||||
|
||||
- Отдельный ресурс под модификацию + `depends_on` — было (`PLAN_modifier_redesign.md`, «resource association»).
|
||||
- Inverse через обратный modify (`count→0`) — было (`inverse_rollback_analysis_2026-09-23.md`).
|
||||
- Идемпотентность (skip run, если live уже целевое) — было.
|
||||
- «Не ждать спеки для формы, а фиксировать сейчас» — по сути было.
|
||||
|
||||
### Реально новое у Опуса
|
||||
|
||||
1. **«Целевое состояние, а не дельта»** — строгий принцип: юзер задаёт целевой `count`, провайдер сам считает `target − current`. Старые планы просто «досылали заданные поля», не формализовали желаемое состояние.
|
||||
2. **`ipSpaceName` — data-source, не аргумент ресурса** — сдвиг от «юзер вписывает значение» к «computed-from-parent». Раньше виделось как ввод.
|
||||
3. **Селектор шлюза (`t0_id`/`provider_gateway`) как optional+computed сейчас** — в старых планах про «один T0» вообще не было (пришло только из реплики Виталия).
|
||||
4. **Чёткая граница «данные vs логика»** — в реестре только «тип поля + что computed-from-parent», цепочка обхода — логика data-source.
|
||||
5. **Блокеры от платформы** — из трёх заявленных Опуса два (Read счётчика, адресное освобождение) **ложны** (опровергнуты тестом), остаётся один реальный: Read цепочки providerVdc→providerGateway→ipSpace.
|
||||
|
||||
### Главное отличие одной фразой
|
||||
|
||||
Старые планы отвечали на «**как сделать модификатор в tf**». Опус отвечает на «**как сделать его идемпотентным и IaC-честным**» — но его центральный вывод «ядро проблемы в платформе (накопительный API)» **оказался ошибочным**, т.к. исходная посылка «накопительный» неверна (см. поправку в шапке). Реальный остаток — только `ipSpaceName` (цепочка providerVdc→providerGateway→ipSpace) и селектор шлюза.
|
||||
|
||||
---
|
||||
|
||||
## 4. Спорный/непроверенный момент — РАЗРЕШЁН
|
||||
|
||||
Посылка Опуса «накопительный `vIPConfigure` без Read-счётчика и адресного освобождения несовместим с декларативной моделью» **опровергнута** тестом `docs/ORG_IP_MODIFIER_TEST_2026-09-22.md`:
|
||||
|
||||
- повторный `modify` с тем же `count` не аккумулирует IP (1→1) → идемпотентно;
|
||||
- `count` меняется в обе стороны (2→1→0) через тот же `modify` → «адресное освобождение» не нужно, достаточно задать меньший/нулевой `count`;
|
||||
- `count=0` принимается (несмотря на `minvalue:1` в схеме), элемент ipSpace остаётся в `state.params`;
|
||||
- Read уже есть: `state.params.vIPConfigure = [{"name":"internet-ipv4-v1","count":N}]`.
|
||||
|
||||
Единственный реально непроверенный момент: полное удаление ipSpace (`[]` / отсутствие элемента) — тест этого не покрывал. Для IaC-задачи «обнулить» достаточно, полное удаление — опционально.
|
||||
|
||||
---
|
||||
|
||||
## 5. Резюме (с поправкой)
|
||||
|
||||
- Форма ресурсов — проектируем сейчас, она не зависит от спеков.
|
||||
- `vIPConfigure` — **НЕ блокер**: идемпотентно, обе стороны, `count` читается/задаётся из `state.params` (тест 2026-09-22).
|
||||
- Единственный подтверждённый блокер: **Read цепочки `providerVdc → providerGateway → ipSpace`** для data-source `ipSpaceName` (c).
|
||||
- Непроверено: полное удаление ipSpace (`[]`), но для задачи достаточно `count=0`.
|
||||
- Конкретный план внедрения пока НЕ составляется (по решению пользователя).
|
||||
|
||||
**Следующий возможный шаг (только по запросу):** проверить (c) чтение цепочки ipSpace живым API.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Проверка модификатора vc_org ip_space (modify 207) — 2026-09-22
|
||||
|
||||
Организация: `NarodOrg` (`9890a8a0-040b-4d56-8018-c31519c35a30`), realm `sandbox.nubes.ru`.
|
||||
Стенд: `DEV_STAND/FullPipe`, провайдер `nubes-dev` 2.0.9.
|
||||
|
||||
## Что делали
|
||||
|
||||
1. Переименовали `org_ips.tf` → `terraform apply` — ошибок нет (ресурс ушёл из state; `Delete` модификатора — no-op).
|
||||
2. Вернули файл → `apply` — ошибок нет, **число IP осталось 1** (повторный `modify` с тем же `count=1` НЕ задвоил).
|
||||
3. `org_ip_count = 2` → `apply` — стало 2.
|
||||
4. `org_ip_count = 1` → `apply` — стало 1.
|
||||
5. `org_ip_count = 0` → `apply` — стало 0.
|
||||
|
||||
## Подтверждено по API
|
||||
|
||||
`GET /instances/9890a8a0-040b-4d56-8018-c31519c35a30`:
|
||||
|
||||
- `state.params.vIPConfigure = [{"name":"internet-ipv4-v1","count":0}]`
|
||||
- последняя операция `modify` — `isSuccessful: true`, `isPending: false`, `isInProgress: false`.
|
||||
|
||||
## Выводы (обновляют прежние гипотезы)
|
||||
|
||||
1. **modify работает в обе стороны** (count вверх/вниз/до 0) на здоровой орге, даже при живых дочерних инстансах (VDC/Edge).
|
||||
2. **modify идемпотентен** — повторный `modify` с тем же `count` не аккумулирует IP (1 → 1).
|
||||
3. **`count=0` принимается**, хотя в схеме операции 207 у `count` стоит `minvalue: 1` / `integer > 0` — валидация не отвергает 0. `count=0` = ноль выделенных IP (элемент ipSpace остаётся в state).
|
||||
4. **`Delete` модификатора — no-op** подтверждён (шаг 1), но это восполнимо: повторный `apply` с нужным `count` корректно восстанавливает состояние.
|
||||
|
||||
## Опровергнуто
|
||||
|
||||
- Утверждение из `docs/HAR_SNAT_MODIFY_FINDINGS.md` «уменьшить/удалить ipSpace нельзя, пока существуют дочерние инстансы» — **не подтвердилось на здоровой орге** (в HAR был сломанный Edge; это и было помечено как неподтверждённое наблюдение).
|
||||
- Опасение из Opus-ревью о «двойном выделении при replace/destroy→apply» — в части повторного `modify` с тем же `count` **не воспроизвелось** (идемпотентно).
|
||||
|
||||
## Открытый вопрос
|
||||
|
||||
- Полное удаление ipSpace (пустой массив `[]` / отсутствие элемента) не тестировалось — `count=0` оставляет элемент `{"name":"internet-ipv4-v1","count":0}` в state.
|
||||
@@ -0,0 +1,169 @@
|
||||
# Штурвал через IaC: анализ проблемы `modify` и скрытых платформенных зависимостей
|
||||
|
||||
**Дата:** 2026-09-23
|
||||
**Контекст:** дискуссия в Telegram про запуск цепочки Штурвал полностью через Terraform.
|
||||
|
||||
---
|
||||
|
||||
## 1. Исходная задача
|
||||
|
||||
Клиенту нужен IaC (Infrastructure as Code): вся инфраструктура описывается одним конфигом, команда `terraform apply` разворачивает её целиком, `plan`/`destroy` дают полную картину. Никаких обязательных ручных шагов посередине.
|
||||
|
||||
Цепочка для стенда Штурвал:
|
||||
|
||||
```text
|
||||
vcOrg -> create
|
||||
vcVdc -> create
|
||||
vcNsxt -> create
|
||||
------------------------------
|
||||
vcOrg -> modify (аллоцировать внешние IP в организацию)
|
||||
vcNsxt -> modify (включить SNAT, указать внешний IP из vcOrg)
|
||||
------------------------------
|
||||
k8sShturval -> create
|
||||
```
|
||||
|
||||
Ключевой конфликт: `create` у Terraform работает штатно, а операции `modify` в текущем провайдере никак не выражаются — Terraform не умеет «создать ресурс, а через несколько шагов поменять в нём же параметр».
|
||||
|
||||
---
|
||||
|
||||
## 2. Почему `modify` не выражается в текущем провайдере
|
||||
|
||||
### 2.1. Генератор строит схему только из `create`
|
||||
|
||||
Провайдер генерируется из YAML-спеков (`generated/{stand}/resources_yaml/*.yaml`). Схема ресурса (какие поля можно писать в `.tf`) строится **только из операции `create`**. Параметры, которые есть только в `modify`, в схему не попадают.
|
||||
|
||||
Подтверждено по файлам:
|
||||
|
||||
- `generated/dev/resources_yaml/19_vc_org.yaml`:
|
||||
- `create` (id 136) → только `resourceRealm` (418), `organizationType` (556), `orgSuffix` (1125);
|
||||
- `vIPConfigure` (выделение внешних IP) есть **только** в `modify` (id 207), с sub-полями `name` (39) и `count` (40).
|
||||
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`:
|
||||
- `create` (id 10) → `vdcUid`, `needEnableAVI` (340), `virtualServicesCount` (341), `qosProfile` (825), `routedNetConfiguration` (1110) и др.;
|
||||
- `ipSpaceName` (372) есть **только** в `modify` (id 111).
|
||||
|
||||
Вывод: `vIPConfigure` (vc_org) и `ipSpaceName` (vc_nsxt) живут только в `modify`, в схеме tf-ресурсов их нет. Поэтому «прописать параметр в tf и сделать apply» падает ещё на `plan` (атрибут не известен провайдеру).
|
||||
|
||||
### 2.2. Эти параметры — не «настройки», а отложенные действия
|
||||
|
||||
- `vIPConfigure=[{name,count}]` — задаёт желаемое число внешних IP целиком. **Не накопительная** (повторный вызов с тем же `count` не аккумулирует, подтверждено `docs/ORG_IP_MODIFIER_TEST_2026-09-22.md`), работает в обе стороны (вверх/вниз/до `count=0`). Это **декларативное значение** в смысле «желаемое количество IP по данному ipSpace».
|
||||
- `ipSpaceName` — включение SNAT на конкретный ipSpace, который возникает **только после** того, как на орге выделены IP.
|
||||
- Эти операции требуют порядка (org.modify → затем nsxt.modify) и зависят от живого состояния инстанса, а не от дефолтов формы.
|
||||
|
||||
### 2.3. Схема в state ≠ реальное состояние
|
||||
|
||||
Вписывать недостающие параметры «насильно» в tfstate нельзя и бесполезно:
|
||||
|
||||
1. Terraform валидирует атрибуты по **схеме провайдера**, а не по state — неизвестный атрибут будет отброшен/вызовет ошибку.
|
||||
2. Записывать в state «SNAT включён», когда этого нет на площадке, — значит получить ложный `plan` (чистый) при сломанной инфраструктуре.
|
||||
3. Ручная правка tfstate/`state push` ломает целостность (серийник, конфликты на следующем apply).
|
||||
|
||||
Работает только косвенно: `terraform_data`/`null_resource` + `local-exec` → в state попадает **факт** «операция выполнена» (маркер с `triggers`), но не **состояние** SNAT/IP. Порядок задаётся через `depends_on`, но дрейф по самим параметрам `plan` не видит.
|
||||
|
||||
---
|
||||
|
||||
## 3. Каноничное решение: отдельный ресурс (resource association)
|
||||
|
||||
Это принятая в Terraform практика — «resource association / separate resource». Классические примеры:
|
||||
|
||||
- `aws_security_group` + `aws_security_group_rule`
|
||||
- `aws_vpc` + `aws_route_table_association`
|
||||
- `google_project` + `google_project_iam_member`
|
||||
|
||||
Базовый ресурс создаётся отдельно, а донастройка/привязка — отдельным ресурсом с `depends_on`. Граф сам выстраивает порядок, `destroy` разворачивает его корректно.
|
||||
|
||||
### 3.1. Прецедент из Cloud Director (VCD)
|
||||
|
||||
В репозитории лежит сторонний шаблон — `/home/naeel/TF/tf_provider/!/` (network.tf.tmpl, vmware_org.tf, vdc.tf), показывающий, как та же цепочка делается провайдером VMware Cloud Director:
|
||||
|
||||
- `resource "vcd_nsxt_alb_settings"` — включение ALB, `count = var.alb_enable ? 1 : 0`, `depends_on = [vcd_nsxt_edgegateway...]`;
|
||||
- `resource "vcd_nsxt_alb_edgegateway_service_engine_group"` — выделение SE, `reserved_virtual_services = var.alb_segroup_count`;
|
||||
- `resource "vcd_network_routed_v2"` — routed-сеть, `edge_gateway_id`, `dns1/dns2/static_ip_pool`;
|
||||
- `resource "vcd_ip_space_custom_quota"` — квота IP на **оргу**, `depends_on = [edge]`.
|
||||
|
||||
Приём «включить/выключить» = `count`. Обратная операция (выключить ALB / снять квоту) получается **удалением ресурса** — inverse логика не нужна.
|
||||
|
||||
Маппинг на наши сервисы:
|
||||
|
||||
| Nubes API | Канон VCD |
|
||||
|---|---|
|
||||
| `needEnableAVI` | `vcd_nsxt_alb_settings` (+ `count`) |
|
||||
| `virtualServicesCount` | `reserved_virtual_services` в SE-группе |
|
||||
| `routedNetConfiguration` (mainDns/secondDns/ipAddrPool) | `vcd_network_routed_v2` (`dns1/dns2/static_ip_pool`) |
|
||||
| `vIPConfigure` (IP на оргу) | `vcd_ip_space_custom_quota` (на оргу) |
|
||||
|
||||
### 3.2. Чем наш случай сложнее канона
|
||||
|
||||
В классическом паттерне ребёнок — **отдельный объект API** со своим CRUD (правило, association, attachment). Его можно создать/прочитать/удалить.
|
||||
|
||||
У нас отдельного объекта нет — есть **операция `modify` над родителем**. Поэтому требуются:
|
||||
|
||||
1. `Read` — не свой объект, а чтение состояния родителя;
|
||||
2. `Delete` — не удаление, а **обратный modify** (inverse);
|
||||
3. `Create/Update` — вызов той же операции с параметрами;
|
||||
4. идемпотентность (не дёргать `run`, если live уже целевое) и live-сверку.
|
||||
|
||||
Именно поэтому «просто завести поля из modify в схему» не работает — нужна полноценная механика, а не одна правка.
|
||||
|
||||
---
|
||||
|
||||
## 4. Более глубокая проблема: скрытые платформенные зависимости
|
||||
|
||||
Это главное из всей дискуссии (реплики Виталия Зайцева, 18:30–18:34).
|
||||
|
||||
### 4.1. `ipSpaceName` нельзя ввести вручную — он выводится
|
||||
|
||||
```text
|
||||
имя ipSpace → зависит от providerGateway
|
||||
providerGateway → зависит от providerVdc
|
||||
providerVdc → никто не знает изначально
|
||||
```
|
||||
|
||||
Пользователь **не может** заполнить `ipSpaceName`, потому что это значение выводится из внутренней топологии (providerVdc → providerGateway → ipSpace), а не из того, что он видел в ЛК. Это не «поле, которое забыли отдать через API», а **вычисляемое от скрытых зависимостей** значение.
|
||||
|
||||
### 4.2. Текущий костыль платформы
|
||||
|
||||
«При создании орги/vdc/edge, если организация ничего не знает про недостающие параметры — они подкладываются». То есть одноразовая подстановка при создании пустой орги, чтобы избавить пользователя от «мучительных приседаний» в ЛК.
|
||||
|
||||
### 4.3. Ограничение модели: один T0
|
||||
|
||||
«Другая проблема — что будет, если в облаке появится больше 1 T0». Пока принято допущение на уровне кода: **в организации всё одно подключение**. Решение осознанно отложено («пара лет спокойствия»), но для IaC это риск: текущее решение завязано на «в орге всегда один провайдер-шлюз».
|
||||
|
||||
### 4.4. Ожидание изменений спеков
|
||||
|
||||
«Мне надо увидеть, как спеки поменяются, чтобы понять, что исправлять… Надеюсь, появится сначала в sandbox.nubes.ru, а не в ngcloud». То есть платформа меняется, форма ресурсов зависит от **новых спеков**, и строить модификатор сейчас = работать по устаревшим спекам.
|
||||
|
||||
---
|
||||
|
||||
## 5. Итог: где правда
|
||||
|
||||
1. **Ручной ЛК и скрипт не подходят** — клиент требует IaC (Георгий прав). Это не «костыль против красоты», это невыполнение требования.
|
||||
|
||||
2. **Отдельный ресурс под модификацию — необходимое, но не достаточное условие.** Он закрывает «как expressить modify», но не закрывает «откуда юзер возьмёт значения».
|
||||
|
||||
3. **Главная блокировка — не Terraform, а платформа.** `ipSpaceName` (и подобные) выводятся из `providerVdc → providerGateway → ipSpace`, которые юзер не знает. Пока платформа не отдаёт эти значения в спеках (или провайдер не резолвит их data-источником), честный IaC не собрать — ни модификаторами, ни скриптом, ни руками.
|
||||
|
||||
4. **«Ломается агностичность» — верно, но это не порок, а цена.** Ресурсы-модификаторы доменные и «ручные», как в VCD. Без них IaC невозможен, прецедент — перед глазами (`!/network.tf.tmpl`).
|
||||
|
||||
5. **Состояние дел:** платформа в движении (ждут новые спеки). Правильная последовательность — дождаться, что придёт в спеках (snandbox), а затем решать форму ресурса; не строить по старым спекам.
|
||||
|
||||
---
|
||||
|
||||
## 6. Возможные пути (по убыванию «честности» перед IaC)
|
||||
|
||||
| Вариант | Что делает | Вердикт |
|
||||
|---|---|---|
|
||||
| **A. Полноценные ресурсы-модификаторы + data-источники** | отдельный tf-ресурс на modify + data-source, резолвящий `ipSpace`. Полный IaC. | правильно, но только после новых спеков |
|
||||
| **B. Data-source через `http`/`external` + `jsondecode`** | DevOps сам дёргает API и подставляет динамические списки, без правки провайдера | рабочая «дожималка», не полный IaC |
|
||||
| **C. `terraform_data`/`null_resource` + `local-exec`** | модификации скриптом, факт в state, порядок через `depends_on` | полумера, состояние SNAT/IP вне state |
|
||||
| **D. Прессеты/дефолтное окружение** | готовый набор компонентов, экспорт через провайдер | снижает боль на старте, IaC не заменяет |
|
||||
| **E. Ручной ЛК / скрипт вне tf** | модификации руками | не подходит (требование клиента) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Моё мнение
|
||||
|
||||
**Коротко:** для настоящего IaC нужны обе вещи одновременно — **отдельный ресурс под `modify`** и **механизм получения динамических значений** (`ipSpace` и пр.). Пока платформа не отдаёт второе через API/спеки, все «быстрые» способы (скрипт, руками, пресеты) закрывают только симптом, а не требование клиента.
|
||||
|
||||
**Рекомендация:** не городить модификатор сейчас по устаревшим спекам. Дождаться изменений спеков (сначала sandbox), параллельно — обсчитать два blockers: (1) как провайдер будет резолвить `providerVdc → providerGateway → ipSpace` без ручного ввода; (2) допущение «один T0». После этого проектировать форму ресурсов.
|
||||
|
||||
**Что точно не делать:** вписывать параметры «насильно» в tfstate; ждать, что «прописал поле в tf → apply» заработает без правки провайдера. (`vIPConfigure` при этом НЕ накопительный — см. §2.2, тест 2026-09-22.)
|
||||
@@ -0,0 +1,194 @@
|
||||
# Анализ 43 YAML-файлов из нового API Gateway
|
||||
|
||||
> Дата: 2026-07-02
|
||||
> Источник: `devops/profiles/test/generated/resources_yaml/`
|
||||
> Gateway: `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc`
|
||||
|
||||
---
|
||||
|
||||
## Идеология построения сервисов
|
||||
|
||||
### Два класса сервисов
|
||||
|
||||
| Класс | Признак | Примеры | Кол-во |
|
||||
|---|---|---|---|
|
||||
| **Простые (flat)** | Плоские параметры: `resourceRealm`, `resourceCPU`, `domain`, ... | s3bucket, vc_vm, dnszone, harbor, gitea, rabbitmq, ... | **31** |
|
||||
| **Сложные (map-fixed)** | Структурированные JSON-блоки: `clusterConfiguration`, `startupConfiguration`, ... | postgres, redis, mariadb, clickhouse, kafka, nextcloud, flask, lucee, nodejs, valoTenant, dummy, template | **12** |
|
||||
|
||||
**Закономерность**: map-fixed используют сервисы, разворачиваемые в Kubernetes (БД, приложения). Простые flat — всё остальное (VM, сеть, DNS, S3).
|
||||
|
||||
---
|
||||
|
||||
## Типы данных (435 параметров всего)
|
||||
|
||||
| Тип | Кол-во | Где |
|
||||
|---|---|---|
|
||||
| `string` | 146 | Имена, домены, UUID |
|
||||
| `integer > 0` | 101 | CPU, memory, disk |
|
||||
| `map-fixed` | 93 | K8s-сервисы (12 шт.) |
|
||||
| `boolean` | 21 | Флаги |
|
||||
| `array-map-fixed` | 17 | Массивы объектов (7 сервисов) |
|
||||
| `uuid` | 14 | ref-параметры |
|
||||
| `json` | 11 | Произвольный JSON |
|
||||
| `map` | 14 | Только в outputs |
|
||||
| `yaml` | 2 | Редко |
|
||||
| `array` | 1 | Редко |
|
||||
| `integer >= 0` | 2 | Числовые, допускающие 0 |
|
||||
|
||||
---
|
||||
|
||||
## Обязательность
|
||||
|
||||
- **required: 340** (78%)
|
||||
- **optional: 95** (22%)
|
||||
- **default: 120** параметров имеют значение по умолчанию
|
||||
|
||||
Большинство параметров обязательные, но многие с дефолтами.
|
||||
|
||||
---
|
||||
|
||||
## Типы операций
|
||||
|
||||
| Kind | Кол-во | Паттерн |
|
||||
|---|---|---|
|
||||
| `instance` | 164 | create/delete + modify/suspend/resume (у 27 из 43) |
|
||||
| `action` | 32 | reconcile (20), redeploy (7), restart (3), recovery (2) |
|
||||
| `subresource` | 31 | user (16), database (6), topic (3), backup (2), vdc (2) |
|
||||
|
||||
---
|
||||
|
||||
## Subresource-паттерн
|
||||
|
||||
Самый частый — `user` (16 сервисов). Создаётся отдельный `nubes_{service}_user` ресурс с параметрами `username` + `role`. Аналогично `database` (6 сервисов) с `dbName` + `dbOwner`.
|
||||
|
||||
| subresource | Кол-во | Параметры |
|
||||
|---|---|---|
|
||||
| `user` | 16 | `username` (string, regex), `role` (string, value_list) |
|
||||
| `database` | 6 | `dbName` (string, regex), `dbOwner` (string) |
|
||||
| `topic` | 3 | Kafka |
|
||||
| `backup` | 2 | S3, PostgreSQL |
|
||||
| `vdc` | 2 | vcOrg |
|
||||
| `sub_user` | 2 | openwhisk |
|
||||
|
||||
---
|
||||
|
||||
## Топ параметров create
|
||||
|
||||
| code | Кол-во | Назначение |
|
||||
|---|---|---|
|
||||
| `resourceRealm` | 19 | Платформа/K8s-кластер |
|
||||
| `resourceMemory` | 11 | Память |
|
||||
| `resourceCPU` | 11 | CPU |
|
||||
| `startupConfiguration` | 10 | map-fixed (K8s) |
|
||||
| `clusterConfiguration` | 10 | map-fixed (K8s) |
|
||||
| `accessConfiguration` | 10 | map-fixed (K8s) |
|
||||
| `resourceInstances` | 10 | Кол-во реплик |
|
||||
| `domain` | 9 | Домен |
|
||||
| `resourceDisk` | 8 | Диск |
|
||||
| `ipSpaceName` | 4 | IP-space |
|
||||
| `appConfiguration` | 4 | Конфигурация приложения |
|
||||
| `jsonEnv` | 4 | Переменные окружения |
|
||||
| `backupConfiguration` | 3 | Бэкапы |
|
||||
| `autoscaleConfiguration` | 3 | Автоскейлинг |
|
||||
| `ipSpaceNameMaster` | 3 | IP для master |
|
||||
| `vdcUid` | 3 | Ссылка на VDC |
|
||||
| `vappName` | 2 | Имя vApp |
|
||||
| `storageConfig` | 2 | Конфигурация хранилища |
|
||||
|
||||
---
|
||||
|
||||
## Action-операции
|
||||
|
||||
| action | Кол-во | У каких сервисов |
|
||||
|---|---|---|
|
||||
| `reconcile` | 20 | Универсальная синхронизация (почти все) |
|
||||
| `redeploy` | 7 | flask, nodejs, lucee, nifi, superset, nextcloud, gitea |
|
||||
| `restart` | 3 | postgres, mariadb, redis |
|
||||
| `recovery` | 2 | postgres, clickhouse |
|
||||
|
||||
---
|
||||
|
||||
## Жизненный цикл
|
||||
|
||||
- **27 из 43** сервисов поддерживают `suspend`
|
||||
- **27 из 43** сервисов поддерживают `resume`
|
||||
- Все кто имеет suspend — имеют и resume (и наоборот)
|
||||
|
||||
---
|
||||
|
||||
## Валидация параметров
|
||||
|
||||
| Механизм | Кол-во | Пример |
|
||||
|---|---|---|
|
||||
| `regex` | 29 | `dbName: ^[A-Za-z0-9]+$` |
|
||||
| `value_list` | 38 | `role: [app_user, ddl_user]` |
|
||||
| `min/max value/length` | 87/74 | Числовые и строковые ограничения |
|
||||
| `default` | 120 | `deleteS3Bucket: true` |
|
||||
| `is_modifiable` | 106 (24%) | Можно менять после создания |
|
||||
| `is_sensitive` | 2 | `vault_secrets` |
|
||||
|
||||
---
|
||||
|
||||
## Сервисы с map-fixed (12)
|
||||
|
||||
`mariadb`, `clickhouse`, `valo_tenant`, `k8s_sthutrval_cluster`, `dummy`, `template`, `nextcloud`, `flask`, `postgres`, `redis`, `lucee`, `nodejs`
|
||||
|
||||
## Сервисы с array-map-fixed (7)
|
||||
|
||||
`mariadb`, `clickhouse`, `k8s_sthutrval_cluster`, `vc_org`, `dummy`, `vc_vdc`, `postgres`
|
||||
|
||||
---
|
||||
|
||||
## Сервисы без операций (4)
|
||||
|
||||
`openwhisk`, `gitea_complex`, `vmpostgre`, `template` — только заглушки/заготовки.
|
||||
|
||||
---
|
||||
|
||||
## Полный список сервисов и их операций
|
||||
|
||||
```
|
||||
100_openwhisk | (нет операций)
|
||||
110_dnszone | instance
|
||||
111_dnsrecord | instance, action
|
||||
112_tenant | instance
|
||||
113_vc_complex | instance
|
||||
114_gitea_complex | (нет операций)
|
||||
115_mariadb | instance, subresource, action
|
||||
116_kafka | instance, subresource
|
||||
117_nifi | instance
|
||||
119_akhq | instance
|
||||
120_clickhouse | instance, subresource, action
|
||||
12_s3 | instance, subresource, action
|
||||
13_s3bucket | instance
|
||||
149_valo_tenant | instance
|
||||
150_k8s_sthutrval_cluster | instance, subresource, action
|
||||
19_vc_org | instance, subresource, action
|
||||
1_dummy | instance, action
|
||||
20_vc_org_saas | instance
|
||||
21_vc_vdc | instance, action
|
||||
22_vc_nsxt | instance, action
|
||||
23_vc_vm | instance
|
||||
25_vcexternalip | instance
|
||||
26_vapp | instance, action
|
||||
27_vc_vm_v2 | instance
|
||||
28_vc_vm_v3 | instance, action
|
||||
29_vc_vdc_group | instance, subresource, action
|
||||
2_template | instance, action
|
||||
32_vmpostgre | (нет операций)
|
||||
50_nextcloud | instance, subresource, action
|
||||
81_superset | instance
|
||||
82_harbor | instance
|
||||
88_k8s_ziti_controller | instance
|
||||
89_flask | instance, action
|
||||
90_postgres | instance, subresource, action
|
||||
91_redis | instance, action
|
||||
92_mongodb | instance, subresource
|
||||
93_rabbitmq | instance
|
||||
94_lucee | instance, action
|
||||
95_nodejs | instance, action
|
||||
96_pgadmin | instance, action
|
||||
97_nodered | instance
|
||||
98_http | instance, action
|
||||
99_gitea | instance
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# Inverse-откат модификаторов: анализ ответа Опуса — 2026-09-23
|
||||
|
||||
Источник: prompt_for_opus_inverse_architecture.md → ответ Опуса (принят, анализ ниже).
|
||||
|
||||
## Принятые решения (по Опусу)
|
||||
|
||||
1. **Модель delete_params** → заменить плоский `{Code, Value}` на `{Code, Mode, Value?}`:
|
||||
- `Mode: static` — значение из `Value` (дефолт, обратная совместимость).
|
||||
- `Mode: zero_count` — обнулить integer-поля в элементах array-map-fixed, взяв live.
|
||||
2. **Баланс данные/логика**: форма преобразования выводится из `dataType`
|
||||
(boolean→"false", array-map-fixed→zero integer); сентинелы-значения — ТОЛЬКО данные в реестре.
|
||||
3. **Маркер поля**: явный `zero_fields:["count"]` (или флаг на sub_param), а НЕ «обнулить все integer»
|
||||
(риск: порт/приоритет/индекс в том же object).
|
||||
4. **Порядок destroy** — обратный порядок создания из `depends_on`.
|
||||
|
||||
## Мои замечания к ответу (что Опуc недоговорил)
|
||||
|
||||
- **A. Граф уже правильный.** Факт: `edge_net` (SNAT) зависит от `org_ips` (IP), `org_ips` — от `edge`.
|
||||
Обратный порядок destroy: `edge_net → org_ips → edge → vdc` уже корректен.
|
||||
Рекомендация Опуса «сделать ip_space зависимым от edge_net» — перепутана направлением; граф уже такой.
|
||||
- **B. Источник live для zero_count в Delete не указан.** `Delete` модификатора имеет только TF `state`,
|
||||
а live `vIPConfigure` надо читать через `GetInstanceStateParams` в рантайме Delete.
|
||||
- **C. Отличие sentinel от имени в valueList не разобрано** (valueList без разметки sentinel в данных API).
|
||||
- **D. Идемпотентность zero_count при повторном destroy не поднята** (count=0 → снова 0: no-op?).
|
||||
|
||||
## Открытые вопросы (второй раунд к Опусу) — ЗАКРЫТЫ
|
||||
|
||||
1. **Источник live для zero_count в Delete** → `GetInstanceStateParams` (не TF state). ✅
|
||||
2. **Sentinel в valueList** → явный `off_value:"no-needed"` в реестре (данные, не логика). ✅
|
||||
3. **Идемпотентность zero_count** → пропускать `run`, если live уже `count=0` (применимо и к static). ✅
|
||||
4. **count=0** → канонический inverse; полное удаление ipSpace = отдельный опциональный `Mode:remove` (не подменять zero_count). ✅
|
||||
|
||||
## ИТОГ — финальная модель inverse
|
||||
|
||||
`delete_params: []{ Code, Mode, Value?, zero_fields?, off_value? }`
|
||||
- `Mode: static` — обратное значение = `Value` (boolean→"false"; string→off_value).
|
||||
- `Mode: zero_count` — взять live array-map-fixed, обнулить поля из `zero_fields:["count"]`.
|
||||
- `Mode: remove` (опц., не для FullPipe) — полное удаление элемента.
|
||||
|
||||
Рантайм: Delete → `GetInstanceStateParams` → построить inverse → если live уже целевое → no-op (skip run) → иначе `modify`.
|
||||
|
||||
Реестр (yaml-generator main.go):
|
||||
- `vc_org.ip_space`: `inverse`, param `vIPConfigure` `Mode:zero_count, zero_fields:[count]`.
|
||||
- `vc_nsxt.network`: `inverse`, params `needEnableAVI`=static `"false"`, `ipSpaceName`=static off_value `"no-needed"`.
|
||||
|
||||
Порядок destroy (уже корректен в .tf): `edge_net → org_ips → edge → vdc`.
|
||||
|
||||
## Факты (не менять, проверено)
|
||||
|
||||
- `count=0` принимается API (minvalue:1 в схеме — не отвергает), идемпотентно (ORG_IP_MODIFIER_TEST_2026-09-22).
|
||||
- SNAT off = `ipSpaceName="no-needed"` (sentinel в valueList, HAR_SNAT_MODIFY_FINDINGS).
|
||||
- `needEnableAVI` boolean → inverse `"false"`.
|
||||
- vc_org.ip_space: `error` → нужен `inverse` (zero_count). vc_nsxt.network: inverse+static `needEnableAVI=false`, добавить `ipSpaceName="no-needed"`.
|
||||
@@ -0,0 +1,264 @@
|
||||
# Ответ Opus 4.8: код-ревью и оценка архитектуры (3 задачи roadmap)
|
||||
|
||||
Дата: 2026-09-22 | Вопрос: `docs/prompt_for_opus_review.md` (коммит `7eab45e`)
|
||||
|
||||
Ответ получен строго по контексту промпта (файлы не открывались).
|
||||
|
||||
---
|
||||
|
||||
Задание — код-ревью по контексту (файлы открывать запрещено). Отвечаю сжато по пунктам.
|
||||
|
||||
## 1. Покрывает ли модель 3 задачи
|
||||
|
||||
| Задача | Вердикт | Причина |
|
||||
|---|---|---|
|
||||
| vcOrg modify (динамич. IP) | 🟡 | Покрыта, если аллокация = modify-параметр (кол-во/список). Проблема — тип массива (array-map-fixed = JSON-строка) и семантика «растёт по мере необходимости» |
|
||||
| vcNsxt modify (SNAT + IP из пула vcOrg) | 🔴 | Текущая модель НЕ покрывает: refSvc ссылается на инстанс/uid, а нужен конкретный элемент списка IP. Нужна новая связка (computed-атрибут + ссылка) |
|
||||
| k8sShturval create | 🟢 | Стандартный CRUD-create, покрыт. Риски — только long-create и типы параметров |
|
||||
|
||||
## 2. Как моделировать пул IP vcOrg
|
||||
|
||||
- **(б) отдельный ресурс/подресурс на IP** — ✗ противоречит архитектуре: «один сервис = один ресурс», подресурсы/data source не генерируются. Потребует ломки генератора.
|
||||
- **(а) атрибут-массив на nubes_vc_org** — ✅ согласуется. Но НЕ через array-map-fixed (JSON-строка: нет семантического diff, ломается порядок, нельзя адресовать элемент).
|
||||
- **Рекомендация:** декларативное желаемое состояние — либо `int64 desired_count` (modify), либо реальный `ListNestedAttribute`; аллоцированные IP отдавать как **типизированный computed** (list объектов), а не плоскую JSON-мапу. Императивная семантика «добавь ещё один» несовместима с декларативным Terraform.
|
||||
|
||||
## 3. Как vcNsxt получает IP из vcOrg
|
||||
|
||||
| Вариант | Оценка |
|
||||
|---|---|
|
||||
| (а) refSvc-параметр | 🔴 не может указать на конкретный элемент списка, только на uid инстанса |
|
||||
| (б) computed-атрибут vcOrg + ссылка `nubes_vc_org.x.<attr>` | 🟢 лучший фит. IP в nsxt — обычная string, пользователь берёт конкретный элемент |
|
||||
| (в) data source | ✗ не генерируется |
|
||||
|
||||
**Рекомендация — (б).** Условие: computed-вывод vcOrg должен быть **индексируемым типизированным** (`allocated_ips[0]`), не спрятанным в JSON-строке `state_out_flat` — иначе граф зависимостей Terraform не построит ребро и придётся ручной `depends_on`.
|
||||
|
||||
## 4. Риски текущего кода для этих потоков
|
||||
|
||||
- 🔴 **modify с массивами:** array-map-fixed = JSON-строка → нет семантического diff, смена порядка API даёт ложные modify, «дорастить пул» = полная замена списка (опасно, возможен ре-аллок).
|
||||
- 🟡 **create-only guard:** если параметры SNAT-IP / аллокации помечены не как `is_modifiable` → ModifyPlan заблокирует задачи 1 и 2. Проверить флаги в YAML.
|
||||
- 🟡 **read-back:** если API возвращает IP в нестабильном порядке → вечный drift на vcOrg.
|
||||
- 🟡 **порядок плана:** зависимость nsxt→vcOrg держится, только если ссылаемый IP — first-class computed (unknown на plan). Внутри JSON-строки Terraform зависимость не отследит → apply-time ошибки.
|
||||
|
||||
## 5. k8sShturval create — что критично
|
||||
|
||||
- refSvc к vdc/org резолвится (display name ИЛИ uid).
|
||||
- 🔴 long-create: create-op должен **ждать готовности** (poll ready-state), а не только «принято»; следить за context timeout.
|
||||
- типы параметров: map-fixed (nested) vs array-map-fixed (JSON), корректность required/default.
|
||||
- read-back state_params после create для computed-полей.
|
||||
|
||||
## 6. Итог — рекомендации по приоритету
|
||||
|
||||
1. 🔴 Добавить в vcOrg **типизированный индексируемый computed** для аллоцированных IP (list объектов, не JSON-мапа) — разблокирует задачу 2 через ссылку.
|
||||
2. 🔴 Моделировать аллокацию как **декларативное желаемое состояние** (int64 count или ListNested) с реконсиляцией в modify; параметры пометить `is_modifiable`.
|
||||
3. 🟡 Заменить array-map-fixed (JSON-строка) на **ListNestedAttribute** — ради адресуемости элементов, семантического diff и стабильного порядка.
|
||||
4. 🟡 Гарантировать граф зависимостей: ссылаемый IP — отдельный computed-атрибут, не поле JSON, чтобы не требовать `depends_on`.
|
||||
5. 🟢 k8sShturval: добавить ожидание готовности (poll) в long-create и проверить резолв refSvc.
|
||||
|
||||
**Ключевой вывод:** задачи 1 и 3 укладываются в текущую модель с доработками генератора; **задача 2 требует новой абстракции** — типизированного computed-вывода на vcOrg (вариант 3б), т.к. refSvc принципиально не адресует элемент списка.
|
||||
|
||||
---
|
||||
|
||||
## Раунд 2 — уточняющие вопросы и пересмотр
|
||||
|
||||
После проверки кода выяснилось, что промпт раунда 1 описывал модель неполно. Opus получил уточнённый контекст и 7 доп. вопросов.
|
||||
|
||||
### Уточнение контекста (передано Opus)
|
||||
|
||||
- Генератор уже поддерживает 4 вида ресурсов: instance (`nubes_<svc>`), subresource, action (только redeploy), modifier (`nubes_<svc>_<modifier>`).
|
||||
- Modifier-ресурсы для задач 1 и 2 УЖЕ СГЕНЕРИРОВАНЫ:
|
||||
- `nubes_vc_org_ip_space` (vc_org, modify 207): `vc_org_id` (required), `v_ip_configure` (required, StringAttribute = JSON-строка). Computed-выводов НЕТ.
|
||||
- `nubes_vc_nsxt_network` (vc_nsxt, modify 111): `vc_nsxt_id` (required), `need_enable_avi` (bool), `virtual_services_count` (int64), `ip_space_name` (string optional), `qos_profile` (string), `routed_net_configuration` (required, JSON-строка). Computed-выводов НЕТ.
|
||||
- Маппинг: `map-fixed` → SingleNestedAttribute; `array-map-fixed` → StringAttribute (JSON-строка).
|
||||
- Завершение операции — поллинг до `dtFinish`. Delete modifier — no-op.
|
||||
- Связка NSXT↔Org идёт **по имени ipSpace**: `vc_org.modify` задаёт `vIPConfigure[].name`, `vc_nsxt.modify` принимает `ipSpaceName` = это имя. Конкретный IP нигде не адресуется.
|
||||
|
||||
### Ответ Opus (пересмотр)
|
||||
|
||||
## 1. Верен ли прошлый вердикт про «новую абстракцию»
|
||||
|
||||
🟢 **Прошлый вывод отменяется.** Он строился на неполной модели. modifier-ресурсы уже существуют → задача 2 реализуема сегодня двумя модификаторами (`nubes_vc_org_ip_space` → `nubes_vc_nsxt_network`), связанными по имени ipSpace. Никакой новой абстракции и ломки генератора не требуется.
|
||||
|
||||
## 2. Минимальная доработка генератора для ссылки вместо строки
|
||||
|
||||
Добавить **один computed-вывод** на `nubes_vc_org_ip_space` — эхо `v_ip_configure[].name` (имя ipSpace). Тогда:
|
||||
```
|
||||
ip_space_name = nubes_vc_org_ip_space.x.ip_space_name
|
||||
```
|
||||
Это даёт ребро графа + защиту от опечатки. Адресовать IP не нужно — связка по имени. Опционально сверх минимума: валидация, что имя существует в org (refSvc-подобная проверка `ip_space_name` по vIPConfigure целевого org).
|
||||
|
||||
## 3. SNAT: свободная строка или computed обязателен
|
||||
|
||||
- Функционально **свободная строка + `depends_on` работает** (связка по имени, IP не адресуется). 🟡 Риски: опечатка, нет автоматического ребра графа без ручного depends_on, нет drift-детекта.
|
||||
- **Экспортировать имя (string), НЕ список IP.** Список IP не нужен — привязка идёт по имени ipSpace, не по конкретному адресу.
|
||||
- Вердикт: не строго обязателен для функции, но настоятельно рекомендован для корректности графа. Минимум — computed `ip_space_name` (string).
|
||||
|
||||
## 4. v_ip_configure как JSON-строка — реально ли бьёт
|
||||
|
||||
🟢 **В этом сценарии практически безвредно.** Значение фактически write-only (computed read-back нет), потребителю нужно только имя, которое пользователь сам задал. Поэлементное сравнение/адресация не требуются → прошлый 🔴 понижается.
|
||||
|
||||
Остаточный 🟡: спурьёзный diff при переформатировании JSON пользователем; если count растёт со временем — семантика «полная замена списка» без гранулярного diff может ре-триггерить modify/ре-аллокацию. Но без read-back и при связке по имени это не блокер.
|
||||
|
||||
## 5. Delete modifier = no-op
|
||||
|
||||
🟡 **No-op допустим** для association/one-shot модификатора, если обратного payload нет — это известный паттерн.
|
||||
|
||||
**Реальные последствия destroy:** Terraform удалит запись из state, но бэкенд НЕ освободит IP и НЕ отключит SNAT → утечка ресурсов + расхождение state↔бэкенд.
|
||||
|
||||
- Если бэкенд поддерживает обратную операцию (де-аллокация / disable SNAT) — no-op это реальный пробел 🔴, надо подключить reverse-op.
|
||||
- НЕДОСТАТОЧНО ДАННЫХ: есть ли у vc_org/vc_nsxt обратный payload. Если его действительно нет — no-op единственный вариант, но обязательно задокументировать поведение destroy.
|
||||
|
||||
## 6. Нужен ли ещё «int64 desired_count на инстансе»
|
||||
|
||||
🟢 **Нет, прошлый п.2 отменяется.** modifier — правильная единица для чередующейся зависимости (org аллоцирует → nsxt потребляет → org доаллоцирует). Счётчик на инстанс-ресурсе связал бы жизненный цикл пула с жизненным циклом инстанса и сломал бы взаимное чередование org↔nsxt. **Отдельный modifier строго лучше.**
|
||||
|
||||
## 7. k8sShturval create — что остаётся критичным
|
||||
|
||||
Помимо резолва refSvc (display name ИЛИ uid) и корректности required/default в map-fixed/array-map-fixed:
|
||||
- 🟡 **read-back computed после create** (kubeconfig/vault-выводы) — что реально возвращается в state.
|
||||
- 🟡 **обработка failure-состояния в поллинге**: dtFinish должен различать «готово успешно» и «завершилось с ошибкой», иначе провал маскируется под успех. НЕДОСТАТОЧНО ДАННЫХ по логике ошибки в poll-цикле.
|
||||
- 🟢 timeout-бюджет long-create vs интервал поллинга.
|
||||
|
||||
---
|
||||
|
||||
**Итог пересмотра:** обе задачи 1 и 2 **укладываются в существующий modifier-паттерн без новых абстракций**. Единственная стоящая доработка — добавить computed-экспорт `ip_space_name` на org-модификатор (п.2/3), чтобы заменить свободную строку ссылкой и построить граф зависимостей. Проблема JSON-строки в этом сценарии не критична (п.4). Главный остаточный риск — no-op delete и утечка IP/SNAT при destroy (п.5), требует проверки наличия обратного payload.
|
||||
|
||||
---
|
||||
|
||||
## Раунд 3 — дрейф сгенерированного кода и защитный механизм
|
||||
|
||||
Отдельная тема (не roadmap): расхождение `generated/{stand}/go` ↔ `provider/internal/resources_gen`, как предотвратить и сделать видимым.
|
||||
|
||||
### Контекст, переданный Opus
|
||||
|
||||
Go-провайдер Terraform. Ресурсы генерируются из YAML-спеков генератором.
|
||||
Цепочка: YAML → generated/{stand}/go/*.go → (копирование) → provider/internal/resources_gen/*.go → сборка бинаря.
|
||||
|
||||
Факты:
|
||||
- generated/ и provider/internal/resources_gen/ внесены в .gitignore → дрейф между ними НЕ виден в git status/diff.
|
||||
- Реальный дрейф уже случился: копия в provider/ устарела (сгенерирована до двух последних фиксов генератора).
|
||||
- Есть два скрипта сборки: (а) правильный — копирует свежий generated во временный каталог provider и собирает оттуда; (б) рисковый — собирает напрямую из provider/ (берёт потенциально устаревшую локальную копию).
|
||||
|
||||
Вопросы: 1) коммитить автоген или .gitignore+проверка; 2) два пути сборки — корень ли; 3) что предотвращает дрейф, а не детектит; 4) убрать вторую копию и генерировать при сборке; 5) как гарантированно «видно сразу при изменении кода».
|
||||
|
||||
### Ответ Opus
|
||||
|
||||
## 1. Коммитить сгенерированный код или .gitignore + проверка дрейфа
|
||||
|
||||
🟢 **Коммитить сгенерированный код** (для соло/малой команды это надёжнее). Обоснование:
|
||||
- Расхождение всплывает в `git diff`/PR-ревью — там, куда уже смотришь, а не в отдельной проверке, которую надо не забыть запустить.
|
||||
- CI-гейт `regenerate && git diff --exit-code` превращает дрейф в жёсткую ошибку сборки.
|
||||
- Минус — шум в диффах от больших автогенов, но это приемлемая цена за видимость.
|
||||
|
||||
.gitignore + внешняя проверка дрейфа 🟡 — работает, только если проверку невозможно обойти; сейчас её обходят вторым скриптом. Скрытый код = скрытый дрейф (уже случилось).
|
||||
|
||||
## 2. Два пути сборки — корень проблемы?
|
||||
|
||||
🔴 **Да, это корень.** Проблема не в дрейфе как таковом, а в том, что **существует путь, собирающий из потенциально устаревшей копии**. Пока рисковый скрипт (б) доступен, дрейф рано или поздно попадёт в бинарь.
|
||||
|
||||
**Оставить ровно один канонический путь.** Скрипт (б) удалить или сделать так, чтобы он физически не мог собрать из несвежей копии (см. п.3–4).
|
||||
|
||||
## 3. Что ПРЕДОТВРАЩАЕТ дрейф, а не только детектит
|
||||
|
||||
| Механизм | Роль |
|
||||
|---|---|
|
||||
| Жёсткий fail на старте сборки | 🟡 **детект** — не даёт собрать плохой бинарь, но дрейф уже есть |
|
||||
| Авто-синхронизация/генерация перед сборкой | 🟢 **предотвращение** — копия физически не может быть устаревшей |
|
||||
| git pre-commit hook | 🟡 **детект** на этапе коммита |
|
||||
|
||||
**Предотвращает только генерация/синк как обязательная часть сборки** (копия всегда свежая). Fail и hook — детекторы. Рекомендуемая комбинация: **генерация-при-сборке (предотвращение) + CI `git diff --exit-code` после регенерации (страховка от обхода)**.
|
||||
|
||||
## 4. Более сильный паттерн: убрать вторую копию, генерировать при сборке
|
||||
|
||||
🟢 **Да, это сильнейший вариант.** Нет постоянной копии → нечему рассинхронизироваться. Единственный источник истины = YAML + генератор (`go generate` / шаг Makefile перед `go build`).
|
||||
|
||||
Минусы:
|
||||
- 🟡 Сборка зависит от наличия и детерминированности генератора (версию генератора надо пинить).
|
||||
- 🟡 Медленнее сборка.
|
||||
- 🔴 Провайдер перестаёт быть чистым Go-модулем: нельзя `go get`/собрать без тулчейна генератора; воспроизводимость завязана на версию генератора.
|
||||
- Сложнее просто «посмотреть» сгенерированный код без сборки.
|
||||
- НЕДОСТАТОЧНО ДАННЫХ: детерминирован ли генератор (стабильный порядок полей/файлов) — без этого diff-гейт будет шуметь.
|
||||
|
||||
**Компромисс:** генерировать при сборке И коммитить результат (генерация переписывает закоммиченную копию, CI ловит расхождение) — совмещает «невозможно устареть» с «видно в диффе».
|
||||
|
||||
## 5. «Видно сразу при изменении кода, не закопано в доках»
|
||||
|
||||
Гарантированно это даёт **только закоммиченный сгенерированный код**: расхождение появляется в `git diff` того же PR, где меняется генератор/YAML. Усилить:
|
||||
- **pre-commit hook** `regenerate` → расхождение всплывает до коммита;
|
||||
- **CI-гейт** `regenerate && git diff --exit-code` → нельзя смёржить с дрейфом.
|
||||
|
||||
Вариант «генерировать при сборке без копии» даёт *другую* гарантию — «устареть невозможно», но там **нет диффа для обзора**. Если требование именно «видно в изменениях кода» — нужен закоммиченный автоген.
|
||||
|
||||
---
|
||||
|
||||
**Итог — рекомендация по приоритету:**
|
||||
1. 🔴 Убрать рисковый путь сборки (б); оставить один канонический.
|
||||
2. 🟢 Сделать генерацию/синк обязательным шагом канонической сборки (предотвращение).
|
||||
3. 🟢 Закоммитить сгенерированный код + CI-гейт `regenerate && git diff --exit-code` (видимость + защита от обхода).
|
||||
4. 🟡 Запинить версию генератора и обеспечить детерминированный вывод, иначе гейт будет ложно срабатывать.
|
||||
|
||||
---
|
||||
|
||||
## Раунд 4 — универсальность и реестр исключений
|
||||
|
||||
Тема: принцип «универсально по умолчанию, исключения — только если API не ложится», и как кодировать исключения, чтобы они были однозначно понимаемы при каждой генерации.
|
||||
|
||||
### Контекст, переданный Opus (кратко)
|
||||
|
||||
Пайплайн: YAML-спека сервиса (из API) → универсальный генератор → Go-ресурсы → сборка. Код/скрипты/логика генерации — одни для всех стендов и сервисов; различается только YAML. Механизм modifier-ресурсов уже универсален (дженерик читает `kind: modifier` + имя). Не универсально сейчас: хардкод в yaml-generator (`svc.ID == 19 → ip_space`, `svc.ID == 22 → network`) и в docs-generator (`ServiceID == 90`, спец-вывод postgres). Отдельно был инцидент дрейфа git-ignored сгенерированного кода (фон).
|
||||
|
||||
Выработанный принцип (просили оценить): «Универсально по умолчанию. Исключение — только если API реально не ложится. Каждое исключение — одна явная таблица/структура, которую КОД ЧИТАЕТ, с полями ЧТО/ЧТО ДЕЛАЕТ/ПОЧЕМУ/ГДЕ. Канонический список — в ARCHITECTURE-доке, код ссылается. Изменение = изменение таблицы → видно в диффе.»
|
||||
|
||||
### Ответ Opus
|
||||
|
||||
## 1. Оценка принципа
|
||||
|
||||
🟢 **Принцип корректен долгосрочно** — «convention over configuration + escape hatch».
|
||||
|
||||
Подводные камни:
|
||||
- 🟡 «API не ложится» субъективно → нужен объективный тест-триггер, иначе exception creep.
|
||||
- 🔴 Исключения не возвращаются в ядро: когда паттерн повторился 2–3 раза, нужен ритуал «промоушена» в ядро.
|
||||
- 🟡 Обратный перекос: обобщать реально одноразовый случай — раздувает ядро.
|
||||
|
||||
## 2. Как кодировать исключения
|
||||
|
||||
| Вариант | Видимость в diff | Нельзя «проспать» | Поддержка | Рассинхрон с YAML |
|
||||
|---|---|---|---|---|
|
||||
| (а) именованная таблица в коде, код её читает | 🟢 | 🟢 (если итерирует и падает на неучтённом) | 🟡 нужна пересборка | 🟢 низкий |
|
||||
| (б) отдельный yaml-конфиг | 🟢 | 🟡 легко забыть подключить | 🟢 без пересборки | 🟡 средний |
|
||||
| (в) аннотации в YAML-спеке | 🔴 | 🔴 | 🔴 | 🔴 фатально: YAML регенерится → аннотации затираются |
|
||||
|
||||
**(в) отклонить.** **Рекомендация: (а)** — именованная структура, которую код итерирует и ассертит.
|
||||
|
||||
## 3. Граница «логика» vs «данные»
|
||||
|
||||
- В ядре — механизм/алгоритм (как модификатор генерится, маппинг схемы). Никогда не per-service.
|
||||
- В реестре — чистые данные («сервис X → имя модификатора Y», «сервис 90 → набор полей Z»).
|
||||
|
||||
Признаки: 1) `if id == N`, меняющий поток исполнения → извлечь данные; 2) убрать пункт → меняются только значения, не поведение → данные; 3) copy-paste кода → механизм (обобщать), разные строки таблицы → данные.
|
||||
|
||||
## 4. Паттерн «override registry» в кодогенераторах
|
||||
|
||||
- tfplugingen-openapi: `generator_config.yml` отдельно от спеки + IR (`terraform-plugin-codegen-spec`).
|
||||
- OpenAPI Generator: vendor extensions `x-*` + template-оверрайды + config-json.
|
||||
- protoc-плагины: custom options (напр. `google.api.http`).
|
||||
|
||||
Что перенять: 1) отдельный версионируемый конфиг оверрайдов; 2) IR-слой; 3) fail на неучтённом; 4) стабильное символьное имя, не сырой ID.
|
||||
|
||||
## 5. Не противоречит ли реестр «YAML — единственный источник»
|
||||
|
||||
Не противоречит — при разделении двух доменов истины:
|
||||
- **API-YAML** = истина про «что есть сервис» (машинно-владеемый, регенерится).
|
||||
- **Реестр оверрайдов** = истина про «наши провайдер-специфичные решения» (человеко-владеемый).
|
||||
|
||||
Теневой источник — только если ОДИН факт лежит в обоих. 🔴 Нельзя аннотировать API-YAML. Конвейер: `API-YAML + overrides → merged IR → codegen`.
|
||||
|
||||
## 6. Риски «таблица + раздел в ARCHITECTURE.md»
|
||||
|
||||
- 🔴 ARCHITECTURE.md дрейфует от таблицы → возврат к `if id==19`, если кто-то добавит ветку в обход.
|
||||
- 🔴 Числовые ID (19/22/90) непрозрачны.
|
||||
|
||||
Как закрыть: 1) 🔴 единая точка маршрутизации + CI-lint/grep-гейт против `svc.ID ==` вне реестра; 2) раздел ARCHITECTURE генерировать ИЗ реестра (golden-test); 3) 4 поля — поля структуры, а не комментарии; 4) стабильные символьные ключи; 5) fail-fast: генератор падает, если спец-обработка без записи в реестре.
|
||||
|
||||
---
|
||||
|
||||
**Итог:** сильнейшая реализация — отдельный человеко-владеемый override-реестр (данные, не логика), стабильные ключи, IR-слой применения, CI-гейт против хардкодов вне реестра, раздел ARCHITECTURE генерируется из реестра. Это устраняет и `if id==N`, и дрейф доки.
|
||||
Reference in New Issue
Block a user