#!/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: ( 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 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 </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 </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 [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 " exit 1 fi cmd_end "$2" ;; end-all) check_auth cmd_end_all "${2:-}" ;; *) cat < [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 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