Files
ss-tools/examples/maintenance/maintenance-api-bash.sh

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