Merge branch 'main' into 48-generator-guide
This commit is contained in:
commit
74061aa499
9 files changed
+131
-41
No files matched your search
@@ -262,6 +262,11 @@ uv run --project generator python -m clickstream_generator batch \
|
|||||||
`0` — очистка закончена, а начальная генерация ещё не завершена. Работники
|
`0` — очистка закончена, а начальная генерация ещё не завершена. Работники
|
||||||
начинают играть только с позиции `8` и больше.
|
начинают играть только с позиции `8` и больше.
|
||||||
|
|
||||||
|
В форме запуска можно выбрать только число дней. Первый день работник берет из
|
||||||
|
`world_position`. Поля «Logical date» и «Run ID» спрятаны в «Advanced Options».
|
||||||
|
Они нужны самому Airflow и на выбор дня не влияют. Сыгранные дни и новую
|
||||||
|
позицию работник пишет в журнал шага `remember_played`.
|
||||||
|
|
||||||
Выключатель создаётся на паузе. Снимите — мир поедет сам; поставите обратно —
|
Выключатель создаётся на паузе. Снимите — мир поедет сам; поставите обратно —
|
||||||
встанет на границе модельных суток, доиграв начатый день. Форма пульта и доводы
|
встанет на границе модельных суток, доиграв начатый день. Форма пульта и доводы
|
||||||
целиком — [ADR 0009](docs/adr/0009-world-control.md).
|
целиком — [ADR 0009](docs/adr/0009-world-control.md).
|
||||||
|
|||||||
+6
-4
@@ -48,10 +48,10 @@ x-kafka-target: &kafka-target
|
|||||||
|
|
||||||
x-world-starting-days: &world-starting-days "8"
|
x-world-starting-days: &world-starting-days "8"
|
||||||
|
|
||||||
|
# Общие образы Airflow и Superset собирают службы инициализации. Причина
|
||||||
|
# отдельного build: ADR 0001.
|
||||||
x-airflow-common: &airflow-common
|
x-airflow-common: &airflow-common
|
||||||
image: clickstream-airflow:local
|
image: clickstream-airflow:local
|
||||||
build:
|
|
||||||
context: ./infra/airflow
|
|
||||||
environment:
|
environment:
|
||||||
AIRFLOW__CORE__EXECUTOR: LocalExecutor
|
AIRFLOW__CORE__EXECUTOR: LocalExecutor
|
||||||
AIRFLOW__CORE__PARALLELISM: "4"
|
AIRFLOW__CORE__PARALLELISM: "4"
|
||||||
@@ -105,8 +105,6 @@ x-generator-common: &generator-common
|
|||||||
|
|
||||||
x-superset-common: &superset-common
|
x-superset-common: &superset-common
|
||||||
image: clickstream-superset:local
|
image: clickstream-superset:local
|
||||||
build:
|
|
||||||
context: ./infra/superset
|
|
||||||
environment:
|
environment:
|
||||||
SUPERSET_CONFIG_PATH: /app/pythonpath/superset_config.py
|
SUPERSET_CONFIG_PATH: /app/pythonpath/superset_config.py
|
||||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:?Скопируйте .env.example в .env}
|
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:?Скопируйте .env.example в .env}
|
||||||
@@ -257,6 +255,8 @@ services:
|
|||||||
|
|
||||||
airflow-init:
|
airflow-init:
|
||||||
<<: *airflow-common
|
<<: *airflow-common
|
||||||
|
build:
|
||||||
|
context: ./infra/airflow
|
||||||
user: "0:0"
|
user: "0:0"
|
||||||
restart: "no"
|
restart: "no"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -340,6 +340,8 @@ services:
|
|||||||
|
|
||||||
superset-init:
|
superset-init:
|
||||||
<<: *superset-common
|
<<: *superset-common
|
||||||
|
build:
|
||||||
|
context: ./infra/superset
|
||||||
restart: "no"
|
restart: "no"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
disable: true
|
disable: true
|
||||||
|
|||||||
+25
-3
@@ -28,6 +28,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from airflow.exceptions import AirflowException
|
from airflow.exceptions import AirflowException
|
||||||
@@ -141,7 +142,19 @@ def _ingest_orders() -> TriggerDagRunOperator:
|
|||||||
tags=TAGS,
|
tags=TAGS,
|
||||||
params={
|
params={
|
||||||
"days": Param(
|
"days": Param(
|
||||||
1, type="integer", minimum=1, maximum=7, title="Сколько дней прожить"
|
1,
|
||||||
|
type="integer",
|
||||||
|
minimum=1,
|
||||||
|
maximum=7,
|
||||||
|
title="Сколько дней прожить",
|
||||||
|
# description_md рендерится подсказкой прямо в форме запуска
|
||||||
|
# (params.rst, Airflow 3.3), чтобы правило было видно сразу.
|
||||||
|
description_md=(
|
||||||
|
"Форма задает только число дней. Номер первого несыгранного "
|
||||||
|
"дня работник берет из переменной `world_position` (Admin → "
|
||||||
|
"Variables). Поля Logical date и Run ID нужны самому Airflow "
|
||||||
|
"и на выбор дня не влияют."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -149,14 +162,20 @@ def world_next_day():
|
|||||||
"""Прожить следующие дни пачкой, без пауз.
|
"""Прожить следующие дни пачкой, без пауз.
|
||||||
|
|
||||||
Запускается руками. День по умолчанию один, но разгон вперёд идёт одним
|
Запускается руками. День по умолчанию один, но разгон вперёд идёт одним
|
||||||
нажимом, а не десятью: сколько дней играть — параметр запуска.
|
нажимом, а не десятью: сколько дней играть — параметр запуска. Первый
|
||||||
|
день работник берет из `world_position`. Логическая дата прогона на выбор
|
||||||
|
дня не влияет.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def remember_played(first_day: int) -> None:
|
def remember_played(first_day: int) -> None:
|
||||||
"""Позиция ставится по сыгранным дням и только по успеху."""
|
"""Позиция ставится по сыгранным дням и только по успеху."""
|
||||||
days = get_current_context()["params"]["days"]
|
days = get_current_context()["params"]["days"]
|
||||||
Variable.set(WORLD_POSITION, str(first_day + days))
|
position = first_day + days
|
||||||
|
Variable.set(WORLD_POSITION, str(position))
|
||||||
|
logging.info(
|
||||||
|
"дни %s–%s сыграны; новая позиция — %s", first_day, position - 1, position
|
||||||
|
)
|
||||||
|
|
||||||
first_day = first_unplayed_day()
|
first_day = first_unplayed_day()
|
||||||
played = _generator(
|
played = _generator(
|
||||||
@@ -196,12 +215,15 @@ def world_live_day():
|
|||||||
Ускорение ×60: модельные сутки укладываются примерно в двадцать четыре
|
Ускорение ×60: модельные сутки укладываются примерно в двадцать четыре
|
||||||
реальные минуты, и суточная волна разворачивается на глазах. Запускается
|
реальные минуты, и суточная волна разворачивается на глазах. Запускается
|
||||||
руками; чтобы мир жил так день за днём сам, есть выключатель `world_live`.
|
руками; чтобы мир жил так день за днём сам, есть выключатель `world_live`.
|
||||||
|
Следующий день работник берет из `world_position`. Логическая дата прогона
|
||||||
|
на выбор дня не влияет.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def remember_played(first_day: int) -> None:
|
def remember_played(first_day: int) -> None:
|
||||||
"""Позиция ставится по сыгранному дню и только по успеху."""
|
"""Позиция ставится по сыгранному дню и только по успеху."""
|
||||||
Variable.set(WORLD_POSITION, str(first_day + 1))
|
Variable.set(WORLD_POSITION, str(first_day + 1))
|
||||||
|
logging.info("сыгран день %s; новая позиция — %s", first_day, first_day + 1)
|
||||||
|
|
||||||
first_day = first_unplayed_day()
|
first_day = first_unplayed_day()
|
||||||
played = _generator("play_day", ["live", "--day", PLAYED_DAY])
|
played = _generator("play_day", ["live", "--day", PLAYED_DAY])
|
||||||
|
|||||||
@@ -16,6 +16,13 @@
|
|||||||
возвращает единицу — поведение проверено при исполнении #37. Зависимость
|
возвращает единицу — поведение проверено при исполнении #37. Зависимость
|
||||||
включает результат подготовки в общий граф ожидания.
|
включает результат подготовки в общий граф ожидания.
|
||||||
|
|
||||||
|
У каждого общего локального образа один блок `build`. Он находится у службы
|
||||||
|
инициализации. Остальные службы зависят от неё и используют тот же `image`.
|
||||||
|
`docker compose up --build` завершает сборку до запуска контейнеров, поэтому
|
||||||
|
одного блока достаточно. В Buildx 0.30.1 одинаковые блоки у нескольких служб
|
||||||
|
запускают параллельный экспорт одного тега. Иногда сборка завершается ошибкой
|
||||||
|
`image already exists`.
|
||||||
|
|
||||||
Для входа выбран SimpleAuthManager. У него нет команды создания пользователя,
|
Для входа выбран SimpleAuthManager. У него нет команды создания пользователя,
|
||||||
поэтому `airflow-init` записывает пароль администратора из окружения в
|
поэтому `airflow-init` записывает пароль администратора из окружения в
|
||||||
JSON-файл отдельного тома `airflow_auth`. Новый именованный том принадлежит
|
JSON-файл отдельного тома `airflow_auth`. Новый именованный том принадлежит
|
||||||
|
|||||||
@@ -137,9 +137,9 @@ ODS и ещё не собранный в DDS, затем при необходи
|
|||||||
## Не входит
|
## Не входит
|
||||||
|
|
||||||
- Партиционная переобработка — переиграть произвольный прошлый день или
|
- Партиционная переобработка — переиграть произвольный прошлый день или
|
||||||
диапазон, включая форму необязательных параметров того же дага, — этап 5
|
диапазон, включая форму необязательных параметров того же дага.
|
||||||
(#7); туда же стык #111 про дату модельного мира и `logical_date`.
|
- Инкрементальный ETL вместо пересборки отклонен (#2, wontfix). Идея отдельной
|
||||||
- Инкрементальный ETL вместо пересборки — техдолг #2.
|
лабы на эту тему — #149.
|
||||||
- Состав витрин `dm_transform` и проверок `dq_check` —
|
- Состав витрин `dm_transform` и проверок `dq_check` —
|
||||||
[дока слоя DM](dm.md).
|
[дока слоя DM](dm.md).
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ ClickHouse, два подготовленных источника данных,
|
|||||||
| `grafana` | `clickstream-grafana:local` | интерфейс мониторинга; собирается из `infra/grafana/`, порт стенда `23000` |
|
| `grafana` | `clickstream-grafana:local` | интерфейс мониторинга; собирается из `infra/grafana/`, порт стенда `23000` |
|
||||||
| `kafka-exporter` | `danielqsj/kafka-exporter:v1.9.0` | переводит состояние брокера в метрики Prometheus, порт `9308` |
|
| `kafka-exporter` | `danielqsj/kafka-exporter:v1.9.0` | переводит состояние брокера в метрики Prometheus, порт `9308` |
|
||||||
|
|
||||||
Плагин ClickHouse 4.20.0 входит в образ Grafana. Фоновая установка плагинов
|
Плагин ClickHouse 4.20.0 входит в образ Grafana. При сборке Grafana CLI берёт
|
||||||
при запуске отключена.
|
закреплённый выпуск из официального репозитория на GitHub, а не из каталога
|
||||||
|
Grafana: прямой запрос к каталогу при проверке вернул HTTP 451. Фоновая
|
||||||
|
установка плагинов при запуске отключена.
|
||||||
|
|
||||||
## Кого опрашивает Prometheus
|
## Кого опрашивает Prometheus
|
||||||
|
|
||||||
|
|||||||
@@ -34,17 +34,18 @@
|
|||||||
спрашивают его отдельно.
|
спрашивают его отдельно.
|
||||||
|
|
||||||
Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено».
|
Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено».
|
||||||
`make config-test`, `make smoke` и `make check-services` перемерены 21 августа
|
`make smoke` и `make check-services` перемерены 28 августа 2026 года,
|
||||||
2026 года, `make check-clickhouse` — 19 августа, корневой `make lint` —
|
`make config-test` — 21 августа, `make check-clickhouse` — 19 августа,
|
||||||
9 августа, `make test` — 7 августа; остальные стоят с замера 6 августа.
|
корневой `make lint` — 9 августа, `make test` — 7 августа; остальные стоят с
|
||||||
|
замера 6 августа.
|
||||||
|
|
||||||
| Цель | Откуда | Что утверждает | Стенд | Цена |
|
| Цель | Откуда | Что утверждает | Стенд | Цена |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `make config-test` | корень | Compose разбирается, файлы DAG и дашборды Grafana синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с |
|
| `make config-test` | корень | Compose разбирается, файлы DAG и дашборды Grafana синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с |
|
||||||
| `make lint` | корень | Код стенда — `dags/` и `infra/superset/` — отформатирован и проходит ruff | не нужен | 0,4 с |
|
| `make lint` | корень | Код стенда — `dags/` и `infra/superset/` — отформатирован и проходит ruff | не нужен | 0,4 с |
|
||||||
| `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 17 с |
|
| `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 25 с |
|
||||||
| `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, ответ словаря товаров, счёт событий стартового мира против описи | нужен | 24 с |
|
| `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, ответ словаря товаров, счёт событий стартового мира против описи | нужен | 24 с |
|
||||||
| `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 93 с |
|
| `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 71 с |
|
||||||
| `make lint` | `generator/` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с |
|
| `make lint` | `generator/` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с |
|
||||||
| `make typecheck` | `generator/` | Типы генератора сходятся (ty) | не нужен | 0,5 с |
|
| `make typecheck` | `generator/` | Типы генератора сходятся (ty) | не нужен | 0,5 с |
|
||||||
| `make test` | `generator/` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [опись мира](../../data/world-inventory.json) — свежими | не нужен | 71 с |
|
| `make test` | `generator/` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [опись мира](../../data/world-inventory.json) — свежими | не нужен | 71 с |
|
||||||
@@ -237,6 +238,27 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
|
|||||||
|
|
||||||
## Что проверено
|
## Что проверено
|
||||||
|
|
||||||
|
**Приемка #172 28 августа 2026 года.** Холодный `create_app()` занимал
|
||||||
|
28,3-28,4 секунды, а чтение подключения после запуска - 0,07 секунды. Предел
|
||||||
|
30 секунд почти целиком уходил на запуск второго приложения. Увеличивать его
|
||||||
|
не стали: Superset не обещает постоянного времени `create_app()`.
|
||||||
|
|
||||||
|
После входа проверка делает три GET-запроса к уже работающему Superset: находит
|
||||||
|
`ClickHouse`, сверяет UUID и безопасный URI, затем требует схему `dm`. Так без
|
||||||
|
второго запуска проверяются сохраненное подключение и хранилище пароля. Форму
|
||||||
|
API сверили через Context7 и с исходным кодом Superset 6.1.0. `superset test-db`
|
||||||
|
отклонен, потому что тоже запускает приложение; прямой SQL к `dbs` не проверяет
|
||||||
|
соединение.
|
||||||
|
|
||||||
|
Проверка больше не подтверждает, что сам Superset использует Postgres для
|
||||||
|
метаданных. Короткий запрос к Postgres показывает лишь готовность базы, а REST
|
||||||
|
API - подключение к ClickHouse. Для `check-services` этого достаточно.
|
||||||
|
|
||||||
|
После перезапуска Superset три прогона прошли 7/7 за 71, 67 и 70 секунд.
|
||||||
|
`make config-test` прошел. `make smoke` дал 21/21 за 25 секунд; его код не
|
||||||
|
менялся, прежний замер был 17 секунд. Неверный UUID дал 5/1: красной стала
|
||||||
|
только проверка метаданных, пароль в вывод не попал.
|
||||||
|
|
||||||
**Приемка #156 27 августа 2026 года.** `make rebuild-storage` занял 7 минут.
|
**Приемка #156 27 августа 2026 года.** `make rebuild-storage` занял 7 минут.
|
||||||
На чистом прикладном мире `world_initialize` сам довел позицию до `8`, дождался
|
На чистом прикладном мире `world_initialize` сам довел позицию до `8`, дождался
|
||||||
`etl_pipeline` и оставил заполненными DDS и обе витрины. После пересборки прошли
|
`etl_pipeline` и оставил заполненными DDS и обе витрины. После пересборки прошли
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ FROM grafana/grafana:13.1.1
|
|||||||
USER root
|
USER root
|
||||||
ENV GF_PATHS_PLUGINS=/usr/share/grafana/plugins \
|
ENV GF_PATHS_PLUGINS=/usr/share/grafana/plugins \
|
||||||
GF_PLUGINS_PREINSTALL_DISABLED=true
|
GF_PLUGINS_PREINSTALL_DISABLED=true
|
||||||
|
ARG CLICKHOUSE_PLUGIN_VERSION=4.20.0
|
||||||
RUN grafana cli --pluginsDir "$GF_PATHS_PLUGINS" \
|
RUN grafana cli --pluginsDir "$GF_PATHS_PLUGINS" \
|
||||||
plugins install grafana-clickhouse-datasource 4.20.0
|
--pluginUrl "https://github.com/grafana/clickhouse-datasource/releases/download/v${CLICKHOUSE_PLUGIN_VERSION}/grafana-clickhouse-datasource-${CLICKHOUSE_PLUGIN_VERSION}.zip" \
|
||||||
|
plugins install grafana-clickhouse-datasource "$CLICKHOUSE_PLUGIN_VERSION"
|
||||||
USER grafana
|
USER grafana
|
||||||
+51
-23
@@ -237,35 +237,42 @@ check_airflow_probes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_superset() {
|
check_superset() {
|
||||||
local clickhouse_password
|
local access_token
|
||||||
|
local available_schemas
|
||||||
|
local clickhouse_databases
|
||||||
local config
|
local config
|
||||||
|
local connection_count
|
||||||
|
local connection_details
|
||||||
|
local connection_id
|
||||||
|
local database_list
|
||||||
local login
|
local login
|
||||||
local metadata_tables
|
local metadata_tables
|
||||||
local password
|
local password
|
||||||
local port
|
local port
|
||||||
local metadata_engine
|
local schemas
|
||||||
local stored_config
|
local stored_safe_uri
|
||||||
local stored_uri
|
|
||||||
local stored_uuid
|
local stored_uuid
|
||||||
local superset_password
|
local superset_password
|
||||||
local superset_user
|
local superset_user
|
||||||
|
local superset_url
|
||||||
local user
|
local user
|
||||||
|
|
||||||
config="$(compose config --format json 2>/dev/null || true)"
|
config="$(compose config --format json 2>/dev/null || true)"
|
||||||
user="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_USER // empty' <<<"$config")"
|
user="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_USER // empty' <<<"$config")"
|
||||||
password="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_PASSWORD // empty' <<<"$config")"
|
password="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_PASSWORD // empty' <<<"$config")"
|
||||||
clickhouse_password="$(jq -r '.services.superset.environment.CLICKHOUSE_BI_PASSWORD // empty' <<<"$config")"
|
|
||||||
superset_user="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_USER // empty' <<<"$config")"
|
superset_user="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_USER // empty' <<<"$config")"
|
||||||
superset_password="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_PASSWORD // empty' <<<"$config")"
|
superset_password="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_PASSWORD // empty' <<<"$config")"
|
||||||
port="$(published_port superset 8088)"
|
port="$(published_port superset 8088)"
|
||||||
|
superset_url="http://127.0.0.1:${port}"
|
||||||
|
|
||||||
login="$(curl -sf --max-time 10 -X POST \
|
login="$(curl -sf --max-time 10 -X POST \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d "$(jq -cn --arg username "$user" --arg password "$password" \
|
-d "$(jq -cn --arg username "$user" --arg password "$password" \
|
||||||
'{username: $username, password: $password, provider: "db", refresh: true}')" \
|
'{username: $username, password: $password, provider: "db", refresh: true}')" \
|
||||||
"http://127.0.0.1:${port}/api/v1/security/login" 2>/dev/null || true)"
|
"${superset_url}/api/v1/security/login" 2>/dev/null || true)"
|
||||||
if curl -sf --max-time 10 "http://127.0.0.1:${port}/health" >/dev/null 2>&1 && \
|
access_token="$(jq -r '.access_token // empty' <<<"$login")"
|
||||||
jq -e '.access_token | length > 0' >/dev/null 2>&1 <<<"$login"; then
|
if curl -sf --max-time 10 "${superset_url}/health" >/dev/null 2>&1 && \
|
||||||
|
[[ -n "$access_token" ]]; then
|
||||||
pass 'интерфейс Superset отвечает и принимает подготовленные учётные данные администратора'
|
pass 'интерфейс Superset отвечает и принимает подготовленные учётные данные администратора'
|
||||||
else
|
else
|
||||||
fail 'интерфейс Superset не отвечает или не принимает учётные данные администратора'
|
fail 'интерфейс Superset не отвечает или не принимает учётные данные администратора'
|
||||||
@@ -276,28 +283,49 @@ check_superset() {
|
|||||||
psql -U "$superset_user" -d superset -tAc \
|
psql -U "$superset_user" -d superset -tAc \
|
||||||
"SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'ab_user';" \
|
"SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'ab_user';" \
|
||||||
2>/dev/null || true)"
|
2>/dev/null || true)"
|
||||||
stored_config="$(timeout 30s "${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" \
|
|
||||||
exec -T superset python -c \
|
# Superset уже собрал приложение. Его REST API читает ту же сохранённую
|
||||||
"from superset.app import create_app; app = create_app(); app.app_context().push(); from superset.extensions import db; from superset.models.core import Database; url = db.engine.url; database = db.session.query(Database).filter_by(database_name='ClickHouse').one(); print('SMOKE_METADATA=' + '|'.join([url.get_backend_name(), url.username or '', url.host or '', str(url.port or ''), url.database or ''])); print('SMOKE_UUID=' + str(database.uuid)); print('SMOKE_URI=' + database.sqlalchemy_uri_decrypted)" \
|
# модель Database без второго тяжёлого запуска Python.
|
||||||
2>/dev/null)"
|
database_list="$(curl -sf --max-time 10 \
|
||||||
metadata_engine="$(sed -n 's/^SMOKE_METADATA=//p' <<<"$stored_config" | tail -n 1)"
|
-H "Authorization: Bearer ${access_token}" \
|
||||||
stored_uuid="$(sed -n 's/^SMOKE_UUID=//p' <<<"$stored_config" | tail -n 1)"
|
"${superset_url}/api/v1/database/" 2>/dev/null || true)"
|
||||||
stored_uri="$(sed -n 's/^SMOKE_URI=//p' <<<"$stored_config" | tail -n 1)"
|
clickhouse_databases="$(jq -c \
|
||||||
|
'[.result[]? | select(.database_name == "ClickHouse")]' \
|
||||||
|
<<<"$database_list")"
|
||||||
|
connection_count="$(jq -r 'length' <<<"$clickhouse_databases")"
|
||||||
|
connection_id="$(jq -r 'if length == 1 then .[0].id else empty end' \
|
||||||
|
<<<"$clickhouse_databases")"
|
||||||
|
stored_uuid="$(jq -r 'if length == 1 then .[0].uuid else empty end' \
|
||||||
|
<<<"$clickhouse_databases")"
|
||||||
|
|
||||||
|
connection_details=''
|
||||||
|
if [[ -n "$connection_id" ]]; then
|
||||||
|
connection_details="$(curl -sf --max-time 10 \
|
||||||
|
-H "Authorization: Bearer ${access_token}" \
|
||||||
|
"${superset_url}/api/v1/database/${connection_id}/connection" \
|
||||||
|
2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
stored_safe_uri="$(jq -r '.result.sqlalchemy_uri // empty' \
|
||||||
|
<<<"$connection_details")"
|
||||||
|
|
||||||
if [[ "$metadata_tables" == '1' ]] && \
|
if [[ "$metadata_tables" == '1' ]] && \
|
||||||
[[ "$metadata_engine" == "postgresql|${superset_user}|postgres-metadata|5432|superset" ]] && \
|
|
||||||
[[ "$stored_uuid" == '4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d' ]] && \
|
[[ "$stored_uuid" == '4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d' ]] && \
|
||||||
[[ "$stored_uri" == "clickhousedb://bi:${clickhouse_password}@clickhouse-02:8123/default" ]]; then
|
[[ "$stored_safe_uri" == 'clickhousedb://bi@clickhouse-02:8123/default' ]]; then
|
||||||
pass 'метаданные Superset живут в Postgres, подготовленное подключение указывает на clickhouse-02'
|
pass 'Postgres для метаданных Superset подготовлен, подключение указывает на clickhouse-02'
|
||||||
else
|
else
|
||||||
fail "Superset не подтвердил Postgres и подготовленное подключение bi к ноде 2: таблицы=${metadata_tables:-нет}, движок=${metadata_engine:-нет}, UUID=${stored_uuid:-нет}"
|
fail "Postgres или подключение Superset не подготовлены: таблицы=${metadata_tables:-нет}, подключений=${connection_count:-нет}, UUID=${stored_uuid:-нет}, URI=${stored_safe_uri:-нет}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if printf 'n\n' | timeout 60s "${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" \
|
schemas="$(curl -sf --max-time 10 \
|
||||||
exec -T superset superset test-db "$stored_uri" >/dev/null 2>&1; then
|
-H "Authorization: Bearer ${access_token}" \
|
||||||
pass 'Superset успешно проверил извлечённое из метаданных подключение ClickHouse'
|
"${superset_url}/api/v1/database/${connection_id}/schemas/" \
|
||||||
|
2>/dev/null || true)"
|
||||||
|
available_schemas="$(jq -r '[.result[]?] | join(", ")' <<<"$schemas")"
|
||||||
|
if jq -e '.result | index("dm") != null' >/dev/null 2>&1 <<<"$schemas"; then
|
||||||
|
pass 'сохранённое подключение Superset видит схему витрин dm'
|
||||||
else
|
else
|
||||||
fail 'Superset не смог проверить извлечённое из метаданных подключение ClickHouse'
|
fail "сохранённое подключение Superset не видит схему витрин dm: схемы=${available_schemas:-нет}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user