- Replace all occurrences of 'ss-tools' with 'superset-tools' in 104 files - Rename git bundle file ss-tools.bundle → superset-tools.bundle - Update .gitignore pattern accordingly - Preserve variable names (hasSsTools etc.) and code identifiers
16 KiB
[DEF:ADR-0012:ADR]
@STATUS ACTIVE
@PURPOSE Зафиксировать архитектурное решение об использовании Testcontainers для запуска Apache Superset в интеграционных тестах. Описать двухконтейнерную архитектуру (init + web), расследование проблем аутентификации, выбор версии 4.1.2, решение проблемы JWT-авторизации через psycopg2 + superset_config.py.
@RELATION DEPENDS_ON -> [ADR-0005-auth-rbac]
@RELATION DEPENDS_ON -> [ADR-0001-module-layout]
@RATIONALE Проект superset-tools интенсивно взаимодействует с Superset REST API (SupersetClient, SupersetSqlLabExecutor, translate-плагины). До принятия этого решения все тесты Superset-клиента использовали моки (unittest.mock.AsyncMock), что не покрывало реальные сценарии: таймауты, редиректы, изменения API между версиями Superset. Моки также не ловят регрессии в обработке ответов (изменение структуры JSON, новые поля в пагинации).
@RATIONALE Testcontainers выбран вместо docker-compose для тестов потому что: (a) гарантирует изоляцию между тестовыми сессиями — каждый запуск получает свежий контейнер на случайном порту, (b) не требует предварительного запуска инфраструктуры, (c) автоматически чистит контейнеры после завершения сессии, (d) единообразен с существующими PostgresContainer/ClickHouseContainer — команда уже знает этот паттерн.
@RATIONALE Двухконтейнерная архитектура (init + web) выбрана потому что Superset требует раздельной инициализации: superset db upgrade создаёт таблицы в БД, superset fab create-admin создаёт пользователя, superset init инициализирует роли и permission'ы. Только после этого можно запускать веб-сервер. Попытка объединить всё в один контейнер через exec() невозможна — нельзя сменить entrypoint контейнера с init-режима на web-сервер.
@REJECTED Docker Compose для тестов — отвергнут, так как требует предварительного docker compose up, порты фиксированы и могут конфликтовать, изоляция между сессиями слабее.
@REJECTED Superset 6.1.0 (latest) — отвергнут. См. раздел «Расследование: почему не 6.x».
@REJECTED Superset версии 5.x — не существует. Нумерация перескочила с 4.x на 6.x.
Расследование: почему JWT-авторизация не работала
Хронология отладки (2026-06-11)
Шаг 1 — Симптом: POST /api/v1/security/login → 401 {"message":"Not authorized"}
при правильных креденшелах (admin:admin123). Form-based логин (POST /login/) работал,
но /api/v1/me/ с session cookie — 401.
Шаг 2 — Ложная гипотеза (FAB permissions): Проверено, что пользователь admin существует,
активен, пароль верен. auth_user_db('admin', 'admin123') возвращает пользователя.
Flask test client внутри контейнера успешно выполняет JWT login. Значит, проблема
не в permissions Flask-AppBuilder, а на уровне HTTP/сети.
Шаг 3 — Ложная гипотеза (CSRF): Проверен CSRF token из session cookie —
не помогает. Проверен X-CSRFToken header — не помогает.
Шаг 4 — Ложная гипотеза (разные серверы): Проверено, что docker run -p
(прямой Docker) работает, а testcontainers — нет. Разница оказалась в том,
что при docker run -p init и web — один контейнер, а у нас — два разных.
Шаг 5 — Прорыв: Проверка: существует ли пользователь admin в web-контейнере?
User found: False
NO USER
Web-контейнер НЕ ВИДИТ пользователя, созданного init-контейнером.
Шаг 6 — Корневая причина: Superset 4.1.2 игнорирует SQLALCHEMY_DATABASE_URI
из переменных окружения. Даже при установленном SUPERSET__SQLALCHEMY_DATABASE_URI
(с префиксом) конфиг всегда возвращает:
SQLALCHEMY_DATABASE_URI: sqlite:////app/superset_home/superset.db?check_same_thread=false
Superset загружает дефолтный конфиг из superset/config.py, который хардкодит SQLite.
Переменные окружения НЕ переопределяют этот параметр (ни с префиксом SUPERSET__, ни без).
Шаг 7 — Следствие: При двухконтейнерной архитектуре:
- Init-контейнер:
superset db upgrade→ создаёт таблицы в своём SQLite - Init-контейнер:
superset fab create-admin→ создаёт admin в своём SQLite - Init-контейнер: умирает → файловая система (включая
/app/superset_home/superset.db) уничтожается - Web-контейнер: стартует с новым пустым SQLite → пользователь admin не существует
auth_user_db('admin', 'admin123')→find_userвозвращает None → 401 "Not authorized"
Шаг 8 — Решение (три компонента):
-
psycopg2-binary:
python -m pip install psycopg2-binary -qв init и web контейнерах. В Superset 4.1.2 Python —/usr/local/bin/python, pip install работает корректно. (В 6.1.0 Python в venv:/app/.venv/bin/python, требует/app/.venv/bin/pip.) -
superset_config.py: Superset НЕ читает
SQLALCHEMY_DATABASE_URIиз env vars ни в какой форме. Единственный способ переопределить — создатьsuperset_config.pyи указатьSUPERSET_CONFIG_PATH. Конфиг пишется через heredoc в/tmp/superset_config.pyи читаетSUPERSET_DB_URIиз переменной окружения:import os SQLALCHEMY_DATABASE_URI = os.environ.get("SUPERSET_DB_URI", "sqlite://")Альтернативы, которые НЕ работают:
SQLALCHEMY_DATABASE_URIenv var,SUPERSET__SQLALCHEMY_DATABASE_URIenv var,DATABASE_URLenv var. -
Docker bridge IP:
postgres_container.get_connection_url()возвращаетlocalhost:PORT. Изнутри Superset-контейнераlocalhost— это сам контейнер, а не хост. Нужно использовать Docker bridge IP Postgres-контейнера (172.17.0.x), полученный черезget_wrapped_container().attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"]. Порт — внутренний (5432), не exposed.
Таблица: что работает, а что нет
| Подход | Результат | Причина |
|---|---|---|
SQLALCHEMY_DATABASE_URI env var |
❌ Игнорируется | superset/config.py хардкодит SQLite |
SUPERSET__SQLALCHEMY_DATABASE_URI env var |
❌ Игнорируется | Тоже не переопределяет дефолт |
DATABASE_URL env var |
❌ Игнорируется | Superset не использует эту переменную |
superset_config.py через SUPERSET_CONFIG_PATH |
✅ Работает | Единственный способ переопределить конфиг |
localhost в URI изнутри контейнера |
❌ Connection refused | localhost внутри контейнера — сам контейнер |
Docker bridge IP (172.17.0.x) |
✅ Работает | Контейнеры на одном bridge network |
| JWT login через Flask test client | ✅ 200 | Нет сетевого слоя, прямой вызов |
JWT login через curl изнутри контейнера |
✅ 200 | localhost внутри контейнера — сам контейнер (и DB в одном контейнере) |
JWT login через curl с хоста (docker run -p) |
✅ 200 | Один контейнер для init+web, DB не теряется |
Form login (POST /login/) без DB |
✅ 200 | Работает даже с SQLite (пока не нужен themes) |
/api/v1/me/ с session cookie |
❌ 401 | REST API требует JWT Bearer, не session |
Сравнение версий Superset
| Характеристика | 4.1.2 | 6.1.0 |
|---|---|---|
| Python | /usr/local/bin/python (3.10) |
/app/.venv/bin/python (3.10) |
| psycopg2 | ❌ нет | ❌ нет |
| pip install работает | ✅ python -m pip |
⚠️ нужен /app/.venv/bin/pip |
SQLALCHEMY_DATABASE_URI env |
❌ игнорируется | ❌ игнорируется |
SUPERSET__SQLALCHEMY_DATABASE_URI env |
❌ игнорируется | ✅ работает |
superset_config.py |
✅ работает | ✅ работает |
| SQLite fallback | Молча (без ошибок) | Падает: no such table: themes |
/health на SQLite |
✅ "OK" | ✅ "OK" |
/login/ на SQLite |
✅ 200 | ❌ 500 (themes table) |
| Вердикт | Выбрана | Отвергнута |
Decision
Архитектура контейнеров
Test Session
├── PostgresContainer (postgres:16-alpine)
│ ├── DB: test_translate (для app-таблиц)
│ ├── DB: superset_meta (для метаданных Superset)
│ └── Docker bridge IP: 172.17.0.x (не localhost!)
│
├── Init Container (apache/superset:4.1.2)
│ ├── python -m pip install psycopg2-binary -q
│ ├── cat > /tmp/superset_config.py (читает SUPERSET_DB_URI)
│ ├── export SUPERSET_CONFIG_PATH=/tmp/superset_config.py
│ ├── superset db upgrade → миграции в superset_meta (через psycopg2)
│ ├── superset fab create-admin → admin:admin123
│ ├── superset init → роли, permission'ы
│ └── Статус: запускается → ждём exit code 0 → останавливается
│
└── Web Container (apache/superset:4.1.2)
├── python -m pip install psycopg2-binary -q
├── cat > /tmp/superset_config.py (читает SUPERSET_DB_URI)
├── export SUPERSET_CONFIG_PATH=/tmp/superset_config.py
├── superset run -h 0.0.0.0 -p 8088 --with-threads
└── Статус: запускается → ждём /health → живёт до конца сессии
Ключевые параметры
| Параметр | Значение | Обоснование |
|---|---|---|
| Версия Superset | 4.1.2 |
Работает с psycopg2 + superset_config.py |
| PostgreSQL | 16-alpine |
Общий с app-тестами, отдельная БД superset_meta |
| psycopg2 | Установка при старте | python -m pip install psycopg2-binary -q |
| Конфиг | /tmp/superset_config.py |
Через SUPERSET_CONFIG_PATH |
| DB host | Docker bridge IP | 172.17.0.x, не localhost |
| SECRET_KEY | Фиксированный test-... |
Детерминированное поведение |
| Admin password | admin123 |
Только для тестового контейнера |
| Startup timeout | 90s | Достаточно для cold-start pull + pip install + init |
| Health check | GET /health → 200 + "OK" |
Самый лёгкий эндпоинт Superset |
Fixtures в conftest
backend/tests/integration/conftest.py
├── superset_db_url (session) — Docker bridge IP + порт 5432
├── superset_secret_key (session) — SECRET_KEY
├── superset_admin_password (session) — admin123
├── superset_container (session) — init + web контейнеры
├── superset_url (function) — http://host:port
└── superset_admin_headers (function) — requests.Session с form-логином
Что тестируется
| Слой | Тесты | Статус |
|---|---|---|
| Health | /health, /login/, /api/v1/database/ |
✅ 4/4 |
| Auth (form) | Логин → session cookie, /api/v1/me/ |
✅ 2/2 |
| Auth (JWT) | /api/v1/security/login → access_token + refresh_token |
✅ 1/1 |
| API (JWT) | /api/v1/dashboard/ с Bearer token |
✅ 1/1 |
| SupersetClient | Конструкция клиента, api_base_url | ✅ 2/2 |
Ограничения
-
Требуется
pip install psycopg2-binaryпри старте контейнера. Образ 4.1.2 не содержит драйвер PostgreSQL. Без него Superset молча фоллбечится на SQLite, что ломает двухконтейнерную архитектуру (init теряет данные).pip installдобавляет ~5s к холодному старту. -
Требуется
superset_config.py. Superset не читаетSQLALCHEMY_DATABASE_URIиз переменных окружения ни в какой форме — только через файл конфигурации. -
Требуется Docker bridge IP.
localhostнедоступен изнутри контейнера. Нужно получать реальный IP через Docker API. -
Версия 6.1.0 несовместима без доработок. Образ не содержит драйвер PostgreSQL, требует
SUPERSET__префикс для env vars (но не дляSQLALCHEMY_DATABASE_URI— толькоsuperset_config.py), форма логина падает при использовании SQLite.
Enforcement
- Все новые тесты, проверяющие взаимодействие с Superset REST API, должны использовать
superset_containerfixture изtests/integration/conftest.py. - Версия образа (
4.1.2) зафиксирована. Изменение версии требует нового ADR. - Три компонента запуска (psycopg2, superset_config.py, Docker bridge IP) являются обязательными — удаление любого из них ломает JWT-авторизацию.
Migration Path к Superset 6.x
При переходе на Superset 6.x потребуется:
- Кастомный Dockerfile:
FROM apache/superset:6.x+RUN /app/.venv/bin/pip install psycopg2-binary superset_config.pyчерезSUPERSET_CONFIG_PATH(обязательно — env vars не работают)- Docker bridge IP для Postgres (без изменений)
- Пересмотр JWT-авторизации — API не изменился, но образ нестабилен на SQLite