#!/usr/bin/env bash set -euo pipefail ROOT_DIR=$(cd "$(dirname "$0")/../.." && pwd) # shellcheck disable=SC1091 source "$ROOT_DIR/scripts/hetzner/lib.sh" load_bootstrap_env TF_DIR="$ROOT_DIR/infra/terraform/hetzner" STATE_DIR="$ROOT_DIR/.state/hetzner" KUBECONFIG_PATH="$STATE_DIR/kubeconfig.yaml" CI_KUBECONFIG_PATH="$STATE_DIR/kubeconfig.incluster.yaml" OVERLAY_DIR="$ROOT_DIR/deploy/k8s/overlays/hetzner-single-node" DEFAULT_PROJECT_NAME="unrip" DEFAULT_PROJECT_NAMESPACE="$DEFAULT_PROJECT_NAME" mkdir -p "$STATE_DIR" require terraform require kubectl require curl require python3 require ssh require git require docker require base64 resolve_secret_var HCLOUD_TOKEN required resolve_secret_var TAILSCALE_AUTH_KEY optional resolve_secret_var NEAR_INTENTS_API_KEY required resolve_secret_var REGISTRY_PASSWORD required resolve_secret_var FORGEJO_ADMIN_PASSWORD required resolve_secret_var CLOUDFLARE_API_TOKEN optional resolve_secret_var PORKBUN_API_KEY optional resolve_secret_var PORKBUN_SECRET_API_KEY optional : "${SSH_PUBLIC_KEY_PATH:?set SSH_PUBLIC_KEY_PATH}" : "${PUBLIC_DOMAIN:?set PUBLIC_DOMAIN}" : "${LETSENCRYPT_EMAIL:?set LETSENCRYPT_EMAIL}" : "${BASE_DOMAIN:?set BASE_DOMAIN}" : "${FORGEJO_DOMAIN:=git.${BASE_DOMAIN}}" : "${FORGEJO_ROOT_URL:=https://${FORGEJO_DOMAIN}/}" : "${REGISTRY_DOMAIN:=registry.${BASE_DOMAIN}}" : "${REGISTRY_USERNAME:?set REGISTRY_USERNAME}" : "${TF_ADMIN_CIDR_BLOCKS:=}" : "${PROJECT_NAME:=$DEFAULT_PROJECT_NAME}" : "${PROJECT_NAMESPACE:=$DEFAULT_PROJECT_NAMESPACE}" : "${PROJECT_OVERLAY_DIR:=$OVERLAY_DIR}" : "${BOOTSTRAP_NODE_NAME:=unrip-1}" : "${SKIP_TERRAFORM_APPLY:=0}" : "${PROJECT_KUSTOMIZE_PATH:=../../projects/${PROJECT_NAME}/base}" : "${PROJECT_SECRET_NAME:=${PROJECT_NAME}-secrets}" : "${PROJECT_SECRET_ENV_BASENAME:=${PROJECT_NAME}.env}" : "${PROJECT_REGISTRY_SECRET_NAME:=${PROJECT_NAME}-registry-creds}" : "${PROJECT_IMAGE_REPOSITORY:=${PROJECT_NAME}}" : "${PROJECT_DEPLOYMENTS:=near-intents-ingest dummy-reactor dummy-executor dummy-consumer}" : "${FORGEJO_ADMIN_USERNAME:?set FORGEJO_ADMIN_USERNAME}" : "${FORGEJO_ADMIN_EMAIL:?set FORGEJO_ADMIN_EMAIL}" : "${FORGEJO_RUNNER_NAME:=k3s-runner}" : "${FORGEJO_RUNNER_LABELS:=linux-amd64:host}" : "${FORGEJO_REPO_OWNER:=$FORGEJO_ADMIN_USERNAME}" : "${FORGEJO_REPO_NAME:=$(basename "$ROOT_DIR")}" : "${FORGEJO_REPO_PRIVATE:=true}" : "${BOOTSTRAP_DELIVERY_MODE:=forgejo-actions}" BOOTSTRAP_IMAGE="${PROJECT_IMAGE_REPOSITORY}:bootstrap" PROJECT_SECRET_ENV_PATH="$PROJECT_OVERLAY_DIR/secrets/$PROJECT_SECRET_ENV_BASENAME" GENERATED_OVERLAY_DIR="$STATE_DIR/generated-overlay" SSH_PUBLIC_KEY=$(cat "$SSH_PUBLIC_KEY_PATH") SSH_PRIVATE_KEY_PATH="${SSH_PUBLIC_KEY_PATH%.pub}" if [[ ! -f "$SSH_PRIVATE_KEY_PATH" ]]; then echo "missing ssh private key for bootstrap: $SSH_PRIVATE_KEY_PATH" >&2 exit 1 fi TF_VARS=( -var "hcloud_token=$HCLOUD_TOKEN" -var "ssh_public_key=$SSH_PUBLIC_KEY" -var "public_domain=$PUBLIC_DOMAIN" -var "bootstrap_repo_url=local-bootstrap" -var "tailscale_auth_key=${TAILSCALE_AUTH_KEY:-}" -var "tailscale_control_plane_hostname=${TAILSCALE_CONTROL_PLANE_HOSTNAME:-}" ) if [[ -n "$TF_ADMIN_CIDR_BLOCKS" && "$TF_ADMIN_CIDR_BLOCKS" != '[]' ]]; then TF_VARS+=(-var "admin_cidr_blocks=$TF_ADMIN_CIDR_BLOCKS") fi if [[ -n "${TAILSCALE_AUTH_KEY:-}" ]]; then bash "$ROOT_DIR/scripts/hetzner/print-tailscale-firewall-note.sh" fi terraform -chdir="$TF_DIR" init if [[ "$SKIP_TERRAFORM_APPLY" != "1" ]]; then terraform -chdir="$TF_DIR" apply -auto-approve "${TF_VARS[@]}" fi SERVER_IP=$(terraform -chdir="$TF_DIR" output -raw server_ipv4) K3S_API_URL=$(terraform -chdir="$TF_DIR" output -raw k3s_api_url) if [[ -n "${TAILSCALE_AUTH_KEY:-}" ]]; then DISCOVERED_TAILSCALE_HOST="${TAILSCALE_CONTROL_PLANE_HOSTNAME:-$(wait_for_tailscale_node "$BOOTSTRAP_NODE_NAME")}" SSH_TARGET="root@${DISCOVERED_TAILSCALE_HOST}" K3S_API_URL="https://${DISCOVERED_TAILSCALE_HOST}:6443" else SSH_TARGET="root@${SERVER_IP}" fi if [[ -n "${CLOUDFLARE_API_TOKEN:-}" && -n "${CLOUDFLARE_ZONE_ID:-}" ]]; then if ! SERVER_IP="$SERVER_IP" BASE_DOMAIN="$BASE_DOMAIN" bash "$ROOT_DIR/scripts/hetzner/configure-cloudflare-dns.sh"; then echo "warning: cloudflare DNS automation failed; continuing without automated DNS" >&2 fi elif [[ -n "${PORKBUN_API_KEY:-}" && -n "${PORKBUN_SECRET_API_KEY:-}" ]]; then if ! SERVER_IP="$SERVER_IP" BASE_DOMAIN="$BASE_DOMAIN" bash "$ROOT_DIR/scripts/hetzner/configure-porkbun-dns.sh"; then echo "warning: porkbun DNS automation failed; continuing without automated DNS" >&2 fi fi wait_for_ssh "$SSH_TARGET" echo "waiting for Kubernetes API on $K3S_API_URL..." wait_for_url "${K3S_API_URL}/readyz" "k3s API readiness" ssh -i "$SSH_PRIVATE_KEY_PATH" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SSH_TARGET" 'sudo cat /etc/rancher/k3s/k3s.yaml' \ | sed "s|https://127.0.0.1:6443|${K3S_API_URL}|" > "$KUBECONFIG_PATH" export KUBECONFIG="$KUBECONFIG_PATH" python3 - "$KUBECONFIG_PATH" "$CI_KUBECONFIG_PATH" <<'PY' import sys import yaml src, dst = sys.argv[1], sys.argv[2] config = yaml.safe_load(open(src)) config['clusters'][0]['cluster']['server'] = 'https://kubernetes.default.svc:443' yaml.safe_dump(config, open(dst, 'w'), sort_keys=False) PY mkdir -p "$PROJECT_OVERLAY_DIR/secrets" "$GENERATED_OVERLAY_DIR" cat > "$PROJECT_SECRET_ENV_PATH" < "$PROJECT_OVERLAY_DIR/secrets/forgejo.env" <&2 exit 1 fi if su-exec git /usr/local/bin/forgejo admin user list | awk 'NR>1 {print \$2}' | grep -qx "\$ADMIN_USERNAME"; then echo "forgejo admin already exists: \$ADMIN_USERNAME" else su-exec git /usr/local/bin/forgejo admin user create --config "$APP_INI" --admin --username "\$ADMIN_USERNAME" --password "\$ADMIN_PASSWORD" --email "\$ADMIN_EMAIL" --must-change-password=false fi if [[ ! -f /data/.runner ]]; then RUNNER_TOKEN=\$(su-exec git /usr/local/bin/forgejo --config "$APP_INI" actions generate-runner-token) forgejo-runner register --no-interactive --name "\$RUNNER_NAME" --instance "$FORGEJO_ROOT_URL" --token "\$RUNNER_TOKEN" --labels "\$RUNNER_LABELS" install -o 1000 -g 1000 -m 600 .runner /data/.runner rm -f .runner echo "registered forgejo runner config at /data/.runner" else echo "forgejo runner already configured: /data/.runner" fi EOF kubectl -n forgejo rollout restart deployment/forgejo-runner kubectl -n forgejo rollout status deployment/forgejo-runner --timeout=300s if [[ "$BOOTSTRAP_DELIVERY_MODE" == "forgejo-actions" ]]; then wait_for_url "$FORGEJO_ROOT_URL" "Forgejo public URL" 180 5 wait_for_http_status "https://$REGISTRY_DOMAIN/v2/" "registry public URL" '200|401' 180 5 forgejo_bootstrap_args=( --forgejo-url "$FORGEJO_ROOT_URL" --admin-username "$FORGEJO_ADMIN_USERNAME" --admin-password "$FORGEJO_ADMIN_PASSWORD" --repo-owner "$FORGEJO_REPO_OWNER" --repo-name "$FORGEJO_REPO_NAME" --kubeconfig "$KUBECONFIG_PATH" --registry-username "$REGISTRY_USERNAME" --registry-password "$REGISTRY_PASSWORD" --registry-host "$REGISTRY_DOMAIN" --project-name "$PROJECT_NAME" --project-namespace "$PROJECT_NAMESPACE" --project-deployments "${PROJECT_DEPLOYMENTS// /,}" ) if [[ "$FORGEJO_REPO_PRIVATE" == "true" ]]; then forgejo_bootstrap_args+=(--repo-private) fi python3 "$ROOT_DIR/scripts/hetzner/forgejo-bootstrap.py" "${forgejo_bootstrap_args[@]}" bash "$ROOT_DIR/scripts/hetzner/seed-forgejo-repo.sh" else docker build -t "$BOOTSTRAP_IMAGE" "$ROOT_DIR" docker save "$BOOTSTRAP_IMAGE" \ | ssh -i "$SSH_PRIVATE_KEY_PATH" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SSH_TARGET" 'sudo k3s ctr images import -' for deployment in $PROJECT_DEPLOYMENTS; do kubectl -n "$PROJECT_NAMESPACE" set image "deployment/${deployment}" app="$BOOTSTRAP_IMAGE" done for deployment in $PROJECT_DEPLOYMENTS; do kubectl -n "$PROJECT_NAMESPACE" rollout status "deployment/${deployment}" --timeout=180s done fi echo "bootstrap complete" echo "project_name=$PROJECT_NAME" echo "project_namespace=$PROJECT_NAMESPACE" echo "project_overlay_dir=$PROJECT_OVERLAY_DIR" echo "server_ip=$SERVER_IP" echo "ssh_target=$SSH_TARGET" echo "k3s_api_url=$K3S_API_URL" echo "kubeconfig=$KUBECONFIG_PATH" echo "ci_kubeconfig=$CI_KUBECONFIG_PATH" echo "bootstrap_delivery_mode=$BOOTSTRAP_DELIVERY_MODE" echo "forgejo_url=$FORGEJO_ROOT_URL" echo "forgejo_repo=${FORGEJO_ROOT_URL%/}/$FORGEJO_REPO_OWNER/$FORGEJO_REPO_NAME" echo "registry_url=https://$REGISTRY_DOMAIN" echo "dns_provider=${CLOUDFLARE_API_TOKEN:+cloudflare}${PORKBUN_API_KEY:+porkbun}"