#!/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" ]]