Files
ss-tools/examples/maintenance/maintenance-api-bash.sh
busya 4bc244c228 fix(examples): maintenance API scripts — error handling, JSON safety, docs
- bash: propagate api_call failures (exit 1 on 400/401/403/404/network),
  write diagnostics to stderr, escape message for JSON safety, help without
  API key
- python: argparse options after subcommand (parents), single error message
  per failure, network errors without traceback, idempotent already_completed
- move scripts to examples/maintenance/ with README instructions
- backend: correct stale envelope-shape comment in maintenance schemas
2026-08-10 12:04:50 +03:00

395 lines
17 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# ============================================================================
# Example: External tool triggers maintenance via superset-tools API (bash)
#
# This script shows how to call the superset-tools maintenance API from any
# shell environment — CI/CD pipeline, cron job, or ad-hoc debugging.
#
# ============================================================================
# API SPECIFICATION / СПЕЦИФИКАЦИЯ API ОБСЛУЖИВАНИЯ
# ============================================================================
# Базовый URL: {BASE_URL}/api/maintenance
# Аутентификация: заголовок X-API-Key: <API_KEY> (или JWT-токен)
#
# КЛЮЧ API — РАЗРЕШЕНИЯ (permissions), которые должны быть у ключа:
# maintenance:start — создание события обслуживания (POST /start)
# maintenance:end — завершение конкретного события (POST /{id}/end)
# maintenance:end_all — массовое завершение всех событий (POST /end-all)
#
# ----------------------------------------------------------------------------
# Все ответы приходят как сырое тело схемы (без обёртки), например:
# { "task_id": "…", "maintenance_id": "…", "status": "pending" }
# Все мутационные (изменяющие) эндпоинты возвращают HTTP 202 и task_id —
# операция выполняется асинхронно через планировщик (TaskManager).
#
# ----------------------------------------------------------------------------
# 1) POST /api/maintenance/start — начать обслуживание таблиц
# HTTP 202 Accepted (задача создана)
# Разрешение: maintenance:start (или RBAC maintenance / WRITE)
#
# Тело запроса (JSON):
# tables: [string] ОБЯЗАТЕЛЬНО. Список таблиц (1..100).
# start_time: datetime ОБЯЗАТЕЛЬНО. Начало окна (ISO 8601).
# end_time: datetime|optional Окончание окна; должно быть
# позже start_time (необязательно).
# auto_end: bool По умолчанию false. Если true и задан end_time,
# планировщик сам завершит обслуживание в end_time.
# message: string|optional Текст баннера (макс. 500 симв.).
# environment_id: string ОБЯЗАТЕЛЬНО. Целевое окружение (ss-dev, ss-prod…).
#
# Ответ 202:
# { "task_id": "…", "maintenance_id": "…", "status": "pending" }
#
# Ошибки:
# 400 — невалидные данные (end_time <= start_time; start_time слишком в
# прошлом — допуск 1 час; > 100 таблиц);
# 401 — неверный/отозванный API-ключ;
# 403 — недостаточно прав;
# 404 — неизвестное environment_id;
# 409 — идемпотентность: такое же (tables, start_time, end_time) уже активно:
# { "maintenance_id": "…", "status": "already_active" }.
#
# ----------------------------------------------------------------------------
# 2) POST /api/maintenance/{maintenance_id}/end — завершить конкретное событие
# HTTP 202 Accepted
# Разрешение: maintenance:end (или RBAC maintenance / WRITE)
# Тело: отсутствует.
# Ответ 202: { "task_id": "…", "status": "pending" }
# Ошибки:
# 404 — событие не найдено;
# 401/403 — аутентификация/права.
# Идемпотентность: повторный end уже завершённого события — тоже 202,
# но с status == "already_completed" (считается успехом).
#
# ----------------------------------------------------------------------------
# 3) POST /api/maintenance/end-all — завершить ВСЕ активные события
# HTTP 202 Accepted (ОСТОРОЖНО: снимает баннеры со всех дашбордов!)
# Разрешение: maintenance:end_all (или RBAC maintenance / WRITE)
# Тело (JSON, опционально): { "environment_id": "…" }
# Если не указан — используется окружение из ключа API.
# Ответ 202: { "task_id": "…", "status": "pending" }
#
# ----------------------------------------------------------------------------
# Прочие (read-only) эндпоинты того же модуля (для справки, требуют READ):
# GET /api/maintenance/events — списки active и completed событий
# GET /api/maintenance/dashboard-banners — состояние баннеров по дашбордам
# POST /api/maintenance/preview-dashboards — какие дашборды затронут таблицы
# GET /api/maintenance/settings — настройки обслуживания
# PUT /api/maintenance/settings — обновить настройки (только admin)
#
# ============================================================================
# ИНСТРУКЦИЯ ПО ИСПОЛЬЗОВАНИЮ (РУССКИЙ)
# ============================================================================
# Назначение: ставить и снимать баннер «ведутся технические работы» на
# дашбордах Superset при обновлении таблиц (ETL, cron, CI/CD).
#
# ПРЕДВАРИТЕЛЬНЫЕ ШАГИ:
# 1. Убедитесь, что установлены curl и (желательно) jq.
# 2. Создайте API-ключ superset-tools с нужными разрешениями
# (maintenance:start / maintenance:end / maintenance:end_all).
# 3. Задайте переменные окружения:
# export SS_TOOLS_URL=http://localhost:8000
# export SS_TOOLS_API_KEY=ssk_ваш_ключ
# (или замените значения прямо в скрипте).
#
# КОМАНДЫ:
# Запустить обслуживание на 4 часа:
# ./maintenance-api-bash.sh start public.messages 4 ss-dev
# Несколько таблиц через запятую, с сообщением и автоснятием баннера:
# ./maintenance-api-bash.sh start public.messages,public.users 4 ss-prod \
# "Плановый ETL" --auto-end
# Завершить конкретное событие:
# ./maintenance-api-bash.sh end m-abc123
# Аварийно снять ВСЕ баннеры в окружении ss-dev:
# ./maintenance-api-bash.sh end-all ss-dev
#
# О ПАРАМЕТРЕ --auto-end:
# start_time и end_time вычисляются автоматически (сейчас и сейчас+часы).
# Без --auto-end end_time носит справочный характер — баннер нужно снять
# вручную командой `end`. С --auto-end баннер снимут автоматически ровно
# в end_time.
#
# ВЫХОДНЫЕ КОДЫ: 0 — успех; 1 — ошибка (неверные данные, отсутствие прав,
# неверный ключ, недоступность сервера).
#
# ЗАМЕЧАНИЯ ПО БЕЗОПАСНОСТИ:
# - Не передавайте ключ API в аргументах командной строки (попадает в
# историю shell/ps). Используйте переменные окружения.
# - `end-all` подтверждается в интерактивном режиме; в неинтерактивных
# окружениях (cron/CI) запрос подтверждения пропускается.
# ============================================================================
set -euo pipefail
# ── Configuration ──────────────────────────────────────────────────────────
# Set these via environment variables or replace inline
BASE_URL="${SS_TOOLS_URL:-http://localhost:8000}"
API_KEY="${SS_TOOLS_API_KEY:-}"
# ── Auto-end flag (opt-in automatic ending at end_time) ───────────────────
# Pass `--auto-end` to `start`: the banner is removed automatically at end_time.
# Without it, end_time is informational only and maintenance must be ended manually.
AUTO_END=0
# ── Color helpers (disabled in non-TTY) ────────────────────────────────────
if [[ -t 1 ]]; then
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
else
GREEN=''; RED=''; YELLOW=''; CYAN=''; BOLD=''; NC=''
fi
ok() { echo -e "${GREEN}${NC} $*"; }
info() { echo -e "${CYAN} ${NC} $*"; }
warn() { echo -e "${YELLOW}⚠️ ${NC} $*"; }
# NOTE: fail() writes to stderr and exits the current shell. Because api_call is
# invoked inside `$(...)` (a subshell), the exit only propagates as a non-zero
# status — callers must `|| return 1` to stop the command chain.
fail() { echo -e "${RED}${NC} $*" >&2; exit 1; }
# ── Auth check ─────────────────────────────────────────────────────────────
check_auth() {
if [[ -z "$API_KEY" ]]; then
fail "API key not set. Export SS_TOOLS_API_KEY or edit the script."
fi
}
# ── JSON escaping helper ───────────────────────────────────────────────────
# Escape backslashes and double quotes, strip control chars, so free-text
# fields (e.g. message) never break the JSON payload.
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037'
}
# ── API helpers ────────────────────────────────────────────────────────────
# Call the API with X-API-Key auth and handle common errors
api_call() {
local method="$1" # GET / POST
local endpoint="$2" # /api/maintenance/start
local data="$3" # JSON payload or empty string
local curl_args=(
-X "$method"
-sS
-H "X-API-Key: $API_KEY"
-H "Content-Type: application/json"
)
if [[ -n "$data" ]]; then
curl_args+=(-d "$data")
fi
local http_code
local response_file
response_file=$(mktemp)
http_code=$(curl -w '%{http_code}' "${curl_args[@]}" \
"${BASE_URL}${endpoint}" \
-o "$response_file" 2>/dev/null)
local body
body=$(<"$response_file")
rm -f "$response_file"
case "$http_code" in
202)
echo "$body" # success — only the raw body goes to stdout (captured by callers)
return 0
;;
400)
warn "Validation error:" >&2
echo "$body" | jq . 2>/dev/null >&2 || echo "$body" >&2
return 1
;;
401)
fail "Authentication failed: invalid or revoked API key"
;;
403)
fail "Permission denied: API key lacks required permission"
;;
404)
warn "Not found (404):" >&2
echo "$body" | jq . 2>/dev/null >&2 || echo "$body" >&2
return 1
;;
409)
local status
status=$(echo "$body" | jq -r '.status // ""' 2>/dev/null)
if [[ "$status" == "already_active" ]]; then
info "Event already active (idempotent)" >&2
echo "$body" # raw body to stdout for the caller (maintenance_id)
return 0
fi
warn "Conflict (409):" >&2
echo "$body" | jq . 2>/dev/null >&2 || echo "$body" >&2
return 1
;;
*)
fail "Unexpected HTTP $http_code: $(echo "$body" | head -c 500)"
;;
esac
}
# ── Commands ───────────────────────────────────────────────────────────────
cmd_start() {
local tables="$1"
local duration_hours="${2:-4}"
local environment_id="$3"
local message="${4:-}"
local start_time
local end_time
start_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
end_time=$(date -u -d "+${duration_hours} hours" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|| date -u -v "+${duration_hours}H" +"%Y-%m-%dT%H:%M:%SZ")
# Build JSON payload
local payload
payload=$(cat <<EOF
{
"tables": [$(echo "$tables" | sed 's/[^,]*/"&"/g')],
"start_time": "$start_time",
"end_time": "$end_time",
"environment_id": "$environment_id"
EOF
)
if [[ "$AUTO_END" -eq 1 ]]; then
payload="$payload"$',\n "auto_end": true'
info "Auto-end enabled: banner will be removed at ${end_time}"
fi
if [[ -n "$message" ]]; then
payload="$payload"$',\n "message": "'"$(json_escape "$message")"'"'
fi
payload="$payload"$'\n}'
info "Starting maintenance: tables=${tables}, env=${environment_id}, ${duration_hours}h"
info "Window: ${start_time}${end_time}"
local response
response=$(api_call POST "/api/maintenance/start" "$payload") || return 1
local event_id
event_id=$(echo "$response" | jq -r '.maintenance_id' 2>/dev/null || echo "unknown")
local task_id
task_id=$(echo "$response" | jq -r '.task_id' 2>/dev/null || echo "unknown")
ok "Maintenance event created: ${BOLD}${event_id}${NC}"
info "Task: ${task_id}"
echo "$response" | jq . 2>/dev/null || echo "$response"
}
cmd_end() {
local event_id="$1"
info "Ending maintenance event: ${event_id}"
local response
response=$(api_call POST "/api/maintenance/${event_id}/end" "") || return 1
ok "Maintenance event end scheduled"
echo "$response" | jq . 2>/dev/null || echo "$response"
}
cmd_end_all() {
local environment_id="${1:-}"
warn "This will remove banners from ALL affected dashboards!"
if [[ -n "$environment_id" ]]; then
info "Scope: environment=${environment_id}"
else
info "Scope: all environments (API key default)"
fi
# Prompt for confirmation in interactive mode
if [[ -t 0 ]]; then
echo -n "Continue? [y/N] "
read -r confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
info "Cancelled."
exit 0
fi
fi
local payload="{}"
if [[ -n "$environment_id" ]]; then
payload=$(cat <<EOF
{"environment_id": "$environment_id"}
EOF
)
fi
local response
response=$(api_call POST "/api/maintenance/end-all" "$payload") || return 1
ok "End-all scheduled"
echo "$response" | jq . 2>/dev/null || echo "$response"
}
# ── Main ───────────────────────────────────────────────────────────────────
# Filter the --auto-end flag out of the positional args
ARGS=()
for arg in "$@"; do
if [[ "$arg" == "--auto-end" ]]; then
AUTO_END=1
else
ARGS+=("$arg")
fi
done
set -- "${ARGS[@]}"
case "${1:-help}" in
start)
check_auth
if [[ $# -lt 3 ]]; then
echo "Usage: $0 start <tables> [duration_hours] <environment> [message] [--auto-end]"
echo ""
echo "Examples:"
echo " $0 start public.messages 4 ss-dev"
echo " $0 start public.messages,public.users 4 ss-prod \"Scheduled ETL\" --auto-end"
echo ""
echo "--auto-end: also pass auto_end=true so the banner is removed automatically"
echo " at end_time. Without it, end_time is informational only."
exit 1
fi
cmd_start "$2" "${3:-4}" "${4:-}" "${5:-}"
;;
end)
check_auth
if [[ $# -lt 2 ]]; then
echo "Usage: $0 end <event-id>"
exit 1
fi
cmd_end "$2"
;;
end-all)
check_auth
cmd_end_all "${2:-}"
;;
*)
cat <<EOF
superset-tools Maintenance CLI
Usage:
$0 start <tables> [hours=4] <environment> [message] [--auto-end]
Start maintenance on tables (comma-separated).
--auto-end also removes the banner automatically at end_time (API param auto_end=true).
$0 end <event-id>
End a specific maintenance event
$0 end-all [environment]
End ALL maintenance (use with caution)
Examples:
$0 start public.messages 4 ss-dev
$0 start public.messages 4 ss-dev "ETL refresh" --auto-end
$0 end m-abc123
$0 end-all ss-dev
Environment:
SS_TOOLS_URL superset-tools base URL (default: http://localhost:8000)
SS_TOOLS_API_KEY API key with required permissions (required)
EOF
exit 0
;;
esac