Files
autotest/DOCS/ARCHITECTURE.md
T

17 KiB
Raw Blame History

Архитектура 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 По умолчанию Допустимые значения
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
Облако Реальный 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/ — один/обновить/удалить 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) -> &, ", < o.operation: JS-escape + HTML-escape escName: \, ', "


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