259 lines
7.3 KiB
Bash
Executable File
259 lines
7.3 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
echo "Usage: ./tools/service.sh <command> <name>"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " dns <name> Create A record for <name>.hantim.net"
|
|
echo " files <name> Create compose.yml and deploy workflow"
|
|
echo " all <name> 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" <<EOF
|
|
services:
|
|
$name:
|
|
image: TODO
|
|
restart: unless-stopped
|
|
container_name: $name
|
|
networks:
|
|
- shared
|
|
|
|
networks:
|
|
shared:
|
|
external: true
|
|
EOF
|
|
else
|
|
echo " docker/$name/compose.yml already exists, skipping."
|
|
fi
|
|
|
|
if [ ! -e "$REPO_ROOT/.gitea/workflows/deploy-$name.yml" ]; then
|
|
echo "==> 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
|