#!/usr/bin/env bash # Shared helpers for the Kvitt Pi scripts (install.sh, deploy.sh, bootstrap.sh, backup-now.sh, install-apk.sh). # Sourced, not run. These scripts run ON the Kvitt Pi as the owner's user (a member of the docker group). # shellcheck disable=SC2034 # variables are used by the scripts that source this file KVITT_DIR=${KVITT_DIR:-/opt/kvitt} # The server's port, published on the Pi's LAN interfaces; Nginx Proxy Manager forwards to it (D-120). SERVER_URL=http://localhost:8080 # Nginx Proxy Manager's HTTPS port on the Home Assistant Pi, the default HTTPS port that Kvitt's public URL uses # (the router's existing external 443 forward reaches it, D-124). NPM_HTTPS_PORT=443 say() { printf '%s\n' "$*"; } die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } # Value of KEY in an env file (default /opt/kvitt/.env); empty when unset. env_value() { local file=${2:-$KVITT_DIR/.env} sed -n "s/^$1=//p" "$file" | tail -n 1 } # Set KEY=VALUE in an env file in place (adds the line when missing). Keeps the file's mode. env_set() { local key=$1 value=$2 file=${3:-$KVITT_DIR/.env} tmp tmp=$(mktemp "$file.XXXXXX") if grep -q "^$key=" "$file"; then awk -v k="$key" -v v="$value" 'index($0, k "=") == 1 { print k "=" v; next } { print }' "$file" > "$tmp" \ || { rm -f "$tmp"; return 1; } else { cat "$file" && printf '%s=%s\n' "$key" "$value"; } > "$tmp" || { rm -f "$tmp"; return 1; } fi if ! { chmod --reference="$file" "$tmp" && mv -f "$tmp" "$file"; }; then rm -f "$tmp"; return 1; fi } compose() { docker compose --project-directory "$KVITT_DIR" -f "$KVITT_DIR/compose.yaml" --env-file "$KVITT_DIR/.env" "$@" } # Health status of a compose service's container: healthy, unhealthy, starting, or none. service_health() { local id id=$(compose ps -q "$1" 2>/dev/null | head -n 1) [[ -n "$id" ]] || { echo none; return; } docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$id" 2>/dev/null || echo none } # Wait until a service is healthy (default 180 s). Returns 1 on timeout. wait_healthy() { local service=$1 timeout=${2:-180} waited=0 status while :; do status=$(service_health "$service") [[ "$status" == healthy ]] && { say "$service: healthy"; return 0; } if (( waited >= timeout )); then say "$service: $status after ${timeout}s" return 1 fi sleep 5 waited=$((waited + 5)) done } # GET /health from the server's published port on this Pi. print_health() { local out if out=$(curl -fsS --max-time 15 "$SERVER_URL/health" 2>&1); then say "GET $SERVER_URL/health: $out" return 0 fi say "GET $SERVER_URL/health failed: $out" return 1 } # GET /health through Nginx Proxy Manager on the Home Assistant Pi (HA_HOST in .env) for KVITT_PUBLIC_HOST, with # the real certificate check (no -k): proves NPM's proxy host, its certificate and the LAN path to this Pi's port # 8080. The router forward is not on this path. print_proxy_health() { local out ha_host host ha_host=$(env_value HA_HOST) host=$(env_value KVITT_PUBLIC_HOST) if out=$(curl -fsS --max-time 15 --resolve "$host:$NPM_HTTPS_PORT:$ha_host" "https://$host/health" 2>&1); then say "GET https://$host/health (via NPM at $ha_host:$NPM_HTTPS_PORT): $out" return 0 fi say "GET https://$host/health (via NPM at $ha_host:$NPM_HTTPS_PORT) failed: $out" return 1 } # Print the registry digest of every image the stack runs and append one line per image to # $KVITT_DIR/deployed-digests: " " (D-117). The registry is plain # HTTP on the LAN (D-001), so a swapped image would otherwise go unnoticed: a tag that was recorded before with a # different digest prints a WARNING (expected only for a tag like `latest` that is meant to move). record_digests() { local tag=$1 log=$KVITT_DIR/deployed-digests stamp ref repo digest prev d local -a refs repo_digests stamp=$(date -u +%Y-%m-%dT%H:%M:%SZ) mapfile -t refs < <(compose config --images) touch "$log" say "Image digests (also in $log):" for ref in "${refs[@]}"; do [[ -n "$ref" ]] || continue digest="" if [[ "$ref" == *@sha256:* ]]; then digest=${ref#*@} else repo=${ref%:*} read -r -a repo_digests <<< "$(docker image inspect --format '{{range .RepoDigests}}{{.}} {{end}}' "$ref" 2>/dev/null || true)" for d in "${repo_digests[@]}"; do if [[ "$d" == "$repo@"* ]]; then digest=${d#*@}; break; fi done fi [[ -n "$digest" ]] || digest=unknown prev=$(awk -v r="$ref" '$3 == r { d = $4 } END { print d }' "$log") if [[ -n "$prev" && "$prev" != "$digest" ]]; then say "WARNING: $ref was $prev at an earlier deploy and is $digest now: the image behind this tag changed" fi say " $ref $digest" printf '%s %s %s %s\n' "$stamp" "$tag" "$ref" "$digest" >> "$log" done }