doc: README для DOCS/HISTORY, polygon-docs/, +.venv в gitignore
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
# Задача: спроектировать мок-полигон 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: <краткий заголовок>
|
||||
<развёрнутый вопрос с контекстом>
|
||||
```
|
||||
@@ -0,0 +1,229 @@
|
||||
# Ответ Claude Sonnet 4.6 — архитектура и план polygon
|
||||
|
||||
Дата: 2026-07-31
|
||||
|
||||
---
|
||||
|
||||
## Ответы на Q1–Q10
|
||||
|
||||
**Q1: map-fixed → dataDescriptor**
|
||||
Конвертер ГЕНЕРИРУЕТ `dataDescriptor` из `sub_params`. Формат каждой записи:
|
||||
```
|
||||
{sub_param_code: {dataType, defaultValue, valueList, isRequired}}
|
||||
```
|
||||
Это обязательно: `normalize_value` в app-autotest использует `dataDescriptor` для генерации JSON из defaults при пустом значении map-параметра.
|
||||
|
||||
**Q2: subresources**
|
||||
Правило простое и точное: `kind == "subresource"` (это явное поле в STANDS YAML).
|
||||
`apply_effect` смотрит на `kind`:
|
||||
- `kind == "instance"` → меняем статус + `state.params`
|
||||
- `kind == "subresource"` → меняем `state.out[plural]`, статус НЕ меняем
|
||||
|
||||
**Q3: stateOut auto-generation**
|
||||
Да, конвертер выводит автоматически. Сканирует операции с `kind=subresource, action=create`, строит `state_out_template`. Пример для postgres: `{users: {}, databases: {}, backups: {}}`. При create инстанса мок копирует этот шаблон в `instance.state.out`.
|
||||
|
||||
**Q4: "лишние" операции**
|
||||
Включить все (restart, recovery, etc.) — иначе `GET /instanceOperations/default/{id}` вернёт 404 и app-autotest упадёт. Реализация: no-op transition (статус остаётся `running`, dtFinish ставится, isSuccessful=True). Если operation_id вообще не найден → 404 с `{"error": "operation not found"}`.
|
||||
|
||||
**Q5: valueList**
|
||||
Хранить и отдавать обязательно. `get_params_with_current_values` в app-autotest заполняет из него дропдауны для `delete_user`/`delete_database`.
|
||||
|
||||
**Q6: Подход A (generate YAML → services/)**
|
||||
Выбрать A. `from_stands.py` генерирует `site/services/N_name.yaml`, `config_loader.py` читает при старте Flask. Плюс: ручная правка для сложных случаев, прозрачность. `from_stands.py` запускается один раз при setup.
|
||||
|
||||
**Q7: Mock-эндпоинты**
|
||||
Все 4:
|
||||
- `POST /_mock/reset` — обязателен для тестовой изоляции
|
||||
- `GET /_mock/state` — текущее состояние (отладка)
|
||||
- `GET /_mock/services` — список загруженных сервисов
|
||||
- `POST /_mock/delay/<float>` — изменить MOCK_OP_DELAY на лету
|
||||
|
||||
**Q8: UUID** → `uuid.uuid4()`. Детерминированные не нужны.
|
||||
|
||||
**Q9: Пагинация** → stop when `len(batch) < pageSize`, cap at 200, page=1 by default. Response: `{"results": [...], "pageSize": N, "page": P, "total": len(all)}`.
|
||||
|
||||
**Q10: Тесты**
|
||||
К 5 сценариям Опуса добавить: (a) тест пагинации (3 инстанса, pageSize=2), (b) тест subresource create→verify state.out→delete→verify removed, (c) тест modify→verify params merged, (d) тест /_mock/state, (e) тест 404 на невалидный op_id. Каждый тест начинается с `POST /_mock/reset` (через `autouse` fixture).
|
||||
|
||||
---
|
||||
|
||||
## Структура файлов
|
||||
|
||||
```
|
||||
polygon/
|
||||
├── requirements.txt
|
||||
└── site/
|
||||
├── app.py # Flask app + все 15 маршрутов (один файл)
|
||||
├── mock_state.py # MockState: instances, operations, op_params
|
||||
├── state_machine.py # apply_effect()
|
||||
├── config_loader.py # load_services() → services_dict + ops_index
|
||||
├── from_stands.py # CLI-конвертер STANDS YAML → services/*.yaml
|
||||
├── services/ # 37 YAML-файлов (generated)
|
||||
└── templates/
|
||||
└── index.html
|
||||
```
|
||||
|
||||
**Ключевое ограничение Nubes:**
|
||||
- `site/__init__.py` — ЗАПРЕЩЁН
|
||||
- `from site.xxx import` — ЗАПРЕЩЁН
|
||||
- В app.py: только `import mock_state`, `import config_loader`, `import state_machine` (работает т.к. Python добавляет директорию скрипта в `sys.path[0]`)
|
||||
|
||||
---
|
||||
|
||||
## Схема данных в памяти
|
||||
|
||||
```python
|
||||
# mock_state.py — синглтон state = MockState()
|
||||
class MockState:
|
||||
instances = {} # instanceUid → instance_dict
|
||||
operations = {} # opUid → operation_dict
|
||||
op_params = {} # opUid → {paramId(int): paramValue(str)}
|
||||
```
|
||||
|
||||
**instance_dict:** `{instanceUid, serviceId, displayName, descr, status, explainedStatus, svc, dtCreate, state: {params: {code: val}, out: {users: {}, databases: {}}}}`
|
||||
|
||||
**operation_dict:** `{instanceOperationUid, instanceUid, svcOperationId, operation, kind, action, subresource|None, dtStart, dtFinish|None, isSuccessful|None, errorLog, svc, stages}`
|
||||
|
||||
---
|
||||
|
||||
## Конвертер from_stands.py
|
||||
|
||||
**HTML entities:** STANDS YAML содержит `integer >= 0`, `"` и т.д. `from_stands.py` **обязан** применять `html.unescape()` ко всем строковым полям (`data_type`, `value_list` элементы, `default`).
|
||||
|
||||
**Маппинг STANDS → cfsParam формат:**
|
||||
|
||||
| STANDS | polygon/cfsParam |
|
||||
|---|---|
|
||||
| `id` | `svcOperationCfsParamId` |
|
||||
| `code` | `svcOperationCfsParam` |
|
||||
| `data_type` (unescape) | `dataType` |
|
||||
| `required` | `isRequired` |
|
||||
| `default` | `defaultValue` |
|
||||
| `value_list` | `valueList` |
|
||||
| `sub_params` (map-fixed) | → `dataDescriptor: {code: {dataType,defaultValue,valueList,isRequired}}` |
|
||||
| `sub_params` (map) | → `sub_params` (хранить как есть для normalize_value) |
|
||||
|
||||
**stateOut auto-generation:**
|
||||
```python
|
||||
for op in operations:
|
||||
if op.kind == "subresource" and op.action == "create":
|
||||
plural = op.subresource + "s" # user→users, database→databases
|
||||
state_out_template[plural] = {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полный список API эндпоинтов (15 шт.)
|
||||
|
||||
| # | Метод | Путь | Назначение |
|
||||
|---|---|---|---|
|
||||
| 1 | GET | `/health` | `"OK"` |
|
||||
| 2 | GET | `/` | HTML-страница |
|
||||
| 3 | GET | `/api/v1/svc/instances` | Список инстансов (paged) |
|
||||
| 4 | GET | `/api/v1/svc/instances/<uid>` | Детали инстанса |
|
||||
| 5 | POST | `/api/v1/svc/instances` | Создать shell → 201 + Location |
|
||||
| 6 | GET | `/api/v1/svc/instanceOperations/default/<int:op_id>` | Шаблон операции |
|
||||
| 7 | POST | `/api/v1/svc/instanceOperations` | Создать операцию → 201 + Location |
|
||||
| 8 | GET | `/api/v1/svc/instanceOperations/<uid>` | Детали операции + cfsParams |
|
||||
| 9 | POST | `/api/v1/svc/instanceOperationCfsParams` | Задать значение параметра |
|
||||
| 10 | GET | `/api/v1/svc/instanceOperations/<uid>/validate-cfs` | Валидация → 200 **пустое тело** |
|
||||
| 11 | POST | `/api/v1/svc/instanceOperations/<uid>/run` | Выполнить операцию |
|
||||
| 12 | POST | `/api/v1/svc/_mock/reset` | Сброс состояния |
|
||||
| 13 | GET | `/api/v1/svc/_mock/state` | Debug: текущее состояние |
|
||||
| 14 | GET | `/api/v1/svc/_mock/services` | Debug: загруженные сервисы |
|
||||
| 15 | POST | `/api/v1/svc/_mock/delay/<float:s>` | Изменить MOCK_OP_DELAY |
|
||||
|
||||
**Критические детали:**
|
||||
|
||||
- **Эндпоинт 5** (`POST /instances`): возвращает `201` + заголовок `Location: ./{uid}`. `HttpClient.post()` берёт uid из последнего сегмента Location, проверяет `len >= 32`. Body: `{"instanceUid": uid}` (двойная защита).
|
||||
|
||||
- **Эндпоинт 7** (`POST /instanceOperations`): при `operation=="create"` тело НЕ содержит `svcOperationId` — polygon находит его сам из `svc_def.operations["create"]["id"]`.
|
||||
|
||||
- **Эндпоинт 10** (validate-cfs): `return "", 200` (НЕ `jsonify`). app-autotest ловит `JSONDecodeError` и считает это успехом.
|
||||
|
||||
- **Эндпоинт 11** (run): синхронный — `time.sleep(MOCK_OP_DELAY)`, затем `apply_effect`, затем `dtFinish = datetime.utcnow().isoformat() + "Z"`. Первый poll после run увидит dtFinish.
|
||||
|
||||
- **Маршруты Flask:** `/instanceOperations/default/<int:op_id>` должен стоять **выше** `/instanceOperations/<uid>` в app.py.
|
||||
|
||||
---
|
||||
|
||||
## Поток запроса (полный цикл create)
|
||||
|
||||
```
|
||||
executor.py polygon
|
||||
───────── ───────
|
||||
POST /instances {serviceId,displayName}
|
||||
→ создать instance shell (status="creating")
|
||||
→ response 201 + Location: ./uid1
|
||||
← instanceUid = uid1
|
||||
|
||||
POST /instanceOperations {instanceUid, operation:"create"}
|
||||
→ найти create op_id из service_def
|
||||
→ создать operation_dict (dtFinish=None)
|
||||
→ response 201 + Location: ./uid2
|
||||
← opUid = uid2
|
||||
|
||||
GET /instanceOperations/uid2?fields=cfsParams
|
||||
→ response {"instanceOperation": {"cfsParams": [...]}}
|
||||
cfsParams из service_def + пустые paramValue
|
||||
|
||||
POST /instanceOperationCfsParams × N → state.op_params[uid2][paramId] = val
|
||||
GET /instanceOperations/uid2/validate-cfs → response "" 200
|
||||
POST /instanceOperations/uid2/run
|
||||
→ sleep(0.1s)
|
||||
→ apply_effect: state.params←codes, status="running"
|
||||
→ dtFinish = now
|
||||
→ response {"ok": true}
|
||||
|
||||
GET /instanceOperations/uid2?fields=dtFinish,...
|
||||
→ response {"instanceOperation": {"dtFinish": "2026-...", "isSuccessful": true, ...}}
|
||||
← poll done: status="OK"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Фазы реализации
|
||||
|
||||
**Этап 1 — Конвертер** (`from_stands.py`): html.unescape, convert_param рекурсивный, build_state_out_template, pluralize, CLI. Запустить: `cd polygon/site && python from_stands.py`. Критерий: 37 YAML в `site/services/`, у postgres `state_out_template: {users:{}, databases:{}, backups:{}}`.
|
||||
|
||||
**Этап 2 — MockState + config_loader + state_machine**: три отдельных модуля. Критерий: `python -c "import config_loader; s,i=config_loader.load_services('services'); print(len(s))"` → 37.
|
||||
|
||||
**Этап 3 — Flask API** в app.py: реализовать все 15 эндпоинтов в порядке от простых к сложным (см. таблицу). Критерий: `python -m py_compile app.py` без ошибок, `curl localhost:5000/health` → OK.
|
||||
|
||||
**Этап 4 — Тесты**:
|
||||
- В polygon: `tests/test_converter.py`, `tests/test_state_machine.py` (юнит)
|
||||
- В app-autotest: `tests/conftest.py` (+fixture `polygon_server` subprocess), `tests/test_polygon_integration.py` (интеграционные)
|
||||
|
||||
|
||||
## Уточнения (второй раунд)
|
||||
|
||||
### Уточнение 1: `map` с `sub_params` → тоже `dataDescriptor`
|
||||
|
||||
Правило: **если у параметра есть `sub_params` — генерировать `dataDescriptor`**, независимо от того `map` это или `map-fixed`.
|
||||
|
||||
| `data_type` | `sub_params` | Действие |
|
||||
|---|---|---|
|
||||
| `map-fixed` | есть | генерировать `dataDescriptor` |
|
||||
| `map` | есть | генерировать `dataDescriptor` |
|
||||
| `array-map-fixed` | есть | генерировать `dataDescriptor` |
|
||||
| любой | нет | `dataDescriptor: null` |
|
||||
|
||||
### Уточнение 2: `state.out` после create — пустые `{}`
|
||||
|
||||
Достаточно пустых `{}` из `state_out_template`. STANDS YAML не описывает runtime-дефолты (`pgadmin`, `mydb`) — это эффект реального Terraform, не мок.
|
||||
|
||||
### Уточнение 3: расположение тестов
|
||||
|
||||
```
|
||||
app-autotest/
|
||||
tests/
|
||||
conftest.py # + fixture polygon_server (subprocess)
|
||||
test_polygon_integration.py # интеграционные тесты
|
||||
|
||||
polygon/
|
||||
tests/
|
||||
test_converter.py # юнит: from_stands.py
|
||||
test_state_machine.py # юнит: apply_effect
|
||||
```
|
||||
|
||||
`polygon_server` fixture: subprocess → poll `/health` (timeout 5s) → после тестов `terminate()`.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Ссылки: генератор YAML (tf_provider)
|
||||
|
||||
Репозиторий Terraform-провайдера Nubes: `~/tf_provider`
|
||||
|
||||
## Как генерируются STANDS YAML
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| `~/tf_provider/README.md` | Общий пайплайн: 4 шага генерации провайдера |
|
||||
| `~/tf_provider/docs/README.md` | **Главный индекс документации.** Формат токенов (не протухают), DDoS-Guard заголовки, Gateway URL |
|
||||
| `~/tf_provider/docs/HOWTO_ADD_NEW_SERVICE.md` | Как добавить сервис: `services_list.txt` → `01_generate_yamls.sh` → API → YAML |
|
||||
| `~/tf_provider/docs/ARCHITECTURE_NEW.md` | Архитектура: слои (core/provider/resources_gen/yaml), метод Виталия без instanceUid |
|
||||
| `~/tf_provider/docs/70_api/api-discovery-algorithms.md` | **Ключевой документ.** Алгоритм получения параметров: `/instanceOperations/default/{id}` → `cfsParams` с `dataDescriptor`, `valueList`, `isModifiable` |
|
||||
|
||||
## Критические API-эндпоинты (метод Виталия)
|
||||
|
||||
Цепочка из 3 запросов — получение ВСЕХ параметров сервиса без создания инстанса:
|
||||
|
||||
1. `GET /api/v1/svc/services` → найти `svcId` по имени
|
||||
2. `GET /api/v1/svc/services/{svcId}` → найти `svcOperationId` операции (create: `{svc.operations[?operation=="create"].svcOperationId}`)
|
||||
3. `GET /api/v1/svc/instanceOperations/default/{svcOperationId}` → **все cfsParams**: `dataDescriptor` (подполя map-fixed), `valueList`, `isModifiable`, `isSensitive`, `regex`, ...
|
||||
|
||||
### Почему `/instanceOperations/default/{id}` а не `/serviceOperation/{id}`:
|
||||
|
||||
| Данные | `/serviceOperation/{id}` | `/instanceOperations/default/{id}` |
|
||||
|--------|--------------------------|-------------------------------------|
|
||||
| ID, code, тип, isRequired | ✅ | ✅ |
|
||||
| `valueList` (допустимые значения) | ❌ | ✅ |
|
||||
| `dataDescriptor` (подполя map-fixed) | ❌ | ✅ |
|
||||
| `isModifiable` | ❌ | ✅ |
|
||||
| `regex`, `maxLength`, `minValue`... | ❌ | ✅ |
|
||||
| `isSensitive` | ❌ | ✅ |
|
||||
|
||||
### Формат valueList:
|
||||
|
||||
- Для **верхнеуровневых** параметров: JSON-массив `["false", "true"]`
|
||||
- Для **подполей** dataDescriptor: comma-separated строка `"1,3,5,7"`
|
||||
|
||||
## Где взять реальные ответы API (для сверки мока)
|
||||
|
||||
```bash
|
||||
# Токены (НЕ протухают до декабря 2026):
|
||||
~/tf_provider/secrets/test.token
|
||||
~/tf_provider/secrets/dev.token
|
||||
~/tf_provider/secrets/prod.token
|
||||
|
||||
# Проверить дату токена:
|
||||
python3 -c "
|
||||
import json,base64
|
||||
t=open('$HOME/tf_provider/secrets/test.token').read().split('.')
|
||||
d=json.loads(base64.urlsafe_b64decode(t[1]+'=='))
|
||||
from datetime import datetime,timezone
|
||||
print(datetime.fromtimestamp(d['exp'],tz=timezone.utc))
|
||||
"
|
||||
|
||||
# Пример curl (test стенд):
|
||||
curl -s --max-time 10 \
|
||||
-H "Authorization: Bearer $(cat ~/tf_provider/secrets/test.token)" \
|
||||
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
|
||||
-H "Referer: https://deck-test.ngcloud.ru/" \
|
||||
"https://lk-api-gateway-test.ngcloud.ru/api/v1/svc/instanceOperations/default/<opId>"
|
||||
```
|
||||
|
||||
### ⛔ DDoS-Guard: ВСЕГДА нужны 3 заголовка
|
||||
|
||||
**Без них — 403 Forbidden даже с валидным токеном.**
|
||||
|
||||
```bash
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"
|
||||
-H "Referer: https://deck-{stand}.ngcloud.ru/"
|
||||
```
|
||||
|
||||
### URL стендов (Gateway)
|
||||
|
||||
| Стенд | Gateway URL | Referer |
|
||||
|-------|-------------|---------|
|
||||
| test | `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc` | `https://deck-test.ngcloud.ru/` |
|
||||
| dev | `https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc` | `https://deck-dev.ngcloud.ru/` |
|
||||
| prod | `https://lk-api-gateway.ngcloud.ru/api/v1/svc` | `https://deck.ngcloud.ru/` |
|
||||
|
||||
## Сгенерированные YAML (все стенды)
|
||||
|
||||
| Стенд | Сервисов | Путь |
|
||||
|-------|----------|------|
|
||||
| dev | 50 | `~/tf_provider/generated/dev/resources_yaml/` |
|
||||
| test | 48 | `~/tf_provider/generated/test/resources_yaml/` |
|
||||
| prod | 46 | `~/tf_provider/generated/prod/resources_yaml/` |
|
||||
|
||||
44 сервиса идентичны на всех трёх стендах.
|
||||
|
||||
## Поток данных: API → polygon
|
||||
|
||||
```
|
||||
API Nubes (реальный)
|
||||
→ 01_generate_yamls.sh
|
||||
→ ~/tf_provider/generated/test/resources_yaml/*.yaml
|
||||
→ копируются в STANDS/test/resources_yaml/ (autotest)
|
||||
→ from_stands.py конвертирует
|
||||
→ polygon/site/services/*.yaml
|
||||
→ config_loader.py загружает при старте
|
||||
→ polygon эмулирует /instanceOperations/default/{id}
|
||||
```
|
||||
Reference in New Issue
Block a user