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
+1 -1
View File
@@ -9,7 +9,7 @@ for technical deep-dives, USECASES.md for expected behaviors.
- `scripts/configure.sh` -- Idempotent server config (firewall, certbot, start services; CI-safe) - `scripts/configure.sh` -- Idempotent server config (firewall, certbot, start services; CI-safe)
- `scripts/deploy.sh` -- SSH-triggered deploy + cert issuance + provision (deploy-*, cert-*, provision commands) - `scripts/deploy.sh` -- SSH-triggered deploy + cert issuance + provision (deploy-*, cert-*, provision commands)
- `tools/app.sh` -- App provisioning by subcommand (dns, repo, files, cert, garage, build, verify, all) - `tools/app.sh` -- App provisioning by subcommand (dns, repo, files, cert, garage, build, verify, all)
- `tools/new-service.sh` -- Scaffold a new Docker Compose service (compose + workflow) - `tools/service.sh` -- Manage Docker services by subcommand (dns, files, all)
- `tools/remove-app.sh` -- Remove a static site (local files + Gitea repo) - `tools/remove-app.sh` -- Remove a static site (local files + Gitea repo)
- `docker/nginx/conf.d/` -- Per-app nginx server blocks - `docker/nginx/conf.d/` -- Per-app nginx server blocks
- `docker/<domain>/compose.yml` -- Per-app compose files - `docker/<domain>/compose.yml` -- Per-app compose files
+1 -1
View File
@@ -29,7 +29,7 @@ scripts/
deploy.sh # Deploy + provision (called via SSH) deploy.sh # Deploy + provision (called via SSH)
tools/ tools/
app.sh # Add a new static site (run from dev machine) app.sh # Add a new static site (run from dev machine)
new-service.sh # Add a new Docker service (run from dev machine) service.sh # Manage Docker services by subcommand (dns, files, all)
remove-app.sh # Remove a static site (run from dev machine) remove-app.sh # Remove a static site (run from dev machine)
docker/ docker/
nginx/ # Reverse proxy (nginx:alpine) + SSL config nginx/ # Reverse proxy (nginx:alpine) + SSL config
-73
View File
@@ -1,73 +0,0 @@
#!/bin/bash
set -euo pipefail
if [ -z "${1:-}" ]; then
echo "Usage: ./tools/new-service.sh <name>"
echo " Example: ./tools/new-service.sh garage"
echo ""
echo "Creates docker/<name>/compose.yml and .gitea/workflows/deploy-<name>.yml"
exit 1
fi
APP="$1"
if ! [[ "$APP" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
echo "Error: name must be alphanumeric (hyphens, dots, underscores allowed)."
exit 1
fi
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
if [ -d "$REPO_ROOT/docker/$APP" ]; then
echo "Error: docker/$APP already exists."
exit 1
fi
echo "Creating docker/$APP/compose.yml..."
mkdir -p "$REPO_ROOT/docker/$APP"
touch "$REPO_ROOT/docker/$APP/.gitignore"
cat > "$REPO_ROOT/docker/$APP/compose.yml" <<EOF
services:
$APP:
image: TODO
restart: unless-stopped
container_name: $APP
networks:
- shared
networks:
shared:
external: true
EOF
echo "Creating .gitea/workflows/deploy-$APP.yml..."
mkdir -p "$REPO_ROOT/.gitea/workflows"
cat > "$REPO_ROOT/.gitea/workflows/deploy-$APP.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 "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh -o StrictHostKeyChecking=accept-new -i ~/.ssh/deploy_key deploy@${{ vars.DEPLOY_HOST }} deploy-APP_PLACEHOLDER
OUTER
sed -i "s/APP_PLACEHOLDER/$APP/g" "$REPO_ROOT/.gitea/workflows/deploy-$APP.yml"
echo "Done. Edit docker/$APP/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/$APP/.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."
+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