diff --git a/.env.example b/.env.example
index 0cb9c2d..55b193b 100644
--- a/.env.example
+++ b/.env.example
@@ -4,6 +4,11 @@ CLICKHOUSE_01_HTTP_PORT=28123
CLICKHOUSE_01_TCP_PORT=29000
CLICKHOUSE_02_HTTP_PORT=28124
CLICKHOUSE_02_TCP_PORT=29001
+CLICKHOUSE_CLUSTER_SECRET=cluster-26-secret
+CLICKHOUSE_DEFAULT_PASSWORD=default-26-local
+CLICKHOUSE_ETL_PASSWORD=etl-26-writer
+CLICKHOUSE_BI_PASSWORD=bi-26-reader
+CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader
KAFKA_EXTERNAL_PORT=29092
PROMETHEUS_PORT=29090
GRAFANA_PORT=23000
diff --git a/README.md b/README.md
index a9e8475..fe91b6b 100644
--- a/README.md
+++ b/README.md
@@ -242,16 +242,27 @@ uv run --project generator python -m clickstream_generator batch \
- Prometheus — `http://127.0.0.1:29090`;
- Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`.
-У локального учебного кластера нет пароля: ноды используют общего пользователя
-`default` для запросов `Distributed`. Порты поэтому привязаны к `127.0.0.1` и
-не открыты во внешнюю сеть.
+В ClickHouse четыре пользователя:
-Пароли интерфейсов, пароли Postgres, ключи Airflow и Superset, отсутствие
-пароля ClickHouse и отсутствие проверки доступа у Kafka и Prometheus —
-намеренно простые и явно ненастоящие настройки локального учебного стенда. Это
-не пример настройки защиты: не копируйте значения из `.env.example` в рабочую
-среду. Все опубликованные порты привязаны только к `127.0.0.1`; Postgres наружу
-не опубликован.
+- `etl` применяет DDL и подключает Airflow; роль `etl_writer` читает и пишет
+ слои хранилища;
+- `bi` подключает Superset; роль `bi_reader` читает будущие слои DDS и DM;
+- `analyst` предназначен для подключения человека и читает все слои;
+- `default` остаётся служебным: им ходят проверки здоровья и скрипты внутри
+ контейнеров, но не приложения.
+
+Решение и его доводы — в [ADR 0007](docs/adr/0007-clickhouse-access.md).
+Пользователи и роли объявлены в `infra/clickhouse/users.d/access.xml`. Пароли и
+общий секрет нод живут в `.env` и передаются в конфигурацию через окружение;
+Superset получает пароль `bi` тем же путём через штатную функцию настройки.
+Значения для локального стенда есть в `.env.example`.
+
+Пароли ClickHouse и интерфейсов, пароли Postgres, ключи Airflow и Superset,
+отсутствие проверки доступа у Kafka и Prometheus — намеренно простые и явно
+ненастоящие настройки локального учебного стенда. Это не пример настройки
+защиты: не копируйте значения из `.env.example` в рабочую среду. Все
+опубликованные порты привязаны только к `127.0.0.1`; Postgres наружу не
+опубликован.
В бою перед репликами ClickHouse обычно был бы балансировщик. Здесь в каждом
шарде одна реплика, поэтому балансировать нечего. Балансировщик и топология
diff --git a/compose.yaml b/compose.yaml
index 663cf94..10e79f2 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -9,8 +9,13 @@ x-clickhouse-common: &clickhouse-common
image: clickhouse/clickhouse-server:26.3.17.56
restart: unless-stopped
environment:
- # Стенд локальный: нодам нужен общий пользователь без пароля для Distributed.
+ # Пользователей объявляет access.xml; настройка учётки образом не нужна.
CLICKHOUSE_SKIP_USER_SETUP: "1"
+ CLICKHOUSE_CLUSTER_SECRET: ${CLICKHOUSE_CLUSTER_SECRET:?Скопируйте .env.example в .env}
+ CLICKHOUSE_DEFAULT_PASSWORD: ${CLICKHOUSE_DEFAULT_PASSWORD:?Скопируйте .env.example в .env}
+ CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env}
+ CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env}
+ CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_PASSWORD:?Скопируйте .env.example в .env}
depends_on:
clickhouse-keeper:
condition: service_healthy
@@ -18,7 +23,7 @@ x-clickhouse-common: &clickhouse-common
# Гигабайта не хватало: половину съедали страницы самого бинарника (ADR 0004).
mem_limit: 4g
healthcheck:
- test: ["CMD-SHELL", "clickhouse-client --host 127.0.0.1 --query 'SELECT 1' >/dev/null 2>&1"]
+ test: ["CMD-SHELL", "clickhouse-client --host 127.0.0.1 --user default --password \"$$CLICKHOUSE_DEFAULT_PASSWORD\" --query 'SELECT 1' >/dev/null 2>&1"]
interval: 5s
timeout: 3s
retries: 30
@@ -44,6 +49,7 @@ x-airflow-common: &airflow-common
AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: "true"
AIRFLOW_ADMIN_USER: ${AIRFLOW_ADMIN_USER:?Скопируйте .env.example в .env}
AIRFLOW_ADMIN_PASSWORD: ${AIRFLOW_ADMIN_PASSWORD:?Скопируйте .env.example в .env}
+ CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env}
volumes:
- ./dags:/opt/airflow/dags:ro
- ./infra/airflow/init.sh:/opt/airflow/init.sh:ro
@@ -82,6 +88,7 @@ x-superset-common: &superset-common
SUPERSET__SQLALCHEMY_DATABASE_URI: "postgresql+psycopg2://${SUPERSET_METADATA_USER:?Скопируйте .env.example в .env}:${SUPERSET_METADATA_PASSWORD:?Скопируйте .env.example в .env}@postgres-metadata:5432/superset"
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:?Скопируйте .env.example в .env}
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:?Скопируйте .env.example в .env}
+ CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env}
volumes:
- ./infra/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
- ./infra/superset/init.sh:/app/infra/init.sh:ro
@@ -109,7 +116,7 @@ services:
retries: 30
start_period: 10s
- # Инициатор DDL и будущее подключение Airflow.
+ # Инициатор DDL и точка подключения Airflow.
clickhouse-01:
<<: *clickhouse-common
hostname: clickhouse-01
@@ -120,9 +127,10 @@ services:
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
- ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
- ./infra/clickhouse/config.d/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml:ro
+ - ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro
- clickhouse_01_data:/var/lib/clickhouse
- # Будущее подключение Superset.
+ # Точка подключения Superset.
clickhouse-02:
<<: *clickhouse-common
hostname: clickhouse-02
@@ -133,6 +141,7 @@ services:
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
- ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
- ./infra/clickhouse/config.d/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml:ro
+ - ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro
- clickhouse_02_data:/var/lib/clickhouse
kafka:
@@ -225,6 +234,7 @@ services:
condition: service_healthy
environment:
LC_ALL: C
+ CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env}
entrypoint: ["/bin/bash", "-ec"]
command:
- |
@@ -237,6 +247,8 @@ services:
printf 'Применяем %s.\n' "$${ddl_file##*/}"
clickhouse-client \
--host clickhouse-01 \
+ --user etl \
+ --password "$$CLICKHOUSE_ETL_PASSWORD" \
--multiquery \
--queries-file "$$ddl_file"
done
diff --git a/dags/test_clickhouse.py b/dags/test_clickhouse.py
index da31590..91866ff 100644
--- a/dags/test_clickhouse.py
+++ b/dags/test_clickhouse.py
@@ -29,18 +29,31 @@ EXPECTED_TABLES = [
(LOCAL_TABLE, "ReplicatedMergeTree"),
]
-# Ноду 2 пробник читает не своим подключением, а запросом remote() с ноды 1: у
-# Airflow подготовлено одно подключение — к clickhouse-01, и второго ради
-# пробника не заводят. При этом remote('clickhouse-02:9000', ...) делает
-# инициатором распределённого запроса саму ноду 2 — проверяется именно это, а
-# не доступность ноды 2 по сети. Порт 9000 — межсерверный, тогда как
-# подключение Airflow ходит по HTTP на 8123.
+# У Airflow одно подключение — к ноде 1; второго ради пробника не заводят.
+# Ноду 2 он читает через remote(), который не использует секрет из описания
+# кластера, поэтому учётные данные etl передаются явно. Порт 9000 — нативный,
+# тогда как подключение Airflow ходит по HTTP на 8123. Trace-журнал сервера
+# видит пароль: это допустимо только для локального учебного стенда.
NODES = (
("ноде 1", "system.tables"),
- ("ноде 2", "remote('clickhouse-02:9000', system.tables)"),
+ (
+ "ноде 2",
+ """remote(
+ 'clickhouse-02:9000', 'system', 'tables',
+ {remote_user:String}, {remote_password:String}
+ )""",
+ ),
)
+def _remote_parameters() -> dict[str, str]:
+ connection = Connection.get("clickhouse_default")
+ return {
+ "remote_user": connection.login,
+ "remote_password": connection.password,
+ }
+
+
def _clickhouse_client():
# clickhouse_connect импортируется внутри функции, а не наверху файла:
# обработчик DAG разбирает этот файл снова и снова, и импорт наверху
@@ -52,8 +65,8 @@ def _clickhouse_client():
return clickhouse_connect.get_client(
host=connection.host,
port=connection.port,
- username=connection.login or "default",
- password=connection.password or "",
+ username=connection.login,
+ password=connection.password,
database=connection.schema or "default",
connect_timeout=5,
send_receive_timeout=30,
@@ -68,7 +81,8 @@ def _table_engines(client, source: str) -> list[tuple[str, str]]:
WHERE database = 'default'
AND name IN ('{LOCAL_TABLE}', '{DISTRIBUTED_TABLE}')
ORDER BY name
- """
+ """,
+ parameters=_remote_parameters(),
)
return result.result_rows
@@ -173,8 +187,12 @@ def test_clickhouse():
node_2_rows = client.query(
"""
SELECT hostName()
- FROM remote('clickhouse-02:9000', system.one)
- """
+ FROM remote(
+ 'clickhouse-02:9000', 'system', 'one',
+ {remote_user:String}, {remote_password:String}
+ )
+ """,
+ parameters=_remote_parameters(),
).result_rows
if len(node_2_rows) != 1:
raise RuntimeError(f"не удалось определить имя ноды 2: {node_2_rows}")
@@ -184,11 +202,16 @@ def test_clickhouse():
FROM remote(
'clickhouse-02:9000',
'default',
- '{DISTRIBUTED_TABLE}'
+ '{DISTRIBUTED_TABLE}',
+ {{remote_user:String}},
+ {{remote_password:String}}
)
WHERE marker = {{marker:String}}
""",
- parameters={"marker": written["marker"]},
+ parameters={
+ "marker": written["marker"],
+ **_remote_parameters(),
+ },
).result_rows
if written["hostname"] == node_2_rows[0][0]:
raise RuntimeError(
diff --git a/docs/adr/0007-clickhouse-access.md b/docs/adr/0007-clickhouse-access.md
index d768b1c..c350832 100644
--- a/docs/adr/0007-clickhouse-access.md
+++ b/docs/adr/0007-clickhouse-access.md
@@ -63,11 +63,9 @@ DDS под ними, потому что так чаще всего и быва
данными по репликам, и там пароль вписан в каждую реплику каждого из трёх
кластеров.
-Довод опирается на измеренное лишь наполовину: колонку `user` видно на живом
-стенде, а вот что права `bi` на соседнем шарде подменяются правами общей
-учётки — вывод из неё, и он ждёт измерения (см. конец документа). Окажись
-вывод неверным, выбор секрета устоит по остальным основаниям, но перестанет
-быть единственно возможным.
+Довод подтверждён замером 13 августа 2026 года: на соседнем шарде сохранился
+пользователь-инициатор, поэтому там применились его права. Результат замера —
+в разделе «Что проверено».
**Файлы, а не SQL.** Объявленное файлом не оставляет состояния в томах:
повторный `make up` на живом стенде ничего не сдвигает, правка пароля
@@ -132,12 +130,16 @@ Postgres и Grafana. Цена выбора известна: объявленн
настройки ClickHouse.
- Весь DDL репозитория идёт через `IF NOT EXISTS` (файлы `sql/ddl/`).
-Осталось проверить при исполнении, и это работа тикета реализации: точная
-форма объявления секрета в описании кластера (готового примера в
-документации не нашлось); работает ли подстановка из окружения именно для
-пароля пользователя, и если нет — пароли лягут в файл настройки прямым
-текстом, а README назовёт это вслух; применяются ли на соседнем шарде права
-`bi`, а не общей учётки (вывод из колонки `user`, живьём не измерен —
-проверка требует создать пользователя); принимает ли ClickHouse права,
-выданные на ещё не созданную базу — на момент решения `dds` и `dm` не
-существуют.
+Проверено на живом стенде 13 августа 2026 года, ClickHouse 26.3.17.56.
+
+- Секрет кластера принимается внутри `remote_servers` в виде
+ ``. Запрос от `etl` через обе
+ ноды выполнился на каждой под пользователем `etl`.
+- `from_env` работает у элемента `password`: `etl`, `bi` и `analyst` вошли с
+ паролями из окружения и получили свои роли.
+- При запросе от `bi` через Distributed на второй ноде `system.query_log`
+ показал `user = bi` и `initial_user = bi`. Значит, на соседнем шарде
+ применяются права инициатора, а не общей учётной записи.
+- Права на отсутствующие базы `dds` и `dm` приняты. До создания баз они видны
+ в `SHOW GRANTS FOR etl_writer`; временная таблица в `dm` подтвердила право
+ `bi` на чтение после появления базы.
diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md
index 4546e80..cf79609 100644
--- a/docs/specs/2026-07-30-stand-v2-realism.md
+++ b/docs/specs/2026-07-30-stand-v2-realism.md
@@ -576,6 +576,8 @@ v2 стартует пустым, поэтому объём ниже — это
стенд работает, но учебного пути на нём ещё нет.
- Инкрементальный ETL (#8) — свой issue.
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
+- TLS между нодами, настоящее хранилище и ротация секретов, внешние учётные
+ записи, квоты, политики строк и аудит запросов — за границей учебного стенда.
- Полный словарь торговых событий Метрики (detail, remove, impressions),
пять уровней категорий, блоки `purchasedProduct*`/`impressions*`.
- Механика `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток
diff --git a/infra/airflow/init.sh b/infra/airflow/init.sh
index dbc5d1c..74ec46b 100755
--- a/infra/airflow/init.sh
+++ b/infra/airflow/init.sh
@@ -26,6 +26,7 @@ runuser -u airflow -- airflow connections add clickhouse_default \
--conn-type generic \
--conn-host clickhouse-01 \
--conn-port 8123 \
- --conn-login default \
+ --conn-login etl \
+ --conn-password "$CLICKHOUSE_ETL_PASSWORD" \
--conn-schema default \
--conn-description "ClickHouse, нода 1; типизированный провайдер появится на этапе ETL"
diff --git a/infra/clickhouse/config.d/cluster.xml b/infra/clickhouse/config.d/cluster.xml
index 3c32c5f..eddafeb 100644
--- a/infra/clickhouse/config.d/cluster.xml
+++ b/infra/clickhouse/config.d/cluster.xml
@@ -5,6 +5,11 @@
-->
+
+
true
@@ -33,4 +38,3 @@
/clickhouse/task_queue/ddl
-
diff --git a/infra/clickhouse/users.d/access.xml b/infra/clickhouse/users.d/access.xml
new file mode 100644
index 0000000..c66c711
--- /dev/null
+++ b/infra/clickhouse/users.d/access.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW ON default.*
+ GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON stg.*
+ GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON ods.*
+ GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON dds.*
+ GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON dm.*
+ GRANT SELECT ON system.*
+ GRANT CLUSTER ON *.*
+ GRANT REMOTE ON *.*
+
+ GRANT KAFKA ON *.*
+
+
+
+
+
+ GRANT SELECT ON dds.*
+ GRANT SELECT ON dm.*
+ GRANT SELECT ON system.settings
+
+
+
+
+ GRANT SELECT ON stg.*
+ GRANT SELECT ON ods.*
+ GRANT SELECT ON dds.*
+ GRANT SELECT ON dm.*
+ GRANT SELECT ON system.settings
+
+
+
+
+
+
+
+
+
+
+ default
+
+ GRANT etl_writer
+
+
+
+
+ default
+
+ GRANT bi_reader
+
+
+
+
+ default
+
+ GRANT analyst_reader
+
+
+
+
diff --git a/infra/superset/import/databases/clickhouse.yaml b/infra/superset/import/databases/clickhouse.yaml
index 4330e58..b52a4a3 100644
--- a/infra/superset/import/databases/clickhouse.yaml
+++ b/infra/superset/import/databases/clickhouse.yaml
@@ -1,5 +1,5 @@
database_name: ClickHouse
-sqlalchemy_uri: clickhousedb://default@clickhouse-02:8123/default
+sqlalchemy_uri: clickhousedb://bi@clickhouse-02:8123/default
uuid: 4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d
cache_timeout: null
expose_in_sqllab: true
diff --git a/infra/superset/superset_config.py b/infra/superset/superset_config.py
index 5ae7413..1ff180f 100644
--- a/infra/superset/superset_config.py
+++ b/infra/superset/superset_config.py
@@ -5,6 +5,15 @@ import os
SQLALCHEMY_DATABASE_URI = os.environ["SUPERSET__SQLALCHEMY_DATABASE_URI"]
SECRET_KEY = os.environ["SUPERSET_SECRET_KEY"]
+
+# Источник данных пока один, поэтому штатному хранилищу паролей Superset
+# достаточно вернуть один секрет из окружения.
+def clickhouse_password(_url):
+ return os.environ["CLICKHOUSE_BI_PASSWORD"]
+
+
+SQLALCHEMY_CUSTOM_PASSWORD_STORE = clickhouse_password
+
SESSION_COOKIE_NAME = "superset_session"
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
diff --git a/scripts/check-clickhouse.sh b/scripts/check-clickhouse.sh
index 51423f3..e0c895a 100755
--- a/scripts/check-clickhouse.sh
+++ b/scripts/check-clickhouse.sh
@@ -18,7 +18,12 @@ compose() {
query() {
local service="$1"
local sql="$2"
- compose exec -T "$service" clickhouse-client --query "$sql" /dev/null || true)"
user="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_USER // 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_password="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_PASSWORD // empty' <<<"$config")"
port="$(published_port superset 8088)"
@@ -284,10 +286,10 @@ check_superset() {
if [[ "$metadata_tables" == '1' ]] && \
[[ "$metadata_engine" == "postgresql|${superset_user}|postgres-metadata|5432|superset" ]] && \
[[ "$stored_uuid" == '4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d' ]] && \
- [[ "$stored_uri" == 'clickhousedb://default@clickhouse-02:8123/default' ]]; then
+ [[ "$stored_uri" == "clickhousedb://bi:${clickhouse_password}@clickhouse-02:8123/default" ]]; then
pass 'метаданные Superset живут в Postgres, подготовленное подключение указывает на clickhouse-02'
else
- fail "Superset не подтвердил Postgres и подготовленное подключение к ноде 2: таблицы=${metadata_tables:-нет}, движок=${metadata_engine:-нет}, UUID=${stored_uuid:-нет}, URI=${stored_uri:-нет}"
+ fail "Superset не подтвердил Postgres и подготовленное подключение bi к ноде 2: таблицы=${metadata_tables:-нет}, движок=${metadata_engine:-нет}, UUID=${stored_uuid:-нет}"
return
fi
diff --git a/scripts/stand-smoke.sh b/scripts/stand-smoke.sh
index e6c9595..a29239b 100755
--- a/scripts/stand-smoke.sh
+++ b/scripts/stand-smoke.sh
@@ -154,6 +154,7 @@ check_grafana_datasource() {
check_airflow() {
local airflow_port
local airflow_token
+ local clickhouse_password
local config
local connection
local dag
@@ -166,6 +167,7 @@ check_airflow() {
config="$(compose config --format json 2>/dev/null || true)"
user="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_USER // empty' <<<"$config")"
password="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_PASSWORD // empty' <<<"$config")"
+ clickhouse_password="$(jq -r '.services["airflow-scheduler"].environment.CLICKHOUSE_ETL_PASSWORD // empty' <<<"$config")"
airflow_port="$(published_port airflow-apiserver 8080)"
health="$(curl -sf --max-time 10 \
@@ -211,12 +213,13 @@ check_airflow() {
.connection_id == "clickhouse_default" and
.host == "clickhouse-01" and
.port == 8123 and
- .login == "default" and
+ .login == "etl" and
.schema == "default"
' >/dev/null 2>&1 <<<"$connection" && \
timeout 20s "${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" \
exec -T airflow-scheduler \
- curl -sf 'http://clickhouse-01:8123/?query=SELECT%201' \
+ curl -sf -u "etl:${clickhouse_password}" \
+ 'http://clickhouse-01:8123/?query=SELECT%201' \
2>/dev/null | grep -qx '1'; then
pass 'подготовленное подключение Airflow указывает на clickhouse-01, нода доступна из контейнера'
else
diff --git a/scripts/wait-for-world.sh b/scripts/wait-for-world.sh
index 9252c6a..de81076 100755
--- a/scripts/wait-for-world.sh
+++ b/scripts/wait-for-world.sh
@@ -42,7 +42,12 @@ fail() {
query() {
"${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" exec -T clickhouse-01 \
- clickhouse-client --query "$1"