17 KiB
Задача: спроектировать мок-полигон 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)
# 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
...
# 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— НЕЛЬЗЯ (конфликтует со stdlibsite.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: <краткий заголовок>
<развёрнутый вопрос с контекстом>