rename new-service.sh to service.sh with subcommands
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."
|
|
||||||
Executable
+243
@@ -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
|
||||||
Reference in New Issue
Block a user