- 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
395 lines
17 KiB
Bash
Executable File
395 lines
17 KiB
Bash
Executable File
#!/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
|