Files
ss-tools/docs/adr/ADR-0012-superset-testcontainers.md
busya ec6421de35 rename ss-tools to superset-tools across the entire project
- 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
2026-06-16 11:15:19 +03:00

16 KiB
Raw Blame History

[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 — Решение (три компонента):

  1. 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.)

  2. 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_URI env var, SUPERSET__SQLALCHEMY_DATABASE_URI env var, DATABASE_URL env var.

  3. 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

Ограничения

  1. Требуется pip install psycopg2-binary при старте контейнера. Образ 4.1.2 не содержит драйвер PostgreSQL. Без него Superset молча фоллбечится на SQLite, что ломает двухконтейнерную архитектуру (init теряет данные). pip install добавляет ~5s к холодному старту.

  2. Требуется superset_config.py. Superset не читает SQLALCHEMY_DATABASE_URI из переменных окружения ни в какой форме — только через файл конфигурации.

  3. Требуется Docker bridge IP. localhost недоступен изнутри контейнера. Нужно получать реальный IP через Docker API.

  4. Версия 6.1.0 несовместима без доработок. Образ не содержит драйвер PostgreSQL, требует SUPERSET__ префикс для env vars (но не для SQLALCHEMY_DATABASE_URI — только superset_config.py), форма логина падает при использовании SQLite.

Enforcement

  • Все новые тесты, проверяющие взаимодействие с Superset REST API, должны использовать superset_container fixture из tests/integration/conftest.py.
  • Версия образа (4.1.2) зафиксирована. Изменение версии требует нового ADR.
  • Три компонента запуска (psycopg2, superset_config.py, Docker bridge IP) являются обязательными — удаление любого из них ломает JWT-авторизацию.

Migration Path к Superset 6.x

При переходе на Superset 6.x потребуется:

  1. Кастомный Dockerfile: FROM apache/superset:6.x + RUN /app/.venv/bin/pip install psycopg2-binary
  2. superset_config.py через SUPERSET_CONFIG_PATH (обязательно — env vars не работают)
  3. Docker bridge IP для Postgres (без изменений)
  4. Пересмотр JWT-авторизации — API не изменился, но образ нестабилен на SQLite

[/DEF:ADR-0012:ADR]