feat(airflow): пробные DAG test_clickhouse и test_kafka вместо демонстрационного
Зачем. Демонстрационный DAG example_clickstream_hello ничего не проверял: он не обращался ни к ClickHouse, ни к Kafka, поэтому его зелёный результат ничего не говорил о стенде. Пробники проверяют связи по-настоящему — и тем же клиентом, каким будут ходить рабочие DAG. Что. - test_clickhouse: пишет строку в ReplicatedMergeTree на ноде 1 и читает её с ноды 2 через Distributed. Данные проходят путь «нода 2 → все шарды → шард ноды 1», то есть проверяется межшардовое чтение, а не одна нода. - test_kafka: пишет в постоянный топик сообщение с меткой прогона и вычитывает его обратно. - infra/airflow/Dockerfile: clickhouse-connect 1.6.0 и confluent-kafka 2.15.0 вшиты в образ, импорт проверяется на сборке — при запуске контейнера пакеты не доустанавливаются. - Проверки: scripts/stand-smoke.sh гоняет оба пробника через API Airflow, scripts/config-test.sh разбирает DAG без стенда, tests/stand-smoke-guards.sh проверяет красный путь, tests/dag-probes-unit.py — модульные проверки разбора. - README и ADR 0001 обновлены тем же изменением. - Удалён dags/example_clickstream_hello.py. Проверка. make config-test — пройдено 3, 3 и 6, ошибок 0. make clean; cp .env.example .env; make up — 116 с на чистых томах. make smoke — пройдено 25, ошибок 0; стенд занимает 2244,0 MiB. make smoke-cluster — все 8 проверок кластера. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,15 @@ jq -e '
|
||||
.services.superset.image == "clickstream-superset:local" and
|
||||
(.services.superset.build.args.SUPERSET_BASE_IMAGE | length > 0)
|
||||
' >/dev/null <<<"$config_json"
|
||||
grep -qx 'ARG AIRFLOW_BASE_IMAGE' "$ROOT_DIR/infra/airflow/Dockerfile"
|
||||
jq -e '
|
||||
.services["airflow-init"].image == "clickstream-airflow:local" and
|
||||
(.services["airflow-init"].build.args.AIRFLOW_BASE_IMAGE | length > 0) and
|
||||
all(
|
||||
.services[];
|
||||
((.environment // {}) | has("_PIP_ADDITIONAL_REQUIREMENTS") | not)
|
||||
)
|
||||
' >/dev/null <<<"$config_json"
|
||||
jq -e '
|
||||
.services["airflow-init"] as $service |
|
||||
($service.environment.AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_PASSWORDS_FILE ==
|
||||
@@ -39,6 +48,20 @@ if [[ "${#shell_files[@]}" -eq 0 ]]; then
|
||||
fi
|
||||
bash -n "${shell_files[@]}"
|
||||
PYTHONPYCACHEPREFIX="$CACHE_DIR" uv run --no-project python -m compileall -q "$ROOT_DIR/dags"
|
||||
unit_status=0
|
||||
unit_output="$(
|
||||
PYTHONPYCACHEPREFIX="$CACHE_DIR" \
|
||||
uv run --no-project python "$ROOT_DIR/tests/dag-probes-unit.py" 2>&1
|
||||
)" || unit_status=$?
|
||||
printf '%s\n' "$unit_output"
|
||||
if [[ "$unit_status" -ne 0 ]]; then
|
||||
exit "$unit_status"
|
||||
fi
|
||||
if grep -Eq '^(Ran [0-9]+ tests|OK|FAILED)' <<<"$unit_output" ||
|
||||
! grep -Eq '^ИТОГ: пройдено [0-9]+, ошибок 0$' <<<"$unit_output"; then
|
||||
printf 'ОШИБКА: малые проверки пробников вывели итог не на русском языке.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
git -C "$ROOT_DIR" diff --check
|
||||
|
||||
printf 'ЗЕЛЁНО: Compose, Bash, Python и пробельные ошибки diff проверены.\n'
|
||||
|
||||
+89
-55
@@ -17,6 +17,7 @@ kafka_cleanup_image=''
|
||||
kafka_cleanup_port=''
|
||||
kafka_cleanup_topic=''
|
||||
airflow_cleanup_port=''
|
||||
airflow_cleanup_dag_id=''
|
||||
airflow_cleanup_original_paused=''
|
||||
airflow_cleanup_run_id=''
|
||||
airflow_cleanup_token=''
|
||||
@@ -232,7 +233,7 @@ cleanup_airflow_run() {
|
||||
encoded_run_id="$(jq -rn --arg value "$airflow_cleanup_run_id" '$value | @uri')"
|
||||
if curl -sf --max-time 10 -X DELETE \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/example_clickstream_hello/dagRuns/${encoded_run_id}" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/${airflow_cleanup_dag_id}/dagRuns/${encoded_run_id}" \
|
||||
>/dev/null 2>&1; then
|
||||
airflow_cleanup_run_id=''
|
||||
return 0
|
||||
@@ -250,7 +251,7 @@ cleanup_airflow_pause() {
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$payload" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/example_clickstream_hello" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/${airflow_cleanup_dag_id}" \
|
||||
>/dev/null 2>&1; then
|
||||
airflow_cleanup_original_paused=''
|
||||
return 0
|
||||
@@ -383,18 +384,84 @@ check_grafana_datasource() {
|
||||
fi
|
||||
}
|
||||
|
||||
run_airflow_probe() {
|
||||
local dag
|
||||
local dag_id="$1"
|
||||
local encoded_run_id
|
||||
local response
|
||||
local run_state=''
|
||||
local unpaused
|
||||
local -i attempt
|
||||
|
||||
dag="$(curl -sf --max-time 10 \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/${dag_id}" \
|
||||
2>/dev/null || true)"
|
||||
if ! jq -e --arg dag_id "$dag_id" '.dag_id == $dag_id' \
|
||||
>/dev/null 2>&1 <<<"$dag"; then
|
||||
fail "пробник ${dag_id} не виден через API Airflow"
|
||||
return 1
|
||||
fi
|
||||
|
||||
airflow_cleanup_dag_id="$dag_id"
|
||||
airflow_cleanup_original_paused="$(jq -r '.is_paused | tostring' <<<"$dag")"
|
||||
unpaused="$(curl -sf --max-time 10 -X PATCH \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"is_paused":false}' \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/${dag_id}" \
|
||||
2>/dev/null || true)"
|
||||
if ! jq -e '.is_paused == false' >/dev/null 2>&1 <<<"$unpaused"; then
|
||||
fail "Airflow не смог включить пробник ${dag_id} перед ручным запуском"
|
||||
cleanup_airflow_pause || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
response="$(curl -sf --max-time 10 -X POST \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"logical_date":null}' \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/${dag_id}/dagRuns" \
|
||||
2>/dev/null || true)"
|
||||
airflow_cleanup_run_id="$(jq -r '.dag_run_id // empty' <<<"$response")"
|
||||
if [[ -z "$airflow_cleanup_run_id" ]]; then
|
||||
fail "Airflow не создал ручной запуск пробника ${dag_id}"
|
||||
cleanup_airflow_pause || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
encoded_run_id="$(jq -rn --arg value "$airflow_cleanup_run_id" '$value | @uri')"
|
||||
for ((attempt = 1; attempt <= 60; attempt++)); do
|
||||
response="$(curl -sf --max-time 10 \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/${dag_id}/dagRuns/${encoded_run_id}" \
|
||||
2>/dev/null || true)"
|
||||
run_state="$(jq -r '.state // empty' <<<"$response")"
|
||||
[[ "$run_state" == 'success' || "$run_state" == 'failed' ]] && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ "$run_state" == 'success' ]] && \
|
||||
cleanup_airflow_run && cleanup_airflow_pause; then
|
||||
pass "пробник ${dag_id} завершился успешно, запуск удалён, исходная пауза восстановлена"
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail "пробник ${dag_id} не завершился чисто: состояние ${run_state:-неизвестно}"
|
||||
cleanup_airflow_run || true
|
||||
cleanup_airflow_pause || true
|
||||
return 1
|
||||
}
|
||||
|
||||
check_airflow() {
|
||||
local config
|
||||
local connection
|
||||
local dag
|
||||
local encoded_run_id
|
||||
local dag_id
|
||||
local health
|
||||
local password
|
||||
local response
|
||||
local run_state=''
|
||||
local unpaused
|
||||
local user
|
||||
local -i attempt
|
||||
|
||||
config="$(compose config --format json 2>/dev/null || true)"
|
||||
user="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_USER // empty' <<<"$config")"
|
||||
@@ -419,29 +486,22 @@ check_airflow() {
|
||||
'{username: $username, password: $password}')" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/auth/token" 2>/dev/null || true)"
|
||||
airflow_cleanup_token="$(jq -r '.access_token // empty' <<<"$response")"
|
||||
dag="$(curl -sf --max-time 10 \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/example_clickstream_hello" \
|
||||
2>/dev/null || true)"
|
||||
if [[ -n "$airflow_cleanup_token" ]] && \
|
||||
jq -e '.dag_id == "example_clickstream_hello"' >/dev/null 2>&1 <<<"$dag"; then
|
||||
airflow_cleanup_original_paused="$(jq -r '.is_paused | tostring' <<<"$dag")"
|
||||
pass 'учётные данные администратора Airflow принимаются, пример DAG виден через API'
|
||||
else
|
||||
fail 'Airflow не принял учётные данные администратора или не показал пример DAG'
|
||||
return
|
||||
fi
|
||||
|
||||
unpaused="$(curl -sf --max-time 10 -X PATCH \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"is_paused":false}' \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/example_clickstream_hello" \
|
||||
2>/dev/null || true)"
|
||||
if ! jq -e '.is_paused == false' >/dev/null 2>&1 <<<"$unpaused"; then
|
||||
fail 'Airflow не смог включить пример DAG перед ручным запуском'
|
||||
if [[ -z "$airflow_cleanup_token" ]]; then
|
||||
fail 'Airflow не принял учётные данные администратора'
|
||||
return
|
||||
fi
|
||||
for dag_id in test_clickhouse test_kafka; do
|
||||
dag="$(curl -sf --max-time 10 \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/${dag_id}" \
|
||||
2>/dev/null || true)"
|
||||
if ! jq -e --arg dag_id "$dag_id" '.dag_id == $dag_id' \
|
||||
>/dev/null 2>&1 <<<"$dag"; then
|
||||
fail "Airflow не показал пробник ${dag_id}"
|
||||
return
|
||||
fi
|
||||
done
|
||||
pass 'учётные данные администратора Airflow принимаются, оба пробника видны через API'
|
||||
|
||||
connection="$(curl -sf --max-time 10 \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
@@ -463,34 +523,8 @@ check_airflow() {
|
||||
fail 'подключение Airflow не указывает на clickhouse-01 или нода недоступна из контейнера'
|
||||
fi
|
||||
|
||||
response="$(curl -sf --max-time 10 -X POST \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"logical_date":null}' \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/example_clickstream_hello/dagRuns" \
|
||||
2>/dev/null || true)"
|
||||
airflow_cleanup_run_id="$(jq -r '.dag_run_id // empty' <<<"$response")"
|
||||
if [[ -z "$airflow_cleanup_run_id" ]]; then
|
||||
fail 'Airflow не создал ручной запуск примера DAG'
|
||||
return
|
||||
fi
|
||||
|
||||
encoded_run_id="$(jq -rn --arg value "$airflow_cleanup_run_id" '$value | @uri')"
|
||||
for ((attempt = 1; attempt <= 60; attempt++)); do
|
||||
response="$(curl -sf --max-time 10 \
|
||||
-H "Authorization: Bearer ${airflow_cleanup_token}" \
|
||||
"http://127.0.0.1:${airflow_cleanup_port}/api/v2/dags/example_clickstream_hello/dagRuns/${encoded_run_id}" \
|
||||
2>/dev/null || true)"
|
||||
run_state="$(jq -r '.state // empty' <<<"$response")"
|
||||
[[ "$run_state" == 'success' || "$run_state" == 'failed' ]] && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [[ "$run_state" == 'success' ]] && cleanup_airflow_run && cleanup_airflow_pause; then
|
||||
pass 'ручной запуск примера DAG завершился успешно, удалён, исходная пауза восстановлена'
|
||||
else
|
||||
fail "ручной запуск примера DAG не завершился чисто: состояние ${run_state:-неизвестно}"
|
||||
fi
|
||||
run_airflow_probe test_clickhouse || true
|
||||
run_airflow_probe test_kafka || true
|
||||
}
|
||||
|
||||
check_superset() {
|
||||
|
||||
Reference in New Issue
Block a user