rename new-service.sh to service.sh with subcommands

This commit is contained in:
2026-03-27 12:28:32 -04:00
parent 313fb4655f
commit c87f76a78a
4 changed files with 245 additions and 75 deletions
+243
View File
@@ -0,0 +1,243 @@
#!/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"
if [ -d "$REPO_ROOT/docker/$name" ]; then
echo "Error: docker/$name already exists."
exit 1
fi
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
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"
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