171 lines
4.4 KiB
Bash
Executable file
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
|
|
}
|