502 lines
16 KiB
Bash
Executable File
502 lines
16 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 . id
|
|
# (: GET {BASE_URL}/api/environments).
|
|
# id 404 "Environment ... not found".
|
|
# ()
|
|
# PROD- (fan-out). fan-out:
|
|
# stage == "PROD" is_production == true;
|
|
# stage DEV/PREPROD fan-out
|
|
# -.
|
|
# 202 batch- events[].
|
|
#
|
|
# 202:
|
|
# - (environment_id ):
|
|
# { "task_id": "", "maintenance_id": "", "status": "pending" }
|
|
# - batch- (environment_id , fan-out PROD):
|
|
# { "status": "pending", "events": [
|
|
# { "environment_id": "ss-prod-a", "task_id": "",
|
|
# "maintenance_id": "", "status": "pending" }, ] }
|
|
#
|
|
# :
|
|
# 400 (end_time <= start_time; start_time
|
|
# 1 ; > 100 );
|
|
# 401 / API-;
|
|
# 403 ;
|
|
# 404 environment_id ( );
|
|
# 422 environment_id , PROD- fan-out
|
|
# ;
|
|
# 409 :
|
|
# (tables, start_time, end_time) :
|
|
# { "maintenance_id": "", "status": "already_active" }.
|
|
# environment_id; batch- 409
|
|
# per-item status "already_active"
|
|
# 202 .
|
|
#
|
|
# ----------------------------------------------------------------------------
|
|
# 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/events/{id}/dashboards ({id,title});
|
|
#
|
|
# ,
|
|
#
|
|
# 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 # '/' ( '//api/' 404)
|
|
# export SS_TOOLS_API_KEY=ssk__
|
|
# ( ).
|
|
# 4. id GET {BASE_URL}/api/environments
|
|
# ( dev/prod ).
|
|
#
|
|
# :
|
|
# 4 :
|
|
# ./maintenance-api-bash.sh start public.messages 4 dev
|
|
# Fan-out PROD- ( environment; batch- events[]):
|
|
# ./maintenance-api-bash.sh start public.messages 4
|
|
# , :
|
|
# ./maintenance-api-bash.sh start public.messages,public.users 4 prod \
|
|
# " ETL" --auto-end
|
|
# environment, message - :
|
|
# ./maintenance-api-bash.sh start public.messages 4 - " ETL"
|
|
# :
|
|
# ./maintenance-api-bash.sh end m-abc123
|
|
# dev:
|
|
# ./maintenance-api-bash.sh end-all 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.
|
|
# NOTE: a trailing "/" in SS_TOOLS_URL produced "//api/maintenance/start", which
|
|
# FastAPI answers with a routing-level 404 before the request ever reaches the
|
|
# maintenance router (no auth, no fan-out logic) always strip it.
|
|
BASE_URL="${SS_TOOLS_URL:-http://localhost:8000}"
|
|
BASE_URL="${BASE_URL%/}"
|
|
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
|
|
INSECURE=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}[OK]${NC} $*"; }
|
|
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${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}[ERROR]${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 [[ "$INSECURE" -eq 1 ]]; then
|
|
curl_args+=(-k)
|
|
fi
|
|
|
|
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): ${method} ${BASE_URL}${endpoint}" >&2
|
|
echo "$body" | jq . 2>/dev/null >&2 || echo "$body" >&2
|
|
# Distinguish the two 404 kinds observed in production:
|
|
# {"detail":"Not Found"} routing-level (bad URL, e.g. "//api/")
|
|
# {"detail":"Environment <id> not found"} environment_id does not match config
|
|
if [[ "$body" == *'"Not Found"'* ]]; then
|
|
warn "Hint: routing-level 404 check SS_TOOLS_URL for a trailing '/' ('//api/')" >&2
|
|
elif [[ "$body" == *"not found"* ]]; then
|
|
warn "Hint: exact env ids are listed by GET ${BASE_URL%/}/api/environments" >&2
|
|
fi
|
|
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
|
|
;;
|
|
422)
|
|
warn "Unprocessable request (422): no PROD environments configured" >&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:-}"
|
|
|
|
# "-" is the explicit "skip this positional" marker omit environment_id
|
|
# from the payload server fans the start out to ALL PROD environments.
|
|
if [[ "$environment_id" == "-" ]]; then
|
|
environment_id=""
|
|
fi
|
|
|
|
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 environment_id is OPTIONAL:
|
|
# present single-environment start; omitted PROD fan-out (batch response).
|
|
local payload
|
|
if [[ -n "$environment_id" ]]; then
|
|
payload=$(cat <<EOF
|
|
{
|
|
"tables": [$(echo "$tables" | sed 's/[^,]*/"&"/g')],
|
|
"start_time": "$start_time",
|
|
"end_time": "$end_time",
|
|
"environment_id": "$environment_id"
|
|
EOF
|
|
)
|
|
else
|
|
payload=$(cat <<EOF
|
|
{
|
|
"tables": [$(echo "$tables" | sed 's/[^,]*/"&"/g')],
|
|
"start_time": "$start_time",
|
|
"end_time": "$end_time"
|
|
EOF
|
|
)
|
|
fi
|
|
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:-ALL PROD (fan-out)}, ${duration_hours}h"
|
|
info "Window: ${start_time} ${end_time}"
|
|
|
|
local response
|
|
response=$(api_call POST "/api/maintenance/start" "$payload") || return 1
|
|
|
|
# Batch shape detection: `.events` present fan-out start across PROD envs.
|
|
if command -v jq >/dev/null 2>&1 && echo "$response" | jq -e '.events' >/dev/null 2>&1; then
|
|
local env_count
|
|
env_count=$(echo "$response" | jq '.events | length')
|
|
ok "Maintenance batch accepted (${env_count} PROD environment(s))"
|
|
local env_id maintenance_id item_status
|
|
while IFS='|' read -r env_id maintenance_id item_status; do
|
|
if [[ "$item_status" == "already_active" ]]; then
|
|
info "${env_id} / ${maintenance_id} / ${item_status} (idempotent)"
|
|
else
|
|
ok "${env_id} / ${maintenance_id} / ${item_status}"
|
|
fi
|
|
done < <(echo "$response" | jq -r '.events[] | [.environment_id, .maintenance_id, .status] | join("|")')
|
|
echo "$response" | jq . 2>/dev/null || echo "$response"
|
|
return 0
|
|
fi
|
|
|
|
# Single shape (explicit environment_id)
|
|
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 global flags out of the positional args
|
|
ARGS=()
|
|
for arg in "$@"; do
|
|
if [[ "$arg" == "--auto-end" ]]; then
|
|
AUTO_END=1
|
|
elif [[ "$arg" == "--insecure" ]]; then
|
|
INSECURE=1
|
|
else
|
|
ARGS+=("$arg")
|
|
fi
|
|
done
|
|
set -- "${ARGS[@]}"
|
|
|
|
case "${1:-help}" in
|
|
start)
|
|
check_auth
|
|
if [[ $# -lt 2 ]]; then
|
|
echo "Usage: $0 start <tables> [duration_hours] [environment|-] [message] [--auto-end] [--insecure]"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 start public.messages 4 dev"
|
|
echo " $0 start public.messages 4 # fan-out: all PROD environments"
|
|
echo " $0 start public.messages 4 - \"Scheduled ETL\" # '-' skips environment fan-out"
|
|
echo " $0 start public.messages,public.users 4 prod \"Scheduled ETL\" --auto-end"
|
|
echo ""
|
|
echo "environment is OPTIONAL: omit it (or pass '-') to fan the start out to ALL"
|
|
echo "PROD environments (stage==PROD or is_production) the API answers with a"
|
|
echo "batch body { status, events[] }."
|
|
echo "Exact environment ids: GET \${SS_TOOLS_URL}/api/environments (unknown id 404)."
|
|
echo "422 is returned when environment is omitted and no PROD env is configured."
|
|
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."
|
|
echo "--insecure: disable TLS certificate verification (use for self-signed certs)"
|
|
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] [--insecure]
|
|
Start maintenance on tables (comma-separated).
|
|
environment is OPTIONAL: omit it (or pass '-') to start in ALL PROD
|
|
environments (fan-out) the response is a batch { status, events[] }.
|
|
--auto-end also removes the banner automatically at end_time (API param auto_end=true).
|
|
--insecure disables TLS certificate verification (use for self-signed certs).
|
|
|
|
$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 dev
|
|
$0 start public.messages 4 # fan-out: all PROD environments
|
|
$0 start public.messages 4 dev "ETL refresh" --auto-end
|
|
$0 end m-abc123
|
|
$0 end-all dev
|
|
|
|
Environment:
|
|
SS_TOOLS_URL superset-tools base URL, no trailing '/' (default: http://localhost:8000)
|
|
SS_TOOLS_API_KEY API key with required permissions (required)
|
|
--insecure disable TLS certificate verification (use for self-signed certs)
|
|
|
|
Notes:
|
|
Exact environment ids: GET \${SS_TOOLS_URL}/api/environments (unknown id -> 404).
|
|
Fan-out targets only stage==PROD / is_production environments (DEV/PREPROD are skipped).
|
|
EOF
|
|
exit 0
|
|
;;
|
|
esac
|