Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
195 lines
6.4 KiB
Bash
Executable File
195 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# stack-health.sh — Check health of all configured Mosaic stack services
|
|
#
|
|
# Usage: stack-health.sh [-f format] [-s service] [-q]
|
|
#
|
|
# Checks connectivity to all services configured in credentials.json.
|
|
# For each service, makes a lightweight API call and reports status.
|
|
#
|
|
# Options:
|
|
# -f format Output format: table (default), json
|
|
# -s service Check only a specific service
|
|
# -q Quiet — exit code only (0=all healthy, 1=any unhealthy)
|
|
# -h Show this help
|
|
set -euo pipefail
|
|
|
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
|
source "$MOSAIC_HOME/tools/_lib/credentials.sh"
|
|
|
|
FORMAT="table"
|
|
SINGLE_SERVICE=""
|
|
QUIET=false
|
|
CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}"
|
|
|
|
while getopts "f:s:qh" opt; do
|
|
case $opt in
|
|
f) FORMAT="$OPTARG" ;;
|
|
s) SINGLE_SERVICE="$OPTARG" ;;
|
|
q) QUIET=true ;;
|
|
h) head -15 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;;
|
|
*) echo "Usage: $0 [-f format] [-s service] [-q]" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -f "$CRED_FILE" ]]; then
|
|
echo "Error: Credentials file not found: $CRED_FILE" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Colors (disabled if not a terminal or quiet mode)
|
|
if [[ -t 1 ]] && [[ "$QUIET" == "false" ]]; then
|
|
GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' RESET='\033[0m'
|
|
else
|
|
GREEN='' RED='' YELLOW='' RESET=''
|
|
fi
|
|
|
|
TOTAL=0
|
|
HEALTHY=0
|
|
RESULTS="[]"
|
|
|
|
check_service() {
|
|
local name="$1"
|
|
local display_name="$2"
|
|
local url="$3"
|
|
local endpoint="$4"
|
|
local auth_header="$5"
|
|
local insecure="${6:-false}"
|
|
|
|
TOTAL=$((TOTAL + 1))
|
|
|
|
local curl_args=(-s -o /dev/null -w "%{http_code} %{time_total}" --connect-timeout 5 --max-time 10)
|
|
[[ -n "$auth_header" ]] && curl_args+=(-H "$auth_header")
|
|
[[ "$insecure" == "true" ]] && curl_args+=(-k)
|
|
|
|
local result
|
|
result=$(curl "${curl_args[@]}" "${url}${endpoint}" 2>/dev/null) || result="000 0.000"
|
|
|
|
local http_code response_time status_text
|
|
http_code=$(echo "$result" | awk '{print $1}')
|
|
response_time=$(echo "$result" | awk '{print $2}')
|
|
|
|
if [[ "$http_code" -ge 200 && "$http_code" -lt 400 ]]; then
|
|
status_text="UP"
|
|
HEALTHY=$((HEALTHY + 1))
|
|
elif [[ "$http_code" == "000" ]]; then
|
|
status_text="DOWN"
|
|
elif [[ "$http_code" == "401" || "$http_code" == "403" ]]; then
|
|
# Auth error but service is reachable
|
|
status_text="AUTH_ERR"
|
|
HEALTHY=$((HEALTHY + 1)) # Service is up, just auth issue
|
|
else
|
|
status_text="ERROR"
|
|
fi
|
|
|
|
# Append to JSON results
|
|
RESULTS=$(echo "$RESULTS" | jq --arg n "$name" --arg d "$display_name" \
|
|
--arg u "$url" --arg s "$status_text" --arg c "$http_code" --arg t "$response_time" \
|
|
'. + [{name: $n, display_name: $d, url: $u, status: $s, http_code: ($c | tonumber), response_time: $t}]')
|
|
|
|
if [[ "$QUIET" == "false" && "$FORMAT" == "table" ]]; then
|
|
local color="$GREEN"
|
|
[[ "$status_text" == "DOWN" || "$status_text" == "ERROR" ]] && color="$RED"
|
|
[[ "$status_text" == "AUTH_ERR" ]] && color="$YELLOW"
|
|
printf " %-22s %-35s ${color}%-8s${RESET} %ss\n" \
|
|
"$display_name" "$url" "$status_text" "$response_time"
|
|
fi
|
|
}
|
|
|
|
# Discover and check services
|
|
[[ "$QUIET" == "false" && "$FORMAT" == "table" ]] && {
|
|
echo ""
|
|
echo " SERVICE URL STATUS RESPONSE"
|
|
echo " ---------------------- ----------------------------------- -------- --------"
|
|
}
|
|
|
|
# Portainer
|
|
if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "portainer" ]]; then
|
|
portainer_url=$(jq -r '.portainer.url // empty' "$CRED_FILE")
|
|
portainer_key=$(jq -r '.portainer.api_key // empty' "$CRED_FILE")
|
|
if [[ -n "$portainer_url" ]]; then
|
|
check_service "portainer" "Portainer" "$portainer_url" "/api/system/status" \
|
|
"X-API-Key: $portainer_key" "true"
|
|
fi
|
|
fi
|
|
|
|
# Coolify
|
|
if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "coolify" ]]; then
|
|
coolify_url=$(jq -r '.coolify.url // empty' "$CRED_FILE")
|
|
coolify_token=$(jq -r '.coolify.app_token // empty' "$CRED_FILE")
|
|
if [[ -n "$coolify_url" ]]; then
|
|
check_service "coolify" "Coolify" "$coolify_url" "/api/v1/teams" \
|
|
"Authorization: Bearer $coolify_token" "false"
|
|
fi
|
|
fi
|
|
|
|
# Authentik
|
|
if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "authentik" ]]; then
|
|
authentik_url=$(jq -r '.authentik.url // empty' "$CRED_FILE")
|
|
if [[ -n "$authentik_url" ]]; then
|
|
check_service "authentik" "Authentik" "$authentik_url" "/-/health/ready/" "" "true"
|
|
fi
|
|
fi
|
|
|
|
# GLPI
|
|
if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "glpi" ]]; then
|
|
glpi_url=$(jq -r '.glpi.url // empty' "$CRED_FILE")
|
|
if [[ -n "$glpi_url" ]]; then
|
|
check_service "glpi" "GLPI" "$glpi_url" "/" "" "true"
|
|
fi
|
|
fi
|
|
|
|
# Gitea instances
|
|
if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "gitea" ]]; then
|
|
for instance in mosaicstack usc; do
|
|
gitea_url=$(jq -r ".gitea.${instance}.url // empty" "$CRED_FILE")
|
|
if [[ -n "$gitea_url" ]]; then
|
|
display="Gitea (${instance})"
|
|
check_service "gitea-${instance}" "$display" "$gitea_url" "/api/v1/version" "" "true"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# GitHub
|
|
if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "github" ]]; then
|
|
github_token=$(jq -r '.github.token // empty' "$CRED_FILE")
|
|
if [[ -n "$github_token" ]]; then
|
|
check_service "github" "GitHub" "https://api.github.com" "/rate_limit" \
|
|
"Authorization: Bearer $github_token" "false"
|
|
fi
|
|
fi
|
|
|
|
# Woodpecker
|
|
if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "woodpecker" ]]; then
|
|
woodpecker_url=$(jq -r '.woodpecker.url // empty' "$CRED_FILE")
|
|
woodpecker_token=$(jq -r '.woodpecker.token // empty' "$CRED_FILE")
|
|
if [[ -n "$woodpecker_url" && -n "$woodpecker_token" ]]; then
|
|
check_service "woodpecker" "Woodpecker CI" "$woodpecker_url" "/api/user" \
|
|
"Authorization: Bearer $woodpecker_token" "true"
|
|
elif [[ "$QUIET" == "false" && "$FORMAT" == "table" ]]; then
|
|
printf " %-22s %-35s ${YELLOW}%-8s${RESET} %s\n" \
|
|
"Woodpecker CI" "—" "NOTOKEN" "—"
|
|
fi
|
|
fi
|
|
|
|
# Output
|
|
if [[ "$FORMAT" == "json" ]]; then
|
|
jq -n --argjson results "$RESULTS" --argjson total "$TOTAL" --argjson healthy "$HEALTHY" \
|
|
'{total: $total, healthy: $healthy, results: $results}'
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$QUIET" == "false" && "$FORMAT" == "table" ]]; then
|
|
echo ""
|
|
UNHEALTHY=$((TOTAL - HEALTHY))
|
|
if [[ "$UNHEALTHY" -eq 0 ]]; then
|
|
echo -e " ${GREEN}All $TOTAL services healthy${RESET}"
|
|
else
|
|
echo -e " ${RED}$UNHEALTHY/$TOTAL services unhealthy${RESET}"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# Exit code: 0 if all healthy, 1 if any unhealthy
|
|
[[ "$HEALTHY" -eq "$TOTAL" ]]
|