Files
autotest/DOCS/ARCHITECTURE.md
T

391 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура app-autotest — полное описание
v1.2.43, 04.08.2026
---
## 1. Обзор
Flask 3.1 + vanilla JS + PostgreSQL (psycopg2). Веб-приложение для автоматического
тестирования сервисов облачной платформы Nubes через REST API.
**Два режима работы:**
- Эмуляция — всё в 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. Единый 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.
---
## 2. Структура файлов
```
app-autotest/
├── requirements.txt
├── site/
│ ├── app.py # Точка входа: VERSION, blueprints, /health
│ ├── api/
│ │ ├── 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/
│ │ ├── 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 /, 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
```
---
## 3. Система режимов — auth.py
Центральный модуль. Все роуты получают HttpClient ТОЛЬКО через функции этого модуля.
### Переменные окружения
| Переменная | По умолчанию | Назначение |
|-----------|-------------|-----------|
| NUBES_API_ENDPOINT | lk-api-gateway-test.../api/v1/svc | Реальный API |
| NUBES_API_TOKEN | (пусто) | Сервисный JWT-токен |
| POLYGON_ENDPOINT | (пусто) | URL полигона. Если задан -> селектор режима |
### Cookie
| Cookie | По умолчанию | Допустимые значения |
|--------|-------------|-------------------|
| mode | "polygon" | "polygon", "cloud" |
| polygon_stand | "test" | "dev", "test", "prod" |
| token | (пусто) | JWT пользователя |
Все 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. 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)
STANDS = [dev, test URL]
detect_endpoint(token) — пробует dev->test, returns URL where results != None
stand_name(endpoint) — ТОЧНОЕ сравнение URL -> "dev"/"test"/"?"
---
## 5. Режимы работы и разделы БД
| Режим | Сервисы | Инстансы | Стенд в БД |
|--------|---------|----------|-----------|
| Эмуляция | Полигон | Полигон | polygon_{dev|test|prod} |
| Облако | Реальный API | Реальный API | dev / test |
Облако (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 (не existe -> пустой set -> все сервисы)
В шаблоне: если список пуст — показываются все сервисы.
---
## 6. Роуты
### main.py
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()
GET /api/operations/<svc_id> — операции + autotest-инстансы
Cloud-first + tracker-fallback, дедупликация
### api_test.py — ручной режим
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)
### 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/<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)
---
## 7. Операции
### executor.py
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 -> запуск
### scenario.py
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
JSON-файловый кеш: /tmp/instances-{clientId}-{stand}.json
fcntl.flock(LOCK_EX|LOCK_NB) + retry 2s
add/remove/list_all — атомарные операции
---
## 8. База данных
### 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
---
## 9. Фронтенд
Сайдбар (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) -> &amp;, &quot;, &lt;
o.operation: JS-escape + HTML-escape
escName: \\, \', &quot;
---
## 10. Nubes API — эндпоинты
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=... — поллинг
---
## 11. Безопасность
Гонки:
_op_results — threading.Lock(), pop(k,None)
idx_one_running — partial unique index (атомарный lock)
Tracker — fcntl.flock, атомарное read->mutate->write
save_run — ручной UPSERT без индекса
Cookie: httponly, samesite=Strict, secure, 1 год
Валидация:
serviceId: int>0, instanceUid: UUID(36), svcOperationId: int>0
params: dict, mode: {polygon,cloud}, stand: {dev,test,prod}
---
## 12. Тесты
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