340 lines
17 KiB
Markdown
340 lines
17 KiB
Markdown
# Задача: спроектировать мок-полигон Nubes API
|
||
|
||
> Адресат: Claude Sonnet 4.6
|
||
> Дата: 2026-07-31
|
||
> ⛔ ОТВЕТ — ТОЛЬКО В ЧАТ. Не редактировать файлы. Не создавать файлы. Только текст в чат.
|
||
|
||
---
|
||
|
||
## 1. Что такое polygon
|
||
|
||
**Polygon** — отдельный managed-сервис на Nubes (`polygon.pythonk8s.dev.nubes.ru`),
|
||
эмулирующий REST API облачной платформы Nubes. Нужен для интеграционных тестов
|
||
приложения **app-autotest** — чтобы тесты гонялись не на реальном облаке, а на
|
||
локальном/CI эмуляторе.
|
||
|
||
**Ключевое:** polygon — это НЕ часть app-autotest. Это самостоятельный сервис
|
||
со своим репозиторием (`https://gitea.services.ngcloud.ru/forcloud/polygon.git`),
|
||
своим деплоем, своей версией (v0.1.0).
|
||
|
||
### Текущее состояние
|
||
|
||
Сервис запущен на Nubes, код минимальный:
|
||
|
||
```
|
||
polygon/
|
||
├── requirements.txt # Flask>=3.0, gunicorn>=21.2, PyYAML>=6.0
|
||
├── README.md
|
||
├── .gitignore
|
||
└── site/
|
||
└── app.py # 4 эндпоинта: /health, /, /api/v1/svc/instances,
|
||
# /api/v1/svc/_mock/reset
|
||
```
|
||
|
||
`site/app.py` — стандартный Flask по инструкции Nubes:
|
||
- `app = Flask(__name__, template_folder="templates", static_folder="static")`
|
||
- `/health` → `"OK"` (healthcheck)
|
||
- `/` → HTML-страница с версией
|
||
- `app.run(debug=True, host="0.0.0.0", port=5000)` в `if __name__ == "__main__"`
|
||
|
||
Gunicorn на проде: `gunicorn app:app --workers 2 --bind 0.0.0.0:8000`.
|
||
|
||
## 2. Исходные данные: STANDS YAML
|
||
|
||
В `STANDS/test/resources_yaml/` лежат **37 YAML-файлов** — описания ВСЕХ сервисов
|
||
Nubes в формате Terraform-провайдера. Это КАНОНИЧЕСКИЙ источник данных о сервисах:
|
||
их параметры, операции, типы данных, дефолты, valueList'ы.
|
||
|
||
### Структура STANDS YAML (на примере dummy и postgres)
|
||
|
||
```yaml
|
||
# 1_dummy.yaml (простой сервис, 339 строк)
|
||
name: dummy
|
||
service_id: 1
|
||
service_display_name: Болванка
|
||
service_short_name: dummy
|
||
lifecycle:
|
||
suspend_on_destroy_default: true
|
||
adopt_existing_on_create_default: false
|
||
outputs:
|
||
params:
|
||
- code: state_params # map
|
||
- code: state_out # map
|
||
- code: vault_secrets # map, sensitive
|
||
# ... ещё 5 outputs
|
||
operations:
|
||
- name: create
|
||
id: 18
|
||
kind: instance
|
||
action: create
|
||
params:
|
||
- id: 242
|
||
code: resourceRealm
|
||
data_type: string
|
||
required: true
|
||
default: dummy
|
||
value_list: [dummy]
|
||
- id: 198
|
||
code: durationMs
|
||
data_type: "integer >= 0"
|
||
required: true
|
||
default: "0"
|
||
- id: 199
|
||
code: failAtStart
|
||
data_type: boolean
|
||
required: true
|
||
default: "false"
|
||
value_list: ["false", "true"]
|
||
- id: 321
|
||
code: someMapParam
|
||
data_type: map
|
||
sub_params:
|
||
- id: 322
|
||
code: subparam1
|
||
data_type: string
|
||
- id: 323
|
||
code: secret
|
||
data_type: string
|
||
- name: modify
|
||
id: 92
|
||
kind: instance
|
||
action: modify
|
||
params: [...] # те же параметры, is_modifiable: true
|
||
- name: delete
|
||
id: 71
|
||
...
|
||
```
|
||
|
||
```yaml
|
||
# 90_postgres.yaml (самый сложный, 934 строки)
|
||
operations:
|
||
- name: create # id: 19, kind: instance
|
||
- name: delete # id: 20
|
||
- name: modify # id: 40
|
||
- name: suspend # id: 41
|
||
- name: resume # id: 42
|
||
- name: restart # id: 43 ← лишняя?
|
||
- name: recovery # id: 24 ← лишняя?
|
||
- name: create_user # id: 44 ← subresource!
|
||
- name: delete_user # id: 45 ← subresource!
|
||
- name: create_database # id: 46 ← subresource!
|
||
- name: delete_database # id: 47 ← subresource!
|
||
|
||
# У create-операции — map-fixed с sub_params:
|
||
- name: create
|
||
params:
|
||
- id: 788
|
||
code: clusterConfiguration
|
||
data_type: map-fixed
|
||
sub_params:
|
||
- id: 106
|
||
code: replicas
|
||
data_type: "integer > 0"
|
||
default: "1"
|
||
value_list: ["1", "3", "5", "7"]
|
||
- id: 107
|
||
code: disk
|
||
data_type: "integer > 0"
|
||
default: "10"
|
||
```
|
||
|
||
### Спектр сложности (37 сервисов)
|
||
|
||
| Тип | Примеры | Особенности |
|
||
|-----|---------|-------------|
|
||
| Простые | dummy, s3, http, gitea, nodejs, flask | Плоские параметры, 6 операций |
|
||
| Средние | redis, mongodb, rabbitmq, clickhouse, kafka | map-fixed (cluster/access/startup) |
|
||
| Сложные | postgres, mariadb | + subresources (create_user, create_database) |
|
||
| Инфраструктурные | vc_org, vc_vdc, vc_nsxt, vapp, vc_vm_v2/v3 | Другие kind'ы, refSvcId на другие сервисы |
|
||
| Особые | s3bucket (refSvcId на s3), template, k8s_velero, llm_ai | Нестандартные параметры |
|
||
|
||
Все 37 файлов лежат здесь: `STANDS/test/resources_yaml/`
|
||
Полный список: `1_dummy.yaml`, `2_template.yaml`, `12_s3.yaml`, `13_s3bucket.yaml`,
|
||
`19_vc_org.yaml`, `21_vc_vdc.yaml`, `22_vc_nsxt.yaml`, `25_vcexternalip.yaml`,
|
||
`26_vapp.yaml`, `27_vc_vm_v2.yaml`, `28_vc_vm_v3.yaml`, `29_vc_vdc_group.yaml`,
|
||
`50_nextcloud.yaml`, `81_superset.yaml`, `82_harbor.yaml`, `86_k8s_velero.yaml`,
|
||
`89_flask.yaml`, `90_postgres.yaml`, `91_redis.yaml`, `92_mongodb.yaml`,
|
||
`93_rabbitmq.yaml`, `94_lucee.yaml`, `95_nodejs.yaml`, `96_pgadmin.yaml`,
|
||
`98_http.yaml`, `99_gitea.yaml`, `109_zones_v2.yaml`, `111_dnsrecord.yaml`,
|
||
`115_mariadb.yaml`, `116_kafka.yaml`, `119_akhq.yaml`, `120_clickhouse.yaml`,
|
||
`148_vc_mgmt_sthutrval_cluster.yaml`, `149_valo_tenant.yaml`,
|
||
`150_k8s_sthutrval_cluster.yaml`, `151_k8s_openbao.yaml`, `163_llm_ai.yaml`.
|
||
|
||
## 3. Архитектурные решения (уже приняты)
|
||
|
||
Предыдущий архитектор (Опус) принял 10 решений. Их нужно УЧЕСТЬ, но можно
|
||
ОСПОРИТЬ если найдёшь лучший подход.
|
||
|
||
| # | Решение | Обоснование |
|
||
|---|---------|-------------|
|
||
| 1 | Отдельный процесс :5000 | `http_client` делает реальные HTTP-запросы — blueprint не проверит |
|
||
| 2 | Data-driven: сервисы из YAML | Не хардкодить 37 сервисов в Python |
|
||
| 3 | Мин. поля + `default_for(dataType)` | 20+ параметров вручную — ад |
|
||
| 4 | Ленивый dtFinish | Без потоков, `MOCK_OP_DELAY` (по умолчанию 0.1с) |
|
||
| 5 | Единая стейт-машина | create→running→suspended→deleted |
|
||
| 6 | Реальный мерж params при modify | Иначе тест modify→проверить state.params бессмысленен |
|
||
| 7 | Статический stateOut из YAML | Для MVP, генерация из параметров — потом |
|
||
| 8 | `/_mock/reset` для тестов | Без сброса тесты влияют друг на друга |
|
||
| 9 | Тесты через `app_client` | Проверяет реальную связку app-autotest ↔ эмулятор |
|
||
| 10 | refSvcId игнорируем в MVP | validate-cfs всегда OK, ссылки не проверяются |
|
||
|
||
## 4. Критические точки интеграции (из кода app-autotest)
|
||
|
||
Это НЕ предположения — это факты из реального кода, который будет ходить в polygon:
|
||
|
||
### 4.1 Location-заголовки обязательны
|
||
|
||
`HttpClient.post()` достаёт UUID из заголовка `Location` (последний сегмент, `len >= 32`).
|
||
Polygon ОБЯЗАН отдавать:
|
||
- `POST /api/v1/svc/instances` → `201` + `Location: ./{instanceUid}`
|
||
- `POST /api/v1/svc/instanceOperations` → `201` + `Location: ./{instanceOperationUid}`
|
||
|
||
Без Location executor не получит UUID → ошибка.
|
||
|
||
### 4.2 Поллинг
|
||
|
||
`poll_until_done()` делает `GET /instanceOperations/{uid}?fields=...`,
|
||
ждёт `dtFinish`, спит 5 секунд между попытками. При `MOCK_OP_DELAY=0.1`
|
||
dtFinish появится на первом же GET — вторая итерация со сном не случится.
|
||
|
||
### 4.3 Short-circuit localhost в auth.py
|
||
|
||
`detect_endpoint()` в app-autotest хардкодит dev/test стенды и игнорирует
|
||
`NUBES_API_ENDPOINT`. Нужно добавить проверку: если endpoint начинается с
|
||
`http://localhost` или `http://127.0.0.1` → пропустить detect, сразу использовать.
|
||
Это будет сделано в app-autotest, не в polygon.
|
||
|
||
### 4.4 validate-cfs = пустое тело
|
||
|
||
`send_params_terraform()` считает успехом пустой/не-JSON ответ.
|
||
Polygon отдаёт `200` с пустым телом.
|
||
|
||
### 4.5 state.params по коду, cfsParams по числовому id
|
||
|
||
`get_params_with_current_values()` мержит `state.params[код]` с шаблоном из
|
||
`GET /instanceOperations/default/{opId}`.
|
||
YAML должен связывать числовой `svcOperationCfsParamId` ↔ код параметра.
|
||
|
||
### 4.6 Nubes-совместимость
|
||
|
||
Polygon деплоится как managed-сервис Nubes (pythonk8s). Следовательно:
|
||
- `site/app.py` — точка входа (платформа ждёт `python site/app.py`)
|
||
- `app.run(host="0.0.0.0", port=5000)` — обязательно
|
||
- ⛔ `site/__init__.py` — НЕЛЬЗЯ (конфликтует со stdlib `site.py`)
|
||
- ⛔ `from site.xxx import ...` — НЕЛЬЗЯ (импорт без префикса)
|
||
- `/health` → `"OK"` — healthcheck для Nubes
|
||
|
||
## 5. Что нужно от тебя
|
||
|
||
### 5.1 Архитектура проекта
|
||
|
||
Полная архитектура polygon с обоснованием каждого решения. Включая:
|
||
|
||
- **Структура файлов** в `site/` — какие модули, за что отвечают
|
||
- **Схема данных в памяти** — MockState: instances, operations, связи
|
||
- **Стейт-машина инстанса** — все статусы, переходы
|
||
- **Поток запроса** — что происходит от POST /instances до GET /instanceOperations/{uid}
|
||
- **Схема API** — полный список эндпоинтов с форматами запросов/ответов
|
||
|
||
### 5.2 Универсальный конвертер STANDS → polygon
|
||
|
||
Главный вопрос: **можно ли написать ОДИН конвертер для всех 37 сервисов без сервис-специфичного кода?**
|
||
|
||
Если да — спроектировать `from_stands.py`:
|
||
- Алгоритм конвертации одного YAML
|
||
- Маппинг ВСЕХ полей STANDS → polygon (таблица)
|
||
- Обработка краевых случаев (map-fixed, sub_params, subresources)
|
||
- Генерация stateParams из create-операции (defaults)
|
||
- Генерация stateOut (из операций create_user/create_database и т.п.)
|
||
- Выходной формат: отдельные файлы в `services/` или один dict?
|
||
|
||
Если нет — объяснить ГДЕ проходит граница и что придётся делать вручную.
|
||
|
||
### 5.3 Подробный план реализации
|
||
|
||
Пошаговый план с разбивкой на этапы. Для каждого этапа:
|
||
- Какие файлы создать/изменить
|
||
- Что именно реализовать (функции, классы, эндпоинты)
|
||
- Критерий готовности этапа
|
||
- Ожидаемые сложности
|
||
|
||
## 6. Конкретные вопросы
|
||
|
||
Ответь на каждый:
|
||
|
||
### Q1. from_stands.py
|
||
Как обрабатывать `map-fixed` параметры (clusterConfiguration, startupConfiguration)?
|
||
В STANDS: `params[].sub_params[]`. В реальном API: `dataDescriptor`.
|
||
Должен ли конвертер генерировать `dataDescriptor` из `sub_params`? Или моку
|
||
достаточно знать структуру чтобы принимать такие параметры при create?
|
||
|
||
### Q2. subresources
|
||
У postgres/mariadb есть `create_user`, `delete_user`, `create_database`, `delete_database`.
|
||
Можно ли вывести **общее правило**: «если операция имеет kind ≠ instance — это subresource»?
|
||
Или «если действие create/delete а операция не create/delete инстанса — subresource»?
|
||
|
||
Как универсально эмулировать subresource-операции? Сейчас идея: `apply_effect` смотрит на
|
||
`action` и `kind` — если `kind != "instance"`, значит меняем `state.out`, не `state.params`.
|
||
|
||
### Q3. stateOut
|
||
В реальном API после create PostgreSQL: `state.out = {users: {pgadmin: {}}, databases: {mydb: {}}}`.
|
||
Может ли конвертер автоматически вывести структуру stateOut из наличия subresource-операций?
|
||
Например: есть `create_user` → добавить `users: {}` в stateOut.
|
||
|
||
### Q4. Лишние операции
|
||
У postgres есть restart, recovery. У некоторых сервисов есть специфичные операции.
|
||
Включать их в мок? Если да — как эмулировать? Если нет — что отвечать на запрос
|
||
`GET /instanceOperations/default/{id}` для несуществующей операции?
|
||
|
||
### Q5. valueList
|
||
В STANDS YAML `value_list` есть у многих параметров. Нужно ли моку хранить их
|
||
и отдавать в `GET /instanceOperations/default/{id}` (cfsParams)? Или мок может
|
||
отдавать пустой `valueList` для всех?
|
||
|
||
### Q6. Config loader vs генерация
|
||
Два подхода:
|
||
- **A)** `from_stands.py` генерит YAML в `services/`, `config_loader.py` их читает при старте
|
||
- **B)** `from_stands.py` сразу возвращает dict, без промежуточных YAML
|
||
|
||
Какой лучше? Плюсы A: можно редактировать сгенерированные YAML руками для сложных сервисов.
|
||
Плюсы B: проще, меньше файлов.
|
||
|
||
### Q7. mock-специфичные эндпоинты
|
||
`/_mock/reset` — сброс состояния. Нужны ли ещё?
|
||
- `/_mock/state` — посмотреть текущее состояние (для отладки)?
|
||
- `/_mock/delay/{seconds}` — изменить MOCK_OP_DELAY на лету?
|
||
- `/_mock/services` — список загруженных сервисов?
|
||
|
||
### Q8. Идентификаторы
|
||
UUID для instanceUid и instanceOperationUid — генерить через `uuid.uuid4()`?
|
||
Или детерминированно (например, `uuid5(namespace, service_id + counter)`)?
|
||
|
||
### Q9. Пагинация
|
||
`GET /instances?pageSize=N&page=P` — как правильно эмулировать?
|
||
Стоп по `len(batch) < pageSize`? Что если pageSize > 200?
|
||
|
||
### Q10. Тестирование
|
||
5 сценариев из плана Опуса — достаточны? Какие ещё сценарии добавить?
|
||
Как организовать тесты чтобы они не зависели друг от друга?
|
||
|
||
## 7. Что можно спрашивать у меня
|
||
|
||
Ты можешь задавать мне уточняющие вопросы в текстовом виде. Я отвечу.
|
||
|
||
Что можно спрашивать:
|
||
- Уточнения по STANDS YAML (структура конкретных сервисов, поля которые не видел)
|
||
- Детали работы app-autotest (какие запросы шлёт, какие форматы ждёт)
|
||
- Приоритеты (что важнее сделать сначала)
|
||
- Ограничения Nubes-платформы
|
||
|
||
Что НЕ надо спрашивать:
|
||
- «Какие технологии использовать?» — только Flask, Python, PyYAML
|
||
- «Может использовать БД?» — нет, всё в памяти
|
||
- «Может Kubernetes API?» — нет, это чистый HTTP-эмулятор
|
||
|
||
Формат вопросов:
|
||
```
|
||
### Вопрос 1: <краткий заголовок>
|
||
<развёрнутый вопрос с контекстом>
|
||
```
|