#!/usr/bin/env bash set -euo pipefail if [[ -t 2 ]]; then _C_RESET='\033[0m' _C_RED='\033[0;31m' _C_GREEN='\033[0;32m' _C_YELLOW='\033[0;33m' _C_BLUE='\033[0;34m' else _C_RESET='' _C_RED='' _C_GREEN='' _C_YELLOW='' _C_BLUE='' fi log_info() { printf '%b[INFO]%b %s\n' "$_C_BLUE" "$_C_RESET" "$*" >&2 } log_warn() { printf '%b[WARN]%b %s\n' "$_C_YELLOW" "$_C_RESET" "$*" >&2 } log_error() { printf '%b[ERROR]%b %s\n' "$_C_RED" "$_C_RESET" "$*" >&2 } log_success() { printf '%b[OK]%b %s\n' "$_C_GREEN" "$_C_RESET" "$*" >&2 } require_cmd() { local cmd for cmd in "$@"; do if ! command -v "$cmd" >/dev/null 2>&1; then log_error "Required command not found: $cmd" return 1 fi done } confirm_action() { local prompt="${1:-Continue?}" local auto_yes="${2:-false}" if [[ "$auto_yes" == "true" ]]; then return 0 fi printf '%s [y/N] ' "$prompt" read -r reply [[ "$reply" =~ ^[Yy]$ ]] } stack_script_dir() { cd "$(dirname "${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}")" && pwd } load_stack_env() { local env_file="$1" if [[ ! -f "$env_file" ]]; then log_error "Missing env file: $env_file" log_info "Copy stack.env.example to stack.env and update secrets first." return 1 fi local line key value while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" || "$line" == \#* ]] && continue [[ "$line" == *=* ]] || continue key="${line%%=*}" value="${line#*=}" value="${value%%# *}" if [[ "$value" =~ ^\"(.*)\"$ ]] || [[ "$value" =~ ^\'(.*)\'$ ]]; then value="${BASH_REMATCH[1]}" fi value="${value%"${value##*[![:space:]]}"}" export "$key=$value" done < "$env_file" : "${OPS_ROOT:=/srv/ops}" : "${COMPOSE_PROJECT_NAME:=pi-monitoring}" } compose_file() { local dir dir="$(stack_script_dir)" printf '%s/docker-compose.yml' "$dir" } ensure_ops_dirs() { local root="$1" sudo mkdir -p \ "$root/portainer/data" \ "$root/grafana/data" \ "$root/prometheus/data" \ "$root/prometheus/targets" \ "$root/uptime-kuma/data" \ "$root/backups" } prepare_permissions() { local root="$1" # Keep operational directories writable by the current admin user. sudo chown -R "$USER:$USER" \ "$root/portainer/data" \ "$root/uptime-kuma/data" \ "$root/prometheus/targets" \ "$root/backups" # Grafana UID/GID in official image is usually 472 sudo chown -R 472:472 "$root/grafana/data" # Prometheus runs as nobody (65534) in official image sudo chown -R 65534:65534 "$root/prometheus/data" } wait_for_container_running() { local container_id="$1" local timeout_sec="$2" local elapsed=0 while (( elapsed < timeout_sec )); do local state state="$(docker inspect -f '{{.State.Status}}' "$container_id" 2>/dev/null || true)" if [[ "$state" == "running" ]]; then return 0 fi sleep 2 elapsed=$((elapsed + 2)) done return 1 }