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
This commit is contained in:
148
examples/maintenance/README.md
Normal file
148
examples/maintenance/README.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Maintenance API — примеры интеграции
|
||||||
|
|
||||||
|
Примеры внешних вызовов API обслуживания (баннеры «ведутся технические работы»
|
||||||
|
на дашбордах Superset) из скриптов: CI/CD, cron, Airflow DAG, ad-hoc отладка.
|
||||||
|
|
||||||
|
## Содержимое
|
||||||
|
|
||||||
|
| Файл | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `maintenance-api-bash.sh` | Пример на bash (curl) — для shell-окружений без Python |
|
||||||
|
| `maintenance-api-python.py` | Пример на Python (requests) — для ETL-пайплайнов |
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- **start** — создать событие обслуживания для таблиц (1..100) с окном
|
||||||
|
начала/окончания, сообщением баннера и опциональным авто-завершением
|
||||||
|
(`--auto-end` / `auto_end=true`);
|
||||||
|
- **end** — завершить конкретное событие (снять баннеры);
|
||||||
|
- **end-all** — аварийно завершить ВСЕ активные события в окружении.
|
||||||
|
|
||||||
|
## API — кратко
|
||||||
|
|
||||||
|
- Базовый URL: `{BASE_URL}/api/maintenance`
|
||||||
|
- Аутентификация: заголовок `X-API-Key: <API_KEY>` (или JWT)
|
||||||
|
- Разрешения ключа: `maintenance:start`, `maintenance:end`, `maintenance:end_all`
|
||||||
|
- Все мутационные эндпоинты возвращают **HTTP 202** и `task_id` — операция
|
||||||
|
выполняется асинхронно через TaskManager.
|
||||||
|
- Ответы — сырые тела схем (без обёртки), например:
|
||||||
|
`{ "task_id": "...", "maintenance_id": "...", "status": "pending" }`.
|
||||||
|
|
||||||
|
### Эндпоинты
|
||||||
|
|
||||||
|
| Метод | Путь | Разрешение | Описание |
|
||||||
|
|---|---|---|---|
|
||||||
|
| POST | `/api/maintenance/start` | `maintenance:start` | Создать событие (202) |
|
||||||
|
| POST | `/api/maintenance/{id}/end` | `maintenance:end` | Завершить событие (202) |
|
||||||
|
| POST | `/api/maintenance/end-all` | `maintenance:end_all` | Завершить все (202) |
|
||||||
|
| GET | `/api/maintenance/events` | `maintenance` READ | Активные/завершённые события |
|
||||||
|
| GET | `/api/maintenance/dashboard-banners` | `maintenance` READ | Баннеры по дашбордам |
|
||||||
|
| POST | `/api/maintenance/preview-dashboards` | `maintenance` READ | Какие дашборды затронуты |
|
||||||
|
| GET/PUT | `/api/maintenance/settings` | READ / admin | Настройки обслуживания |
|
||||||
|
|
||||||
|
### Параметры `start`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tables": ["public.messages", "public.users"],
|
||||||
|
"start_time": "2026-08-10T08:00:00Z",
|
||||||
|
"end_time": "2026-08-10T12:00:00Z",
|
||||||
|
"environment_id": "ss-dev",
|
||||||
|
"auto_end": false,
|
||||||
|
"message": "Плановое обновление данных"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `tables` — обязателен (1..100); `start_time`, `environment_id` — обязательны.
|
||||||
|
- `auto_end` — по умолчанию `false`; если `true` и задан `end_time`,
|
||||||
|
планировщик сам завершит обслуживание ровно в `end_time`. **Без `auto_end`
|
||||||
|
событие останется активным до ручного завершения командой `end`.**
|
||||||
|
- Ошибки: `400` (невалидные данные, `end_time <= start_time`, >100 таблиц),
|
||||||
|
`401`, `403`, `404` (неизвестное окружение), `409` — идемпотентность
|
||||||
|
(`status: "already_active"` — такое же окно уже активно).
|
||||||
|
- `end` идемпотентен: повторное завершение уже завершённого события даёт
|
||||||
|
`202` с `status: "already_completed"` (считается успехом).
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
|
||||||
|
- **bash**: `curl`, желательно `jq` (форматирование ответов; без него вывод
|
||||||
|
остаётся сырым JSON).
|
||||||
|
- **python**: Python 3.10+, `requests` (`pip install requests`).
|
||||||
|
|
||||||
|
### Подготовка
|
||||||
|
|
||||||
|
1. Создайте API-ключ в superset-tools с разрешениями
|
||||||
|
`maintenance:start` / `maintenance:end` / `maintenance:end_all`.
|
||||||
|
2. Задайте переменные окружения (bash) или флаги `--base-url` / `--api-key`
|
||||||
|
(python).
|
||||||
|
|
||||||
|
### Bash
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SS_TOOLS_URL=http://localhost:8000
|
||||||
|
export SS_TOOLS_API_KEY=ssk_ваш_ключ
|
||||||
|
|
||||||
|
# Обслуживание на 4 часа (без авто-завершения)
|
||||||
|
./maintenance-api-bash.sh start public.messages 4 ss-dev
|
||||||
|
|
||||||
|
# Несколько таблиц, сообщение, автоснятие баннера через 4 часа
|
||||||
|
./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
|
||||||
|
```
|
||||||
|
|
||||||
|
Выходные коды: `0` — успех; `1` — ошибка (данные, права, ключ, недоступность
|
||||||
|
сервера). `end-all` в интерактивном режиме запрашивает подтверждение; в
|
||||||
|
неинтерактивных окружениях (cron/CI) подтверждение пропускается.
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Старт (аргументы можно указывать и после подкоманды)
|
||||||
|
python maintenance-api-python.py start \
|
||||||
|
--api-key ssk_ваш_ключ \
|
||||||
|
--base-url https://superset-tools.example.com \
|
||||||
|
--tables public.messages,public.users \
|
||||||
|
--environment ss-dev --duration-hours 4 \
|
||||||
|
--message "Плановый ETL" --auto-end
|
||||||
|
|
||||||
|
# Завершить конкретное событие
|
||||||
|
python maintenance-api-python.py end \
|
||||||
|
--api-key ssk_ваш_ключ \
|
||||||
|
--base-url https://superset-tools.example.com \
|
||||||
|
--event-id m-abc123
|
||||||
|
|
||||||
|
# Аварийно завершить всё в окружении
|
||||||
|
python maintenance-api-python.py end-all \
|
||||||
|
--api-key ssk_ваш_ключ \
|
||||||
|
--base-url https://superset-tools.example.com \
|
||||||
|
--environment ss-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Функции `start_maintenance` / `end_maintenance` / `end_all_maintenance` можно
|
||||||
|
импортировать напрямую в свой ETL-пайплайн.
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- **Не передавайте API-ключ аргументами командной строки** (попадает в историю
|
||||||
|
shell/ps) — используйте переменные окружения или защищённые секреты
|
||||||
|
(CI/CD secrets, vault).
|
||||||
|
- Ключ с ограниченным окружением может работать только с этим окружением —
|
||||||
|
выберите правильный `environment_id`.
|
||||||
|
- `end-all` снимает баннеры со всех дашбордов — применяйте только в аварийных
|
||||||
|
сценариях.
|
||||||
|
|
||||||
|
## Примеры автоматизации
|
||||||
|
|
||||||
|
- **cron** (снятие баннера ровно в конце окна через `auto_end` не нужно —
|
||||||
|
планировщик сам завершит; `end` пригодится для досрочного завершения).
|
||||||
|
- **CI/CD pipeline**: после деплоя ETL — `start`; по завершении джобы — `end`.
|
||||||
|
- **Airflow DAG**: Python-функции из `maintenance-api-python.py` в
|
||||||
|
`PythonOperator` либо `BashOperator` со скриптом.
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
# maintenance:end — завершение конкретного события (POST /{id}/end)
|
# maintenance:end — завершение конкретного события (POST /{id}/end)
|
||||||
# maintenance:end_all — массовое завершение всех событий (POST /end-all)
|
# maintenance:end_all — массовое завершение всех событий (POST /end-all)
|
||||||
#
|
#
|
||||||
# Все ответы приходят в конверте-обёртке {status, data, error, meta}.
|
# ----------------------------------------------------------------------------
|
||||||
|
# Все ответы приходят как сырое тело схемы (без обёртки), например:
|
||||||
|
# { "task_id": "…", "maintenance_id": "…", "status": "pending" }
|
||||||
# Все мутационные (изменяющие) эндпоинты возвращают HTTP 202 и task_id —
|
# Все мутационные (изменяющие) эндпоинты возвращают HTTP 202 и task_id —
|
||||||
# операция выполняется асинхронно через планировщик (TaskManager).
|
# операция выполняется асинхронно через планировщик (TaskManager).
|
||||||
#
|
#
|
||||||
@@ -54,9 +56,10 @@
|
|||||||
# Тело: отсутствует.
|
# Тело: отсутствует.
|
||||||
# Ответ 202: { "task_id": "…", "status": "pending" }
|
# Ответ 202: { "task_id": "…", "status": "pending" }
|
||||||
# Ошибки:
|
# Ошибки:
|
||||||
# 404 — событие не найдено; если status == "already_completed" — событие
|
# 404 — событие не найдено;
|
||||||
# уже завершено (идемпотентно, считается успехом);
|
|
||||||
# 401/403 — аутентификация/права.
|
# 401/403 — аутентификация/права.
|
||||||
|
# Идемпотентность: повторный end уже завершённого события — тоже 202,
|
||||||
|
# но с status == "already_completed" (считается успехом).
|
||||||
#
|
#
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# 3) POST /api/maintenance/end-all — завершить ВСЕ активные события
|
# 3) POST /api/maintenance/end-all — завершить ВСЕ активные события
|
||||||
@@ -139,7 +142,10 @@ fi
|
|||||||
ok() { echo -e "${GREEN}✅${NC} $*"; }
|
ok() { echo -e "${GREEN}✅${NC} $*"; }
|
||||||
info() { echo -e "${CYAN}ℹ️ ${NC} $*"; }
|
info() { echo -e "${CYAN}ℹ️ ${NC} $*"; }
|
||||||
warn() { echo -e "${YELLOW}⚠️ ${NC} $*"; }
|
warn() { echo -e "${YELLOW}⚠️ ${NC} $*"; }
|
||||||
fail() { echo -e "${RED}❌${NC} $*"; exit 1; }
|
# 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 ─────────────────────────────────────────────────────────────
|
# ── Auth check ─────────────────────────────────────────────────────────────
|
||||||
check_auth() {
|
check_auth() {
|
||||||
@@ -148,6 +154,13 @@ check_auth() {
|
|||||||
fi
|
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 ────────────────────────────────────────────────────────────
|
# ── API helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# Call the API with X-API-Key auth and handle common errors
|
# Call the API with X-API-Key auth and handle common errors
|
||||||
@@ -181,12 +194,12 @@ api_call() {
|
|||||||
|
|
||||||
case "$http_code" in
|
case "$http_code" in
|
||||||
202)
|
202)
|
||||||
echo "$body" # success
|
echo "$body" # success — only the raw body goes to stdout (captured by callers)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
400)
|
400)
|
||||||
warn "Validation error:"
|
warn "Validation error:" >&2
|
||||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
echo "$body" | jq . 2>/dev/null >&2 || echo "$body" >&2
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
401)
|
401)
|
||||||
@@ -196,26 +209,20 @@ api_call() {
|
|||||||
fail "Permission denied: API key lacks required permission"
|
fail "Permission denied: API key lacks required permission"
|
||||||
;;
|
;;
|
||||||
404)
|
404)
|
||||||
local status
|
warn "Not found (404):" >&2
|
||||||
status=$(echo "$body" | jq -r '.status // "not_found"' 2>/dev/null)
|
echo "$body" | jq . 2>/dev/null >&2 || echo "$body" >&2
|
||||||
if [[ "$status" == "already_completed" ]]; then
|
|
||||||
info "Event was already completed (idempotent)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
warn "Not found (404):"
|
|
||||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
409)
|
409)
|
||||||
local status
|
local status
|
||||||
status=$(echo "$body" | jq -r '.status // ""' 2>/dev/null)
|
status=$(echo "$body" | jq -r '.status // ""' 2>/dev/null)
|
||||||
if [[ "$status" == "already_active" ]]; then
|
if [[ "$status" == "already_active" ]]; then
|
||||||
info "Event already active (idempotent)"
|
info "Event already active (idempotent)" >&2
|
||||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
echo "$body" # raw body to stdout for the caller (maintenance_id)
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
warn "Conflict (409):"
|
warn "Conflict (409):" >&2
|
||||||
echo "$body" | jq . 2>/dev/null || echo "$body"
|
echo "$body" | jq . 2>/dev/null >&2 || echo "$body" >&2
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -253,7 +260,7 @@ EOF
|
|||||||
info "Auto-end enabled: banner will be removed at ${end_time}"
|
info "Auto-end enabled: banner will be removed at ${end_time}"
|
||||||
fi
|
fi
|
||||||
if [[ -n "$message" ]]; then
|
if [[ -n "$message" ]]; then
|
||||||
payload="$payload"$',\n "message": "'"$message"'"'
|
payload="$payload"$',\n "message": "'"$(json_escape "$message")"'"'
|
||||||
fi
|
fi
|
||||||
payload="$payload"$'\n}'
|
payload="$payload"$'\n}'
|
||||||
|
|
||||||
@@ -261,15 +268,14 @@ EOF
|
|||||||
info "Window: ${start_time} → ${end_time}"
|
info "Window: ${start_time} → ${end_time}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
if response=$(api_call POST "/api/maintenance/start" "$payload"); then
|
response=$(api_call POST "/api/maintenance/start" "$payload") || return 1
|
||||||
local event_id
|
local event_id
|
||||||
event_id=$(echo "$response" | jq -r '.maintenance_id' 2>/dev/null || echo "unknown")
|
event_id=$(echo "$response" | jq -r '.maintenance_id' 2>/dev/null || echo "unknown")
|
||||||
local task_id
|
local task_id
|
||||||
task_id=$(echo "$response" | jq -r '.task_id' 2>/dev/null || echo "unknown")
|
task_id=$(echo "$response" | jq -r '.task_id' 2>/dev/null || echo "unknown")
|
||||||
ok "Maintenance event created: ${BOLD}${event_id}${NC}"
|
ok "Maintenance event created: ${BOLD}${event_id}${NC}"
|
||||||
info "Task: ${task_id}"
|
info "Task: ${task_id}"
|
||||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_end() {
|
cmd_end() {
|
||||||
@@ -278,10 +284,9 @@ cmd_end() {
|
|||||||
info "Ending maintenance event: ${event_id}"
|
info "Ending maintenance event: ${event_id}"
|
||||||
|
|
||||||
local response
|
local response
|
||||||
if response=$(api_call POST "/api/maintenance/${event_id}/end" ""); then
|
response=$(api_call POST "/api/maintenance/${event_id}/end" "") || return 1
|
||||||
ok "Maintenance event end scheduled"
|
ok "Maintenance event end scheduled"
|
||||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_end_all() {
|
cmd_end_all() {
|
||||||
@@ -313,16 +318,13 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local response
|
local response
|
||||||
if response=$(api_call POST "/api/maintenance/end-all" "$payload"); then
|
response=$(api_call POST "/api/maintenance/end-all" "$payload") || return 1
|
||||||
ok "End-all scheduled"
|
ok "End-all scheduled"
|
||||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Main ───────────────────────────────────────────────────────────────────
|
# ── Main ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
check_auth
|
|
||||||
|
|
||||||
# Filter the --auto-end flag out of the positional args
|
# Filter the --auto-end flag out of the positional args
|
||||||
ARGS=()
|
ARGS=()
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
@@ -336,6 +338,7 @@ set -- "${ARGS[@]}"
|
|||||||
|
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
start)
|
start)
|
||||||
|
check_auth
|
||||||
if [[ $# -lt 3 ]]; then
|
if [[ $# -lt 3 ]]; then
|
||||||
echo "Usage: $0 start <tables> [duration_hours] <environment> [message] [--auto-end]"
|
echo "Usage: $0 start <tables> [duration_hours] <environment> [message] [--auto-end]"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -350,6 +353,7 @@ case "${1:-help}" in
|
|||||||
cmd_start "$2" "${3:-4}" "${4:-}" "${5:-}"
|
cmd_start "$2" "${3:-4}" "${4:-}" "${5:-}"
|
||||||
;;
|
;;
|
||||||
end)
|
end)
|
||||||
|
check_auth
|
||||||
if [[ $# -lt 2 ]]; then
|
if [[ $# -lt 2 ]]; then
|
||||||
echo "Usage: $0 end <event-id>"
|
echo "Usage: $0 end <event-id>"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -357,6 +361,7 @@ case "${1:-help}" in
|
|||||||
cmd_end "$2"
|
cmd_end "$2"
|
||||||
;;
|
;;
|
||||||
end-all)
|
end-all)
|
||||||
|
check_auth
|
||||||
cmd_end_all "${2:-}"
|
cmd_end_all "${2:-}"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ Usage:
|
|||||||
maintenance:end — завершение конкретного события (POST /{id}/end)
|
maintenance:end — завершение конкретного события (POST /{id}/end)
|
||||||
maintenance:end_all — массовое завершение всех событий (POST /end-all)
|
maintenance:end_all — массовое завершение всех событий (POST /end-all)
|
||||||
|
|
||||||
Все ответы приходят в обёртке {status, data, error, meta}.
|
Ответы мутационных эндпоинтов — это сырые тела схем (без обёртки), например
|
||||||
|
{ "task_id": "...", "maintenance_id": "...", "status": "pending" }.
|
||||||
Все мутационные эндпоинты возвращают HTTP 202 и task_id — операция
|
Все мутационные эндпоинты возвращают HTTP 202 и task_id — операция
|
||||||
выполняется асинхронно планировщиком (TaskManager).
|
выполняется асинхронно планировщиком (TaskManager).
|
||||||
|
|
||||||
@@ -76,9 +77,10 @@ Usage:
|
|||||||
HTTP 202 Accepted | Разрешение: maintenance:end | Тело: отсутствует.
|
HTTP 202 Accepted | Разрешение: maintenance:end | Тело: отсутствует.
|
||||||
Ответ 202: { "task_id": "...", "status": "pending" }
|
Ответ 202: { "task_id": "...", "status": "pending" }
|
||||||
Ошибки:
|
Ошибки:
|
||||||
404 — событие не найдено; если status == "already_completed" — уже
|
404 — событие не найдено;
|
||||||
завершено (идемпотентно, успех);
|
|
||||||
401/403 — аутентификация/права.
|
401/403 — аутентификация/права.
|
||||||
|
Идемпотентность: повторный end уже завершённого события — тоже 202,
|
||||||
|
но с status == "already_completed" (считается успехом).
|
||||||
|
|
||||||
--- POST /api/maintenance/end-all ---
|
--- POST /api/maintenance/end-all ---
|
||||||
HTTP 202 Accepted | Разрешение: maintenance:end_all
|
HTTP 202 Accepted | Разрешение: maintenance:end_all
|
||||||
@@ -260,14 +262,13 @@ def end_maintenance(base_url: str, api_key: str, event_id: str,
|
|||||||
|
|
||||||
if response.status_code == 202:
|
if response.status_code == 202:
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
if result.get("status") == "already_completed":
|
||||||
|
print("[maintenance] ℹ️ Event was already completed (idempotent)")
|
||||||
|
return result
|
||||||
print(f"[maintenance] ✅ Event end scheduled (task={result.get('task_id', 'N/A')})")
|
print(f"[maintenance] ✅ Event end scheduled (task={result.get('task_id', 'N/A')})")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
data = response.json()
|
|
||||||
if data.get("status") == "already_completed":
|
|
||||||
print("[maintenance] ℹ️ Event was already completed (idempotent)")
|
|
||||||
return data
|
|
||||||
print(f"[maintenance] ❌ Event not found: {event_id}")
|
print(f"[maintenance] ❌ Event not found: {event_id}")
|
||||||
elif response.status_code == 401:
|
elif response.status_code == 401:
|
||||||
print("[maintenance] ❌ Authentication failed: invalid or revoked API key")
|
print("[maintenance] ❌ Authentication failed: invalid or revoked API key")
|
||||||
@@ -364,12 +365,17 @@ Examples:
|
|||||||
%(prog)s end-all --api-key ssk_... --environment ss-dev
|
%(prog)s end-all --api-key ssk_... --environment ss-dev
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
|
# Common options are defined on each subparser (via parents) so they can be
|
||||||
|
# placed AFTER the subcommand — matching the usage examples above. argparse
|
||||||
|
# does not reliably accept parent-parser options after the subcommand.
|
||||||
|
common_opts = argparse.ArgumentParser(add_help=False)
|
||||||
|
common_opts.add_argument(
|
||||||
"--base-url",
|
"--base-url",
|
||||||
default="http://localhost:8000",
|
default="http://localhost:8000",
|
||||||
help="superset-tools base URL (default: %(default)s)",
|
help="superset-tools base URL (default: %(default)s)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
common_opts.add_argument(
|
||||||
"--api-key",
|
"--api-key",
|
||||||
required=True,
|
required=True,
|
||||||
help="API key with required permissions",
|
help="API key with required permissions",
|
||||||
@@ -378,7 +384,7 @@ Examples:
|
|||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
# start
|
# start
|
||||||
start_parser = subparsers.add_parser("start", help="Start maintenance")
|
start_parser = subparsers.add_parser("start", parents=[common_opts], help="Start maintenance")
|
||||||
start_parser.add_argument(
|
start_parser.add_argument(
|
||||||
"--tables",
|
"--tables",
|
||||||
required=True,
|
required=True,
|
||||||
@@ -409,7 +415,7 @@ Examples:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# end
|
# end
|
||||||
end_parser = subparsers.add_parser("end", help="End a specific maintenance event")
|
end_parser = subparsers.add_parser("end", parents=[common_opts], help="End a specific maintenance event")
|
||||||
end_parser.add_argument(
|
end_parser.add_argument(
|
||||||
"--event-id",
|
"--event-id",
|
||||||
required=True,
|
required=True,
|
||||||
@@ -418,7 +424,7 @@ Examples:
|
|||||||
|
|
||||||
# end-all
|
# end-all
|
||||||
end_all_parser = subparsers.add_parser(
|
end_all_parser = subparsers.add_parser(
|
||||||
"end-all", help="End ALL maintenance events (use with caution)"
|
"end-all", parents=[common_opts], help="End ALL maintenance events (use with caution)"
|
||||||
)
|
)
|
||||||
end_all_parser.add_argument(
|
end_all_parser.add_argument(
|
||||||
"--environment",
|
"--environment",
|
||||||
@@ -454,6 +460,11 @@ Examples:
|
|||||||
environment_id=args.environment_id,
|
environment_id=args.environment_id,
|
||||||
)
|
)
|
||||||
except requests.HTTPError:
|
except requests.HTTPError:
|
||||||
|
# Specific error message was already printed by the command function.
|
||||||
|
sys.exit(1)
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
# Network-level failure (connection refused, timeout, DNS, ...).
|
||||||
|
print(f"[maintenance] ❌ Request failed: {exc}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user