webapps/console/scripts/maintenance-mode.sh
Rama newjitsu · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# Maintenance-mode helper. Reads/writes the maintenance descriptor in a
# Kubernetes ConfigMap that the console mounts via MAINTENANCE_CONFIG_FILE.
# The console picks up changes within the TTL window (~10s) — no restart.
#
# Requires: kubectl, jq.
#
# Environment overrides:
# MAINTENANCE_NAMESPACE k8s namespace (default: jitsu-platform)
# MAINTENANCE_CONFIGMAP ConfigMap name (default: jitsu-console-maintenance)
# MAINTENANCE_KEY Key inside ConfigMap (default: maintenance.json)
set -euo pipefail
NS="${MAINTENANCE_NAMESPACE:-jitsu-platform}"
CM="${MAINTENANCE_CONFIGMAP:-jitsu-console-maintenance}"
KEY="${MAINTENANCE_KEY:-maintenance.json}"
require() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "error: '$1' is required but not installed" >&2
exit 1
fi
}
require kubectl
require jq
usage() {
cat <<EOF
Usage: $(basename "$0") <command>
Commands:
status Show the current descriptor.
start Turn on maintenance (interactive prompts; pre-fills from current state).
stop Set active=false on the existing descriptor (keeps the record).
edit Open the current descriptor in \$EDITOR; validates JSON on save.
delete Delete the ConfigMap entirely.
help Show this help.
Targets ${NS}/${CM} (key: ${KEY}). Override via MAINTENANCE_NAMESPACE / MAINTENANCE_CONFIGMAP / MAINTENANCE_KEY.
EOF
}
# Returns the current descriptor JSON, or empty string if the ConfigMap doesn't exist
# or the key isn't set. We go through -o json + jq rather than jsonpath to avoid
# escaping woes around dots in the key.
read_descriptor() {
kubectl -n "$NS" get cm "$CM" -o json 2>/dev/null | jq -r --arg key "$KEY" '.data[$key] // ""' 2>/dev/null || true
}
apply_descriptor() {
local json="$1"
# Validate JSON before sending it to the cluster.
if ! echo "$json" | jq . >/dev/null; then
echo "error: descriptor is not valid JSON" >&2
return 1
fi
# Upsert pattern — create+apply via dry-run YAML so it works whether the
# ConfigMap exists or not.
kubectl -n "$NS" create configmap "$CM" \
--from-literal="${KEY}=${json}" \
--dry-run=client -o yaml | kubectl apply -f -
}
confirm() {
local prompt="${1:-Apply?} (y/N) "
local reply
read -r -p "$prompt" reply
case "$reply" in
[Yy]*) return 0 ;;
*) return 1 ;;
esac
}
prompt_with_default() {
# Usage: prompt_with_default "Label" "default"; reads value into REPLY_VAL.
local label="$1" default="${2:-}" input
if [[ -n "$default" ]]; then
read -r -p "${label} [${default}]: " input
else
read -r -p "${label}: " input
fi
REPLY_VAL="${input:-$default}"
}
cmd_status() {
local current
current="$(read_descriptor)"
if [[ -z "$current" ]]; then
echo "No maintenance descriptor at ${NS}/${CM} (key: ${KEY})."
return 0
fi
echo "${NS}/${CM} → ${KEY}:"
echo "$current" | jq .
}
parse_bool() {
# Coerce common user input to "true"/"false". Falls back to $2 on unknown.
case "${1,,}" in
true|yes|y|1) echo "true" ;;
false|no|n|0) echo "false" ;;
*) echo "$2" ;;
esac
}
cmd_start() {
local current active visible show_in_advance description
local planned_start planned_end database_access stop_consuming
current="$(read_descriptor)"
active="$(echo "$current" | jq -r '.active // empty' 2>/dev/null || true)"
visible="$(echo "$current" | jq -r '.visible // empty' 2>/dev/null || true)"
show_in_advance="$(echo "$current" | jq -r '.show_in_advance // empty' 2>/dev/null || true)"
description="$(echo "$current" | jq -r '.description // empty' 2>/dev/null || true)"
planned_start="$(echo "$current" | jq -r '.planned_start // empty' 2>/dev/null || true)"
planned_end="$(echo "$current" | jq -r '.planned_end // empty' 2>/dev/null || true)"
database_access="$(echo "$current" | jq -r '.database_access // empty' 2>/dev/null || true)"
stop_consuming="$(echo "$current" | jq -r '.stop_consuming // empty' 2>/dev/null || true)"
# `active=true` blocks writes immediately. `active=false` + `show_in_advance=true`
# is the "schedule an upcoming window" mode: writes still allowed, banner shown.
prompt_with_default "active (true | false) — block writes now?" "${active:-true}"
active="$(parse_bool "$REPLY_VAL" "true")"
prompt_with_default "visible (true | false) — expose descriptor to the browser?" "${visible:-true}"
visible="$(parse_bool "$REPLY_VAL" "true")"
# Default show_in_advance to true when scheduling (active=false), otherwise
# false — for an already-active window the upcoming-banner is redundant.
local sia_default="${show_in_advance:-}"
if [[ -z "$sia_default" ]]; then
if [[ "$active" == "false" ]]; then sia_default="true"; else sia_default="false"; fi
fi
prompt_with_default "show_in_advance (true | false) — show banner before the window starts?" "$sia_default"
show_in_advance="$(parse_bool "$REPLY_VAL" "$sia_default")"
prompt_with_default "Description" "${description:-Scheduled maintenance}"
description="$REPLY_VAL"
prompt_with_default "Planned start (ISO8601, e.g. 2026-06-02T17:00:00Z; blank for none)" "$planned_start"
planned_start="$REPLY_VAL"
prompt_with_default "Planned end (ISO8601, e.g. 2026-06-02T18:00:00Z; blank for none)" "$planned_end"
planned_end="$REPLY_VAL"
prompt_with_default "database_access (read_only | off)" "${database_access:-read_only}"
database_access="$REPLY_VAL"
if [[ "$database_access" != "read_only" && "$database_access" != "off" ]]; then
echo "error: database_access must be 'read_only' or 'off'" >&2
exit 1
fi
prompt_with_default "stop_consuming (true | false) — pause Rotor/Bulker consumers" "${stop_consuming:-false}"
stop_consuming="$(parse_bool "$REPLY_VAL" "false")"
local new_json
new_json="$(jq -n \
--argjson active "$active" \
--argjson visible "$visible" \
--argjson show_in_advance "$show_in_advance" \
--arg description "$description" \
--arg planned_start "$planned_start" \
--arg planned_end "$planned_end" \
--arg database_access "$database_access" \
--argjson stop_consuming "$stop_consuming" \
'{
active: $active,
visible: $visible,
show_in_advance: $show_in_advance,
description: $description,
database_access: $database_access,
stop_consuming: $stop_consuming
}
| if $planned_start != "" then .planned_start = $planned_start else . end
| if $planned_end != "" then .planned_end = $planned_end else . end')"
echo
echo "New maintenance descriptor:"
echo "$new_json" | jq .
echo
if ! confirm; then
echo "Aborted."
return 0
fi
apply_descriptor "$new_json"
echo "Applied. The console will pick up the change within ~10s."
}
cmd_stop() {
local current new_json
current="$(read_descriptor)"
if [[ -z "$current" ]]; then
echo "No maintenance descriptor at ${NS}/${CM} — nothing to stop."
return 0
fi
# Clear both `active` and `show_in_advance` — the in-app banner renders on
# either flag, so leaving `show_in_advance=true` behind would keep a "Jitsu
# maintenance is scheduled" message visible after the operator stopped the
# window. `planned_start` / `planned_end` are kept so the next `start` flow
# can pre-fill them as sensible defaults.
new_json="$(echo "$current" | jq '.active = false | .show_in_advance = false')"
echo "Setting active=false, show_in_advance=false:"
echo "$new_json" | jq .
echo
if ! confirm; then
echo "Aborted."
return 0
fi
apply_descriptor "$new_json"
echo "Maintenance stopped. The console will pick up the change within ~10s."
}
cmd_edit() {
local current tmp
current="$(read_descriptor)"
if [[ -z "$current" ]]; then
# Start from a sensible empty template so $EDITOR doesn't open a blank file.
current='{"active":false,"visible":true,"description":"","database_access":"read_only"}'
fi
tmp="$(mktemp 2>/dev/null || mktemp -t maintenance)"
trap 'rm -f "$tmp"' EXIT
# Pretty-print into the temp file so $EDITOR users see indented JSON.
echo "$current" | jq . > "$tmp"
"${EDITOR:-vi}" "$tmp"
if ! jq . "$tmp" >/dev/null; then
echo "error: edited content is not valid JSON — aborting" >&2
exit 1
fi
echo
echo "Edited descriptor:"
jq . "$tmp"
echo
if ! confirm; then
echo "Aborted."
return 0
fi
apply_descriptor "$(jq -c . "$tmp")"
echo "Applied. The console will pick up the change within ~10s."
}
cmd_delete() {
if ! kubectl -n "$NS" get cm "$CM" >/dev/null 2>&1; then
echo "ConfigMap ${NS}/${CM} doesn't exist — nothing to delete."
return 0
fi
if ! confirm "Delete ConfigMap ${NS}/${CM}?"; then
echo "Aborted."
return 0
fi
kubectl -n "$NS" delete configmap "$CM"
echo "Deleted. The console will fall back to the MAINTENANCE env var (if set) within ~10s."
}
cmd="${1:-help}"
case "$cmd" in
status) cmd_status ;;
start) cmd_start ;;
stop) cmd_stop ;;
edit) cmd_edit ;;
delete) cmd_delete ;;
help|-h|--help) usage ;;
*)
echo "unknown command: $cmd" >&2
echo >&2
usage >&2
exit 1
;;
esac