doran/scripts/hetzner/lib.sh

171 lines
4.4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
BOOTSTRAP_ENV_FILE_DEFAULT="$ROOT_DIR/scripts/hetzner/bootstrap-secrets.env"
require() {
command -v "$1" >/dev/null 2>&1 || { echo "missing command: $1" >&2; exit 1; }
}
load_bootstrap_env() {
local env_file="${BOOTSTRAP_ENV_FILE:-$BOOTSTRAP_ENV_FILE_DEFAULT}"
if [[ -f "$env_file" ]]; then
# shellcheck disable=SC1090
source "$env_file"
fi
}
pass_get_first_line() {
local path="$1"
pass show "$path" | awk 'NR==1 { print; exit }'
}
resolve_secret_var() {
local name="$1"
local mode="${2:-required}"
local ref_name="${name}_PASS"
local value="${!name:-}"
local ref="${!ref_name:-}"
if [[ -z "$value" && -n "$ref" ]]; then
require pass
value="$(pass_get_first_line "$ref")"
printf -v "$name" '%s' "$value"
export "$name"
fi
if [[ "$mode" == "required" && -z "${!name:-}" ]]; then
echo "set $name or $ref_name" >&2
exit 1
fi
}
store_secret_to_pass() {
local path="$1"
local value="$2"
[[ -n "$path" ]] || return 0
require pass
printf '%s\n' "$value" | pass insert -m -f "$path" >/dev/null
}
wait_for_url() {
local url="$1"
local label="$2"
local max_attempts="${3:-120}"
local sleep_seconds="${4:-5}"
local attempt=1
until curl -kfsS "$url" >/dev/null 2>&1; do
if (( attempt >= max_attempts )); then
echo "timed out waiting for ${label}: ${url}" >&2
return 1
fi
if (( attempt == 1 || attempt % 6 == 0 )); then
echo "waiting for ${label} (${attempt}/${max_attempts})..."
fi
sleep "$sleep_seconds"
attempt=$((attempt + 1))
done
}
wait_for_http_status() {
local url="$1"
local label="$2"
local expected_regex="$3"
local max_attempts="${4:-120}"
local sleep_seconds="${5:-5}"
local attempt=1
while true; do
local status
status="$(curl -ksS -o /dev/null -w '%{http_code}' "$url" || true)"
if [[ "$status" =~ ^(${expected_regex})$ ]]; then
return 0
fi
if (( attempt >= max_attempts )); then
echo "timed out waiting for ${label}: ${url} (last status=${status:-n/a})" >&2
return 1
fi
if (( attempt == 1 || attempt % 6 == 0 )); then
echo "waiting for ${label} (${attempt}/${max_attempts}) status=${status:-n/a}..."
fi
sleep "$sleep_seconds"
attempt=$((attempt + 1))
done
}
wait_for_ssh() {
local target="$1"
local max_attempts="${2:-120}"
local sleep_seconds="${3:-5}"
local attempt=1
until ssh -i "$SSH_PRIVATE_KEY_PATH" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 "$target" 'echo ssh-ready' >/dev/null 2>&1; do
if (( attempt >= max_attempts )); then
echo "timed out waiting for ssh: ${target}" >&2
return 1
fi
if (( attempt == 1 || attempt % 6 == 0 )); then
echo "waiting for ssh (${attempt}/${max_attempts})..."
fi
sleep "$sleep_seconds"
attempt=$((attempt + 1))
done
}
wait_for_tailscale_node() {
local host_name="$1"
local max_attempts="${2:-120}"
local sleep_seconds="${3:-5}"
local attempt=1
command -v tailscale >/dev/null 2>&1 || {
echo "tailscale CLI is required locally for tailscale-first bootstrap" >&2
return 1
}
while true; do
local discovered
discovered=$(tailscale status --json 2>/dev/null | python3 - "$host_name" <<'PY'
import json,sys
host=sys.argv[1]
try:
data=json.load(sys.stdin)
except Exception:
print("")
raise SystemExit(0)
peers=data.get('Peer',{})
matches=[]
for peer in peers.values():
if peer.get('HostName') == host:
matches.append(peer)
for peer in sorted(matches, key=lambda p: ((p.get('Online') is True), p.get('DNSName') or ''), reverse=True):
if peer.get('Online'):
dns=(peer.get('DNSName') or '').rstrip('.')
if dns:
print(dns)
raise SystemExit(0)
for peer in sorted(matches, key=lambda p: p.get('DNSName') or '', reverse=True):
if peer.get('TailscaleIPs'):
print(peer['TailscaleIPs'][0])
raise SystemExit(0)
print("")
PY
)
if [[ -n "$discovered" ]]; then
printf '%s\n' "$discovered"
return 0
fi
if (( attempt >= max_attempts )); then
echo "timed out waiting for tailscale node: ${host_name}" >&2
return 1
fi
if (( attempt == 1 || attempt % 6 == 0 )); then
echo "waiting for tailscale node ${host_name} (${attempt}/${max_attempts})..." >&2
fi
sleep "$sleep_seconds"
attempt=$((attempt + 1))
done
}