doc: ARCHITECTURE.md — полное переописание (v1.2.43, 13 разделов)
This commit is contained in:
+306
-188
@@ -1,27 +1,36 @@
|
|||||||
# Архитектура app-autotest — текущее состояние
|
# Архитектура app-autotest — полное описание
|
||||||
|
|
||||||
v1.0.93, 28.07.2026
|
v1.2.43, 04.08.2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Обзор
|
## 1. Обзор
|
||||||
|
|
||||||
Flask-приложение (один HTML-файл, без SPA-фреймворка) для ручного тестирования операций
|
Flask 3.1 + vanilla JS + PostgreSQL (psycopg2). Веб-приложение для автоматического
|
||||||
сервисов облачной платформы Nubes. Работает через REST API Nubes, автоопределяет стенд
|
тестирования сервисов облачной платформы Nubes через REST API.
|
||||||
(dev/test) по JWT-токену.
|
|
||||||
|
|
||||||
**Деплой**: Nubes pythonk8s (gunicorn, несколько воркеров).
|
**Два режима работы:**
|
||||||
**Кластер**: `iot-naeel` (K8s resourceRealm, DEV-стенд)
|
- Эмуляция — всё в Polygon (мок-сервер, без реальной инфраструктуры)
|
||||||
**URL**: `https://atest.pythonk8s.dev.nubes.ru`
|
- Облако — реальный Nubes API (dev/test, автоопределение по JWT)
|
||||||
**Репозиторий**: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git`
|
|
||||||
|
**Ручной режим:** сервис -> операция (create/modify/delete/suspend/resume/redeploy) ->
|
||||||
|
параметры -> запуск -> поллинг.
|
||||||
|
|
||||||
|
**Сценарный режим:** последовательность шагов с передачей контекста, редактор через
|
||||||
|
модальный UI, хранение в БД.
|
||||||
|
|
||||||
|
**Деплой:** Nubes pythonk8s managed service, gunicorn --workers 2.
|
||||||
|
**URL:** https://atest.pythonk8s.dev.nubes.ru
|
||||||
|
**Полигон:** https://polygon.pythonk8s.dev.nubes.ru
|
||||||
|
**Репозиторий:** https://gitea.services.ngcloud.ru/forcloud/app-autotest.git (master)
|
||||||
|
|
||||||
### Ключевые принципы
|
### Ключевые принципы
|
||||||
|
|
||||||
1. **Источник правды — Nubes API**. Все данные (инстансы, параметры, статусы) получаются
|
1. Единый HttpClient — auth.py:get_client(), одна точка для обоих режимов.
|
||||||
только из API. Никаких локальных копий или кеша (кроме краткосрочного трекера).
|
2. Изоляция по (client_id, stand) — все таблицы БД, трекер, сценарии.
|
||||||
2. **Разделение CREATE и non-CREATE**. Это два принципиально разных потока.
|
3. Cloud-first + tracker-fallback — инстансы сначала из API, трекер для только что созданных.
|
||||||
3. **Multi-worker safety**. Все общие ресурсы (/tmp файлы) защищены `fcntl.flock`.
|
4. Multi-worker safety — /tmp файлы защищены fcntl.flock, _op_results под threading.Lock.
|
||||||
4. **Мелкие функции**. Каждое действие — отдельная функция.
|
5. Защита от XSS — _esc() на всех данных из API перед innerHTML.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -29,27 +38,59 @@ Flask-приложение (один HTML-файл, без SPA-фреймвор
|
|||||||
|
|
||||||
```
|
```
|
||||||
app-autotest/
|
app-autotest/
|
||||||
├── site/ ← НЕ пакет (без __init__.py)
|
├── requirements.txt
|
||||||
│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health
|
├── site/
|
||||||
│ ├── runner.py ← [LEGACY] Раннер тестов
|
│ ├── app.py # Точка входа: VERSION, blueprints, /health
|
||||||
│ ├── config.yaml ← [LEGACY] Конфиг раннера
|
|
||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ │ └── http_client.py ← HttpClient, автостенд
|
│ │ ├── http_client.py # HttpClient, detect_endpoint, stand_name
|
||||||
|
│ │ ├── auth.py # get_client(), get_mode(), get_stand(), токены, cookie
|
||||||
|
│ │ └── utils.py # find_uid(), uid_from_location()
|
||||||
│ ├── operations/
|
│ ├── operations/
|
||||||
│ │ ├── get_services.py ← get_services(), get_service_detail()
|
│ │ ├── executor.py # ЕДИНЫЙ запуск: create->params->run
|
||||||
│ │ ├── get_instances.py ← get_instances() (пагинация), get_organization()
|
│ │ ├── poll.py # poll_until_done()
|
||||||
│ │ ├── get_params.py ← get_params_with_current_values() — слияние state.params + шаблон
|
│ │ ├── scenario.py # run_scenario() — шаги с резолвингом
|
||||||
│ │ └── tracker.py ← /tmp/instances-{clientId}-{stand}.json + flock
|
│ │ ├── terraform.py # send_params_terraform() — нормализация
|
||||||
|
│ │ ├── tracker.py # JSON-файловый кеш /tmp/instances-*.json (flock)
|
||||||
|
│ │ ├── get_instances.py # GET /instances с пагинацией
|
||||||
|
│ │ ├── get_params.py # Параметры с ТЕКУЩИМИ значениями из state.params
|
||||||
|
│ │ ├── get_services.py # GET /services
|
||||||
|
│ │ └── service_list.py # services_{stand}.txt фильтр
|
||||||
|
│ ├── db/
|
||||||
|
│ │ ├── pool.py # ThreadedConnectionPool(1,5)
|
||||||
|
│ │ ├── init_db.py # CREATE TABLE + миграции + idx_one_running
|
||||||
|
│ │ ├── save_run.py # Ручной UPSERT (UPDATE -> INSERT)
|
||||||
|
│ │ └── scenario_defs.py # CRUD + lock_check
|
||||||
│ ├── routes/
|
│ ├── routes/
|
||||||
│ │ ├── main.py ← GET/POST /, /api/operations/<svc_id>
|
│ │ ├── main.py # GET/POST /, set_mode, api_operations
|
||||||
│ │ ├── api.py ← [LEGACY] /api/run, /api/status, /api/config
|
│ │ ├── api_test.py # /api/test, /api/params, /api/log, _finish_op
|
||||||
│ │ └── api_test.py ← /api/test, /api/params, /api/log
|
│ │ ├── api_scenario_run.py # /api/scenario/run, /api/scenario/status
|
||||||
│ ├── templates/
|
│ │ ├── api_scenario_defs.py# CRUD /api/scenario/definitions
|
||||||
│ │ └── index.html ← Весь UI (Jinja2 + ванильный JS)
|
│ │ ├── api_scenario.py # [ДУБЛИКАТ] Не зарегистрирован
|
||||||
│ └── static/
|
│ │ └── api.py # [LEGACY] /api/run
|
||||||
│ ├── style.css
|
│ ├── static/
|
||||||
│ ├── logo.svg
|
│ │ ├── app.js # Инициализация: startLogPoll(), selectService()
|
||||||
│ └── favicon.svg
|
│ │ ├── style.css # Дизайн-система Nubes
|
||||||
|
│ │ └── js/
|
||||||
|
│ │ ├── utils.js # _esc(), validateJson(), logPoll, relativeTime()
|
||||||
|
│ │ ├── views.js # switchView()
|
||||||
|
│ │ ├── instances.js # selectService(), toggleInstance()
|
||||||
|
│ │ ├── operations.js # startCreate(), runOp(), showParams()
|
||||||
|
│ │ ├── params-render.js# renderParamRow(), collectParams()
|
||||||
|
│ │ ├── history.js # toggleHistory(), loadHistory()
|
||||||
|
│ │ ├── scenario-list.js# toggleScenario(), runScenario(), поллинг
|
||||||
|
│ │ ├── scenario-form.js# showScenarioEditor()
|
||||||
|
│ │ ├── scenario-create.js, scenario-edit.js, scenario-delete.js
|
||||||
|
│ │ ├── icons.js # SVG-иконки
|
||||||
|
│ │ └── snackbar.js # showSnackbar()
|
||||||
|
│ └── templates/
|
||||||
|
│ └── index.html # Весь UI: сайдбар, виды, лог-панель
|
||||||
|
├── tests/
|
||||||
|
│ ├── conftest.py # app_client, polygon_server фикстуры
|
||||||
|
│ ├── test_api_scenario_run.py# 3 теста: 503, 409, unique violation
|
||||||
|
│ ├── test_db_scenario_defs.py# 2 теста: lock_check None
|
||||||
|
│ ├── test_polygon_integration.py# 15 интеграционных тестов
|
||||||
|
│ ├── test_static_regressions.py# 2 статических теста
|
||||||
|
│ └── README.md
|
||||||
└── secrets/
|
└── secrets/
|
||||||
├── dev.token
|
├── dev.token
|
||||||
└── test.token
|
└── test.token
|
||||||
@@ -57,216 +98,293 @@ app-autotest/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Бэкенд: HTTP-клиент и автостенд
|
## 3. Система режимов — auth.py
|
||||||
|
|
||||||
### `http_client.py`
|
Центральный модуль. Все роуты получают HttpClient ТОЛЬКО через функции этого модуля.
|
||||||
|
|
||||||
- `HttpClient(endpoint, token)` — обёртка над `requests.Session`:
|
### Переменные окружения
|
||||||
- Заголовки: `Authorization: Bearer`, `User-Agent: Mozilla/5.0` (DDoS-Guard)
|
|
||||||
- `get(path)` → `raise_for_status()` → `.json()`
|
|
||||||
- `post(path, data)` → проверка `r.ok`, парсинг JSON, извлечение `Location`
|
|
||||||
- `detect_endpoint(token)` — пробует dev→test стенды, возвращает рабочий URL
|
|
||||||
- `stand_name(endpoint)` — "dev"/"test" по URL
|
|
||||||
- `create_client(token, fallback)` — HttpClient + endpoint
|
|
||||||
|
|
||||||
### `get_instances.py`
|
| Переменная | По умолчанию | Назначение |
|
||||||
|
|-----------|-------------|-----------|
|
||||||
|
| NUBES_API_ENDPOINT | lk-api-gateway-test.../api/v1/svc | Реальный API |
|
||||||
|
| NUBES_API_TOKEN | (пусто) | Сервисный JWT-токен |
|
||||||
|
| POLYGON_ENDPOINT | (пусто) | URL полигона. Если задан -> селектор режима |
|
||||||
|
|
||||||
- `get_instances(client)` — `GET /instances` с пагинацией (pageSize=200). Остановка по `len(batch) < pageSize`.
|
### Cookie
|
||||||
- `get_organization(client)` — первый инстанс с `serviceId == 19`
|
|
||||||
|
|
||||||
### `get_services.py`
|
| Cookie | По умолчанию | Допустимые значения |
|
||||||
|
|--------|-------------|-------------------|
|
||||||
|
| mode | "polygon" | "polygon", "cloud" |
|
||||||
|
| polygon_stand | "test" | "dev", "test", "prod" |
|
||||||
|
| token | (пусто) | JWT пользователя |
|
||||||
|
|
||||||
- `get_services(client)` → `GET /services` → список
|
Все cookie: httponly=True, samesite=Strict, secure=True, max_age=1 год.
|
||||||
- `get_service_detail(client, svc_id)` → `GET /services/{id}` → детали + операции
|
|
||||||
|
### Функции
|
||||||
|
|
||||||
|
get_mode() -> "polygon" | "cloud"
|
||||||
|
Без POLYGON_ENDPOINT -> всегда "cloud"
|
||||||
|
Из cookie, whitelist-валидация
|
||||||
|
|
||||||
|
get_polygon_stand() -> "dev" | "test" | "prod"
|
||||||
|
Из cookie, whitelist-валидация
|
||||||
|
|
||||||
|
get_token() -> JWT-строка
|
||||||
|
Эмуляция: только NUBES_API_TOKEN (env)
|
||||||
|
Облако: cookie "token" -> NUBES_API_TOKEN
|
||||||
|
|
||||||
|
_cached_detect(token) -> URL эндпоинта (кеш в flask.g)
|
||||||
|
Вызывает detect_endpoint() только при первом обращении за запрос
|
||||||
|
Устраняет двойной HTTP-запрос (get_client + get_stand)
|
||||||
|
|
||||||
|
get_client() -> HttpClient
|
||||||
|
Эмуляция: polygon_url.replace("/api/v1/svc", "/{stand}/api/v1/svc")
|
||||||
|
Облако: _cached_detect(token)
|
||||||
|
|
||||||
|
get_stand() -> "polygon_test" | "dev" | "test" | "mock"
|
||||||
|
Эмуляция: "polygon_" + get_polygon_stand()
|
||||||
|
Облако: stand_name(_cached_detect(token))
|
||||||
|
|
||||||
|
### URL полигона
|
||||||
|
|
||||||
|
POLYGON_ENDPOINT = https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc
|
||||||
|
|
||||||
|
Стенд ВСТАВЛЯЕТСЯ перед /api/v1/svc:
|
||||||
|
DEV: https://polygon.../dev/api/v1/svc
|
||||||
|
TEST: https://polygon.../test/api/v1/svc
|
||||||
|
PROD: https://polygon.../prod/api/v1/svc
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Режимы работы и разделы БД (v1.2.43+)
|
## 4. HTTP-клиент — http_client.py
|
||||||
|
|
||||||
### Режимы
|
HttpClient — обёртка над requests.Session:
|
||||||
|
- Заголовки: Authorization: Bearer, User-Agent: Mozilla/5.0 (DDoS-Guard)
|
||||||
|
- get(path) -> raise_for_status() -> .json(), таймаут 10с
|
||||||
|
- post(path, data) -> r.ok, JSON, Location -> UUID, таймаут 30с
|
||||||
|
- raw_delete(url) -> DELETE без авторизации (CMDB)
|
||||||
|
|
||||||
| Режим | Cookie | Сервисы | Инстансы | Стенд |
|
STANDS = [dev, test URL]
|
||||||
|--------|--------|---------|----------|-------|
|
detect_endpoint(token) — пробует dev->test, returns URL where results != None
|
||||||
| Эмуляция | `mode=polygon` | Полигон | Полигон | `polygon_{dev\|test\|prod}` |
|
stand_name(endpoint) — ТОЧНОЕ сравнение URL -> "dev"/"test"/"?"
|
||||||
| Облако | `mode=cloud` | Реальный API | Реальный API | `dev` / `test` (авто по токену) |
|
|
||||||
|
|
||||||
### Разделы в БД (колонка `stand`)
|
---
|
||||||
|
|
||||||
Все таблицы (`runs`, `scenario_runs`, `scenario_defs`) изолированы по `(client_id, stand)`.
|
## 5. Режимы работы и разделы БД
|
||||||
|
|
||||||
**Облако (2 стенда):**
|
| Режим | Сервисы | Инстансы | Стенд в БД |
|
||||||
- `dev` — DEV-стенд (`lk-api-gateway-dev.ngcloud.ru`)
|
|--------|---------|----------|-----------|
|
||||||
- `test` — TEST-стенд (`lk-api-gateway-test.ngcloud.ru`)
|
| Эмуляция | Полигон | Полигон | polygon_{dev|test|prod} |
|
||||||
|
| Облако | Реальный API | Реальный API | dev / test |
|
||||||
|
|
||||||
**Эмуляция (3 стенда):**
|
Облако (2 стенда): dev, test
|
||||||
- `polygon_dev` — Polygon DEV (37 сервисов)
|
Эмуляция (3 стенда): polygon_dev, polygon_test, polygon_prod
|
||||||
- `polygon_test` — Polygon TEST (37 сервисов)
|
PROD в облаке недоступен — только через эмуляцию.
|
||||||
- `polygon_prod` — Polygon PROD (35 сервисов)
|
|
||||||
|
|
||||||
PROD в облаке недоступен (нет lk-api-gateway-prod), только через эмуляцию.
|
|
||||||
|
|
||||||
### Сервис-листы
|
### Сервис-листы
|
||||||
|
|
||||||
`load_service_ids(stand)` обрезает префикс `polygon_`:
|
load_service_ids(stand) обрезает префикс polygon_:
|
||||||
- `polygon_test` → `test` → `services_test.txt`
|
polygon_test -> test -> services_test.txt
|
||||||
- `polygon_dev` → `dev` → `services_dev.txt`
|
polygon_dev -> dev -> services_dev.txt
|
||||||
- `polygon_prod` → `prod` → `services_prod.txt` (не существует → все сервисы)
|
polygon_prod -> prod -> services_prod.txt (не existe -> пустой set -> все сервисы)
|
||||||
|
|
||||||
### Полигон URL
|
В шаблоне: если список пуст — показываются все сервисы.
|
||||||
|
|
||||||
`POLYGON_ENDPOINT = https://polygon.pythonk8s.dev.nubes.ru/api/v1/svc`
|
|
||||||
|
|
||||||
Стенд вставляется перед `/api/v1/svc`:
|
|
||||||
- DEV: `https://polygon.pythonk8s.dev.nubes.ru/dev/api/v1/svc`
|
|
||||||
- TEST: `https://polygon.pythonk8s.dev.nubes.ru/test/api/v1/svc`
|
|
||||||
- PROD: `https://polygon.pythonk8s.dev.nubes.ru/prod/api/v1/svc`
|
|
||||||
|
|
||||||
### `get_params.py` — КЛЮЧЕВОЙ модуль
|
|
||||||
|
|
||||||
`get_params_with_current_values(client, op_id, instance_uid)`:
|
|
||||||
1. `GET /instances/{uid}` → `state.params` (текущие значения: `{"whereFail":"1",...}`)
|
|
||||||
2. `GET /instanceOperations/default/{opId}` → шаблон (коды, типы, valueList, dataDescriptor)
|
|
||||||
3. Слияние: `defaultValue = state.params["код"] ?? template.defaultValue`
|
|
||||||
4. Приведение типов: `bool` → `"true"/"false"`, `dict` → `json.dumps()`
|
|
||||||
|
|
||||||
### `tracker.py`
|
|
||||||
|
|
||||||
- Файл: `/tmp/instances-{clientId}-{stand}.json` (изолирован по пользователю и стенду)
|
|
||||||
- Блокировка: `fcntl.flock(LOCK_EX | LOCK_NB)` + retry 2s
|
|
||||||
- `add(client_id, stand, uid, svc_id, name)`, `remove(...)`, `list_all(...)`
|
|
||||||
- Нужен ТОЛЬКО как fallback — когда инстанс только создан и ещё не в `/instances`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Бэкенд: Роуты
|
## 6. Роуты
|
||||||
|
|
||||||
### `main.py`
|
### main.py
|
||||||
|
|
||||||
- **`GET/POST /`** — главная: организация, инфраструктура (serviceId 2,12,19,21,22,25,26,29,110,150), сервисы
|
GET/POST / — главная страница
|
||||||
- `action=save` → сохранить токен в cookie
|
action=save — сохранить токен в cookie
|
||||||
- `action=clear` → удалить cookie
|
action=clear — удалить cookie
|
||||||
- **`GET /api/operations/<svc_id>`** — операции + autotest-инстансы:
|
action=set_mode — сохранить mode + polygon_stand в cookie
|
||||||
- Cloud-first: фильтр по префиксу `autotest-`
|
polygon_stand: из формы, иначе из cookie (Облако->Эмуляция)
|
||||||
- Tracker-fallback: статус "creating" для ещё невидимых
|
Валидация: mode in {polygon,cloud}, stand in {dev,test,prod}
|
||||||
|
Загрузка: mode=="polygon" or active_token
|
||||||
|
Единый client = get_client()
|
||||||
|
|
||||||
### `api_test.py` — основной модуль операций
|
GET /api/operations/<svc_id> — операции + autotest-инстансы
|
||||||
|
Cloud-first + tracker-fallback, дедупликация
|
||||||
|
|
||||||
- **`GET /api/params/<op_id>[?instanceUid=xxx]`**:
|
### api_test.py — ручной режим
|
||||||
- Без `instanceUid` → шаблонные `defaultValue`
|
|
||||||
- С `instanceUid` → `get_params_with_current_values()`
|
|
||||||
- **`POST /api/test`** — запуск операции:
|
|
||||||
- **CREATE**: `POST /instances` → `instanceUid` → `POST /instanceOperations` → `opUid` → `tracker_add` → params → `/run`
|
|
||||||
- **non-CREATE**: `POST /instanceOperations` → params → `/run` (redeploy: сразу `/run`)
|
|
||||||
- `displayName` = `_get_instance_display_name()` (из API, не UUID!)
|
|
||||||
- Фоновый поток: `_finish_op()` — поллинг до `dtFinish` (300s таймаут)
|
|
||||||
- **`GET /api/test/status/<op_uid>`** — поллинг: in-memory `_op_results` → прямой API
|
|
||||||
- **`GET /api/log`** — последние 200 строк из `/tmp/app-autotest.log`
|
|
||||||
|
|
||||||
### Логирование
|
GET /api/services — список сервисов
|
||||||
|
GET /api/instances/list — все инстансы
|
||||||
|
GET /api/params/<op_id>[?instanceUid=xxx] — параметры (шаблон или текущие)
|
||||||
|
POST /api/test — запуск операции, возвращает opUid
|
||||||
|
Валидация: serviceId(int>0), operation, svcOperationId(int>0), UUID(36), params(dict)
|
||||||
|
CREATE: _unique_display_name() + execute_operation()
|
||||||
|
non-CREATE: execute_operation()
|
||||||
|
delete: CMDB -> fallback API delete
|
||||||
|
Фон: _finish_op() — поллинг + save_run + tracker_remove
|
||||||
|
GET /api/test/status/<op_uid> — статус (_op_results -> API)
|
||||||
|
GET /api/log — последние 200 строк из /tmp/app-autotest.log
|
||||||
|
GET /api/history — последние 50 записей из runs (client_id + stand)
|
||||||
|
|
||||||
`_log(msg)` в `api_test.py`:
|
### api_scenario_run.py — сценарии
|
||||||
- stdout (gunicorn) + `/tmp/app-autotest.log` (flock, общий для воркеров)
|
|
||||||
- Ротация при 512 КБ
|
GET /api/scenarios — список определений
|
||||||
- UI: скрытая панель, кнопка `log` (правый нижний угол)
|
POST /api/scenario/run — запуск
|
||||||
|
lock_check -> None(503), False(409)
|
||||||
|
INSERT scenario_runs status=RUNNING (partial unique index)
|
||||||
|
UniqueViolation(pgcode 23505) -> 409
|
||||||
|
Фон: run_scenario(client, steps, cid, stand, ...)
|
||||||
|
GET /api/scenario/run/<run_id> — статус
|
||||||
|
GET /api/scenario/status — последние 10 запусков
|
||||||
|
|
||||||
|
### api_scenario_defs.py — CRUD
|
||||||
|
|
||||||
|
GET/POST /api/scenario/definitions — список/создать
|
||||||
|
GET/PUT/DELETE /api/scenario/definitions/<id> — один/обновить/удалить
|
||||||
|
PUT: оптимистичная блокировка (version -> 409)
|
||||||
|
DELETE: мягкое (is_active=FALSE)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Фронтенд
|
## 7. Операции
|
||||||
|
|
||||||
### Структура
|
### executor.py
|
||||||
|
|
||||||
Один HTML-файл, три колонки:
|
execute_operation() — единый запуск:
|
||||||
- Левая (280px): инфраструктура
|
1. CREATE: POST /instances -> instanceUid -> tracker_add -> POST /instanceOperations -> opUid
|
||||||
- Средняя (240px): сервисы
|
2. non-CREATE: POST /instanceOperations -> opUid
|
||||||
- Правая (flex): инстансы + параметры + кнопка + этапы
|
3. send_params_terraform() — нормализация, refSvcId, validate-cfs
|
||||||
|
4. POST /run -> запуск
|
||||||
|
|
||||||
### Глобальное состояние (JS)
|
### scenario.py
|
||||||
|
|
||||||
```
|
run_scenario() — для каждого шага:
|
||||||
svcInstances — кеш инстансов (из /api/operations)
|
1. get_service_detail() -> svcOperationId
|
||||||
selectedInst — UID выбранного инстанса
|
2. GET /instanceOperations/default/{id} -> резолв параметров
|
||||||
selectedOp — {opId, opName, svcId}
|
3. Резолвинг instance_uid: явный uid > instance_ref > service_id
|
||||||
pollTimer — таймер поллинга
|
4. execute_operation()
|
||||||
SVC_ID = 1 — фиксированный сервис (Болванка)
|
5. save_run(RUNNING)
|
||||||
```
|
6. poll_until_done()
|
||||||
|
7. save_run(final) — UPDATE существующей строки
|
||||||
|
8. tracker_remove() после успешного delete
|
||||||
|
9. _save_scenario_run() — прогресс
|
||||||
|
|
||||||
### Потоки операций
|
### tracker.py
|
||||||
|
|
||||||
**CREATE**: `startCreate()` → `showParams(18,'create')` → форма → `executeOp(pp)` → POST /api/test → поллинг → refreshInstances
|
JSON-файловый кеш: /tmp/instances-{clientId}-{stand}.json
|
||||||
|
fcntl.flock(LOCK_EX|LOCK_NB) + retry 2s
|
||||||
**MODIFY**: `toggleInstance()` → кнопки → `runOp('modify',opId)` → `showParams(opId,'modify')` → GET /api/params?instanceUid= → форма с ТЕКУЩИМИ значениями → валидация map → `executeOp(pp)` → POST /api/test → поллинг
|
add/remove/list_all — атомарные операции
|
||||||
|
|
||||||
**Без параметров** (suspend/delete/resume/redeploy): `runOp()` → confirm → `executeOp({})` → POST /api/test → поллинг
|
|
||||||
|
|
||||||
### Валидация JSON
|
|
||||||
|
|
||||||
- `_esc(s)` — HTML-экранирование (`"` → `"`, `&` → `&`, `<` → `<`)
|
|
||||||
- `validateJson(el, quiet)` — `JSON.parse()` на `onblur` (красная рамка + текст)
|
|
||||||
- Batch-проверка перед отправкой — ошибка → запрос не уходит
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Nubes API — используемые endpoint'ы
|
## 8. База данных
|
||||||
|
|
||||||
| Метод | Путь | Назначение |
|
### pool.py
|
||||||
|-------|------|-----------|
|
ThreadedConnectionPool(1,5), ленивый init в каждом воркере
|
||||||
| GET | `/instances?pageSize=200&page=N` | Все инстансы (пагинация) |
|
_ensure_schema() -> init_db() при первом обращении
|
||||||
| GET | `/instances/{uid}` | Детали + state.params |
|
|
||||||
| GET | `/services` | Список сервисов |
|
### Таблицы
|
||||||
| GET | `/services/{id}` | Детали + операции |
|
|
||||||
| GET | `/instanceOperations/default/{opId}` | Шаблон параметров |
|
runs — история операций:
|
||||||
| POST | `/instances` | Создать инстанс |
|
id, created_at, client_id, stand, user_email, svc_id, svc_name,
|
||||||
| POST | `/instanceOperations` | Создать операцию |
|
op_name, svc_op_id, op_uid, instance_uid, display_name, status,
|
||||||
| POST | `/instanceOperationCfsParams` | Установить параметр |
|
duration_sec, error_log, params(JSONB), stages(JSONB), app_version,
|
||||||
| POST | `/instanceOperations/{opUid}/run` | Запустить операцию |
|
scenario_run_id, step_number, instance_meta(JSONB)
|
||||||
| GET | `/instanceOperations/{opUid}?fields=...` | Поллинг статуса |
|
Индексы: client_stand, instance, created, op_uid(UNIQUE WHERE NOT NULL)
|
||||||
|
|
||||||
|
scenario_runs — запуски сценариев:
|
||||||
|
id, client_id, stand, scenario_name, status, current_step, total_steps,
|
||||||
|
instance_bindings(JSONB), duration_sec, error_log
|
||||||
|
Partial UNIQUE INDEX: (client_id, stand) WHERE status='RUNNING'
|
||||||
|
|
||||||
|
scenario_definitions — определения:
|
||||||
|
id, client_id, stand, name, steps(JSONB), version, is_active
|
||||||
|
UNIQUE: (client_id, stand, LOWER(name))
|
||||||
|
Seed: client_id="" AND stand="" — видны всем
|
||||||
|
|
||||||
|
### save_run.py
|
||||||
|
|
||||||
|
Ручной UPSERT:
|
||||||
|
1. UPDATE runs SET ... WHERE op_uid = %s
|
||||||
|
2. Если rowcount == 0 -> INSERT
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Ограничения платформы
|
## 9. Фронтенд
|
||||||
|
|
||||||
- `site/` — НЕ пакет (без `__init__.py`), импорты без префикса `site.`
|
Сайдбар (192px, flex column):
|
||||||
- `app.run(host="0.0.0.0", port=5000)`
|
sidebar-nav (flex:0) — кнопки видов
|
||||||
- Gunicorn multi-worker → общие данные через файлы + flock
|
Селектор режима (радиокнопки, только при POLYGON_ENDPOINT)
|
||||||
- `/tmp/` теряется при редеплое
|
sidebar-bottom — версия, email, токен (disabled в эмуляции)
|
||||||
- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard)
|
|
||||||
|
Виды: workbench, overview, history-view, scenarios-view, logs-view
|
||||||
|
|
||||||
|
JS модули (12 файлов):
|
||||||
|
utils.js — _esc(), startLogPoll(), relativeTime()
|
||||||
|
views.js — switchView()
|
||||||
|
instances.js — selectService(), toggleInstance()
|
||||||
|
operations.js — startCreate(), runOp(), executeOp()
|
||||||
|
history.js — toggleHistory(), loadHistory()
|
||||||
|
scenario-list.js — runScenario(), поллинг (5 ошибок -> стоп)
|
||||||
|
scenario-form.js — редактор сценариев
|
||||||
|
|
||||||
|
Защита от XSS:
|
||||||
|
_esc(s) -> &, ", <
|
||||||
|
o.operation: JS-escape + HTML-escape
|
||||||
|
escName: \\, \', "
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Безопасность и конкуренция (аудит GPT-5.3-Codex, 2026-07-31)
|
## 10. Nubes API — эндпоинты
|
||||||
|
|
||||||
Полный аудит 29 файлов (~6000 строк Python + vanilla JS). Исправлено в v1.2.19-v1.2.20.
|
GET /services — список
|
||||||
|
GET /services/{id} — детали + операции
|
||||||
|
GET /instances — все (пагинация)
|
||||||
|
GET /instances/{uid} — детали + state.params
|
||||||
|
GET /instanceOperations/default/{id} — шаблон cfsParams
|
||||||
|
POST /instances — создать -> 201 + Location
|
||||||
|
POST /instanceOperations — создать операцию -> 201 + Location
|
||||||
|
POST /instanceOperationCfsParams — установить параметр
|
||||||
|
GET /.../validate-cfs — валидация
|
||||||
|
POST /.../run — запустить
|
||||||
|
GET /.../{uid}?fields=... — поллинг
|
||||||
|
|
||||||
### Защита от XSS (Frontend)
|
---
|
||||||
|
|
||||||
- **`_esc(s)` в utils.js** — HTML-escape: `&` → `&`, `"` → `"`, `<` → `<`
|
## 11. Безопасность
|
||||||
Применяется ко ВСЕМ данным из API перед `innerHTML`.
|
|
||||||
- **params в scenario-list.js** — `_esc(k)+'='+_esc(v)` (было `k+'='+v` без экранирования).
|
|
||||||
- **JS injection в onclick** — имена сценариев с `'` теперь `replace(/'/g, "\\'")` перед
|
|
||||||
вставкой в JS-строку внутри HTML-атрибута.
|
|
||||||
|
|
||||||
### Защита от гонок (Backend)
|
Гонки:
|
||||||
|
_op_results — threading.Lock(), pop(k,None)
|
||||||
|
idx_one_running — partial unique index (атомарный lock)
|
||||||
|
Tracker — fcntl.flock, атомарное read->mutate->write
|
||||||
|
save_run — ручной UPSERT без индекса
|
||||||
|
|
||||||
- **`_op_results`** — `threading.Lock()` вокруг всех операций чтения/записи/cleanup.
|
Cookie: httponly, samesite=Strict, secure, 1 год
|
||||||
`pop(k, None)` вместо `del dict[k]` — безопасно при конкурентном доступе.
|
|
||||||
- **Partial unique index для сценариев** — `CREATE UNIQUE INDEX idx_one_running
|
|
||||||
ON scenario_runs (client_id, stand) WHERE status = 'RUNNING'`.
|
|
||||||
Делает `INSERT INTO scenario_runs ... status='RUNNING'` атомарной проверкой:
|
|
||||||
вторая параллельная вставка получает unique violation → 409.
|
|
||||||
Это заменило сломанную реализацию на `pg_try_advisory_lock` (v1.2.20),
|
|
||||||
где lock брался на одном соединении, а unlock — на другом (из пула).
|
|
||||||
- **Tracker** — `_atomic_update()`: read→mutate→write под одним `fcntl.flock`.
|
|
||||||
Исключает lost-update между `add()` и `remove()` из разных воркеров gunicorn.
|
|
||||||
|
|
||||||
### Защита от зависания (Frontend polling)
|
Валидация:
|
||||||
|
serviceId: int>0, instanceUid: UUID(36), svcOperationId: int>0
|
||||||
|
params: dict, mode: {polygon,cloud}, stand: {dev,test,prod}
|
||||||
|
|
||||||
- **Счётчик ошибок** в `scenarioPollTimer` — после 5 последовательных ошибок:
|
---
|
||||||
`stopScenarioPoll()` + `busy=false` + сообщение об ошибке.
|
|
||||||
- **Generation token** в `scenario-form.js` — `_renderGen` предотвращает перезапись
|
|
||||||
нового DOM старыми данными от async `loadStepParams()`.
|
|
||||||
|
|
||||||
### Известные ограничения
|
## 12. Тесты
|
||||||
|
|
||||||
- **`_op_results` in-memory на воркер** — не shared между gunicorn-воркерами.
|
22 теста: python3 -m pytest tests/ -v
|
||||||
При отсутствии stickiness статус может читаться из API fallback вместо кеша.
|
|
||||||
Решение (отложено): Redis или общая таблица в БД для статусов операций.
|
test_api_scenario_run.py — 3 теста (503, 409, unique violation)
|
||||||
|
test_db_scenario_defs.py — 2 теста (lock_check None)
|
||||||
|
test_polygon_integration.py — 15 тестов (полный цикл с полигоном)
|
||||||
|
test_static_regressions.py — 2 теста (DDL, escName)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. История версий (v1.2.28 -> v1.2.43)
|
||||||
|
|
||||||
|
1.2.28 — исходная (03.08)
|
||||||
|
1.2.29 — фикс сервисов при POLYGON_ENDPOINT
|
||||||
|
1.2.30 — селектор режима/стенда
|
||||||
|
1.2.31 — фикс URL полигона
|
||||||
|
1.2.35 — sidebar-nav flex:0 + форма по центру
|
||||||
|
1.2.36 — tracker_remove в сценариях + логи
|
||||||
|
1.2.39 — ручной UPSERT
|
||||||
|
1.2.40 — код-ревью #1: _esc() для o.operation
|
||||||
|
1.2.41 — код-ревью #2: кеш detect_endpoint
|
||||||
|
1.2.42 — код-ревью #3: whitelist mode/stand
|
||||||
|
1.2.43 — код-ревью #5,#6,#8,#9,#10: finally, stand_name, индекс, flush
|
||||||
|
|||||||
Reference in New Issue
Block a user