#!/bin/bash set -euo pipefail usage() { echo "Usage: ./tools/service.sh " echo "" echo "Commands:" echo " dns Create A record for .hantim.net" echo " files Create compose.yml and deploy workflow" echo " all Run dns + files" echo "" echo "Examples:" echo " ./tools/service.sh files garage" echo " ./tools/service.sh dns garage # garage.hantim.net -> server IP" echo " ./tools/service.sh all garage" echo "" echo "Requires: bws, jq, dig (for dns)" exit 1 } COMMAND="${1:-}" if [ -z "$COMMAND" ]; then usage fi REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" # --- Dependency checks --- require_bws() { if ! command -v bws &>/dev/null; then echo "Error: Bitwarden Secrets Manager CLI (bws) is not installed." echo " https://github.com/bitwarden/sdk-sm/releases" exit 1 fi if ! command -v jq &>/dev/null; then echo "Error: jq is not installed." exit 1 fi BWS_TOKEN_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/hantim/bws-token" if [ -z "${BWS_ACCESS_TOKEN:-}" ]; then if [ -f "$BWS_TOKEN_FILE" ]; then BWS_ACCESS_TOKEN=$(cat "$BWS_TOKEN_FILE") else echo "==> Bitwarden Secrets Manager access token required." echo " Generate one for your machine account at:" echo " https://vault.bitwarden.com/#/sm/machine-accounts" read -rp " Paste access token: " BWS_ACCESS_TOKEN if [ -z "$BWS_ACCESS_TOKEN" ]; then echo "ERROR: Access token cannot be empty." exit 1 fi mkdir -p "$(dirname "$BWS_TOKEN_FILE")" echo "$BWS_ACCESS_TOKEN" > "$BWS_TOKEN_FILE" chmod 600 "$BWS_TOKEN_FILE" fi fi export BWS_ACCESS_TOKEN } bws_get() { local name="$1" local value value=$(bws secret list | jq -r --arg name "$name" '.[] | select(.key == $name) | .value') if [ -z "$value" ]; then echo "ERROR: Secret '$name' not found in Bitwarden Secrets Manager." >&2 exit 1 fi echo "$value" } require_dig() { if ! command -v dig &>/dev/null; then echo "Error: dig is not installed." echo " On macOS: brew install bind" echo " On Linux: dnf install bind-utils" exit 1 fi } validate_name() { local name="$1" if [ -z "$name" ]; then echo "Error: service name is required." echo "" usage fi if ! [[ "$name" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then echo "Error: name must be alphanumeric (hyphens, dots, underscores allowed)." exit 1 fi } # --- Commands --- cmd_dns() { local name="$1" local zone="hantim.net" local fqdn="$name.$zone" require_dig require_bws SERVER_IP=$(dig +short "$zone" | head -1) if [ -z "$SERVER_IP" ]; then echo "Error: Could not resolve $zone to get server IP." exit 1 fi echo "==> Fetching Vultr API key..." VULTR_API_KEY=$(bws_get "hantim-vultr-api-key") VULTR_API="https://api.vultr.com/v2" VULTR_AUTH="Authorization: Bearer $VULTR_API_KEY" echo "==> Checking existing A records for $name in $zone..." RECORDS=$(curl -s "$VULTR_API/domains/$zone/records" -H "$VULTR_AUTH") echo "$RECORDS" | jq -r --arg sub "$name" \ '.records[] | select(.type == "A" and .name == $sub) | .id' | while read -r id; do echo " Deleting existing A record for $fqdn (id: $id)" curl -s -X DELETE "$VULTR_API/domains/$zone/records/$id" -H "$VULTR_AUTH" done echo " Creating A record: $fqdn -> $SERVER_IP" RESP=$(mktemp) HTTP_CODE=$(curl -s -o "$RESP" -w "%{http_code}" \ -X POST "$VULTR_API/domains/$zone/records" \ -H "$VULTR_AUTH" \ -H "Content-Type: application/json" \ -d "{\"name\": \"$name\", \"type\": \"A\", \"data\": \"$SERVER_IP\", \"ttl\": 3600}") if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "204" ]; then echo "Error: Failed to create A record (HTTP $HTTP_CODE)" cat "$RESP" rm -f "$RESP" exit 1 fi rm -f "$RESP" echo "==> Verifying DNS propagation for $fqdn..." for i in $(seq 1 12); do RESOLVED=$(dig +short "$fqdn" @ns1.vultr.com 2>/dev/null | head -1) if [ "$RESOLVED" = "$SERVER_IP" ]; then echo " DNS is live: $fqdn -> $SERVER_IP" return fi if [ "$i" = "12" ]; then echo " Warning: $fqdn did not resolve to $SERVER_IP after 60 seconds. It may take longer to propagate." return fi sleep 5 done } cmd_files() { local name="$1" local all_exist=true for f in "docker/$name/compose.yml" ".gitea/workflows/deploy-$name.yml"; do if [ ! -e "$REPO_ROOT/$f" ]; then all_exist=false break fi done if [ "$all_exist" = true ]; then echo " All files already exist, skipping." return fi if [ ! -e "$REPO_ROOT/docker/$name/compose.yml" ]; then echo "==> Creating docker/$name/compose.yml..." mkdir -p "$REPO_ROOT/docker/$name" touch "$REPO_ROOT/docker/$name/.gitignore" cat > "$REPO_ROOT/docker/$name/compose.yml" < Creating .gitea/workflows/deploy-$name.yml..." mkdir -p "$REPO_ROOT/.gitea/workflows" cat > "$REPO_ROOT/.gitea/workflows/deploy-$name.yml" <<'OUTER' name: Deploy APP_PLACEHOLDER on: push: branches: [main] paths: - 'docker/APP_PLACEHOLDER/**' jobs: deploy: runs-on: ubuntu-latest steps: - name: Deploy via SSH run: | mkdir -p ~/.ssh echo "${{ vars.DEPLOY_HOST_KEY }}" > ~/.ssh/known_hosts echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh -o StrictHostKeyChecking=yes -i ~/.ssh/deploy_key deploy@${{ vars.DEPLOY_HOST }} deploy-APP_PLACEHOLDER OUTER sed -i "s/APP_PLACEHOLDER/$name/g" "$REPO_ROOT/.gitea/workflows/deploy-$name.yml" else echo " .gitea/workflows/deploy-$name.yml already exists, skipping." fi echo "" echo "Done. Edit docker/$name/compose.yml, then commit and push." echo "" echo "If this service needs secrets:" echo " 1. Add secrets to Bitwarden Secrets Manager" echo " 2. Map them in docker/$name/.env.keys (format: ENV_VAR=bws-secret-name)" echo " 3. Add env_file: .env to compose.yml" echo " configure.sh will generate the .env file on the server automatically." } cmd_all() { local name="$1" cmd_dns "$name" cmd_files "$name" } # --- Dispatch --- case "$COMMAND" in dns) validate_name "${2:-}" cmd_dns "${2:-}" ;; files) validate_name "${2:-}" cmd_files "${2:-}" ;; all) validate_name "${2:-}" cmd_all "${2:-}" ;; *) echo "Unknown command: $COMMAND" echo "" usage ;; esac