From 8d111a05e16c69dc4ba72ba88e98b2fa231e20c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Tue, 4 Aug 2026 08:23:35 +0400 Subject: [PATCH] =?UTF-8?q?doc:=20ARCHITECTURE.md=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=BE=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BE?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20(v1.2.43,=2013=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=BE=D0=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOCS/ARCHITECTURE.md | 494 +++++++++++++++++++++++++++---------------- 1 file changed, 306 insertions(+), 188 deletions(-) diff --git a/DOCS/ARCHITECTURE.md b/DOCS/ARCHITECTURE.md index f5d4f20..b95bdeb 100644 --- a/DOCS/ARCHITECTURE.md +++ b/DOCS/ARCHITECTURE.md @@ -1,27 +1,36 @@ -# Архитектура app-autotest — текущее состояние +# Архитектура app-autotest — полное описание -v1.0.93, 28.07.2026 +v1.2.43, 04.08.2026 --- ## 1. Обзор -Flask-приложение (один HTML-файл, без SPA-фреймворка) для ручного тестирования операций -сервисов облачной платформы Nubes. Работает через REST API Nubes, автоопределяет стенд -(dev/test) по JWT-токену. +Flask 3.1 + vanilla JS + PostgreSQL (psycopg2). Веб-приложение для автоматического +тестирования сервисов облачной платформы Nubes через REST API. -**Деплой**: Nubes pythonk8s (gunicorn, несколько воркеров). -**Кластер**: `iot-naeel` (K8s resourceRealm, DEV-стенд) -**URL**: `https://atest.pythonk8s.dev.nubes.ru` -**Репозиторий**: `https://gitea.services.ngcloud.ru/forcloud/app-autotest.git` +**Два режима работы:** +- Эмуляция — всё в Polygon (мок-сервер, без реальной инфраструктуры) +- Облако — реальный Nubes API (dev/test, автоопределение по JWT) + +**Ручной режим:** сервис -> операция (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**. Все данные (инстансы, параметры, статусы) получаются - только из API. Никаких локальных копий или кеша (кроме краткосрочного трекера). -2. **Разделение CREATE и non-CREATE**. Это два принципиально разных потока. -3. **Multi-worker safety**. Все общие ресурсы (/tmp файлы) защищены `fcntl.flock`. -4. **Мелкие функции**. Каждое действие — отдельная функция. +1. Единый HttpClient — auth.py:get_client(), одна точка для обоих режимов. +2. Изоляция по (client_id, stand) — все таблицы БД, трекер, сценарии. +3. Cloud-first + tracker-fallback — инстансы сначала из API, трекер для только что созданных. +4. Multi-worker safety — /tmp файлы защищены fcntl.flock, _op_results под threading.Lock. +5. Защита от XSS — _esc() на всех данных из API перед innerHTML. --- @@ -29,27 +38,59 @@ Flask-приложение (один HTML-файл, без SPA-фреймвор ``` app-autotest/ -├── site/ ← НЕ пакет (без __init__.py) -│ ├── app.py ← Flask(__name__), VERSION, blueprints, /health -│ ├── runner.py ← [LEGACY] Раннер тестов -│ ├── config.yaml ← [LEGACY] Конфиг раннера +├── requirements.txt +├── site/ +│ ├── app.py # Точка входа: VERSION, blueprints, /health │ ├── 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/ -│ │ ├── get_services.py ← get_services(), get_service_detail() -│ │ ├── get_instances.py ← get_instances() (пагинация), get_organization() -│ │ ├── get_params.py ← get_params_with_current_values() — слияние state.params + шаблон -│ │ └── tracker.py ← /tmp/instances-{clientId}-{stand}.json + flock +│ │ ├── executor.py # ЕДИНЫЙ запуск: create->params->run +│ │ ├── poll.py # poll_until_done() +│ │ ├── scenario.py # run_scenario() — шаги с резолвингом +│ │ ├── 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/ -│ │ ├── main.py ← GET/POST /, /api/operations/ -│ │ ├── api.py ← [LEGACY] /api/run, /api/status, /api/config -│ │ └── api_test.py ← /api/test, /api/params, /api/log -│ ├── templates/ -│ │ └── index.html ← Весь UI (Jinja2 + ванильный JS) -│ └── static/ -│ ├── style.css -│ ├── logo.svg -│ └── favicon.svg +│ │ ├── main.py # GET/POST /, set_mode, api_operations +│ │ ├── api_test.py # /api/test, /api/params, /api/log, _finish_op +│ │ ├── api_scenario_run.py # /api/scenario/run, /api/scenario/status +│ │ ├── api_scenario_defs.py# CRUD /api/scenario/definitions +│ │ ├── api_scenario.py # [ДУБЛИКАТ] Не зарегистрирован +│ │ └── api.py # [LEGACY] /api/run +│ ├── static/ +│ │ ├── app.js # Инициализация: startLogPoll(), selectService() +│ │ ├── 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/ ├── dev.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`. -- `get_organization(client)` — первый инстанс с `serviceId == 19` +### Cookie -### `get_services.py` +| Cookie | По умолчанию | Допустимые значения | +|--------|-------------|-------------------| +| mode | "polygon" | "polygon", "cloud" | +| polygon_stand | "test" | "dev", "test", "prod" | +| token | (пусто) | JWT пользователя | -- `get_services(client)` → `GET /services` → список -- `get_service_detail(client, svc_id)` → `GET /services/{id}` → детали + операции +Все cookie: httponly=True, samesite=Strict, secure=True, max_age=1 год. + +### Функции + +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 | Сервисы | Инстансы | Стенд | -|--------|--------|---------|----------|-------| -| Эмуляция | `mode=polygon` | Полигон | Полигон | `polygon_{dev\|test\|prod}` | -| Облако | `mode=cloud` | Реальный API | Реальный API | `dev` / `test` (авто по токену) | +STANDS = [dev, test URL] +detect_endpoint(token) — пробует dev->test, returns URL where results != None +stand_name(endpoint) — ТОЧНОЕ сравнение URL -> "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 стенда):** -- `polygon_dev` — Polygon DEV (37 сервисов) -- `polygon_test` — Polygon TEST (37 сервисов) -- `polygon_prod` — Polygon PROD (35 сервисов) - -PROD в облаке недоступен (нет lk-api-gateway-prod), только через эмуляцию. +Облако (2 стенда): dev, test +Эмуляция (3 стенда): polygon_dev, polygon_test, polygon_prod +PROD в облаке недоступен — только через эмуляцию. ### Сервис-листы -`load_service_ids(stand)` обрезает префикс `polygon_`: -- `polygon_test` → `test` → `services_test.txt` -- `polygon_dev` → `dev` → `services_dev.txt` -- `polygon_prod` → `prod` → `services_prod.txt` (не существует → все сервисы) +load_service_ids(stand) обрезает префикс polygon_: + polygon_test -> test -> services_test.txt + polygon_dev -> dev -> services_dev.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), сервисы - - `action=save` → сохранить токен в cookie - - `action=clear` → удалить cookie -- **`GET /api/operations/`** — операции + autotest-инстансы: - - Cloud-first: фильтр по префиксу `autotest-` - - Tracker-fallback: статус "creating" для ещё невидимых +GET/POST / — главная страница + action=save — сохранить токен в cookie + action=clear — удалить cookie + action=set_mode — сохранить mode + polygon_stand в cookie + polygon_stand: из формы, иначе из cookie (Облако->Эмуляция) + Валидация: mode in {polygon,cloud}, stand in {dev,test,prod} + Загрузка: mode=="polygon" or active_token + Единый client = get_client() -### `api_test.py` — основной модуль операций +GET /api/operations/ — операции + autotest-инстансы + Cloud-first + tracker-fallback, дедупликация -- **`GET /api/params/[?instanceUid=xxx]`**: - - Без `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/`** — поллинг: in-memory `_op_results` → прямой API -- **`GET /api/log`** — последние 200 строк из `/tmp/app-autotest.log` +### api_test.py — ручной режим -### Логирование +GET /api/services — список сервисов +GET /api/instances/list — все инстансы +GET /api/params/[?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_results -> API) +GET /api/log — последние 200 строк из /tmp/app-autotest.log +GET /api/history — последние 50 записей из runs (client_id + stand) -`_log(msg)` в `api_test.py`: -- stdout (gunicorn) + `/tmp/app-autotest.log` (flock, общий для воркеров) -- Ротация при 512 КБ -- UI: скрытая панель, кнопка `log` (правый нижний угол) +### api_scenario_run.py — сценарии + +GET /api/scenarios — список определений +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/ — статус +GET /api/scenario/status — последние 10 запусков + +### api_scenario_defs.py — CRUD + +GET/POST /api/scenario/definitions — список/создать +GET/PUT/DELETE /api/scenario/definitions/ — один/обновить/удалить + PUT: оптимистичная блокировка (version -> 409) + DELETE: мягкое (is_active=FALSE) --- -## 5. Фронтенд +## 7. Операции -### Структура +### executor.py -Один HTML-файл, три колонки: -- Левая (280px): инфраструктура -- Средняя (240px): сервисы -- Правая (flex): инстансы + параметры + кнопка + этапы +execute_operation() — единый запуск: +1. CREATE: POST /instances -> instanceUid -> tracker_add -> POST /instanceOperations -> opUid +2. non-CREATE: POST /instanceOperations -> opUid +3. send_params_terraform() — нормализация, refSvcId, validate-cfs +4. POST /run -> запуск -### Глобальное состояние (JS) +### scenario.py -``` -svcInstances — кеш инстансов (из /api/operations) -selectedInst — UID выбранного инстанса -selectedOp — {opId, opName, svcId} -pollTimer — таймер поллинга -SVC_ID = 1 — фиксированный сервис (Болванка) -``` +run_scenario() — для каждого шага: +1. get_service_detail() -> svcOperationId +2. GET /instanceOperations/default/{id} -> резолв параметров +3. Резолвинг instance_uid: явный uid > instance_ref > service_id +4. execute_operation() +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 - -**MODIFY**: `toggleInstance()` → кнопки → `runOp('modify',opId)` → `showParams(opId,'modify')` → GET /api/params?instanceUid= → форма с ТЕКУЩИМИ значениями → валидация map → `executeOp(pp)` → POST /api/test → поллинг - -**Без параметров** (suspend/delete/resume/redeploy): `runOp()` → confirm → `executeOp({})` → POST /api/test → поллинг - -### Валидация JSON - -- `_esc(s)` — HTML-экранирование (`"` → `"`, `&` → `&`, `<` → `<`) -- `validateJson(el, quiet)` — `JSON.parse()` на `onblur` (красная рамка + текст) -- Batch-проверка перед отправкой — ошибка → запрос не уходит +JSON-файловый кеш: /tmp/instances-{clientId}-{stand}.json +fcntl.flock(LOCK_EX|LOCK_NB) + retry 2s +add/remove/list_all — атомарные операции --- -## 6. Nubes API — используемые endpoint'ы +## 8. База данных -| Метод | Путь | Назначение | -|-------|------|-----------| -| GET | `/instances?pageSize=200&page=N` | Все инстансы (пагинация) | -| GET | `/instances/{uid}` | Детали + state.params | -| GET | `/services` | Список сервисов | -| GET | `/services/{id}` | Детали + операции | -| GET | `/instanceOperations/default/{opId}` | Шаблон параметров | -| POST | `/instances` | Создать инстанс | -| POST | `/instanceOperations` | Создать операцию | -| POST | `/instanceOperationCfsParams` | Установить параметр | -| POST | `/instanceOperations/{opUid}/run` | Запустить операцию | -| GET | `/instanceOperations/{opUid}?fields=...` | Поллинг статуса | +### pool.py +ThreadedConnectionPool(1,5), ленивый init в каждом воркере +_ensure_schema() -> init_db() при первом обращении + +### Таблицы + +runs — история операций: + id, created_at, client_id, stand, user_email, svc_id, svc_name, + op_name, svc_op_id, op_uid, instance_uid, display_name, status, + duration_sec, error_log, params(JSONB), stages(JSONB), app_version, + scenario_run_id, step_number, instance_meta(JSONB) + Индексы: 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.` -- `app.run(host="0.0.0.0", port=5000)` -- Gunicorn multi-worker → общие данные через файлы + flock -- `/tmp/` теряется при редеплое -- User-Agent: `Mozilla/5.0` обязателен (DDoS-Guard) +Сайдбар (192px, flex column): + sidebar-nav (flex:0) — кнопки видов + Селектор режима (радиокнопки, только при POLYGON_ENDPOINT) + sidebar-bottom — версия, email, токен (disabled в эмуляции) + +Виды: 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: `&` → `&`, `"` → `"`, `<` → `<` - Применяется ко ВСЕМ данным из API перед `innerHTML`. -- **params в scenario-list.js** — `_esc(k)+'='+_esc(v)` (было `k+'='+v` без экранирования). -- **JS injection в onclick** — имена сценариев с `'` теперь `replace(/'/g, "\\'")` перед - вставкой в JS-строку внутри HTML-атрибута. +## 11. Безопасность -### Защита от гонок (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. - `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. +Cookie: httponly, samesite=Strict, secure, 1 год -### Защита от зависания (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-воркерами. - При отсутствии stickiness статус может читаться из API fallback вместо кеша. - Решение (отложено): Redis или общая таблица в БД для статусов операций. +22 теста: python3 -m pytest tests/ -v + +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