add: documentation

This commit is contained in:
“Naeel”
2026-06-30 15:45:24 +04:00
parent 540c1f7293
commit ca276d200f
1055 changed files with 47294 additions and 0 deletions
+461
View File
@@ -0,0 +1,461 @@
# Infrastructure Resources & AI Integration Vision
**Date**: 24 January 2026
**Status**: VDC/Edge/vApp resources implementation & AI architecture design
## Current State
### Resources Implemented
- ✅ VDC (Virtual Data Center) - service ID 21
- ✅ Edge Gateway - service ID 22
- ✅ vApp (Virtual Application) - service ID 26
- ✅ VM Instance - service ID 28
### API Patterns Discovered
#### Creation Flow
```
1. POST /instances → {serviceId, displayName, descr} → 201 + Location: //{instanceId}
2. POST /instanceOperations → {instanceUid, operation: "create"} → 201 + Location: //{operationId}
3. GET /instanceOperations/{operationId}?fields=cfsParams → список параметров
4. POST /instanceOperationCfsParams → для каждого параметра отправить значение
5. POST /instanceOperations/{operationId}/run → запустить операцию → 201
6. POLLING: GET /instanceOperations/{operationId} → проверять operationIsInProgress: false
7. POLLING: GET /instances/{instanceId} → проверять explainedStatus: "running"
```
#### Critical Bug Fixed
**Problem**: Provider timeout после 10 минут, хотя VDC создаётся за ~45 секунд.
**Root Cause**:
1. JSON field был `status` вместо `explainedStatus`
2. Polling проверял только instance, пропуская завершение operation
**Solution**:
```go
// Исправленная структура
type InstanceResponse struct {
Status string `json:"explainedStatus"` // было: json:"status"
}
// Двухэтапный polling
1. Ждать operationIsInProgress: false (операция завершена)
2. Проверять explainedStatus: "running" (instance запущен)
```
### Actual Creation Times (from API data)
- **VDC**: ~43-46 секунд
- **Edge Gateway**: ~5-10 минут (требует проверки)
- **vApp**: ~3-5 минут (требует проверки)
- **VM**: ~5-10 минут
### VDC Lifecycle Quirk
**Delete operation не удаляет VDC**, а переводит в suspend:
```go
operationReq := CreateOperationRequest{
Operation: "suspend", // не "delete"!
}
```
Полное удаление: suspend → ждать 14 дней → удалить вручную через UI.
---
## Data Sources for Existing Infrastructure
### Problem Statement
Создание VDC+Edge+vApp занимает ~30-40 минут. Если инфраструктура уже существует, не нужно ждать.
### Solution: Data Sources
```text
# Поиск существующих ресурсов по имени
data "nubes_vdc" "existing" {
display_name = "production-vdc"
}
data "nubes_edge" "existing" {
display_name = "production-edge"
}
data "nubes_vapp" "existing" {
display_name = "production-vapp"
}
# Создание только VM в существующей инфраструктуре (~5-10 мин вместо ~40)
# resource "nubes_vm_instance" "web" {
vapp_uid = data.nubes_vapp.existing.id
# ...
}
```
### Flexible Deployment Pattern
Один конфиг - выбор через переменные:
```text
variable "create_vdc" { type = bool }
variable "create_edge" { type = bool }
variable "create_vapp" { type = bool }
# Условное создание
resource "nubes_vdc" "new" {
count = var.create_vdc ? 1 : 0
# ...
}
# Условный поиск
data "nubes_vdc" "existing" {
count = var.create_vdc ? 0 : 1
display_name = var.existing_vdc_name
}
# Выбор ID
locals {
vdc_id = var.create_vdc ? nubes_vdc.new[0].id : data.nubes_vdc.existing[0].id
}
```
**Use Cases**:
- `create_vdc=true, create_edge=true, create_vapp=true` → всё с нуля (~30-40 мин)
- `create_vdc=false, create_edge=false, create_vapp=false` → всё существует (~5-10 мин)
- `create_vdc=false, create_edge=true, create_vapp=true` → VDC корпоративный, свой Edge+vApp (~15-20 мин)
---
## AI Integration Vision
### Philosophy
**"Домохозяйка-friendly" облако** - любой человек без технических знаний может создать инфраструктуру описав что ему нужно обычным языком.
### Architecture Concept
#### 1. Композитный AI-ресурс `nubes_application_stack`
**Input - естественный язык**:
```text
# resource "nubes_application_stack" "myapp" {
ai_description = <<-EOT
Создай простой сайт с базой данных
EOT
vdc_uid = data.nubes_vdc.existing.id
edge_uid = data.nubes_edge.existing.id
}
```
**AI парсит через Gemini**:
```json
{
"components": [
{"type": "vm", "role": "web", "cpu": 2, "ram": 4, "image": "nginx"},
{"type": "postgres", "version": "14", "storage": 50}
],
"network": "shared"
}
```
**Provider создаёт**:
1. vApp (контейнер)
2. VM для nginx (с cloud-init)
3. VM для PostgreSQL (с cloud-init + конфиг)
4. Настраивает сеть между ними
**Output**:
```
Apply complete!
Outputs:
web_vm_ip = "10.0.1.5"
postgres_connection_string = "postgresql://10.0.1.6:5432/myapp"
```
#### 2. Инкрементальные обновления через естественный язык
**Концепция**: История изменений как git changelog
```text
# resource "nubes_application_stack" "myapp" {
ai_description = <<-EOT
# День 1 (23.01.2026)
Создай простой сайт с базой данных
# День 7 (30.01.2026)
+ добавь Redis для кеша
# День 14 (06.02.2026)
+ добавь балансировщик на 2 фронтенда
# День 20 (12.02.2026)
- убери Redis, не пригодился
EOT
}
```
**AI видит**:
- Базовая конфигурация (первая запись)
- `+` = добавить компонент
- `-` = удалить компонент
- Обычный текст = модификация
**Provider применяет только delta**:
- Не трогает существующие VM
- Создаёт новые компоненты
- Удаляет ненужные
**Преимущества**:
- Видна эволюция инфраструктуры
- Audit trail встроен
- Rollback = удалить последние строки
- Понятно что и когда менялось
#### 3. AI-powered проверка квот и рекомендации
**При `terraform plan`**:
```go
func (r *ApplicationStackResource) ModifyPlan(ctx, req, resp) {
// Парсим AI описание
aiParams := parseWithGemini(plan.AiDescription.ValueString())
// Получаем квоты VDC
vdcQuotas := getVDCQuotas(plan.VdcUid)
usedResources := calculateUsedResources(plan.VdcUid)
available := vdcQuotas - usedResources
// Сравниваем с запросом AI
if aiParams.TotalRAM > available.RAM {
resp.Diagnostics.AddWarning(
"Недостаточно ресурсов",
fmt.Sprintf(`
AI запрашивает: %d GB RAM
Доступно в VDC: %d GB
Используется: %d GB
Рекомендации:
- Уменьшите количество VM
- Или увеличьте квоту VDC
- Или используйте другой VDC
`, aiParams.TotalRAM, available.RAM, usedResources.RAM))
}
// Обратная ситуация - избыточность
if aiParams.TotalRAM < available.RAM * 0.1 {
resp.Diagnostics.AddWarning(
"Избыточная конфигурация",
fmt.Sprintf(`
AI запрашивает: %d GB RAM для "%s"
Доступно: %d GB RAM в VDC
Можете смело увеличить ресурсы для лучшей производительности.
`, aiParams.TotalRAM, aiParams.Purpose, available.RAM))
}
}
```
**Вывод при `terraform plan`**:
```
╷
│ Warning: Недостаточно ресурсов
│
│ AI запрашивает: 40 GB RAM
│ Доступно в VDC: 20 GB (квота) - 12 GB (используется) = 8 GB свободно
│
│ Рекомендации:
│ - Уменьшите количество VM до 1-2
│ - Или увеличьте квоту VDC
╵
```
#### 4. Gemini API Integration
**Technology Stack**:
- Google Gemini API (есть аккаунт)
- HTTP JSON API
- Structured output parsing
**Промпт пример**:
```
Parse this infrastructure request into JSON:
"Создай простой сайт с базой данных + добавь Redis для кеша"
Return format:
{
"components": [
{"type": "vm", "role": "frontend", "cpu": 2, "ram": 4, "image": "nginx"},
{"type": "postgres", "version": "14", "storage": 50},
{"type": "redis", "ram": 2}
],
"network": "shared",
"purpose": "simple website with database and cache"
}
```
**Provider код**:
```go
func parseWithGemini(description string, apiKey string) (*StackSpec, error) {
reqBody := map[string]interface{}{
"contents": []map[string]interface{}{
{"parts": []map[string]string{{"text": buildPrompt(description)}}},
},
}
resp, err := http.Post(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key="+apiKey,
"application/json",
jsonBody(reqBody),
)
// Parse JSON response
var result StackSpec
json.Unmarshal(resp.Body, &result)
return &result, nil
}
```
#### 5. Advanced: Schedule & Lifecycle через AI
**User input**:
```text
# resource "nubes_application_stack" "test_env" {
ai_description = "Создай тестовое окружение, запусти через 2 дня на 8 часов"
}
```
**AI парсит**:
```json
{
"schedule": {
"startAt": "2026-01-26T10:00:00Z",
"duration": "8h",
"autoDelete": true
},
"components": [...]
}
```
**Provider behavior**:
- Записывает метаданные в state
- Выдаёт Warning о необходимости внешнего оркестратора
- Или интегрируется с Kubernetes Operator для cron jobs
**Full automation** (с Operator):
```
Terraform Provider (создание ресурсов)
↓
Kubernetes Operator (расписание)
↓
CronJob (проверка времени)
↓
Terraform (автоматический destroy)
```
---
## Unique Value Proposition
### Что никто не делает
❌ **Pulumi AI** - генерит код, не управляет напрямую
❌ **Google Duet** - автодополнение, не композитные ресурсы
❌ **AWS Copilot** - фиксированные шаблоны, не AI парсинг
✅ **Наше решение**:
1. **AI парсинг → композитный ресурс → проверка квот → рекомендации** в одном флоу
2. **История изменений через естественный язык** как git changelog
3. **Инкрементальные обновления** - AI понимает delta, Provider применяет только изменения
4. **"Домохозяйка-friendly"** - нулевой порог входа
### Example User Journey
**Обычный пользователь без IT знаний**:
1. Пишет в Terraform:
```hcl
# resource "nubes_application_stack" "myshop" {
ai_description = "Мне нужен интернет-магазин с корзиной и оплатой картой"
}
```
2. `terraform plan` показывает:
```
AI создаст:
- 2 VM для фронтенда (nginx) - 2 CPU, 4 GB RAM каждая
- 1 VM для бэкенда (Node.js) - 4 CPU, 8 GB RAM
- 1 PostgreSQL для данных - 50 GB storage
- 1 Redis для сессий - 2 GB RAM
Итого: 10 CPU, 18 GB RAM, 50 GB storage
Доступно в VDC: 20 CPU, 40 GB RAM, 200 GB storage ✅
```
3. `terraform apply` → через 20 минут готовый интернет-магазин
4. Через неделю добавляет:
```hcl
ai_description = <<-EOT
Мне нужен интернет-магазин с корзиной и оплатой картой
+ добавь систему рекомендаций товаров
EOT
```
5. AI понимает delta, создаёт только ML-сервис, не трогая существующее
---
## Implementation Roadmap
### Phase 1: Infrastructure Resources (Current)
- [x] VDC, Edge, vApp, VM resources
- [x] Data sources для существующих ресурсов
- [x] Правильный polling (operation → instance)
- [ ] Тестирование Edge/vApp создания
- [ ] Update/Delete операции для всех ресурсов
### Phase 2: AI Integration MVP
- [ ] Gemini API клиент в Provider
- [ ] Простой AI-парсинг для VM ресурса
- [ ] Warning'и с рекомендациями при plan
- [ ] Проверка квот VDC
### Phase 3: Композитный ресурс
- [ ] `nubes_application_stack` ресурс
- [ ] AI парсинг complex descriptions
- [ ] Автоматическое создание vApp + multiple VMs
- [ ] Cloud-init генерация для разных компонентов
### Phase 4: Advanced Features
- [ ] История изменений (git-like changelog)
- [ ] Инкрементальные обновления (delta detection)
- [ ] Интеграция с Kubernetes Operator для scheduling
- [ ] Terraform module генератор
---
## Technical Notes
### API Quirks
1. `explainedStatus` не `status` для instance state
2. `operationIsInProgress` для отслеживания операций
3. VDC delete = suspend (14 дней до полного удаления)
4. POST /run возвращает 201 мгновенно, реальная работа начинается после
### Performance
- VDC: 45 сек
- Edge: 5-10 мин (TBD)
- vApp: 3-5 мин (TBD)
- VM: 5-10 мин
- **Full stack**: ~30-40 мин
### Token Management
- JWT from Keycloak
- 2-hour expiry
- Stored in ~/.nubes_token
- Format: raw JWT (без "Bearer" prefix)
- Требует trim whitespace
---
## Questions for Product Team
1. **AI API Key**: где хранить? Provider config? Environment variable? Terraform Cloud?
2. **Scheduling**: нужен ли Kubernetes Operator или достаточно метаданных в state?
3. **Billing**: как считать стоимость AI-созданных ресурсов для показа пользователю?
4. **Templates**: создавать ли библиотеку готовых AI-промптов? ("WordPress site", "Django app", etc.)
5. **Validation**: нужна ли human-approval после AI парсинга перед apply?
+26
View File
@@ -0,0 +1,26 @@
# Полный дамп параметров API (test)
Этот документ фиксирует расположение и структуру полной выгрузки параметров API для окружения test. Дамп нужен для проверки универсального алгоритма генерации YAML.
## Где лежит
- Полный набор артефактов: [artifacts/api-meta/test](../../artifacts/api-meta/test)
## Состав дампа
- Список сервисов: [artifacts/api-meta/test/services.json](../../artifacts/api-meta/test/services.json)
- Детали сервисов по svcId: [artifacts/api-meta/test/services](../../artifacts/api-meta/test/services)
- Операции и их параметры по svcId: [artifacts/api-meta/test/operations](../../artifacts/api-meta/test/operations)
- Сводный JSONL по всем операциям: [artifacts/api-meta/test/all_params.jsonl](../../artifacts/api-meta/test/all_params.jsonl)
- Индекс по сервисам и количествам: [artifacts/api-meta/test/all_params_index.json](../../artifacts/api-meta/test/all_params_index.json)
- Аномалии полей параметров: [artifacts/api-meta/test/anomalies.txt](../../artifacts/api-meta/test/anomalies.txt)
- Маркеры готовности: [artifacts/api-meta/test/STATUS.txt](../../artifacts/api-meta/test/STATUS.txt), [artifacts/api-meta/test/STATUS_SUMMARY.txt](../../artifacts/api-meta/test/STATUS_SUMMARY.txt)
## Что внутри
- В файлах операций содержатся `svcOperation.cfsParams` — входные параметры операций.
- Сводный файл JSONL агрегирует операции и их параметры в одной строке на операцию.
## Использование
Этот дамп используется как эталон для анализа универсального алгоритма генерации YAML и проверки покрытия всех операций и параметров.
@@ -0,0 +1,133 @@
# Cloud Director Organization Service
## Обнаружено из HAR: director.har
Дата: 2026-01-23
## Основная информация
- **Service ID**: 19
- **Service Name**: "Организация в Cloud Director" (Cloud Director Organization)
- **Operation create ID**: 136
## Параметры операции create
### Параметр 418: Платформа для развертывания
- **svcOperationCfsParamId**: 418
- **Описание**: Платформа, на которой будет создана организация
- **Пример значения**: "ngcloud.ru"
- **Тип**: String
- **Обязательный**: Да
### Параметр 556: Тип организации
- **svcOperationCfsParamId**: 556
- **Описание**: Тип создаваемой организации
- **Значения**:
- `iaas` - организация с прямым доступом к облаку и авторизацией через Keycloak
- `saas` - организация под управлением Nubes, без прямого доступа к облаку
- **Тип**: String (enum)
- **Обязательный**: Да
- **Пример**: "iaas"
## Архитектура
Cloud Director Organization - корневая сущность для всей инфраструктуры:
```
Organization (serviceId=19)
└─ VDC - Virtual Data Center (serviceId=21)
└─ Edge Gateway (serviceId=22)
└─ vApp (serviceId=26)
└─ VM (serviceId=28)
```
## Типы организаций
### IaaS (Infrastructure as a Service)
- Прямой доступ к Cloud Director
- Авторизация через Keycloak
- Полный контроль над инфраструктурой
- Рекомендуется для тестовых окружений
### SaaS (Software as a Service)
- Управляется Nubes
- Без прямого доступа к облаку
- Упрощённая модель использования
- Рекомендуется для промышленных окружений
## Операции
### create (ID: 136)
Создание новой организации с заданными характеристиками и типом доступа.
**Параметры**:
- 418: platform (ngcloud.ru)
- 556: organizationType (iaas/saas)
### delete
Удаление организации после перехода в состояние suspend.
- Возможно только через 14 дней после suspend
- Удаляет все связанные сущности: VDC, Edge, vApp, VM
### modify
Изменение параметров организации (поддерживается изменение ipSpaces).
### suspend
Заморозка организации:
- Не останавливает VM
- Блокирует любые изменения инфраструктуры
### resume
Разморозка организации, восстановление функций управления.
## Выходные параметры
- **URL организации**: для быстрого доступа в тенант
- **Статус организации**: текущее состояние и параметры
## Зависимости
Для конфигурации внешних IP внутри организации необходимо:
1. Создать хотя бы один VDC, привязанный к организации
2. Создать хотя бы один Edge Gateway, привязанный к VDC
## Примечания
- Для iaas-организаций доступ к Keycloak предоставляется пользователю, который создаёт услугу
- Для saas-организаций доступ к Keycloak отсутствует
- Удаление организации приводит к удалению всех связанных сущностей
## Пример из HAR
```json
// POST /instances
{
"serviceId": 19,
"displayName": "f12",
"descr": "for f12 record"
}
// POST /instanceOperations
{
"instanceUid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"operation": "create"
}
// POST /instanceOperationCfsParams (для каждого параметра)
{
"svcOperationCfsParamId": 418,
"paramValue": "ngcloud.ru",
"instanceOperationUid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
{
"svcOperationCfsParamId": 556,
"paramValue": "iaas",
"instanceOperationUid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
## Ошибки
Из лога операции UUID xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:
- Этап "Работа с сервисом" завершился с ошибкой
- Ошибка удаления: "Не удалось удалить организацию WZ01325-iaas. Ошибка: script returned exit code 1"
- Операция выполнялась 70.322 секунды
- Результат подтверждения: 201 (Created)
+940
View File
@@ -0,0 +1,940 @@
# Terraform Provider для Nubes Cloud - История разработки
**Дата**: 21 января 2026
**Проект**: terraform-provider-mycloud
**Цель**: Создание Terraform Provider для управления ресурсами в облачной платформе Nubes
---
## 1. Инициализация проекта
### 1.1 Начальная структура
**Требования от заказчика:**
- Использовать современный **Terraform Plugin Framework** (не старый SDK)
- Реализовать ресурс `mycloud_dummy_instance` - "Болванка для тестов"
- Схема атрибутов (первоначальная, до изучения реального API):
- `parameter` (string, required)
- `sleep_ms` (int64, optional)
- `silent_quit` (bool, optional)
- `fail_in_progress` (bool, optional)
- `fail_stage` (int64, optional)
**Созданные файлы:**
```
---
## 14. Практика 2026‑02‑02: universal_rebuild, генератор и UX‑предупреждения
### 14.1 Что сделано
- Убрали `resource_realm` из схемы ресурсов: поле скрыто от пользователя, фиксированное значение подставляется генератором на этапе `create` (value = имя сервиса).
- Сделали `delete_mode` **опциональным**: по умолчанию `state_only`, пользователь сам решает, когда ставить `delete`/`suspend`.
- Добавили **явные предупреждения** в plan/apply при совпадении `display_name` и `resume_if_exists=true`:
- отдельное предупреждение для **suspended** ресурса;
- предупреждение для уже существующего **active** ресурса.
### 14.2 Ошибки и как нашли решение
- Ошибка: `resource_realm` оставался в схеме и конфигурациях — нарушало правило “поле не должно существовать вообще”.
- Решение: фильтрация `resourceRealm` на этапе генерации (скрываем в схеме, подставляем фиксированное значение в create‑params).
- Ошибка: `delete_mode` оставался в конфиге тестов, хотя требование — “юзер сам решает, если нужно удалять”.
- Решение: убрать из тестовых конфигов и оставить только optional‑поле с дефолтом `state_only`.
- Проблема UX: пользователь может “забыть” ресурс и повторить `display_name`, не понимая, что будет adopt/resume без modify.
- Решение: предупреждения в plan/apply, чтобы сначала выполнить `plan` и увидеть, что ресурс найден.
### 14.3 Итоговая логика для adopt/resume
- При `resume_if_exists=true` и совпадении имени:
- ресурс **принимается в state**;
- если он был `suspended`, выполняется `resume`;
- **новые параметры не применяются** (modify не делается автоматически).
- Пользователь видит предупреждение и может осознанно сделать следующий apply/modify.
terra/
├── main.go # Точка входа провайдера
├── go.mod # Go модуль с зависимостями
├── internal/provider/
│ ├── provider.go # Настройка клиента и провайдера
│ └── dummy_resource.go # Логика CRUD операций
└── examples/
└── main.tf # Пример использования
```
### 1.2 Выбор версий зависимостей
**Первоначальные версии:**
```go
go 1.21
github.com/hashicorp/terraform-plugin-framework v1.4.2
github.com/hashicorp/terraform-plugin-go v0.19.1
```
**Проблема #1: Попытка обновить до v1.5.0**
Попробовал использовать более новую версию:
```go
github.com/hashicorp/terraform-plugin-framework v1.5.0
github.com/hashicorp/terraform-plugin-go v0.22.0
```
**Ошибка при сборке:**
```
unknown field Diagnostics in struct literal of type tfprotov5.CallFunctionResponse
tfprotov5Diagnostic.FunctionArgument undefined
```
**Решение:**
Вернулся к стабильной версии v1.4.2 - она полностью совместима с terraform-plugin-go v0.19.1
---
## 2. Исследование реального API Nubes
### 2.1 Анализ веб-интерфейса
**URL личного кабинета:**
```
https://deck.ngcloud.ru/
```
**Обнаруженные сервисы в каталоге:**
- Инфраструктура и сеть: VM, vDC, vApp, Edge Gateway
- PaaS: Gitea, RabbitMQ, Nextcloud, Container Registry
- DBaaS: PostgreSQL, MariaDB, MongoDB, Redis
- **Болванка для тестов** ← наш целевой ресурс
### 2.2 Анализ Network запросов (DevTools)
**API Endpoint (реальный):**
```
https://deck-api.ngcloud.ru/api/v1/index.cfm
```
**Авторизация:**
```
Authorization: Bearer <JWT_TOKEN>
```
**Токен из Keycloak:**
```
https://keycloak.nubes.ru/realms/cloud
```
### 2.3 Изучение структуры данных "Болванки"
**ВАЖНО**: Общий паттерн создания ресурсов описан в отдельном гайде: [resource-creation-pattern.md](resource-creation-pattern.md)
**Сначала думал, что API простое:**
```json
POST /api/v1/dummy
{
"parameter": "test",
"sleep_ms": 1000,
"fail_in_progress": false
}
```
**Но реальность оказалась другой!**
Через Network увидел реальные поля из созданного инстанса:
```json
{
"whereFail": 1,
"durationMs": 0,
"failAtStart": false,
"failInProgress": false,
"resourceRealm": "dummy",
"mapExample": null,
"bodymessage": null,
"jsonExample": null,
"yamlExample": null,
"someKey": ""
}
```
### 2.4 Критическое открытие: Двухшаговая архитектура API
**Ошибка в понимании #1:**
Изначально думал, что создание - это один запрос POST /dummy
**Реальность:**
API использует двухшаговую модель:
1. **Шаг 1: Создание инстанса**
```http
POST /api/v1/index.cfm/instances
Content-Type: application/json
{
"serviceId": 1, // 1 = Болванка
"displayName": "terr",
"descr": ""
}
Response: 201 Created
Location: ./xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
2. **Шаг 2: Создание операции**
```http
POST /api/v1/index.cfm/instanceOperations
Content-Type: application/json
{
"instanceUid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"operation": "create"
}
Response: 201 Created
```
**Это изменило всю архитектуру провайдера!**
---
## 3. Переработка провайдера под реальное API
### 3.1 Упрощение схемы (минимализм)
**Было (теоретическая схема):**
```go
type DummyResourceModel struct {
Parameter types.String
SleepMs types.Int64
SilentQuit types.Bool
FailInProgress types.Bool
FailStage types.Int64
// ... много полей
}
```
**Стало (реальный минимум):**
```go
type DummyResourceModel struct {
ID types.String `tfsdk:"id"`
DisplayName types.String `tfsdk:"display_name"`
Description types.String `tfsdk:"description"`
Status types.String `tfsdk:"status"`
}
```
**Причина:**
Заказчик попросил "ПО МИНИМУМУ", чтобы не нагромождать опциями для первой демонстрации.
### 3.2 Структуры запросов
```go
// Шаг 1: Создание инстанса
type CreateInstanceRequest struct {
ServiceId int `json:"serviceId"`
DisplayName string `json:"displayName"`
Descr string `json:"descr,omitempty"`
}
// Шаг 2: Операция (create/modify/delete)
type CreateOperationRequest struct {
InstanceUid string `json:"instanceUid"`
Operation string `json:"operation"`
}
// Ответ от API
type InstanceResponse struct {
Uid string `json:"uid"`
DisplayName string `json:"displayName"`
Descr string `json:"descr"`
Status string `json:"status"`
}
```
### 3.3 Реализация метода Create
**Ключевые моменты:**
1. **Двухшаговое создание:**
```go
func (r *DummyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Шаг 1: POST /instances
createReq := CreateInstanceRequest{
ServiceId: 1, // Болванка
DisplayName: data.DisplayName.ValueString(),
Descr: data.Description.ValueString(),
}
// Получаем ID из Location header
location := httpResp.Header.Get("Location")
instanceId := location[2:] // Убираем "./"
// Шаг 2: POST /instanceOperations
operationReq := CreateOperationRequest{
InstanceUid: instanceId,
Operation: "create",
}
// Шаг 3: Ждем и читаем статус
time.Sleep(2 * time.Second)
readData, _ := r.readInstance(ctx, instanceId)
}
```
2. **Обработка Location header:**
API возвращает `Location: ./UUID`, нужно обрезать `./`
3. **Задержка после создания:**
Операция асинхронная, даем 2 секунды на обработку перед чтением
### 3.4 Реализация Update и Delete
**Update (modify):**
```go
func (r *DummyResource) Update(...) {
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "modify",
}
// POST /instanceOperations с operation: "modify"
}
```
**Delete:**
```go
func (r *DummyResource) Delete(...) {
operationReq := CreateOperationRequest{
InstanceUid: data.ID.ValueString(),
Operation: "delete",
}
// POST /instanceOperations с operation: "delete"
}
```
**Read (проверка состояния):**
```go
func (r *DummyResource) readInstance(ctx context.Context, id string) (*InstanceResponse, error) {
// GET /instances/{id}
}
```
---
## 4. Сборка и локальная установка
### 4.1 Сборка провайдера
```bash
cd /home/naeel/terra
go mod tidy
go build -o terraform-provider-nubes
```
**Результат:**
Создан бинарный файл `terraform-provider-nubes`
### 4.2 Установка в локальный репозиторий Terraform
**Структура плагинов Terraform:**
```
~/.terraform.d/plugins/
└── registry.terraform.io/
└── mycloud/
└── mycloud/
└── 0.1.0/
└── linux_amd64/
└── terraform-provider-mycloud_v0.1.0
```
**Команда:**
```bash
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/mycloud/mycloud/0.1.0/linux_amd64
cp terraform-provider-nubes ~/.terraform.d/plugins/registry.terraform.io/mycloud/mycloud/0.1.0/linux_amd64/terraform-provider-mycloud_v0.1.0
```
---
## 5. Тестирование с Terraform
### 5.1 Установка Terraform
**Проблема #2: Terraform не установлен**
```
Command 'terraform' not found
```
**Решение:**
```bash
sudo snap install terraform --classic
```
**Версия:** Terraform 1.14.3
### 5.2 Конфигурация Terraform
**Файл:** `test/main.tf`
```text
terraform {
required_providers {
mycloud = {
source = "registry.terraform.io/mycloud/mycloud"
version = "~> 0.1"
}
}
}
provider "mycloud" {
api_endpoint = "https://deck-api.ngcloud.ru/api/v1/index.cfm"
}
resource "mycloud_dummy_instance" "example" {
display_name = "terraform-test"
description = "Created by Terraform provider"
}
output "instance_id" {
value = mycloud_dummy_instance.example.id
}
output "instance_status" {
value = mycloud_dummy_instance.example.status
}
```
### 5.3 Проблемы при инициализации
**Проблема #3: Несовпадение имен провайдера и ресурса**
**Первая попытка:**
```hcl
provider "nubes" { }
resource "nubes_dummy_instance" "example" { }
```
**Ошибка:**
```
The provider mycloud/mycloud does not support resource type "nubes_dummy_instance"
```
**Причина:**
Provider TypeName = "mycloud", поэтому ресурсы должны называться `mycloud_*`
**Решение:**
```hcl
provider "mycloud" { }
resource "mycloud_dummy_instance" "example" { }
```
### 5.4 Проблема с авторизацией
**Проблема #4: Токен не передается**
**Ошибка при apply:**
```
Error: API Error
Create instance failed with status 401: "Authorization header expected"
```
**Причина:**
Переменная `export MYCLOUD_API_TOKEN` работает только в текущей сессии терминала. При запуске terraform в новом процессе токена нет.
**Решение:**
Устанавливать токен в той же команде:
```bash
export MYCLOUD_API_TOKEN="..." && terraform apply -auto-approve
```
**JWT токен (из Network DevTools):**
```
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6...
```
### 5.5 Успешное выполнение
**Команда:**
```bash
cd test
terraform init
export MYCLOUD_API_TOKEN="..." && terraform apply -auto-approve
```
**Результат:**
```
mycloud_dummy_instance.example: Creating...
mycloud_dummy_instance.example: Creation complete after 4s [id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
instance_status = ""
```
---
## 6. Проверка в веб-интерфейсе
**URL:**
```
https://deck.ngcloud.ru/services/instance/detail/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
**Результат:**
- ✅ Экземпляр создан: `terraform-test`
- ✅ Описание: "Created by Terraform provider"
- ✅ Состояние: `running`
- ✅ 2 операции create (код 201)
- ✅ Uptime: 2 секунды
**Примечание заказчика:**
> "ОТЛИЧНО! хоть он и не создался, пришлось вручную подтолкнуть НО - РАБОТАЕТ!!!!"
Операция create выполнилась асинхронно, потребовалось ручное подтверждение в UI, но провайдер корректно создал ресурс!
---
## 7. Итоговая архитектура
### 7.1 Схема взаимодействия
```
Terraform
↓
terraform-provider-mycloud
↓
API Gateway (deck-api.ngcloud.ru)
↓
┌──────────────────────────────────────┐
│ POST /instances │ ← Создание инстанса
│ → serviceId, displayName, descr │
│ ← Location: ./UUID │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ POST /instanceOperations │ ← Запуск операции
│ → instanceUid, operation │
│ ← 201 Created │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ GET /instances/{id} │ ← Чтение статуса
│ ← uid, status, displayName, descr │
└──────────────────────────────────────┘
↓
Kubernetes Operator (backend)
↓
CRDs / K8s Resources
```
### 7.2 Реализованные методы
| Метод | Действие | API вызовы |
|----------|------------------------------------------------|-----------------------------------------------|
| Create | Создание ресурса | POST /instances → POST /instanceOperations |
| Read | Чтение текущего состояния | GET /instances/{id} |
| Update | Изменение параметров (через modify операцию) | POST /instanceOperations (operation: modify) |
| Delete | Удаление ресурса (через delete операцию) | POST /instanceOperations (operation: delete) |
### 7.3 Обработка ошибок
Все ошибки обрабатываются через `resp.Diagnostics`:
```go
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("..."))
resp.Diagnostics.AddWarning("...", "...")
```
---
## 8. Выводы и лучшие практики
### 8.1 Что узнали
1. **API не всегда такое, каким кажется на первый взгляд**
- Документация может отсутствовать
- Реверс-инжиниринг через DevTools - must have навык
2. **Terraform Plugin Framework v1.4.2 - золотая середина**
- Стабильность важнее новизны
- v1.5.0 имеет проблемы совместимости
3. **Асинхронные операции требуют особого подхода**
- Нужны задержки или polling
- Статус может быть не сразу доступен
4. **Локальные провайдеры удобны для разработки**
- Быстрое тестирование без публикации
- Структура каталогов важна для Terraform
### 8.2 Потенциальные улучшения
**Для production:**
1. **Polling вместо sleep:**
```go
func (r *DummyResource) waitForReady(ctx context.Context, id string) error {
timeout := time.After(5 * time.Minute)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-timeout:
return fmt.Errorf("timeout")
case <-ticker.C:
resp, _ := r.readInstance(ctx, id)
if resp.Status == "running" {
return nil
}
}
}
}
```
2. **Расширенная схема с параметрами:**
- whereFail, durationMs, failAtStart
- Добавить после успешной демонстрации минимума
3. **Retry логика для API:**
- Обработка временных сбоев сети
- Exponential backoff
4. **Логирование через tflog:**
```go
import "github.com/hashicorp/terraform-plugin-log/tflog"
tflog.Debug(ctx, "Creating instance", map[string]any{
"display_name": displayName,
})
```
5. **Валидация входных данных:**
```go
"display_name": schema.StringAttribute{
Required: true,
Validators: []validator.String{
stringvalidator.LengthBetween(3, 50),
stringvalidator.RegexMatches(
regexp.MustCompile(`^[a-zA-Z0-9-]+$`),
"must contain only alphanumeric characters and hyphens",
),
},
}
```
### 8.3 Следующие шаги
1. **Добавить другие ресурсы:**
- `mycloud_vm` (виртуальная машина)
- `mycloud_postgresql` (база данных)
- `mycloud_s3_bucket` (хранилище)
2. **Data Sources:**
```hcl
data "mycloud_service_catalog" "available" {}
```
3. **Документация:**
- Генерация через tfplugindocs
- README с примерами
- Публикация в Terraform Registry
4. **CI/CD:**
- Автоматическая сборка
- Тесты (acceptance tests)
- Release workflow
---
## 9. Технические детали
### 9.1 Структура проекта
```
terra/
├── main.go # Entry point
├── go.mod # Dependencies
├── go.sum # Lock file
├── terraform-provider-nubes # Binary
├── docs/
│ └── discovery/
│ └── development-journey.md # Этот файл
├── examples/
│ └── main.tf # Example configuration
├── test/
│ ├── main.tf # Test configuration
│ ├── terraform.tfstate # State file
│ └── .terraform/ # Terraform cache
└── internal/
└── provider/
├── provider.go # Provider definition
└── dummy_resource.go # Resource implementation
```
### 9.2 Зависимости
```go
module terraform-provider-mycloud
go 1.22
require (
github.com/hashicorp/terraform-plugin-framework v1.4.2
github.com/hashicorp/terraform-plugin-go v0.19.1
github.com/hashicorp/terraform-plugin-log v0.9.0
)
```
### 9.3 Размер бинарника
```bash
$ ls -lh terraform-provider-nubes
-rwxr-xr-x 1 naeel naeel 23M Jan 21 22:12 terraform-provider-nubes
```
### 9.4 Пример Terraform State
```json
{
"version": 4,
"terraform_version": "1.14.3",
"resources": [
{
"mode": "managed",
"type": "mycloud_dummy_instance",
"name": "example",
"provider": "provider[\"registry.terraform.io/mycloud/mycloud\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"display_name": "terraform-test",
"description": "Created by Terraform provider",
"status": null
}
}
]
}
]
}
```
---
## 10. Заключение
**Итог:** За одну сессию создан работающий Terraform Provider для облачной платформы Nubes Cloud.
**Ключевые достижения:**
- ✅ Изучена реальная архитектура API через реверс-инжиниринг
- ✅ Адаптирован провайдер под двухшаговую модель создания ресурсов
- ✅ Реализованы все CRUD операции
- ✅ Успешно протестировано создание реального ресурса
- ✅ Получен работающий минимум для демонстрации заказчику
**Время разработки:** ~3-4 часа
**Статус:** Рабочий прототип, готов к демонстрации
**Дата завершения:** 21 января 2026, 22:13 UTC+3
---
*Документ создан автоматически на основе истории разработки*
## 11. Post-Mortem Анализ трафика (HAR)
**Контекст**: После успешного запуска провайдера был получен HAR-файл (`deck.ngcloud.ru.har`) с записью "эталонного" процесса создания ресурса через веб-интерфейс. Это позволило подтвердить правильность реализованной логики.
### 11.1 Реконструкция потока вызовов (Browser Flow)
Анализ показал, что UI выполняет процесс в 5 четких этапов:
1. **Разведка**: Загрузка определений сервиса.
2. **Создание болванки (Instance)**:
- `POST /api/v1/index.cfm/instances`
- Payload: `{ "serviceId": 1, "displayName": "...", "descr": "" }`
- Результат: Создан объект в БД, получен `instanceUid`.
3. **Создание операции (Operation)**:
- Сразу после инстанса создается "wizard" операции.
- `POST /api/v1/index.cfm/instanceOperations`
- Payload: `{ "instanceUid": "...", "operation": "create" }`
- Результат: Получен `instanceOperationUid`.
4. **Инициализация параметров (The Key Step)**:
- Браузер отправляет **серию POST-запросов**, по одному на каждый параметр формы.
- `POST .../instanceOperationCfsParams` отправляется ~10 раз подряд.
- **Важно**: Отправляются даже пустые значения и значения по умолчанию (например, `paramValue: ""` или `"false"`). Это "прокликивает" форму.
- Параллельно идут вызовы `validate-cfs` и `PUT` для валидации "на лету", что Terraform может пропустить, подавая сразу валидные данные.
5. **Исполнение (Run)**:
- Финальное нажатие кнопки "Выполнить".
- `POST .../instanceOperations/{UID}/run`
- Payload: `{}`
- После этого UI переходит в режим поллинга статуса (`GET`).
### 11.2 Выводы для провайдера
Сравнение реализованного Go-кода с реальным трафиком подтвердило правильность финального решения:
* ✅ **Схематика верна**: Instance -> Operation -> Parameters Cycle -> Run.
* ✅ **Итерация параметров**: Решение итерироваться по полученному списку `cfsParams` и отправлять их обратно через `POST` полностью соответствует поведению браузера.
* ✅ **Run**: Пустой JSON `{}` в `run` endpoint — это стандартный паттерн API.
Провайдер версии `v15` успешно имитирует шаги 2, 3 и 5, а шаг 4 выполняет программным циклом, что является корректной автоматизацией ручного ввода.
### 11.3 Анализ Update и Delete (Good Modify vs Bad Delete)
Были проанализированы дополнительные дампы трафика: `goodmodify.har` (успешное изменение) и `baddelete.har` (проблемное удаление).
**Good Modify (Update Flow):**
* **Операция**: Создается с типом `"operation": "modify"`.
* **Параметры**:
* Поток **идентичен созданию**: UI снова отправляет серию `POST .../instanceOperationCfsParams`.
* Параметры содержат как пустые строки, так и специфические значения (например, `"paramValue": "2"`).
* Подтверждено использование `"paramValue": "{}"` для сложных типов (Map/JSON), что валидирует наше исправление с пустым JSON-объектом.
* **Вывод**: Логика `Create` и `Update` структурно идентична. Для реализации `Update` в Terraform, нам нужно будет мапить изменения из плана в значения параметров (сейчас мы отправляем дефолты).
**Bad Delete (Delete Flow):**
* **Операция**: Создается с типом `"operation": "delete"`.
* **Параметры**:
* **Отличие**: В логе **отсутствуют** вызовы `POST .../instanceOperationCfsParams`.
* Это логично: удаление обычно не требует параметров.
* Однако, вызов `GET .../validate-cfs` всё равно происходит.
* **Почему "Bad"?**: Судя по логу, после нажатия `run` система долго поллит статус операции, а в конце проверяет сам инстанс. Если инстанс не исчез, значит операция "зависла" или завершилась с ошибкой на бэкенде.
* **Вывод для Провайдера**: Наша универсальная логика (`range resp.CfsParams`) корректно обработает удаление: так как API вернет пустой список параметров для операции `delete`, цикл просто не выполнится ни разу, и провайдер сразу перейдет к `run`. Это правильное поведение.
## 12. Рефакторинг (Nubes & Tubulus)
**Задача:** Привести именование ресурсов в соответствие с требованиями заказчика.
* Провайдер переименован в `nubes`.
* Ресурс `mycloud_dummy_instance` переименован в `nubes_tubulus_instance`.
* Все внутренние структуры и методы обновлены.
**Изменения:**
1. **Provider Type**: `nubes` (ранее `mycloud`).
2. **Resource Type**: `nubes_tubulus_instance`.
3. **Go Structs**: `DummyResource` -> `TubulusResource`.
**Почему это важно сейчас:** Лучше выполнить переименование на этапе прототипа, чем рефакторить продакшн-код с миграциями стейта.
## 13. Подготовка Клиентского Пакета и Интеграционное Тестирование (22 Января 2026)
### 13.1 Создание дистрибутива
Сформирована папка `client_package` для передачи заказчику:
* Скомпилированный бинарный провайдер (Linux).
* Конфигурационные файлы Terraform (`resources.tf`).
* Инструкция `README.md`.
* Скрипты для локальной установки (`~/.terraform.d/plugins/...`).
### 13.2 Проблема: 401 Unauthorized
При попытке `terraform apply` на реальном стенде `https://deck-api.ngcloud.ru/` возникла ошибка авторизации, несмотря на переданный токен.
**Диагностика:**
1. Анализ HAR-файлов пользователя (`keycloak.nubes.ru.har`).
2. Выяснилось, что пользователь копировал значение Cookie `KEYCLOAK_IDENTITY` (HS256).
3. API требует Bearer Access Token (RS256), который выдается в ответе на запрос `/token` в OpenID Connect flow.
4. Токены визуально похожи (JWT), но имеют разные подписи и claims. API Nubes не мог валидировать Cookie-токен.
### 13.3 Решение и Успешный Запуск
1. Обновлена инструкция: указан точный источник токена (Network tab -> `token` response -> `access_token`).
2. Получен валидный токен.
3. **Result**: Успешное создание ресурса `Bolvanka Full Test` (ID: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).
4. Все параметры (Duration, JSON, Map) передались корректно.
### 13.4 Планы на v1.1: Service Accounts
Обсуждено архитектурное решение для автоматизированных систем (CI/CD, Операторы).
* Текущее решение (v1.0): User-based token (для ручных тестов и админов).
* Следующий этап (v1.1): Client Credentials Flow. Провайдер будет сам запрашивать токен по `CLIENT_ID` и `CLIENT_SECRET`, исключая ручное копирование из браузера. Это соответствует паттернам, используемым в NiFi Operator.
## 14. Финализация деплоя Private Registry (v1.0.0)
**Дата**: 23 января 2026
### 14.1 Проблемы "Последней мили"
После настройки SSL и Ingress возникли сложности с установкой провайдера командой `terraform init`:
1. **Checksum Mismatch**: Пересборка кода провайдера меняла хэш бинарника, требуя обновления файла `SHA256SUMS`.
2. **S3 Signature Mismatch**: Registry Server выдавал клиенту (Terraform) прямые Presigned URL на S3. Terraform пытался скачать файл, обращаясь к `terra.k8c.ru`, но подпись была сгенерирована для внутреннего хоста. Это приводило к ошибкам 403 Forbidden.
3. **GPG Verification Failure**: Terraform (современные версии) требует обязательной подписи бинарных файлов GPG ключом. Без этого установка прерывалась с ошибкой `authentication signature from unknown issuer`.
### 14.2 Комплексное решение
Для устранения всех проблем была проведена глубокая доработка инфраструктуры доставки:
#### A. Архитектура "Registry Proxy"
Мы отказались от схемы Redirect/Presigned URL в пользу проксирования.
* **Было**: Registry -> 302 Redirect -> S3 (клиент качает сам, ломая подписи).
* **Стало**: Registry -> Stream Copy -> S3 -> Client.
* Registry Server получил новый эндпоинт `/v1/proxy`, который транслирует файлы из S3 наружу. Это изолировало Terraform от сложностей S3 авторизации.
#### B. Внедрение GPG Подписи
Terraform Registry Protocol требует передачи публичных ключей (GPG Public Keys) прямо в ответе API на запрос загрузки версии.
1. Сгенерирована пара ключей GPG для `nubes-provider`.
2. Файл `terrafrom-provider-nubes_1.0.0_SHA256SUMS` подписан (`gpg --detach-sign`), создан `.sig` файл.
3. **Критический шаг**: Публичный ключ GPG (ASCII Armor) был "зашит" прямо в исходный код Registry Server (`main.go`). Теперь сервер при отдаче ссылки на скачивание также отдает ключ, которым клиент может проверить подпись.
### 14.3 Итог
Провайдер `terra.k8c.ru/nubes/nubes` версии `1.0.0` успешно инициализируется!
```
Installed terra.k8c.ru/nubes/nubes v1.0.0 (self-signed, key ID FFE0F4D723F14BCA)
Terraform has been successfully initialized!
```
Это полностью закрывает вопрос доставки провайдера (Provider Delivery) и позволяет переходить к полноценному использованию.
---
## 13. Универсальный архитектурный паттерн "Nubes Flow" (Master Pattern)
Для любого нового ресурса в Nubes Cloud ОБЯЗАТЕЛЬНО соблюдение следующего протокола вызовов. Любое отклонение приведет к тому, что ресурс зависнет в статусе "Not Created" или не применит параметры.
### 13.1 Пятиэтапный жизненный цикл (The 5-Step Execution)
1. **POST /instances**: Создание "болванки" инстанса. Получаем .
2. **POST /instanceOperations**: Создание "визарда" операции (, или ). Получаем .
3. **GET /instanceOperations/{uid}?fields=cfsParams**: **КРИТИЧЕСКИЙ ШАГ**.
- Мы ОБЯЗАНЫ запросить у сервера список параметров, которые ОН ожидает для этой операции.
- Даже если мы знаем ID параметров, этот вызов инициализирует их состояние на бэкенде.
4. **Цикл POST /instanceOperationCfsParams**: Настройка параметров.
- Для каждого параметра из списка, полученного на шаге 3, мы отправляем .
- Если у нас есть значение из Terraform — отправляем его.
- Если значения нет — отправляем (или пустой JSON/Array , для сложных типов).
- **ОШИБКА**: Пропуск "необязательных" параметров часто ломает финальный запуск.
5. **POST /instanceOperations/{uid}/run**: Финальное подтверждение (Кнопка "Выполнить").
- **Payload**: ОБЯЗАТЕЛЬНО пустой JSON-объект `{}`. Отправка `nil` или пустого тела приведет к игнорированию команды сервером.
### 13.2 Правила для "Умного Агента" (Cheat Sheet)
| Действие | Код статуса | Нюанс |
| :--- | :--- | :--- |
| **DisplayName** | 400 Bad Request | Должен быть уникальным. Используй префиксы или timestamp. |
| **Run Payload** | | Никогда не отправляй пустое тело. Только валидный пустой JSON. |
| **Parameters** | Full Sync | Отправляй ВСЕ параметры, которые вернул GET на шаге 3. |
| **Placement** | / | Значение не принимается для S3. |
### 13.3 Реализация в коде (client_impl.go)
Вся эта логика инкапсулирована в методе `CreateInstance`. При написании нового ресурса (например, Postgres или MariaDB), достаточно просто подготовить список специфичных параметров (`InstanceParam`) и вызвать этот универсальный метод. Метод сам выполнит GET запросы, заполнит дефолты и нажмет "Выполнить".
---
## 13. Универсальный архитектурный паттерн "Nubes Flow" (Master Pattern)
Для любого нового ресурса в Nubes Cloud ОБЯЗАТЕЛЬНО соблюдение следующего протокола вызовов. Любое отклонение приведет к тому, что ресурс зависнет в статусе "Not Created" или не применит параметры.
### 13.1 Пятиэтапный жизненный цикл (The 5-Step Execution)
1. **POST /instances**: Создание "болванки" инстанса. Получаем `instanceUid`.
2. **POST /instanceOperations**: Создание "визарда" операции (`create`, `modify` или `delete`). Получаем `instanceOperationUid`.
3. **GET /instanceOperations/{uid}?fields=cfsParams**: **КРИТИЧЕСКИЙ ШАГ**.
- Мы ОБЯЗАНЫ запросить у сервера список параметров, которые ОН ожидает для этой операции.
- Даже если мы знаем ID параметров, этот вызов инициализирует их состояние на бэкенде.
4. **Цикл POST /instanceOperationCfsParams**: Настройка параметров.
- Для каждого параметра из списка, полученного на шаге 3, мы отправляем `POST`.
- Если у нас есть значение из Terraform — отправляем его.
- Если значения нет — отправляем `defaultValue` (или пустой JSON/Array `{}`, `[]` для сложных типов).
- **ОШИБКА**: Пропуск "необязательных" параметров часто ломает финальный запуск.
5. **POST /instanceOperations/{uid}/run**: Финальное подтверждение (Кнопка "Выполнить").
- **Payload**: ОБЯЗАТЕЛЬНО пустой JSON-объект `{}`. Отправка `nil` или пустого тела приведет к игнорированию команды сервером.
### 13.2 Правила для "Умного Агента" (Cheat Sheet)
| Действие | Код статуса | Нюанс |
| :--- | :--- | :--- |
| **DisplayName** | 400 Bad Request | Должен быть уникальным. Используй префиксы или timestamp. |
| **Run Payload** | `{}` | Никогда не отправляй пустое тело. Только валидный пустой JSON. |
| **Parameters** | Full Sync | Отправляй ВСЕ параметры, которые вернул GET на шаге 3. |
| **Placement** | `HOT`/`COLD` | Значение `default` не принимается для S3. |
### 13.3 Реализация в коде (client_impl.go)
Вся эта логика инкапсулирована в методе `CreateInstance`. При написании нового ресурса (например, Postgres или MariaDB), достаточно просто подготовить список специфичных параметров (`InstanceParam`) и вызвать этот универсальный метод. Метод сам выполнит GET запросы, заполнит дефолты и нажмет "Выполнить".
@@ -0,0 +1,67 @@
# Documentation Build and Deployment Process
## Overview
This document describes the process of building and deploying the Terraform Provider documentation for the Nubes Registry. The system uses `mkdocs` with the `material` theme for static site generation and S3 for storage. The Registry Server serves these files directly from the S3 bucket.
## Architecture
1. **Source**: Documentation source files are in `docs/` and configuration in `mkdocs.yml`.
2. **Build**: The static HTML site is built using `mkdocs`.
* *Note*: Since `mkdocs` is not installed securely in the shell environment, we use the Docker image `squidfunk/mkdocs-material`.
3. **Storage**: The built site is uploaded to an S3 bucket named `terraform-registry`.
4. **Serving**: The Registry Server (`registry-server-build/`) reads files from `docs/{namespace}/{name}/{version}/` keys in the bucket and serves them via HTTP.
## Requirements
* Docker (for building the docs safely)
* `mc` (S3 client) configured with an alias (e.g., `nubes_s3`) pointing to the production S3 endpoint.
* Access keys for the S3 bucket.
## Step-by-Step Process
### 1. Build the Documentation
Run the build using Docker to generate the `site/` directory:
```bash
docker run --rm -v ${PWD}:/docs squidfunk/mkdocs-material build
```
This mounts the current directory to `/docs` in the container and runs `mkdocs build`. The output is placed in the local `site/` directory.
### 2. Deployment Script (`scripts/publish-docs.sh`)
We use a helper script to normalize the upload process. The script has been updated to point to the correct bucket path.
**Usage:**
```bash
./scripts/publish-docs.sh <site-dir> <registry-host> <namespace> <name> <version>
```
**Parameters:**
* `site-dir`: Path to the built site (usually `site`).
* `registry-host`: The host alias or endpoint (e.g., `s3.msk-1.ngcloud.ru` or internal logic, but effectively acts as part of the path structure in some versions, currently simplified to upload to `docs/...`).
* `namespace`: Provider namespace (e.g., `nubes`).
* `name`: Provider name (e.g., `nubes`).
* `version`: Semver version (e.g., `1.0.0`).
### 3. Manual Deployment Command
If running manually without the script, use `mc` to mirror the `site/` folder to the target S3 path:
```bash
# Example for version 1.0.0
mc cp --recursive site/ nubes_s3/terraform-registry/docs/nubes/nubes/1.0.0/
```
## Troubleshooting
* **Missing Dependencies**: If `mkdocs` is not found, always prefer the Docker command above.
* **Path Errors**: Ensure the S3 path matches `docs/<namespace>/<name>/<version>/`. The registry server expects this exact structure to map URL paths to S3 keys.
* **Bucket Access**: Verify `mc ls nubes_s3/terraform-registry` works before attempting upload.
## Recent Changes (Jan 2026)
* Fixed `scripts/publish-docs.sh` to target the `terraform-registry` bucket instead of `terraform-providers`.
* Standardized the S3 path structure to remove `REGISTRY_HOST` from the directory tree, aligning with `registry-server-build/docs.go` logic.
+131
View File
@@ -0,0 +1,131 @@
# Edge Gateway Service
## Обнаружено из HAR: edge.har
Дата: 2026-01-23
## Основная информация
- **Service ID**: 22
- **Service Name**: "Сетевой шлюз периметра (Edge Gateway)"
- **Operation create ID**: 10
## Параметры операции create
### Параметр 8: VDC UUID
- **svcOperationCfsParamId**: 8
- **Описание**: UUID виртуального датацентра
- **Пример значения**: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
- **Тип**: UUID String
- **Обязательный**: Да
### Параметр 340: Unknown boolean
- **svcOperationCfsParamId**: 340
- **Пример значения**: "false"
- **Тип**: Boolean (as string)
### Параметр 341: Edge Count
- **svcOperationCfsParamId**: 341
- **Описание**: Количество Edge Gateway (возможно)
- **Пример значения**: "1"
- **Тип**: Integer (as string)
### Параметр 367: External Network (optional)
- **svcOperationCfsParamId**: 367
- **Пример значения**: "" (пустая строка)
- **Тип**: String
### Параметр 621: Edge Type
- **svcOperationCfsParamId**: 621
- **Пример значения**: "vdc"
- **Тип**: String
### Параметр 622: Unknown (optional)
- **svcOperationCfsParamId**: 622
- **Пример значения**: "" (пустая строка)
- **Тип**: String
## Пример из HAR
```json
// POST /instances
{
"serviceId": 22,
"displayName": "f12vc_nsxt-2",
"descr": ""
}
// POST /instanceOperations
{
"instanceUid": "<instance-uuid>",
"operation": "create"
}
// POST /instanceOperationCfsParams (для каждого параметра)
{
"svcOperationCfsParamId": 8,
"paramValue": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 340,
"paramValue": "false",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 341,
"paramValue": "1",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 367,
"paramValue": "",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 621,
"paramValue": "vdc",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 622,
"paramValue": "",
"instanceOperationUid": "<operation-uuid>"
}
```
## Связи
## Discovery Recipe (Live API)
Получено из живого API (`/instances/{id}`) 2026-01-26.
### Маппинг имен полей в ID параметров (для Create/Modify)
На основе сопоставления `state.params` (чтение) и `instanceOperationCfsParams` (запись):
| Ключ в API (Read) | ID Параметра (Write) | Описание | Пример |
|-------------------|----------------------|----------|--------|
| `vdcUid` | **8** | UUID родительского VDC | `9b9d79a...` |
| `vdcType` | **621** | Тип родителя (`vdc` / `groupvdc`) | `vdc` |
| `needEnableAVI` | **- (TBD)** | Флаг включения ALB AVI | `true` |
| `virtualServicesCount`| **341 (?)** | Количество виртуальных сервисов | `"2"` |
| `vIPCount` | **-** | Кол-во внешних IP (Public IPs) | `"6"` |
| `ipSpaceName` | **-** | Название IP Space | `internet-v1` |
| `accessIpList` | **-** | Список разрешенных IP (JSON) | `["0.0.0.0/0"]`|
### Выходные атрибуты (Computed)
Объект `state.out`:
- `nsxName`: Название в инфраструктуре NSX-T.
- `vdcName`: Название VDC.
- `routedNet`: Имя созданной routed-сети.
### Жизненный цикл (Live Status)
- `explainedStatus`: "running" (ресурс активен).
- `operationIsInProgress`: `false` (нет активных задач).
- `man`: Содержит разметку Markdown с инструкцией по использованию (можно парсить для документации).
## Примечания
- Параметр 621 со значением "vdc" может указывать на тип Edge Gateway
- Параметры 367 и 622 пустые в этом примере - возможно опциональные
- Параметр 341 = "1" может означать количество Edge Gateway
@@ -0,0 +1,99 @@
# Сценарий восстановления GPG ключей и починки Registry (28.01.2026)
Этот документ описывает последовательность действий, выполненных для исправления проблемы с верификацией GPG подписи Terraform провайдера `nubes`.
## 1. Проблема (Контекст)
При попытке выполнить `terraform init` клиент получал ошибку проверки сигнатуры.
**Диагностика показала:**
* Terraform Registry Server (на `terra.k8c.ru`) в своем коде (`main.go`) отдавал клиентам публичный GPG ключ с ID `3534B7A1E185F2C1`.
* Приватный ключ для этого ID отсутствовал в окружении, поэтому новые сборки провайдера невозможно было подписать так, чтобы они прошли проверку этим ключом.
* Реестр был "рассинхронизирован" с артефактами.
## 2. Реализованное решение
Мы полностью заменили криптографическую пару ключей и обновили всю цепочку поставки провайдера.
### Шаг 1: Генерация новой пары ключей
Сгенерирован новый GPG ключ (EDDSA) для подписи релизов.
* **Key ID:** `BC2B32E138B12582`
* **Fingerprint:** `B157380F7572A0BA8DF6F708BC2B32E138B12582`
* **User ID:** `Nubes Terraform Provider <admin@nubes.ru>`
Команда генерации:
```bash
gpg --batch --generate-key gpg-gen-key.conf
```
### Шаг 2: Внедрение Публичного ключа в Registry Server
Публичная часть нового ключа (ASCII Armor) должна отдаваться сервером реестра по протоколу Terraform Registry Protocol.
1. Экспортирован публичный ключ:
```bash
gpg --armor --export BC2B32E138B12582
```
2. Обновлен исходный код сервера `operator/cmd/registry/main.go`:
* Заменено значение поля `ASCIIArmor` в структуре `gpgKey` на новый блок ключа.
* Обновлен `KeyID`.
3. Пересборка и деплой:
* Собран Docker образ: `naeel/terraform-registry-server:docs-dev`.
* Выполнен пуш в Docker Hub: `make push-registry`.
* Обновлен Deployment в K8s: `kubectl rollout restart deployment/registry-server -n terra`.
### Шаг 3: Сборка и Подписание провайдера (Release Engineering)
Чтобы клиент (`terraform init`) принял провайдер, файлы должны лежать в S3 бакете и иметь корректную подпись.
1. **Сборка:**
```bash
go build -o build_artifacts/terraform-provider-nubes_v1.0.0 .
```
2. **Упаковка (ZIP):**
Terraform требует определенного формата имени архива.
```bash
zip terraform-provider-nubes_1.0.0_linux_amd64.zip terraform-provider-nubes_v1.0.0
```
3. **Хеширование (SHA256SUMS):**
```bash
sha256sum terraform-provider-nubes_1.0.0_linux_amd64.zip > terraform-provider-nubes_1.0.0_SHA256SUMS
```
4. **Подписание (Signature):**
Критически важно использовать **бинарную (detached)** подпись, а не ASCII armor, так как Terraform ожидает именно бинарный формат для `.sig` файла.
```bash
gpg --batch --detach-sign --default-key BC2B32E138B12582 --output terraform-provider-nubes_1.0.0_SHA256SUMS.sig terraform-provider-nubes_1.0.0_SHA256SUMS
```
### Шаг 4: Публикация в S3
Загружены три обязательных файла в структуру директорий реестра (`terra.k8c.ru/nubes/nubes/1.0.0/`):
1. Сам архив: `terraform-provider-nubes_1.0.0_linux_amd64.zip`
2. Файл сумм: `terraform-provider-nubes_1.0.0_SHA256SUMS`
3. Подпись сумм: `terraform-provider-nubes_1.0.0_SHA256SUMS.sig`
Использовался `mc cp` (S3 client).
## 3. Итог и Верификация (End-to-End Test)
### Инициализация
Проверка на "чистом" клиенте (`client_package`) без `dev_overrides` прошла успешно. Terraform скачал провайдер из приватного реестра и валидировал подпись.
```hcl
provider "nubes" {
source = "terra.k8c.ru/nubes/nubes"
version = "1.0.0"
}
```
Лог `terraform init`:
> Installed terra.k8c.ru/nubes/nubes v1.0.0 (self-signed, key ID BC2B32E138B12582)
### Функциональное тестирование
Был проведен полный цикл создания ресурсов через скачанный провайдер:
1. **Plan**: Успешное планирование создания S3 бакета `terraform-registry-client-test-bucket-01`.
2. **Apply**: Ресурс успешно создан в облаке Nubes (API вернул 200 OK, провайдер обработал ответ).
3. **Destroy**: Ресурс успешно удален.
Это подтверждает, что цепочка **Registry -> Signed Download -> Provider Execution -> Cloud API** полностью работоспособна.
## 4. Бэкап ключей
Ключи сохранены локально в директории `client_package` (не для коммита в репозиторий, а как рабочий артефакт сессии):
* `public_key.asc`
* `private_key.asc`
+26
View File
@@ -0,0 +1,26 @@
# Name to UUID Mapping (Discovery)
Date: 2026-02-10
Branch: name-mapping
Goal: users supply UI names only; provider resolves names to UUID/ID internally in the core CRUD flow. Resource files must remain generated and template-like.
## Architecture sources
- docs/help/architecture-and-methods.md: universal core + CRUD flow, resources generated from YAML.
- docs/ARCHITECTURE_NEW.md: core in universal_rebuild/internal/core, generated resources in internal/resources_gen, shared CRUD in crud.go.
- docs/00_overview/ai_universal_provider_gen.md: core utilities include FindInstanceByDisplayName; resources generated from YAML.
- docs/ai_universal_provider_gen.md: same architecture; core utilities include FindInstanceByDisplayName.
## Mapping rule
- Input: human-readable name as shown in UI (display_name).
- Internal: resolve to instanceUid/UUID using core helper(s) and GetInstances list.
- Resource code must not implement per-resource lookup logic; mapping happens in core/CRUD and is driven by YAML metadata.
## Implications for generator
- service_params_gen should capture refSvcId / param metadata when available.
- tools/gen should use metadata to mark parameters that need name->UUID resolution.
- CRUD layer should resolve names to UUIDs for any param flagged as refSvcId or dataType uuid (when input is not already a UUID).
## Safety
- Backward compatibility: if input already looks like UUID, do not re-resolve.
- Use a single shared resolver in core; no per-resource code changes.
@@ -0,0 +1,219 @@
# Natural Language Infrastructure (NLI) Pattern
**Статус:** ✅ Реализовано
**Компонент:** Tubulus Resource + Gemini AI
**Файлы:** `internal/provider/tubulus_ai.go`, `internal/provider/tubulus_resource.go`
## Концепция
Вместо того, чтобы пользователь писал технические параметры в HCL, он может указать инструкцию на естественном языке, которую AI (Gemini) преобразует в конфигурацию на этапе `terraform plan`.
## Архитектура
```
┌──────────────────┐
│ User Instruction│ "создай на 30 секунд,
│ (Natural Lang) │ запиши пароль_123"
└────────┬─────────┘
│
▼
┌──────────────────┐
│ ModifyPlan() │ Terraform Plugin Framework
│ (Hook) │ Перехватывает план до показа
└────────┬─────────┘
│
▼
┌──────────────────┐
│ askGemini() │ Gemini 2.0 Flash API
│ (AI Parser) │ Возвращает структурированный JSON
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Plan Injection │ plan.DurationMs = 30000
│ (Computed) │ plan.BodyMessage = "пароль_123"
└──────────────────┘
```
## Пример использования
```text
resource "nubes_tubulus_instance" "test" {
display_name = "My Test"
description = "AI-driven resource"
# Естественный язык вместо технических параметров
instruction = "поработай секунд 20, положи 'hello world' и вали на 1 этапе"
# Эти поля рассчитаются автоматически через AI:
# duration_ms = 20000
# body_message = "hello world"
# where_fail = 1
# fail_in_progress = true
}
```
## Системный промпт (ключевые элементы)
1. **Типизация с примерами:**
```
"duration_ms": (integer) Длительность в миллисекундах
Примеры: "5 секунд" → 5000, "минута" → 60000, "быстро" → 1000
```
2. **Строгие ограничения:**
```
"where_fail": ТОЛЬКО [0, 1, 2, 3]
0 = не ломается, 1 = prepare, 2 = data_fill, 3 = after_vault
```
3. **Логические зависимости:**
```
Если fail_in_progress=true, но этап не указан — ставь where_fail=2
```
4. **Дефолты для null:**
```
Если НЕ указан текст явно для body_message — оставь null (не "ai_generated")
```
5. **Понимание разговорного языка:**
```
"долго" = 120000, "очень долго" = 300000, "полминутки" = 30000
```
## Технические детали
### ModifyPlan Lifecycle
```go
func (r *TubulusResource) ModifyPlan(ctx, req, resp) {
// 1. Проверка: это удаление?
if req.Plan.Raw.IsNull() { return }
// 2. Извлечение плана
var plan TubulusResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
// 3. Проверка: есть инструкция?
if plan.Instruction.IsNull() { return }
// 4. СТАБИЛИЗАЦИЯ: уже вызывали AI?
// (Предотвращает "plan inconsistency" между plan/apply)
if !plan.DurationMs.IsUnknown() &&
!plan.BodyMessage.IsUnknown() {
return
}
// 5. Вызов Gemini
aiConfig, err := r.askGemini(ctx, plan.Instruction.ValueString())
// 6. Инъекция значений
if aiConfig.DurationMs != nil {
plan.DurationMs = types.Int64Value(*aiConfig.DurationMs)
}
// 7. Обновление плана
resp.Plan.Set(ctx, &plan)
}
```
### Стабилизация (Critical!)
**Проблема:** Terraform вызывает `ModifyPlan()` дважды:
- 1-й раз: при `terraform plan` (показ пользователю)
- 2-й раз: при `terraform apply` (финальная проверка)
Если Gemini возвращает разные значения → ошибка:
```
Error: Provider produced inconsistent final plan
```
**Решение:** Проверяем, что значения ещё `Unknown`:
```go
if !plan.BodyMessage.IsUnknown() {
return // Уже вызывали AI, не повторяем
}
```
### Null vs Empty String
**Важно:** Если AI не вернул `body_message`, устанавливаем `null`, а не пустую строку:
```go
if aiConfig.BodyMessage != nil {
plan.BodyMessage = types.StringValue(*aiConfig.BodyMessage)
} else {
plan.BodyMessage = types.StringNull() // ← Критично!
}
```
Иначе: план при `apply` будет видеть `""` вместо `null` и выдаст inconsistency.
## Тестовые кейсы (100% Success Rate)
| Инструкция | Duration | BodyMessage | WhereFail | FailInProgress |
|------------|----------|-------------|-----------|----------------|
| "сделай быстро" | 1000 | null | 0 | false |
| "долго долго" | 300000 | null | 0 | false |
| "напиши привет" | 5000 | "привет" | 0 | false |
| "сломай сразу" | 5000 | null | 0 | true (at_start) |
| "зделай нормална на 10 сикунд" | 10000 | null | 0 | false |
| "30 сек, упади в середине, пароль_123" | 30000 | "пароль_123" | 2 | true |
| "duration 15000ms, fail at stage 3" | 15000 | null | 3 | true |
| "просто сделай что-нибудь" | 5000 | null | 0 | false |
## Расширяемость
Этот паттерн можно применить к **любому ресурсу** провайдера:
### PostgreSQL NLI
```text
# resource "nubes_postgres_instance" "db" {
instruction = "создай базу на 50 гигов с репликой в двух зонах"
# AI → cpu=2, ram=4096, disk=50000, replicas=2
}
```
### VM NLI
```text
# resource "nubes_vm_instance" "server" {
instruction = "подними ubuntu 22.04 с 8 ядрами и 16 гигами памяти"
# AI → os="ubuntu-22.04", cpu=8, ram=16384
}
```
### Edge Gateway NLI
```hcl
# resource "nubes_edge_gateway" "firewall" {
instruction = "настрой файрвол: блокируй китай и открой 80, 443"
# AI → firewall_rules=[...], allowed_ports=[80,443]
}
```
## Преимущества
1. **Снижение порога входа:** Новички могут писать конфигурации без изучения схемы ресурсов
2. **Скорость разработки:** "Создай Postgres" вместо 30 строк HCL
3. **Читаемость:** `instruction = "быстрая база"` понятнее, чем `cpu=1, ram=2048, disk=10000`
4. **Прозрачность:** Результат виден в `terraform plan` (не чёрный ящик)
5. **Аудит:** Все AI вызовы логируются → можно отследить "что просил vs что получил"
## Ограничения
1. **Latency:** Каждый `terraform plan` вызывает Gemini (~1-2 секунды)
2. **Cost:** Free Tier Gemini → 15 requests/min. Для крупных проектов нужен Billing
3. **Determinism:** AI может интерпретировать один запрос по-разному (минимизировано через примеры в промпте)
4. **Offline:** Требует интернет для доступа к Gemini API
## Улучшения (TODO)
- [ ] **Caching:** Кэшировать `(instruction → JSON)` локально, чтобы повторный plan не вызывал API
- [ ] **Offline Mode:** Fallback на дефолтные значения, если Gemini недоступен
- [ ] **Multi-language:** Поддержка английского, немецкого и т.д.
- [ ] **Validation:** Предупреждение, если AI вернул некорректные значения (например, `where_fail=99`)
## Выводы
Natural Language Infrastructure — это **первый шаг к "инфраструктуре как разговор"**. Вместо того, чтобы быть промежуточным звеном между человеком и API, Terraform становится **интерфейсом**, а AI — **компилятором**.
Эта реализация доказывает, что Plugin Framework достаточно гибок для таких экспериментов, и открывает дверь к новому способу управления облаком.
+130
View File
@@ -0,0 +1,130 @@
# PostgreSQL UI Parameters Verification
**Date:** 2026-01-28
**Source:** User provided screenshots of "Create PostgreSQL" operation in Nubes UI.
## Observed UI Fields (Creation Form)
The form "Создание экземпляра: PostgreSQL" contains the following fields in order:
1. **Платформа для развертывания** (Dropdown) *Required*
* Value: `k8s-3.ext.nubes.ru`
* Provider Mapping: `resource_realm` (Confirmed)
2. **Услуга S3 для резервных копий** (Dropdown) *Required*
* Label: `Услуга S3 для резервных копий.`
* Example Value: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
* Note: "Экземпляр (учетная запись) S3 для резервного копирования. Резервное копирование обязательно"
* Provider Mapping: `s3_uid` (Confirmed) https://registry.terraform.io/providers/nubes/nubes/latest/docs/resources/postgres#s3_uid
3. **Квота ядра пода (mili)** (Input) *Required*
* Label: `Квота ядра пода (Указывается в mili)`
* Value: `500`
* Provider Mapping: `cpu`
4. **Квота памяти пода (MB)** (Input) *Required*
* Label: `Квота памяти пода (Указывается в MegaBytes)`
* Value: `512`
* Provider Mapping: `memory`
5. **Размер диска для базы данных (GB)** (Input) *Required*
* Label: `Размер диска для базы данных (GigaBytes)`
* Value: `10`
* Provider Mapping: `disk`
6. **Количество узлов** (Input)
* Label: `Количество узлов`
* Value: `1`
* Provider Mapping: `instances`
7. **Выделение белого ip для master ноды** (Checkbox)
* Label: `Выделение белого ip для master ноды`
* Value: Unchecked
* Provider Mapping: `need_external_address_master`
8. **Имя ipSpace для публикации VIP master** (Input)
* Label: `Имя ipSpace для публикации VIP master`
* Note: "Не может быть пустым, если включен needExternalAddressMaster"
* Provider Mapping: `ip_space_name_master`
9. **Выделение белого ip для slave ноды** (Checkbox)
* Label: `Выделение белого ip для slave ноды`
* Value: Unchecked
* Provider Mapping: `need_external_address_slave`
10. **Имя ipSpace для публикации VIP slave** (Input)
* Label: `Имя ipSpace для публикации VIP slave`
* Note: "Не может быть пустым, если включен needExternalAddressSlave"
* Provider Mapping: `ip_space_name_slave`
11. **Количество резервных копий** (Input)
* Label: `Количество резервных копий`
* Value: `7`
* Note: "Не обязательный. По умолчанию: 24 (sic! screenshot says 24, provider default is 7? check code)"
* Provider Mapping: `backup_retention`
12. **Расписание резервного копирования** (Input)
* Label: `Расписание резервного копирования`
* Value: `0 0 * * *`
* Note: "Не обязательный. Время запуска бекапа"
* Provider Mapping: `backup_schedule`
13. **Версия PostgreSQL** (Dropdown) *Required*
* Label: `Версия PostgreSQL`
* Value: `17`
* Provider Mapping: `version`
14. **Параметры postgresql.conf в виде JSON** (Text Area/Beautify)
* Label: `Параметры postgresql.conf в виде JSON`
* Value: `{"log_connections": "off", "log_disconnections": "off"}` (Screenshot example)
* Provider Mapping: `parameters` (Note: Resource field is `parameters`, docs refer to it as `config_json` sometimes? Check consistency).
15. **Включить pgPooler для Master** (Checkbox)
* Label: `Включить pgPooler для Master`
* Value: Unchecked
* Provider Mapping: `enable_pgpooler_master`
16. **Включить pgPooler для Slave** (Checkbox)
* Label: `Включить pgPooler для Slave`
* Value: Unchecked
* Provider Mapping: `enable_pgpooler_slave`
17. **Выключить sslmode** (Checkbox)
* Label: `Выключить sslmode`
* Value: Unchecked
* Provider Mapping: `allow_no_ssl`
18. **Включить autoScale** (Checkbox)
* Label: `Включить autoScale`
* Value: Checked (in screenshot)
* Provider Mapping: `auto_scale`
19. **На сколько % расширяется диск (мин. 1G)** (Input)
* Label: `На сколько % расширяется диск (мин. 1G)`
* Value: `10`
* Provider Mapping: `auto_scale_percentage`
20. **Тех окно обновления дисков** (Input)
* Label: `Тех окно обновления дисков`
* Value: `0`
* Note: "Окно обновления (пока не реализовано). Указывается час в виде Integer (0 / 5 / 12 / 23)"
* Provider Mapping: `auto_scale_tech_window`
21. **Квота для autoScale** (Input)
* Label: `Квота для autoScale`
* Value: `1`
* Provider Mapping: `auto_scale_quota_gb`
## Discrepancies & Action Items
1. **Backup Retention Default**: Screenshot note says "По умолчанию: 24", but Terraform code default might be `7`. Should probably clarify or match UI default if critical.
2. **Parameters Name**: Check if Terraform argument is `parameters` or `config_json`.
* *Check*: `internal/provider/postgres_resource.go` uses `Parameters`.
* *Check*: `docs/registry/resources/postgres.md` uses `config_json`. **MISMATCH**.
3. **Required fields**:
* `Платформа для развертывания` and `Услуга S3...` are definitely required and match our updates.
## Plan
1. **Fix Mismatch**: Rename `config_json` -> `parameters` in documentation to match Go struct, or rename Go struct field to `config_json` to match documentation. (Prefer `parameters` as it maps to UI "Параметры...").
2. **Verify Defaults**: Ensure `backup_retention` defaults make sense.
+45
View File
@@ -0,0 +1,45 @@
# PgAdmin Service Discovery
**Date:** 2026-01-28
**Subject:** `pgAdmin` service analysis (ID: 96)
**Source:** `har/pg_admin.har`
## Service Overview
- **Service Name:** PgAdmin
- **Service ID:** 96
- **Purpose:** Web-based administration tool for PostgreSQL.
- **Resource Realm:** Kubernetes (e.g. `k8s-3.ext.nubes.ru`)
## API Analysis
### Operations
- **Create**: `POST /instances` with `serviceId: 96`.
- **Parameters**: `POST /instanceOperationCfsParams`.
### Parameters Map (from HAR)
| Param ID | Label (UI) | Type | Value (Sample) | Terraform Field | Notes |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **164** | Имя домена | String | `pgadmin-2113` | `domain` | Used as display name too. |
| **169** | Платформа | String | `k8s-3.ext.nubes.ru` | `resource_realm` | |
| **165** | Квота ядра (mili) | Int | `200` | `cpu` | Default 200m. |
| **166** | Квота памяти (MB) | Int | `256` | `memory` | Default 256MB. |
| **167** | Размер диска (GB) | Int | `1` | `disk` | Default 1GB. |
| **170** | (None) | Email | `log@log.log` | `email` | Admin user email. |
| **171** | (None) | Password | `pass` | `password` | Admin user password. |
### Dependencies
- No explicit link to a Postgres instance found in **create** parameters.
- It seems the PgAdmin instance is standalone. Servers are likely added manually via UI or via config injection (not visible in this HAR).
## Terraform Resource Status
- **Implemented**: `internal/provider/pgadmin_resource.go`.
- **Resource Name**: `nubes_pgadmin`.
- **Status**: Implements `Create`, but `Read` is dummy (state-only) and `Delete` is skipped (protection).
- **Import**: Not supported (due to dummy Read).
- **Anomalies**:
- Fields `organization_id` and `organization_name` are required in schema but unused in API calls. Likely technical debt or metadata.
## User Actions Required
To use this resource:
1. Define `nubes_pgadmin` resource.
2. Provide dummy values for `organization_*` fields.
3. Set `resource_realm` to match your K8s cluster.
+176
View File
@@ -0,0 +1,176 @@
# PostgreSQL Service Analysis & Implementation Plan
**Date:** 2026-01-27
**Artifacts Analyzed:** `pg.har` (Traffic), `pg.html` (Form Schema)
## 1. Service Overview
PostgreSQL "Database as a Service" in Nubes.
- **Service Type**: Stateful Application
- **Lifecycle Constraint**: **Strict Quarantine (14 days)**.
- Like the VM resource, this service **cannot be immediately deleted**.
- Attempting to `delete` an active instance returns: `Услуга ПРОМ. Не в состоянии ПРИОСТАНОВЛЕНА более 14 дней`.
- **Implication for Terraform**: The `Delete` operation in the provider must likely perform a `Suspend` instead, or Terraform will require the user to manually suspend and wait 14 days before a successful destroy (which is bad UX).
- *Strategy Reference*: Similar to `nubes_vm`, `destroy` should trigger `suspend`.
## 2. Resource Parameter Mapping
Extracted from `pg.html` (labels) and `pg.har` (values/IDs).
| Terraform Argument | Label (UI) | API Key (`instanceOperationCfsParams`) | Type | ID | Notes |
| :--- | :--- | :--- | :--- | :--- | :--- |
| `nodes_count` | Количество узлов | `resourceInstances` | Int | **80** | |
| `cpu` | Квота ядра пода (mili) | `resourceCPU` | Int | **81** | `500` = 0.5 CPU |
| `ram` | Квота памяти пода (MB) | `resourceMemory` | Int | **82** | |
| `disk_size` | Размер диска для базы данных (GB) | `resourceDisk` | Int | **83** | |
| `version` | Версия PostgreSQL | `appVersion` | String | **310** | `"17"`, `"16"`, etc. |
| `enable_external_master` | Выделение белого ip для master ноды | `needExternalAddressMaster` | Bool | **145** | |
| `ip_space_master` | Имя ipSpace для публикации VIP master | `ipSpaceNameMaster` | String | **102** | Req if external master=true |
| `enable_external_slave` | Выделение белого ip для slave ноды | `needExternalAddressSlave` | Bool | **146** | |
| `ip_space_slave` | Имя ipSpace для публикации VIP slave | `ipSpaceNameSlave` | String | **---** | Req if external slave=true (ID mismatch in artifacts, verify) |
| `backup_schedule` | ext_BACKUP_SCHEDULE | `ext_BACKUP_SCHEDULE` | String | **265** | Cron format |
| `backup_retention` | ext_BACKUP_NUM_TO_RETAIN | `ext_BACKUP_NUM_TO_RETAIN` | Int | **266** | |
| `config_json` | jsonParameters | `jsonParameters` | JSON | **311** | Postgres runtime config |
| `enable_pooler_master` | enablePgPoolerMaster | `enablePgPoolerMaster` | Bool | **---** | ID needs verification |
| `enable_pooler_slave` | enablePgPoolerSlave | `enablePgPoolerSlave` | Bool | **---** | ID needs verification |
| `allow_no_ssl` | allowNoSSL | `allowNoSSL` | Bool | **---** | ID needs verification |
| `dns_name` | (Inferred from HAR) | `dnsRecord` (Likely) | String | **102** | In HAR `k8s-3...` was sent as ID 102, which matches `ipSpaceNameMaster` or DNS? Needs check. |
*Note: Some IDs (Pooler, SSL, IP Space Slave) were visible in HTML but their submission IDs need confirmation from a full submission log or deduction.*
### Mismatches/Detailed Logic
- **ID 102**: In HAR, `svcOperationCfsParamId: 102` sent value `k8s-3.ext.nubes.ru`. This looks like a DNS name or IP Space name. HTML label is "Имя ipSpace...".
- **ID 23**: `instanceOperationUid`? No, HAR shows paramId 23 with UUID value. Check if this is `account_id` or similar.
## 3. Implementation Plan
### Resource Schema (`nubes_postgres`)
```text
resource "nubes_postgres" "db" {
organization_uuid = "..."
vdc_uuid = "..." # If applicable, or just organization context
name = "my-pg"
version = "17"
cpu = 500
ram = 512
disk_gb = 10
nodes = 1
# Network
external_master = false
# ...
# Backup
backup_schedule = "0 0 * * *"
backup_retention = 7
# Advanced
config = jsonencode({
"max_connections" = 100
})
}
```
### Lifecycle Logic
- **Create**: POST `/instanceOperationCfsParams` with mapped IDs.
- **Read**: GET `/instances/{id}` -> Parse `cfsParams` from `instanceConfig`.
- **Delete**:
- Check `explainedStatus`.
- If `active`: Call `suspend`. State remains in Terraform (or marked as tainted? No, usually we just update state to show suspended or remove if we consider suspend=deleted).
- Given the "quarantine", Terraform `destroy` **cannot** fully remove the resource.
- **Decision**: `destroy` will execute `suspend`. The resource will arguably still exist in the cloud. Terraform state should probably be removed to simulate "deletion" from the Infrastructure as Code perspective, alerting the user via logs that actual deletion requires 14 days wait.
## 4. Next Steps
1. Create `internal/provider/postgres_resource.go` (skeleton exists?).
2. Implement `Create` method with correct param IDs.
3. specific testing of the `Suspend` workflow.
## 5. Technical Implementation Details (For AI Agent)
### A. API Request Pattern
Analysis of `pg.har` reveals that parameters are **NOT** sent in a single JSON body.
The client sends multiple sequential `POST` requests to `/api/v1/index.cfm/instanceOperationCfsParams`.
**Payload Schema per Parameter:**
```json
{
"paramValue": "1", // The value (stringified)
"instanceOperationUid": "...", // The operation ID (from the create instance response)
"svcOperationCfsParamId": 80 // The specific ID mapped above
}
```
**Implementation Strategy:**
The provider must loop through the defined parameters and make individual API calls for each one using the `client.CreateInstanceParam` (or similar existing method).
### B. Code References
* **Lifecycle Logic**: See `internal/provider/vm_resource.go`. This resource implements the "Suspend instead of Delete" logic required for quarantine-constrained resources.
* **API Client**: Check `internal/provider/client_impl.go` for `SendCfsParam` or similar helpers.
### C. Implementation Order (User Request)
**PRIORITY 1: Documentation First**
The user wants to visualize parameters on the documentation site before code implementation.
1. **Create Documentation File**:
* Path: `docs/registry/resources/postgres.md`
* Content: Description of the resource, schema table (Inputs determined in Section 2), and an example usage block.
2. **Update Navigation**:
* File: `mkdocs.yml`
* Action: Add `- Postgres: resources/postgres.md` under `Resources`.
3. **Review**: Ask user to check the visual representation.
4. **Codegen**: Only then proceed to Go implementation.
## 6. Live Instance Analysis (2026-01-27)
User provided a full text dump of a running PostgreSQL instance (`test-org-pg-1769262059`).
**Key Findings:**
1. **Quarantine Confirmation**:
* Operation Log: `delete` failed with "Услуга ПРОМ. Не в состоянии ПРИОСТАНОВЛЕНА более 14 дней".
* This confirms `terraform destroy` MUST call `suspend` and remove state, warning the user.
2. **Sensitive Outputs (Credentials)**:
* `adminUser`: `postgres`
* `adminPass`: (Provided in plain text in UI/API response)
* `standbyUser` / `standbyPass`: For HA configurations.
3. **Connection Info**:
* `internalConnect.master`: internal K8s DNS (e.g., `postgresqlk8s-master...svc.`).
* `externalConnect`: JSON object with `master` and `slave` keys, each containing `ip`, `fqdn`, `uuid`, `isExternal`.
4. **Dependencies**:
* Explicit dependency on an S3 Bucket (`s3Uid`: `6d6061cb-...`).
* UI shows: "Экземпляр зависит от: S3 Object Storage".
* *Implication*: The Terraform resource likely needs an `s3_bucket_id` input argument.
5. **Constraints**:
* **Disk Shrink Forbidden**: Operation log shows error "ERROR | Ресурсы под Disk меньше чем в текущем экземпляре".
* Terraform validation must prevent reducing `disk_size`.
6. **New Parameters Identified**:
* `resourceRealm`: `k8s-3.ext.nubes.ru` (Likely corresponds to ID 102 inferred earlier).
* `monitoring`: Contains Grafana URL.
## 7. API Diff Check (2026-02-18)
Based on service metadata for Postgres (svcId 90) across prod/test/dev.
### Operations
- **prod**: `create`, `delete`, `modify`, `recovery`, `restart`, `resume`, `suspend`
- **test/dev**: all prod ops **plus** `create_user`, `delete_user`, `create_database`, `delete_database`
### Input Params (operation-level)
- **create**: identical param set across prod/test/dev (21 params)
- **modify/delete/restart/resume/suspend**: identical across prod/test/dev
- **recovery**: **prod missing** `resourceCPU`, `resourceDisk`, `resourceInstances`, `resourceMemory`, `resourceRealm` compared to test/dev
### Outputs
- Output fields are not declared in service metadata. Only visible on instance state (`state_out`, `state_params`).
## 8. YAML Generator Check (service_params_gen)
Generator reads service metadata via:
- `/index.cfm?endpoint=/services/{svcId}`
- `/index.cfm?endpoint=/serviceOperation/{svcOperationId}`
Limitations found:
- Only collects params for **create** and **modify** operations.
- Does **not** include `create_user`, `delete_user`, `create_database`, `delete_database` even when present in API (test/dev).
- Outputs are hardcoded defaults, not pulled from API.
@@ -0,0 +1,91 @@
# Паттерн создания ресурсов "Tubulus" (Platinum Standard)
В платформе Nubes Cloud большинство ресурсов (VDC, S3, DBaaS, Tubulus-болванки) подчиняются строгому 7-шаговому циклу создания. Простого `POST /instances` недостаточно — ресурс появится в UI, но останется в статусе "not created", пока не будет выполнен финальный `POST /run`.
## Пошаговый алгоритм
### Шаг 1: Инициализация (Create Instance)
Отправляем `POST /instances`.
- **Payload**: `contractId`, `serviceId`, `specificationItemId`, `displayName`.
- **Результат**: Получаем `instanceUid`.
### Шаг 2: Создание операции (Create Operation)
Отправляем `POST /instanceOperations` для привязки операции создания к инстансу.
- **Payload**: `instanceUid`, `operation: "create"`.
- **Результат**: Получаем `instanceOperationUid`.
### Шаг 3: Получение параметров (Fetch Parameters)
Отправляем `GET /instanceOperations/{uid}?fields=cfsParams`.
- **Цель**: Получить список параметров (`svcOperationCfsParamId`), которые платформа ожидает для этого ресурса.
### Шаг 4: Отправка параметров (Submit Parameters) — **Критично!**
Для **каждого** параметра из списка (даже если он пустой или имеет значение по умолчанию) нужно отправить `POST /instanceOperationCfsParams`.
- **Payload**: `instanceOperationUid`, `svcOperationCfsParamId`, `paramValue`.
- **Важно**: Браузер всегда переотправляет все параметры. Пропуск этого шага может привести к ошибкам валидации.
### Шаг 5: Валидация (Validate)
Отправляем `GET /instanceOperations/{uid}/validate-cfs`.
- Проверяем, что ответ `200 OK` и нет ошибок в теле ответа.
### Шаг 6: Запуск (Run) — **Самый важный шаг!**
Отправляем `POST /instanceOperations/{uid}/run`.
- **Payload**: Обязательно `{}` (пустой JSON-объект). **НЕ `nil`**, а именно `{}`.
- **Результат**: После этого шага бэкенд начинает реальную работу (например, вызывает Jenkins или Ansible).
### Шаг 7: Ожидание готовности (Polling)
Периодически опрашиваем `GET /instances/{uid}`.
- Ждем, когда `explainedStatus` станет `running` (для S3) или `Active`.
---
## Специфика S3 бакетов (`nubes_s3_bucket`)
1. **Модификация (Update)**: В текущем UI и API **отсутствует** операция `modify`. Ресурс S3 является иммутабельным в плане параметров бакета через этот API. Для изменения (например, размера) требуется пересоздание или использование других API (если они будут найдены).
2. **Удаление (Delete)**: Выполняется через стандартный `DELETE /instances/{id}`.
3. **Уникальность**: `displayName` и `bucket_name` должны быть уникальны в рамках контракта/платформы. Рекомендуется использовать суффиксы с временной меткой в тестах.
4. **Placement**: Допустимые значения: `HOT` (по умолчанию) или `COLD`.
---
## Рекомендации для разработчика провайдера
- Объединяйте шаги 1-6 в одну функцию клиента (например, `CreateInstance` в `client_impl.go`).
- Всегда передавайте пустую карту `map[string]interface{}{}` в метод `Run`, чтобы она сериализовалась в `{}`.
- Тщательно логируйте `instanceOperationUid`, так как по нему можно отследить ошибки в логах платформы.
### Troubleshooting
- **Висит в "not created"**: Вы забыли вызвать Шаг 6 (Run) или отправили неверный payload.
- **Ошибка 400 на параметрах**: Проверьте, что все ID параметров соответствуют результату из Шага 3.
## Operation Lifecycle & Success Criteria (Official Developer Info)
Операции в Nubes Cloud выполняются асинхронно.
- **201 Created**: Операция запущена успешно.
- **Polling (GET status)**: Каждый запрос статуса возвращает `200 OK`. Это означает, что платформа успешно обрабатывает запрос состояния, но не гарантирует успех самой операции.
- **Completion Timestamp**: Операция считается завершенной только тогда, когда в ответе появляется временная метка завершения.
- **isSuccessful**: Ключевой флаг, который определяет успех операции и отрисовывает "зелёную галочку" в интерфейсе. Если операция завершена, но `isSuccessful: false`, ресурс может находиться в неконсистентном состоянии.
---
## Suspend/Resume и совпадение параметров (Discovery Notes)
Контекст: в Nubes Cloud многие сервисы **не удаляются напрямую**. `destroy` приводит к `suspend` и двухнедельному карантину. Это означает, что следующий `apply` может столкнуться с существующим инстансом в `suspend`.
### Принцип восстановления
Если найден `suspended` инстанс с тем же `resource_name`, корректное поведение:
1. **Сравнить параметры** в Terraform (create‑params) с `instance.state.params`.
2. **Resume** выполнять только при полном совпадении параметров.
3. При несовпадении — вернуть стандартную ошибку "resource already exists".
### Потенциальные мины
- **Несовпадение ключей**: `state.params` может использовать ключи, отличные от `code` в YAML — сравнение даст false.
- **Типы/форматы**: bool/int/JSON/масивы могут быть сериализованы по‑разному (строка vs JSON).
- **Defaults**: если параметр не задан в TF и приходит default от API, возможны ложные mismatches.
- **RefSvcId**: в create могут отправляться UUID, а в `state.params` может быть имя — mismatch.
- **Ручные изменения**: любые правки вне TF делают resume невозможным.
- **Пустые params**: если API не возвращает `state.params`, сравнение всегда провалится.
- **YAML/Go рассинхрон**: неверный маппинг ID→code приводит к ложным mismatch.
- **Сеть/таймауты**: дополнительный GET `/instances/{id}` может фейлиться и блокировать resume.
### Экономический аспект
`suspend` сохраняет данные и продолжает потреблять дисковое пространство — пользователь **продолжает платить**. Регулярное создание новых инстансов вместо восстановления `suspended` приведёт к росту расходов.
+37
View File
@@ -0,0 +1,37 @@
digraph Resources {
rankdir=LR;
node [shape=rectangle, style=filled, fillcolor="#EFEFEF", fontname="Helvetica"];
// Nodes: resource (type/name) with key params
Provider [label="provider: nubes\n(api_endpoint, api_token)", fillcolor="#E8F4FF"];
Organization [label="nubes_organization\n(id, display_name)"];
VDC [label="nubes_vdc\n(id, display_name, organization_uid, provider_vdc, network_pool)"];
Edge [label="nubes_edge\n(id, display_name, vdc_uid, external_network)"];
VApp [label="nubes_vapp\n(id, display_name, vdc_uid, edge_uid, vapp_name)"];
VM [label="nubes_vm_instance\n(id, display_name, vapp_uid, vm_name, user_public_key, ip_space_name, access_port_list, access_ip_list)"];
TLSKey [label="tls_private_key\n(private_key_pem, public_key_openssh)", fillcolor="#FFF4E8"];
LocalFile [label="local_sensitive_file\n(filename, content = tls_private_key.private_key_pem)"];
RemoteState [label="data.terraform_remote_state.vapp\n(outputs.vapp_id)", fillcolor="#F0FFF0"];
Consumer [label="consumer module\n(e.g. client_package_windows)\nuses access_ip_list"];
// Dependency edges (resource -> resource it depends on) with parameter labels
VDC -> Organization [label="organization_uid", fontsize=10];
Edge -> VDC [label="vdc_uid", fontsize=10];
VApp -> VDC [label="vdc_uid", fontsize=10];
VApp -> Edge [label="edge_uid", fontsize=10];
VM -> VApp [label="vapp_uid", fontsize=10];
VM -> TLSKey [label="user_public_key <- public_key_openssh", fontsize=10];
LocalFile -> TLSKey [label="content <- private_key_pem", fontsize=10];
Consumer -> VM [label="access_ip_list (jsondecode(...)[0])", fontsize=10];
VM -> RemoteState [label="vapp_uid = vapp.outputs.vapp_id (remote)", fontsize=10, style=dashed];
// Provider relations (dotted to many)
Provider -> VDC [style=dotted];
Provider -> Edge [style=dotted];
Provider -> VApp [style=dotted];
Provider -> VM [style=dotted];
Provider -> Organization [style=dotted];
// Visual tweaks
edge [fontname="Helvetica", fontsize=10];
}
+49
View File
@@ -0,0 +1,49 @@
flowchart LR
%% Стиль: Центровка узла, моноширинный шрифт, запрет переноса
classDef resourceNode text-align:center,font-size:16px,font-family:monospace
%% 1. Организация
ORG["<i>Организация&nbsp;(Org)</i><br/><b>nubes_organization.main</b><br/>━━━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;displayName&nbsp;:&nbsp;Saas‑Org&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;platform&nbsp;:&nbsp;ngcloud.ru&nbsp;&nbsp;&nbsp;<br/>&nbsp;organizationType&nbsp;:&nbsp;saas&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>━━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━━<br/>&nbsp;organizationUid&nbsp;:&nbsp;e537517‑...‑bd&nbsp;"]:::resourceNode
%% 2. Виртуальный датацентр (vDC)
VDC["<i>Виртуальный&nbsp;датацентр&nbsp;(vDC)</i><br/><b>nubes_vdc.main</b><br/>━━━━━━━━━━━━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━━━━━━━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cpuAllocated&nbsp;:&nbsp;10&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;memAllocated&nbsp;:&nbsp;20&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cpuGuaranteed&nbsp;:&nbsp;20&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;memGuaranteed&nbsp;:&nbsp;20&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;vdcNetworkPool&nbsp;:&nbsp;nsxt‑v1cl1‑geneve‑np&nbsp;&nbsp;&nbsp;&nbsp;<br/>vdcProviderGateway&nbsp;:&nbsp;v1cl4‑vsan‑pvdc&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;storageConfig&nbsp;:&nbsp;V1CL4‑F1R1‑SSD&nbsp;(100GB)&nbsp;<br/>━━━━━━━━━━━━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━━━━━━━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vdcUid&nbsp;:&nbsp;WZ01325‑saas‑...‑6743q&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;organizationUid&nbsp;:&nbsp;e5375174‑...‑bd&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"]:::resourceNode
%% 3. Edge Gateway
EDGE["<i>Edge&nbsp;Gateway</i><br/><b>nubes_edge.main</b><br/>━━━━━━━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;displayName&nbsp;:&nbsp;Main‑Edge&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vdcUid&nbsp;:&nbsp;WZ01325‑saas‑...&nbsp;<br/>&nbsp;externalNetwork&nbsp;:&nbsp;Ext‑Net&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>━━━━━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;edgeUid&nbsp;:&nbsp;g72h1511‑...‑bd&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;status&nbsp;:&nbsp;READY&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"]:::resourceNode
%% 4. vApp
VAPP["<i>Контейнер&nbsp;vApp</i><br/><b>nubes_vapp.main</b><br/>━━━━━━━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;displayName&nbsp;:&nbsp;Frontend‑vApp&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vdcUid&nbsp;:&nbsp;WZ01325‑saas‑...&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;edgeUid&nbsp;:&nbsp;g72h1511‑...‑bd&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vappName&nbsp;:&nbsp;web‑service‑1&nbsp;&nbsp;&nbsp;&nbsp;<br/>━━━━━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vappUid&nbsp;:&nbsp;vapp‑6743q‑...‑bd&nbsp;"]:::resourceNode
%% 5. Virtual Machine
VM["<i>Виртуальная&nbsp;машина&nbsp;(VM)</i><br/><b>nubes_vm_instance.main</b><br/>━━━━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━━━━<br/>&nbsp;displayName&nbsp;:&nbsp;Nginx‑Server&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vappUid&nbsp;:&nbsp;vapp‑6743q‑...&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vmCpu&nbsp;:&nbsp;4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vmRam&nbsp;:&nbsp;8&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;imageVm&nbsp;:&nbsp;Ubuntu‑22.04&nbsp;<br/>━━━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vmUid&nbsp;:&nbsp;vm‑9912x‑...‑bd&nbsp;"]:::resourceNode
%% 6. S3 Бакет
S3["<i>S3&nbsp;Bucket</i><br/><b>nubes_s3_bucket.main</b><br/>━━━━━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━━━━━<br/>&nbsp;displayName&nbsp;:&nbsp;Assets&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vdcUid&nbsp;:&nbsp;WZ01325‑...&nbsp;&nbsp;&nbsp;<br/>&nbsp;bucket_name&nbsp;:&nbsp;static‑prod&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;placement&nbsp;:&nbsp;HOT&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>━━━━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;s3Uid&nbsp;:&nbsp;s3‑6112‑...‑bd&nbsp;"]:::resourceNode
%% 7. PostgreSQL
DB["<i>PostgreSQL&nbsp;Cluster</i><br/><b>nubes_postgres.main</b><br/>━━━━━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━━━━━<br/>&nbsp;displayName&nbsp;:&nbsp;Orders‑DB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vdcUid&nbsp;:&nbsp;WZ01325‑...&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cpu&nbsp;:&nbsp;2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ram&nbsp;:&nbsp;4GB&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>━━━━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━━━━<br/>&nbsp;postgresUid&nbsp;:&nbsp;db‑991x‑...‑bd&nbsp;"]:::resourceNode
%% 8. PgAdmin
PGA["<i>PgAdmin&nbsp;UI</i><br/><b>nubes_pgadmin.main</b><br/>━━━━━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━━━━━<br/>&nbsp;displayName&nbsp;:&nbsp;DB‑Manager&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;vdcUid&nbsp;:&nbsp;WZ01325‑...&nbsp;&nbsp;&nbsp;<br/>&nbsp;postgresUid&nbsp;:&nbsp;db‑991x‑...‑bd&nbsp;<br/>━━━━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━━━━<br/>&nbsp;&nbsp;pgAdminUid&nbsp;:&nbsp;pga‑22x‑...‑bd&nbsp;"]:::resourceNode
%% Связи
ORG --> VDC
VDC --> EDGE
VDC --> VAPP
VDC --> S3
VDC --> DB
VDC --> PGA
EDGE -.-> VAPP
VAPP --> VM
DB --> PGA
%% Связи
ORG --> VDC
VDC --> EDGE
VDC --> VAPP
EDGE -.-> VAPP
VAPP --> VM
@@ -0,0 +1,22 @@
# Resource Realm: универсальное определение через API
## Зачем это нужно
Для некоторых сервисов (`dummy` и др.) при создании требуется `resource_realm`. Чтобы не хардкодить значения, realm нужно получать из API по `svcId`.
## Универсальный метод (для всех сервисов)
Используется общий эндпойнт:
```
GET /api/v1/index.cfm?endpoint=/resourceRealms/available&svcId=<serviceId>
```
- Возвращает доступные realm для сервиса в поле `results`.
- Пример: `svcId=1` (dummy) → `results = "dummy"`.
## Практика
1. Определить `serviceId` ресурса.
2. Запросить `/resourceRealms/available&svcId=<serviceId>`.
3. Использовать значение `results` как `resource_realm` в Terraform.
## Важно
Это **универсальный** способ (не только для dummy) и должен применяться во всех случаях, когда нужен `resource_realm`.
+18
View File
@@ -0,0 +1,18 @@
# S3 Service Discovery
**Date:** 2026-01-28
**Source:** User input
## Documentation
* **Description**: https://docs.s3.msk-1.ngcloud.ru/s3/s3.description.html
## Key Concepts
* **S3 Instance (Account/User)**: This is the service level entity. It represents the S3 account/user credentials (access_key, secret_key).
* *Usage*: PostgreSQL dependencies refer to this **S3 Instance**, NOT a specific bucket.
* Examples of UIDs: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
* **S3 Bucket**: A container for objects within an S3 Instance.
## Future Implementation Notes
When implementing or documenting `nubes_s3_instance` (or similar), refer to the provided documentation link.
@@ -0,0 +1,21 @@
# Discovery: S3 Storage & Documentation Architecture
## Архитектура хранения (S3 Cloud)
В ходе эволюции проекта мы отказались от локального хранилища в пользу облачного Nubes Cloud S3 (`s3.msk-1.ngcloud.ru`).
### Преимущества облачного S3:
1. **Персистентность:** Артефакты провайдера и файлы документации теперь хранятся независимо от жизненного цикла подов в Kubernetes.
2. **Безопасность:** Доступ по HTTPS (SSL) и использование выделенных IAM-пользователей.
3. **Совместимость:** Реестр использует S3‑совместимый SDK, что подтверждает совместимость Nubes S3 с протоколами Amazon.
## Подход к документации (Registry Style)
Мы внедрили стандарт "Strict Branding", имитирующий официальный реестр Terraform, но в стиле Nubes.
### Ключевые концепции:
- **Zero-Logic Pathing:** Сервер документации (`docsHandler`) проксирует запросы напрямую в S3 бакет `terraform-registry`. Пути формируются как `docs/{ns}/{name}/{version}/{path}`.
- **Soft Delete Visibility:** Документирование статусов `Suspended` и `Retention Period` — это критический элемент "правды" между Terraform State и UI облака. Пользователь должен понимать, что ресурс в режиме ожидания удаления — это осознанное состояние платформы.
### Инструментарий:
- **MkDocs Material:** Генерация статических страниц из Markdown.
- **Docker-based Build:** Сборка документации изолирована от хоста (используется `squidfunk/mkdocs-material`).
- **mc (S3 client):** Основной инструмент синхронизации (`mc mirror`) локальных артефактов с облаком.
@@ -0,0 +1,31 @@
# Session report — 2026-02-03
## Summary
- Worked in `/home/naeel/terra/universal_rebuild` and `test_dummy` to validate provider behavior and resource creation for multiple services.
- Used dev override to run Terraform against a locally built provider.
- Focused on Kafka creation and state consistency.
## Actions performed
- Ran `terraform apply` in `universal_rebuild/test_dummy` with dev override and API token.
- Observed long-running Kafka creation, followed by interruption/operation cancellation during apply.
- Verified Terraform state via `terraform show` (Kafka was not in state after cancel).
- Confirmed Kafka exists in UI while state lacked it (resource created out-of-band of state).
- Collected service ID list for: Flask(89), Redis(91), MongoDB(92), RabbitMQ(93), NodeJS(95), pgAdmin(96), NodeRed(97), Gitea(99), Mariadb(115), Nifi(117), ApacheKafka(116).
## Errors and anomalies
- Terraform apply reported cancellation during Kafka creation; operation marked cancelled while service still continued to create.
- Result: `nubes_kafka.test_kafka` missing from state, requiring import to reconcile.
## Current state
- Existing resources in state: dummy, s3bucket, postgres, vm, lucee.
- Kafka exists in UI but is not yet imported into Terraform state.
## Next steps (approved by user)
1. Import Kafka into Terraform state using its instance UUID.
2. Add resources one by one for the listed service IDs and run `terraform apply` after each addition.
## Provider logic changes (2026-02-03)
- Enforced adopt/resume only for ready instances: `not created/pending/failed` now return errors (no adopt).
- After resume, instance status is re-checked; non-ready states cause an error.
- Resource `Read` now surfaces non-ready instance status as an error instead of silently keeping state.
- Files updated/regenerated: `internal/resources_core/crud.go`, `tools/gen/main.go`, `internal/resources_gen/*`.
@@ -0,0 +1,79 @@
# Session report 2026-02-13
## What was done
- Implemented input param sync from state_params to avoid drift (e.g., git_path).
- Added parsers for state_params string values (bool/int64/string).
- Localized diagnostics to Russian in core paths and generated resources.
- Bumped provider version to 2.0.6.
- Regenerated resources with tools/gen and built the provider.
## Files touched
- universal_rebuild/internal/resources_core/helpers.go
- universal_rebuild/internal/resources_core/outputs.go
- universal_rebuild/internal/resources_core/resource_diagnostics.go
- universal_rebuild/tools/gen/generate_resources.go
- universal_rebuild/internal/resources_gen/*.go (regenerated)
- universal_rebuild/main.go
- docs/30_registry/guides/getting-started.md
## Commands
- go run ./tools/gen
- go build ./...
## Follow-up
- Saved new access/refresh tokens to ${ROOT_DIR}/19-28-48.token and .refresh.
- Updated test_dummy api_token and provider version to 2.0.6.
- Published provider 2.0.6 to registry S3 via devops/03_build_and_upload_provider.sh.
## Terraform test_dummy
- terraform init -upgrade pulled terra.k8c.ru/nubes/nubes v2.0.6.
- terraform plan: 2 changes (nubes_postgres.db2, nubes_lucee.app1) with Vault TLS handshake timeouts.
- terraform apply failed: API error 408 during modify (failed to set param 267).
## RefSvcId display name sync
- Added reverse mapping UUID -> display_name for refSvcId params to avoid drift.
- Regenerated resources and rebuilt provider.
## Registry publish retry
- devops/03_build_and_upload_provider.sh 2.0.6 failed: mc alias set timeout to s3.msk-1.ngcloud.ru.
- Retry succeeded: provider 2.0.6 uploaded to registry S3.
## UUID->display_name lookup
- Switched refSvcId reverse mapping to direct /instances/<uid> lookup with list fallback.
- Regenerated resources and rebuilt provider.
- Publish attempt failed: DNS timeout to s3.msk-1.ngcloud.ru.
- Publish retry succeeded: provider 2.0.6 uploaded to registry S3.
## Как правильно запускать CURL modify (Lucee)
- Шаг 1: POST /instanceOperations (operation=modify, svcOperationId=55, instanceUid) -> получить instanceOperationUid.
- Шаг 2: GET /instanceOperations/{opUid}?fields=cfsParams -> получить список параметров, включая instanceOperationCfsParamUid.
- Шаг 3: Для каждого параметра отправить paramValue как строку.
- Если instanceOperationCfsParamUid есть -> PUT /instanceOperationCfsParams (UID в теле JSON).
- Если UID нет -> POST /instanceOperationCfsParams (instanceOperationUid + svcOperationCfsParamId).
- Шаг 4: GET /instanceOperations/{opUid}/validate-cfs.
- Шаг 5: POST /instanceOperations/{opUid}/run с payload {}.
- Шаг 6: Polling GET /instanceOperations/{opUid} до dtFinish.
Важно:
- Все paramValue отправлять как строки (включая числа и JSON), чтобы не ломать checkParam.
- Нельзя пропускать required и дефолтные параметры.
- При пустом healthPath отправлять пустую строку.
## Lucee + Gitea: итоги и гипотезы
- Успешный деплой на своём Gitea: git_path = https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/gitftomhub-mirror.git (modify via curl прошёл, isSuccessful=true).
- Неуспешный деплой на чужом Gitea: git_path = https://gitea.services.ngcloud.ru/smishchuk/testlucee.git (ошибка: pod(ы) не работают).
- UI деплой для smishchuk/testlucee успешно завершался (HAR: lucee.har).
### Проверка репозиториев
- Изначально naeel/gitftomhub был пустой -> деплой падал (нет файлов).
- Создано зеркало GitHub репо xahys/testlucee -> naeel/gitftomhub-mirror (файлы появились, деплой прошёл).
- Попытка зеркала smishchuk/testlucee через API migrate -> стабильный 504 (nginx), повтор не помог.
### Обходной путь
- Создан репозиторий naeel/testlucee-mirror и выполнен локальный clone --mirror + push --mirror.
- После этого деплой на naeel/testlucee-mirror проходит.
### Вывод
- Проблема не в коде репозитория, а в доступе/доверии к хосту gitea.services.ngcloud.ru из инфраструктуры деплоя.
- Возможные причины: TLS/CA недоверен, egress/ACL на хост, различие маршрутов между UI и API-оркестратором.
- Суффикс .git не является корнем проблемы; ключевое — наличие контента и доступность хоста.
@@ -0,0 +1,10 @@
# Session report 2026-02-20
## Issue
The new gen_v2 Update template only used `plan.ID` when running modify. In Terraform update operations, `plan.ID` can be unknown, so the provider passed an empty instance ID and failed with "missing instance id for modify".
## Fix
The Update template now reads both plan and state, then falls back to `state.ID` when `plan.ID` is null or unknown. The resolved instance ID is used for modify and for the subsequent state refresh.
## Why this should work
`state.ID` is always known for an existing resource in Terraform state. Using it guarantees a valid instance ID during update, matching the previous generator behavior and preventing empty-ID modify calls.
@@ -0,0 +1,57 @@
# Session report 2026-02-21
## Context
- Test stand: TEST_STAND/MARIA_DB.
- Target: MariaDB + Lucee (testlucee) on k8s-4-sandbox-nubes-ru.
## Actions performed
- Switched test stand to MariaDB and deployed Lucee with MariaDB datasource.
- Updated testlucee repo for MariaDB:
- README and UI label updated to MariaDB.
- DDL changed to `INT AUTO_INCREMENT` for MariaDB.
- MariaDB instance created successfully (id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
- Lucee instance created successfully (id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
## Observations
- Lucee `connectionUrl`: https://web03.luceek8s.dev.nubes.ru.
- HTTP returns 404 from nginx; HTTPS fails with certificate chain error (local trust issue).
- Grafana dashboard links with `orgId=28` return Not Found; instance data points to Grafana org `WZ01325`.
## Redeploy attempt
- Created `nubes_lucee_redeploy` action resource and ran redeploy.
- Operation failed in stage "Работа с сервисом" with error:
- "NE RABOTAET. NUZHNO NAPISAT FUNKCIYU redeploy dlya dns V jlib.jobs"
- Operation ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
- Conclusion: redeploy is broken server-side due to missing DNS handler.
## Restart attempt
- Switched to `nubes_lucee_restart` action and executed restart.
- Operation ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
- Result: success, duration 50.696s.
## Notes
- MariaDB secrets are stored in Vault and exposed via `vault_fields` (adminUser/adminPass).
- Lucee datasource uses MariaDB driver `org.mariadb.jdbc.Driver` and JDBC URL to internal service.
## Flask deployment
- Flask instance created: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
- URL: https://flask.pythonk8s.dev.nubes.ru/.
- Observed the same symptoms as Lucee: HTTPS certificate issue and HTTP 404 from nginx.
- HTTP: 404 Not Found (nginx default).
- HTTPS: TLS error "unable to get local issuer certificate".
## NodeJS deployment
- NodeJS instance created: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
- URL: https://node.nodejsk8s.dev.nubes.ru/.
- Observed the same symptoms as Lucee and Flask (HTTP 404, HTTPS certificate issue).
## NodeJS redeploy attempt
- Action resource `nubes_nodejs_redeploy` executed.
- Operation ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
- Result: failed on stage "Работа с сервисом" (VIP handling).
- Error: "NE RABOTAET. NUZHNO NAPISAT FUNKCIYU MODIFIKACII VIP V jlib.jobs".
## Next steps
- Backend: implement DNS redeploy handler in `jlib.jobs` for Lucee redeploy.
- Platform: confirm TLS certificate issuance for `web03.luceek8s.dev.nubes.ru`.
- Grafana: confirm correct orgId or access to org `WZ01325`.
@@ -0,0 +1,58 @@
# Terraform Import Capabilities Analysis
**Date:** 2026-01-28
**Status:** Analysis of current Provider capability to import existing resources.
## Overview
Terraform Import allows bringing existing cloud resources under Terraform management. This is critical for brownfield deployments where resources were created manually or by other tools.
## Resource Status Matrix
| Resource Type | Terraform Resource | Import Supported? | Implementation Type | Notes |
| :--- | :--- | :--- | :--- | :--- |
| **VM** | `nubes_vm` | ✅ Yes | Passthrough + Read | Uses `readInstance` in Read method to populate state. |
| **PostgreSQL** | `nubes_postgres` | ✅ Yes | **Rich Import** | Specially implemented `ImportState` fetching full details via `GetInstanceFull`. |
| **VDC** | `nubes_vdc` | ✅ Yes | Passthrough | Relying on `Read` method. |
| **vApp** | `nubes_vapp` | ✅ Yes | Passthrough | Relying on `Read` method. |
| **Edge Gateway** | `nubes_edge` | ✅ Yes | Passthrough | Relying on `Read` method. |
| **Tubulus (AI)** | `nubes_tubulus` | ✅ Yes | Passthrough | Relying on `Read` method. |
| **Quick Start** | `nubes_quick_start` | ✅ Yes | Passthrough | Relying on `Read` method. |
| **S3 Bucket** | `nubes_s3bucket` | ❌ No | - | Missing `ImportState` method. |
| **PgAdmin** | `nubes_pgadmin` | ❌ No | - | Missing `ImportState` method. |
| **Organization** | `nubes_organization` | ❌ No | - | Missing `ImportState` method. |
## Implementation Patterns
### 1. Simple Passthrough (Most Resources)
Most resources use the standard Terraform framework passthrough. This works **only if** the `Read` method is robust enough to fully populate the state from just an ID.
```go
func (r *MyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
```
### 2. Rich Import (PostgreSQL Pattern)
The `nubes_postgres` resource required a more complex implementation because standard API responses (used by `Read`) often return truncated data or missing parameters (like `jsonParameters` or specific configuration flags).
**Pattern used:**
1. **Extended Client API**: Added `GetInstanceFull(ctx, uid)` to `client_impl.go`.
2. **Custom Import Logic**:
* Fetch full JSON.
* Manually map all fields (`cpu`, `ram`, `parameters`).
* Handle type conversions (string "1024" -> int64 1024).
* Set computed fields (like `deletion_protection = true` to match safe defaults).
## Future Work / Roadmap
1. **Implement S3 Import**:
* S3 buckets in Nubes API are tricky because Update is not supported (Immutable).
* Import must accurately capture the `service_uuid` and keys to prevent accidental destruction/recreation planning.
2. **Implement PgAdmin Import**:
* Low priority, purely auxiliary resource.
3. **Implement Organization Import**:
* Useful for managing existing hierarchy, but typically Organizations are static.
## Recommendation for Developers
When adding new resources, **always** implement `ImportState`.
If `Read()` is comprehensive, use `PassthroughID`.
If `Read()` relies on inputs (Plan) that might be missing during import, use the **Rich Import** pattern to fetch reliable truth from the API.
@@ -0,0 +1,55 @@
# Troubleshooting Note: GPG, Terraform Custom Registry & Alpine
Этот документ описывает технические нюансы и "грабли", обнаруженные при отладке Registry Protocol 23.01.2026.
## 1. Ошибки Terraform GPG
### `authentication signature from unknown issuer`
**Симптом**: Terraform скачивает провайдер, но отказывается его устанавливать.
**Механизм**:
1. `terraform init` запрашивает у Registry URL для скачивания.
2. Registry возвращает JSON, в котором есть поле `signing_keys` (массив public keys).
3. Terraform скачивает `.sig` файл и `.zip` файл.
4. Terraform проверяет, соответствует ли подпись `.sig` одному из ключей полученных из JSON.
**Решение**: Публичный ключ, "зашитый" в код Registry Server, должен СТРОГО соответствовать приватному ключу, которым выполняется `gpg --detach-sign`.
Практический чек-лист:
1. Сгенерируйте ключи и сохраните `secrets/private_key.asc` и `secrets/public_key.asc`.
2. Обновите ASCII Armor блока публичного ключа в:
- `registry-server-build/main.go`
- `operator/cmd/registry/main.go`
3. Пересоберите и задеплойте registry server.
4. Пересоберите и перезагрузите артефакты провайдера.
### `error checking signature: openpgp: invalid data: tag byte does not have MSB set`
**Симптом**: Terraform считает подпись битой.
**Причина**: Формат файла подписи.
- Если файл называется `.asc`, Terraform ожидает ASCII Armor (`-----BEGIN PGP...`).
- Если файл называется `.sig`, Terraform (в некоторых версиях/контекстах) ожидает **бинарный** OpenPGP формат.
**Решение**:
Для файлов `.sig` создавайте бинарную подпись:
`gpg --detach-sign ...` (БЕЗ `--armor`).
---
## 2. Проблемы запуска Go бинарников в Alpine
### `exec /path/to/binary: no such file or directory`
**Симптом**: Файл существует (проверено через ls), права на исполнение есть (+x), но при запуске ядро выдает "файл не найден".
**Причина**: Бинарник скомпилирован динамически и требует динамический линковщик (например, `/lib64/ld-linux-x86-64.so.2`), которого нет в Alpine Linux (там используется `musl`, а не `glibc`).
**Решение**:
Компилировать Go приложения **абсолютно статически**:
```bash
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-extldflags "-static"' .
```
---
## 3. Паттерн "Hot Deployment" (InitContainer Injection)
Если нет доступа к Docker Registry, но нужно обновить код в запущенном кластере:
1. Скомпилируйте бинарник локально.
2. Загрузите его в S3.
3. Используйте `initContainer` в Pod'е для скачивания бинарника в `emptyDir` volume.
4. Смонтируйте этот volume в основной контейнер.
5. Переопределите `command` основного контейнера, чтобы запускать скачанный файл.
@@ -0,0 +1,44 @@
# Tubulus (Bolvanka) & Polling Logic Discovery
## Overview
This document describes the critical discovery made regarding the Nubes Tubulus (Bolvanka) service and, more importantly, the strict polling logic required to interact with the Nubes API reliably.
## The "Bolvanka" Service
Tubulus is a test service ("Bolvanka") that mimics long-running operations. It is used to test the provider's lifecycle management capabilities, including:
- Creating instances with delays.
- Handling failures at different stages (Start, InProgress).
- Soft Delete (Resume) logic.
## Critical Polling Logic (The "Iron Logic")
After analyzing 14+ HAR files (HTTP Archives) from real API interactions, the following **INVARIANT** behavior was established for Instance Operations (`instanceOperations`):
### 1. The `dtFinish` Rule
* **dtFinish is NULL** while the operation is running (PENDING, IN_PROGRESS).
* **dtFinish is NOT NULL** (contains a timestamp) **IMMEDIATELY** when the operation finishes.
* **IMPLICATION**: `dtFinish` is the **ONLY** reliable source of truth for completion. Do not rely on `status`, `isInProgress`, or `isPending`.
### 2. The Success/Failure Rule
Once `dtFinish` is detected (not null), the success is determined solely by `isSuccessful`:
* **isSuccessful == true**: Operation succeeded. Proceed to read instance.
* **isSuccessful == false**: Operation failed.
* **isSuccessful == null**: Operation failed (or indeterminate state treated as failure).
### 3. The Instance Status Rule
* Do NOT check instance status while the operation is running. It will be "not created" or "suspended".
* Only after Operation Success (dtFinish != nil && isSuccessful == true) should you expect the Instance status to be `running`.
## Parameter Submission Logic
To successfully execute an operation (`run`), parameters must be submitted correctly:
1. **Empty Maps/JSONs**: Must be sent as `"{}"`. Sending `""` causes `400 Bad Request`.
2. **Empty Lists**: Must be sent as `"[]"`.
3. **Mapping keys**: The API returns parameters with `code`, `name`, and `svcOperationCfsParam`. Terraform resource attributes must be mapped to one of these (fallback order: Code -> Name -> SvcOperationCfsParam).
## Terraform "Unknown" Values
The Terraform Provider Framework requires that all attributes marked as `Computed` have a known value after `Apply`.
* The Nubes API `readInstance` does NOT return fields like `fail_at_start`, `duration_ms`, etc.
* **SOLUTION**: In the `Create` method, after reading the instance status, all optional computed fields that are still `Unknown` must be explicitly set to `Null`.
## Reference Code
See `internal/provider/tubulus_resource.go` for the implementation.
**DO NOT CHANGE THE POLLING OR PARAMETER LOGIC WITHOUT REVIEWING THIS DOCUMENT.**
+93
View File
@@ -0,0 +1,93 @@
# vApp (Virtual Application Catalog) Service
## Обнаружено из HAR: vapp.har
Дата: 2026-01-23
## Основная информация
- **Service ID**: 26
- **Service Name**: "Каталог виртуальных приложений (vApp)"
- **Operation create ID**: 69
## Параметры операции create
### Параметр 190: Edge Gateway UUID
- **svcOperationCfsParamId**: 190
- **Описание**: UUID Edge Gateway
- **Пример значения**: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
- **Тип**: UUID String
- **Обязательный**: Да
### Параметр 191: vApp Name
- **svcOperationCfsParamId**: 191
- **Описание**: Имя виртуального приложения
- **Пример значения**: "f12vapp"
- **Тип**: String
- **Обязательный**: Да
### Параметр 623: VDC UUID
- **svcOperationCfsParamId**: 623
- **Описание**: UUID виртуального датацентра
- **Пример значения**: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
- **Тип**: UUID String
- **Обязательный**: Да
## Архитектура зависимостей
vApp находится в середине иерархии:
```
Organization (WZ01325-saas)
└─ VDC (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) ← параметр 623
└─ Edge Gateway (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) ← параметр 190
└─ vApp (f12vapp) ← параметр 191
└─ VM
```
## Пример из HAR
```json
// POST /instances
{
"serviceId": 26,
"displayName": "f12vapp-3",
"descr": ""
}
// POST /instanceOperations
{
"instanceUid": "<instance-uuid>",
"operation": "create"
}
// POST /instanceOperationCfsParams (для каждого параметра)
{
"svcOperationCfsParamId": 190,
"paramValue": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 191,
"paramValue": "f12vapp",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 623,
"paramValue": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"instanceOperationUid": "<operation-uuid>"
}
```
## Связи
vApp требует:
- VDC (параметр 623) - родительский датацентр
- Edge Gateway (параметр 190) - сетевой шлюз внутри VDC
- vApp Name (параметр 191) - уникальное имя приложения
## Примечания
- vApp является контейнером для VM
- Каждая VM должна быть привязана к конкретному vApp через параметр vappUid (407)
- В HAR использованы ресурсы, созданные ранее через Terraform:
- VDC: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- Edge: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (создан в той же сессии)
+142
View File
@@ -0,0 +1,142 @@
# Virtual Data Center (VDC) Service
## Обнаружено из HAR: vdc.har
Дата: 2026-01-23
## Основная информация
- **Service ID**: 21
- **Service Name**: "Виртуальный датацентр (vDC)"
- **Operation create ID**: 9
## Параметры операции create
### Параметр 30: UUID Организации
- **svcOperationCfsParamId**: 30
- **Описание**: UUID организации Cloud Director
- **Пример значения**: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
- **Тип**: UUID String
- **Обязательный**: Да
### Параметр 335: Provider VDC
- **svcOperationCfsParamId**: 335
- **Описание**: Provider VDC в Cloud Director
- **Пример значения**: "v1cl4-vsan-pvdc"
- **Тип**: String
- **Обязательный**: Да
### Параметр 361: Storage Profiles
- **svcOperationCfsParamId**: 361
- **Описание**: Список storage профилей с размерами
- **Пример значения**: `[{"name":"storoj","size":"12"}]`
- **Тип**: JSON Array
- **Обязательный**: Да
- **Формат**: Массив объектов с полями name и size (в GB)
### Параметр 366: Network Pool
- **svcOperationCfsParamId**: 366
- **Описание**: Network Pool в Cloud Director
- **Пример значения**: "nsxt-v1cl1-geneve-np"
- **Тип**: String
- **Обязательный**: Да
### Параметр 397: Гарантированная доля ЯДЕР
- **svcOperationCfsParamId**: 397
- **Описание**: Процент резервирования CPU
- **Пример значения**: "20"
- **Тип**: Integer (как строка)
- **Обязательный**: Да
- **Единицы**: %
### Параметр 398: Гарантированная доля ОЗУ
- **svcOperationCfsParamId**: 398
- **Описание**: Процент резервирования RAM
- **Пример значения**: "20"
- **Тип**: Integer (как строка)
- **Обязательный**: Да
- **Единицы**: %
### Параметр 557: Квота CPU (неизвестно)
- **svcOperationCfsParamId**: 557
- **Пример значения**: "10"
- **Тип**: Integer (как строка)
### Параметр 558: Квота RAM (неизвестно)
- **svcOperationCfsParamId**: 558
- **Пример значения**: "20"
- **Тип**: Integer (как строка)
## Пример из HAR
```json
// POST /instances
{
"serviceId": 21,
"displayName": "f12vdc-2",
"descr": ""
}
// POST /instanceOperations
{
"instanceUid": "<instance-uuid>",
"operation": "create"
}
// POST /instanceOperationCfsParams (для каждого параметра)
{
"svcOperationCfsParamId": 30,
"paramValue": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 335,
"paramValue": "v1cl4-vsan-pvdc",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 361,
"paramValue": "[{\"name\":\"storoj\",\"size\":\"12\"}]",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 366,
"paramValue": "nsxt-v1cl1-geneve-np",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 397,
"paramValue": "20",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 398,
"paramValue": "20",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 557,
"paramValue": "10",
"instanceOperationUid": "<operation-uuid>"
}
{
"svcOperationCfsParamId": 558,
"paramValue": "20",
"instanceOperationUid": "<operation-uuid>"
}
```
## Проблемы и Блокеры (Status: 2026-01-28)
### 1. Storage Profiles Mismatch (BLOCKER)
- **Симптом**: API возвращает ошибку `error retrieving storage profile [NAME] from provider VDC 'v1cl4-vsan-pvdc'`.
- **Проверенные значения**:
- `storoj` (из HAR) -> **FAIL** (No records found).
- `SATA` (из документации) -> **FAIL** (No records found).
- **Контекст**: Для Provider VDC `v1cl4-vsan-pvdc` (ID 335) указанные профили не существуют.
- **Необходимое действие**: Получить список валидных Storage Profiles для данного Provider VDC.
### 2. Timeouts
- **Текущее состояние**: Таймаут создания уменьшен до 3 минут (Fail Fast).
- **Результат**: Операция падает по ошибке валидации профиля хранения быстрее, что позволяет ускорить цикл отладки.
### 3. Terraform Resource State
- Ресурс `nubes_vdc` временно скрыт из документации (mkdocs.yml) до решения проблем с деплоем.
@@ -0,0 +1,65 @@
# Методология визуализации ресурсов и зависимостей (Golden Standard)
Этот документ описывает стандарт визуального представления инфраструктуры Nubes Cloud в виде Mermaid-диаграмм. Этот стандарт был выработан для обеспечения максимальной читаемости, точности параметров и отсутствия искажений при рендеринге в IDE и браузере.
---
## 1. Концепция "Золотого стандарта" узла
Каждый ресурс (Organization, vDC, VM и т.д.) представляется как "карточка" с жестко структурированным содержимым.
### Визуальные правила
1. **Моноширинный шрифт**: Обязательное использование `font-family:monospace` через `classDef`. Это позволяет выровнять символы по вертикали.
2. **Запрет переноса строк (No-Wrap)**:
- Имена и UUID не должны разрываться.
- Используются **неразрывные дефисы** (`‑`, Unicode `U+2011`) вместо обычных.
- Используются **неразрывные пробелы** (`&nbsp;`) для отступов.
3. **Выравнивание "Две колонки"**:
- **Центральная ось**: Двоеточие `:`. Все двоеточия в блоке должны находиться на одной вертикали.
- **Левая колонка (Ключи)**: Выравнивание по правому краю (Padding слева).
- **Правая колонка (Значения)**: Выравнивание по левому краю (Padding справа).
### Структура карточки
- **Заголовок**: Тип ресурса (курсивом) и Terraform-имя (жирным).
- **Разделитель ПАРАМЕТРЫ**: Содержит входные данные (Required/Optional в TF).
- **Разделитель СГЕНЕРИРОВАНО**: Содержит выходные данные (Computed в TF), такие как UUID и статусы.
---
## 2. Процесс обнаружения (Discovery)
Для составления точной карточки ресурса используется двухступенчатый процесс:
### Этап А: Анализ исходного кода (Go)
1. Переход в `internal/provider/`.
2. Анализ функции `Schema` в соответствующем файле ресурса (например, `vdc_resource.go`).
3. Классификация атрибутов:
- Если `Required: true` или `Optional: true` -> попадает в **ПАРАМЕТРЫ**.
- Если `Computed: true` -> попадает в **СГЕНЕРИРОВАНО**.
### Этап Б: Анализ API трафика (HAR)
1. Проверка реальных значений в HAR-файлах (`har/`).
2. Сопоставление параметров Terraform с полями JSON в API (используя `instanceOperationCfsParams`).
---
## 3. Техническая реализация в Mermaid
Пример кода, реализующего стандарт:
```mermaid
flowchart LR
classDef resourceNode text-align:center,font-size:16px,font-family:monospace
NODE["<i>Тип&nbsp;ресурса</i><br/><b>имя_в_terraform</b><br/>━━━━&nbsp;ПАРАМЕТРЫ&nbsp;━━━━<br/>&nbsp;&nbsp;&nbsp;key&nbsp;:&nbsp;value&nbsp;&nbsp;&nbsp;<br/>━━━━&nbsp;СГЕНЕРИРОВАНО&nbsp;━━━━<br/>&nbsp;&nbsp;&nbsp;&nbsp;id&nbsp;:&nbsp;uuid‑123&nbsp;"]:::resourceNode
```
---
## 4. Иерархия и связи
Диаграммы строятся по принципу логической зависимости (Dependency Graph):
- **Прямая стрелка** (`-->`): Жёсткая зависимость (параметр одного ресурса ссылается на UID другого).
- **Пунктирная стрелка** (`-.->`): Косвенная зависимость или логическая связь (например, Edge и vApp в одной сети).
Рекомендуемое направление: `flowchart LR` (слева направо) для отображения жизненного цикла развертывания.
+83
View File
@@ -0,0 +1,83 @@
# VM Service UI Verification
**Date:** 2026-01-28
**Source:** User provided screenshots of "Create VM" operation in Nubes UI.
## Observed UI Fields (Creation Form)
The following fields were identified in the "Параметры операции" section for "Сервис: Виртуальная машина" -> "Экземпляр: vc_vm...":
1. **UUID vApp** (Select)
* Label: `UUID vApp`
* Mapping: Likely `vapp_uid`
2. **Имя ВМ** (Text)
* Label: `Имя ВМ *`
* Value in screenshot: `web01iop`
* Mapping: `vm_name` / `display_name`
3. **Кол-во ядер** (Number/Unit)
* Label: `Кол-во ядер *`
* Unit: `шт`
* Value: `1`
* Mapping: `vm_cpu`
4. **Кол-во ОЗУ** (Number/Unit)
* Label: `Кол-во ОЗУ *`
* Unit: `Gb`
* Value: `1`
* Mapping: `vm_ram`
5. **Размер дополнительного диска** (Input)
* Label: `Размер дополнительного диска`
* Note: "Основной диск у vm зависит от образа"
* Mapping: `vm_disk`
6. **Выделять внешний IP** (Dropdown)
* Label: `Выделять внешний IP`
* Value: `no-needed` ("no-needed, без внешнего ip...")
* Mapping: `ip_space_name`
7. **Белый список подключений к ВМ** (Input/Beautify)
* Label: `Белый список подключений к ВМ`
* Format: "Формат массива..."
* Mapping: `access_ip_list`
8. **По каким портам открыть доступ к ВМ** (Input)
* Label: `По каким портам открыть доступ к ВМ *`
* Value: `["22"]`
* Mapping: `access_port_list`
9. **Образ ВМ** (Dropdown)
* Label: `Образ ВМ *`
* Value: `Ubuntu_22-20G`
* Mapping: `image_vm`
10. **Логин для подключения** (Text)
* Label: `Логин для подключения *`
* Value: `vpuser`
* Mapping: `user_login`
11. **Публичная часть SSH-Ключа** (Text)
* Label: `Публичная часть SSH-Ключа *`
* Value: `ssh-ed25519 AAA...`
* Mapping: `user_public_key`
12. **Шаблон Cloud Init** (Text Area)
* Label: `Шаблон Cloud Init`
* Note: "Можно передать создание дополнительных пользователей..."
* Mapping: `cloud_init`
13. **needAddZabbixTemplate** (Checkbox)
* Label: `needAddZabbixTemplate`
* Value: Checked
* Description: "Поле позволяет автоматически добавить новый хост в систему мониторинга Zabbix..."
* Mapping: `need_add_zabbix_template`
## Comparison with Current Provider (`internal/provider/vm_resource.go`)
The current `VMResourceModel` appears to cover these fields:
- `VappUid` ✅
- `VmName` ✅
- `VmCpu` ✅
- `VmRam` ✅
- `VmDisk` ✅
- `IpSpaceName` ✅
- `AccessIpList` ✅
- `AccessPortList` ✅
- `ImageVm` ✅
- `UserLogin` ✅
- `UserPublicKey` ✅
- `CloudInit` ✅
- `NeedAddZabbixTemplate` ✅
**Discrepancy Check:**
- The screenshot does **not** explicitly show a `resource_realm` field for VM creation, unlike the PostgreSQL screenshot which showed "Платформа для развертывания". It might be hidden or inferred from the vApp. (However, the provider has `ResourceRealm` as optional).
@@ -0,0 +1,49 @@
# VM Provisioning Failures Investigation
## Overview
As of Jan 2026, VM creation is failing on the environment due to a backend error in the Cloud Director / Nubes platform.
## Technical Details
### The "String[] to GUID" Crash
The server returns a ColdFusion error indicating a type mismatch during parameter validation.
**Error Log:**
```
"detail": "The value String [] cannot be converted to a value of type [guid].",
"message": "Cannot cast String [] to a value of type [guid]",
"rootCause": {
"message": "Invalid call of the function [checkParam]",
...
}
```
### Failure Stage
Using detailed HAR capture and analysis (`analyze_har_vm.py`), we identified the exact workflow step where the crash occurs.
**Workflow:**
1. `vm.create` (Success)
2. `vm.config_network` (Success)
3. `vm.configure_vip` (Success)
4. `vm.fw_rules` -> **FAILURE**
**HAR Extract:**
```json
{
"stage": "Работа с сервисом",
"stageMsg": "Добавление правил FW",
"isSuccessful": false
}
```
### Provider Verification
We performed a code audit to ensure the Terraform Provider was not accidentally sending a malformed "list of strings" where a GUID was expected.
- **File**: `internal/provider/vm_resource.go`
- **Function**: `submitVMOperationParams`
- **Finding**: The provider sets `SvcOperationCfsParamId` (int) and `ParamValue` (string). It specifically *omits* optional GUID fields. Use of `analyze_har_vm.py` on the HTTP dump verified that the request payload from Terraform is identical to the (also failing) manual request.
## Conclusion
The firewall rule application logic on the backend is reacting to an empty or malformed input (likely an empty list `[]`) that it fails to validate before casting to a GUID.
**Action Item**: Elevate to Cloud Support.
+90
View File
@@ -0,0 +1,90 @@
# VM Service Discovery & Implementation Details
## Обзор
Сервис управления виртуальными машинами (VM) в облаке Nubes.
Позволяет выполнять операции полного жизненного цикла: создание, изменение, остановка, запуск, удаление.
**ВАЖНО:** Удаление ВМ происходит через механизм карантина (`suspend`), аналогично VDC. Реальное удаление происходит через 14 дней административными процессами облака.
## API Endpoints
* **Base URL**: `/api/v1/index.cfm`
* **Service ID**: 28
### Основные методы
| Операция | HTTP метод | URL | Описание |
| :--- | :--- | :--- | :--- |
| **Create Draft** | `POST` | `/instances` | Создание черновика инстанса. |
| **Start Op** | `POST` | `/instanceOperations` | Инициация операции (create, modify, suspend, resume). |
| **Add Params** | `POST` | `/instanceOperationCfsParams` | Добавление параметров к операции. |
| **Validate** | `GET` | `/instanceOperations/{opId}/validate-cfs` | Валидация (опционально). |
| **Run Op** | `POST` | `/instanceOperations/{opId}/run` | Запуск выполнения операции. |
## Parameter Mapping (Code & IDs)
API использует `svcOperationCfsParamId` для идентификации параметров.
**Внимание:** ID отличаются для создани (`create`) и изменения (`modify`).
В провайдере (v1.0.3+) реализована логика маппинга по `code` (имени), а также fallback на ID.
| Parameter Name (Terraform) | API Code | Create ID | Modify ID | Тип данных | Описание |
| :--- | :--- | :--- | :--- | :--- | :--- |
| `vapp_uid` | `vappUid` | **407** | - | String | UUID vApp контейнера |
| `vm_name` | `vmName` | **408** | - | String | Уникальное имя ВМ |
| `vm_cpu` | `vmCpu` | **409** | **493** | Int | Кол-во ядер |
| `vm_ram` | `vmRam` | **410** | **494** | Int | RAM (GB) |
| `vm_disk` | `vmDisk` | **411** | **495**? | Int | Доп. диск (GB) |
| `ip_space_name` | `ipSpaceName` | **412** | **496** | String | Public IP setting ("no-needed") |
| `access_ip_list` | `accessIpList` | **413** | **497** | JSON/List | Whitelist IP |
| `image_vm` | `imageVm` | **414** | - | String | OS Template (e.g. Ubuntu_22-20G) |
| `cloud_init` | `cloudInit` | **415** | - | String | YAML конфиг |
| `user_login` | `userLogin` | **416** | - | String | Admin User |
| `user_public_key` | `userPublicKey` | **417** | - | String | SSH Key (OpenSSH) |
| `access_port_list` | `accessPortList` | **448** | **498** | JSON/List | Whitelist Ports |
| `need_add_zabbix_template` | `needAddZabbixTemplate` | **449** | **499** | Bool | Мониторинг |
## Операции и Жизненный цикл
### Create (Создание)
1. `POST /instances` -> создает объект.
2. `POST /instanceOperations` (op="create").
3. Заполняются параметры 407-449.
4. `Run`.
### Modify (Изменение)
1. `POST /instanceOperations` (op="modify").
2. Заполняются параметры (ID 493-499).
3. `Run`.
4. Влияет на ресурсы (CPU, RAM), сеть и мониторинг. Имя и OS обычно не меняются.
### Suspend (Карантин/Удаление)
1. `POST /instanceOperations` (op="suspend").
2. `Run` (обычно без параметров).
3. ВМ останавливается и помечается для удаления.
### Resume (Восстановление)
1. `POST /instanceOperations` (op="resume").
2. `Run`.
3. ВМ запускается из состояния `suspend`.
## Реализация Terraform
### Deletion Protection
Ресурс `nubes_vm_instance` поддерживает параметр `deletion_protection` (default: `true`).
* **true**: При `terraform destroy` ресурс удаляется только из tfstate. В облаке остается активным.
* **false**: При `terraform destroy` выполняется операция **suspend**. ВМ выключается и попадает в карантин (14 дней).
### Modify Logic
Метод `Update` распознает изменения и вызывает операцию `modify`.
Параметры CPU, RAM, PortList, IpList, Zabbix и др. обновляются.
Параметры создания (Template, Login, Key, vApp, Name) обычно immutable или требуют пересоздания (ForceNew).
## Результаты анализа HAR
* `vmOK.har`: Успешное создание ВМ. Подтверждены ID 407-449.
* `vmsuspendresumemodify.har`: Анализ операций `modify` (ID 493-499), `suspend`, `resume`.
## Известные проблемы
* API не возвращает `code` (имя параметра) в некоторых случаях, полагаемся на хардкод маппинг ID или порядок. (Исправлено в v1.0.3 через универсальный маппер).
* `accessIpList` и `accessPortList` требуют явной передачи зависимостей JSON/Array даже если пусты.