Makes ./run forge:dns use ctforge by default so http://ctforge:3000 works like mcforge:3000 for magic-civilization. Updated help text, docs examples, and default in cmd_forge_dns.
144 lines
6.9 KiB
Bash
Executable file
144 lines
6.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Forgejo origin lifecycle on DigitalOcean. Sourced by ./run (defines cmd_forge_*).
|
|
# ./run forge:down stop service, snapshot, destroy droplet (~$6/mo -> ~$0.30/mo idle)
|
|
# ./run forge:up recreate from newest snapshot, refresh ~/.vault/cocotte_forge_creds
|
|
#
|
|
# DO bills powered-off droplets; only destroy stops billing, so "down" =
|
|
# snapshot + destroy and "up" = create-from-snapshot. The droplet gets a NEW ip
|
|
# each time, so forge:up refreshes the vault creds file (the single source of
|
|
# truth for the forge URL).
|
|
|
|
_FORGE_TAG="forgejo"
|
|
_FORGE_SIZE="s-1vcpu-1gb"
|
|
_FORGE_REGION="nyc3"
|
|
_FORGE_SNAP_PREFIX="cocotte-forge-snap"
|
|
_FORGE_KEY_NAME="cocotte-fleet"
|
|
_VAULT_PAT="$HOME/.vault/do_pat_cocotte"
|
|
_VAULT_CREDS="$HOME/.vault/cocotte_forge_creds"
|
|
|
|
_forge_pat() { cat "$_VAULT_PAT" 2>/dev/null; }
|
|
|
|
_forge_curl() {
|
|
# _forge_curl METHOD PATH [JSON-body]
|
|
local method="$1" path="$2" data="${3:-}" pat
|
|
pat="$(_forge_pat)"
|
|
if [ -n "$data" ]; then
|
|
curl -s -X "$method" -H "Authorization: Bearer $pat" -H "Content-Type: application/json" -d "$data" "https://api.digitalocean.com/v2${path}"
|
|
else
|
|
curl -s -X "$method" -H "Authorization: Bearer $pat" "https://api.digitalocean.com/v2${path}"
|
|
fi
|
|
}
|
|
|
|
_forge_droplet_id() {
|
|
_forge_curl GET "/droplets?tag_name=${_FORGE_TAG}" \
|
|
| python3 -c "import sys,json;d=json.load(sys.stdin).get('droplets',[]);print(d[0]['id'] if d else '')"
|
|
}
|
|
|
|
_forge_project_id() {
|
|
_forge_curl GET "/projects" \
|
|
| python3 -c "import sys,json;print(next((p['id'] for p in json.load(sys.stdin)['projects'] if p['name']=='cocotte:dev'),''))"
|
|
}
|
|
|
|
_forge_key_id() {
|
|
# Lookup numeric ID for the registered SSH key by name (avoids hardcoding CHANGE_ME after registering the pubkey in DO).
|
|
local name="${1:-${_FORGE_KEY_NAME}}"
|
|
_forge_curl GET "/account/keys" \
|
|
| python3 -c "
|
|
import sys, json
|
|
name='${name}'
|
|
for k in json.load(sys.stdin).get('ssh_keys', []):
|
|
if k.get('name') == name:
|
|
print(k['id'])
|
|
break
|
|
else:
|
|
print('')
|
|
" 2>/dev/null || echo ''
|
|
}
|
|
|
|
_forge_wait_action() {
|
|
local aid="$1" st
|
|
for _ in $(seq 1 120); do
|
|
st=$(_forge_curl GET "/actions/$aid" | python3 -c "import sys,json;print(json.load(sys.stdin)['action']['status'])" 2>/dev/null)
|
|
[ "$st" = completed ] && return 0
|
|
[ "$st" = errored ] && { echo "action $aid errored" >&2; return 1; }
|
|
sleep 5
|
|
done
|
|
echo "action $aid timed out" >&2
|
|
return 1
|
|
}
|
|
|
|
cmd_forge() {
|
|
cat <<'EOF'
|
|
Forgejo origin lifecycle (DigitalOcean). Needs ~/.vault/do_pat_cocotte + the cocotte-fleet SSH key registered in DO.
|
|
(We just generated ~/.ssh/id_cocotte_fleet + .pub — add the .pub to your DO account as 'cocotte-fleet' if not done.)
|
|
./run forge:down stop + snapshot + destroy (~$6/mo -> ~$0.30/mo idle)
|
|
./run forge:up restore from newest snapshot, refresh vault creds (auto-looks up key ID)
|
|
./run forge:dns point the 'ctforge' hostname at the current forge IP (sudo; macOS /etc/hosts)
|
|
EOF
|
|
}
|
|
|
|
cmd_forge_dns() {
|
|
# Map a friendly hostname to the current forge IP in /etc/hosts (macOS).
|
|
# Re-run after forge:up (the IP changes). Browse the forge at http://ctforge:3000.
|
|
local name="${1:-ctforge}" ip
|
|
ip="$(grep -E '^FORGE_IP=' "$_VAULT_CREDS" 2>/dev/null | cut -d= -f2)"
|
|
[ -n "$ip" ] || { echo "no FORGE_IP in $_VAULT_CREDS" >&2; return 1; }
|
|
sudo sh -c "sed -i '' '/[[:space:]]${name}\$/d' /etc/hosts 2>/dev/null; printf '%s\t%s\n' '$ip' '$name' >> /etc/hosts"
|
|
echo "/etc/hosts: $name -> $ip → http://$name:3000"
|
|
}
|
|
|
|
cmd_forge_down() {
|
|
local id ip aid snap
|
|
id="$(_forge_droplet_id)"
|
|
[ -n "$id" ] || { echo "no live cocotte-forge droplet (already down?)" >&2; return 1; }
|
|
ip=$(grep -E '^FORGE_IP=' "$_VAULT_CREDS" 2>/dev/null | cut -d= -f2)
|
|
echo "[1/4] stopping forgejo service on ${ip:-?}"
|
|
[ -n "$ip" ] && ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -i ~/.ssh/id_cocotte_fleet root@"$ip" 'systemctl stop forgejo' 2>/dev/null || true
|
|
echo "[2/4] powering off droplet $id"
|
|
aid=$(_forge_curl POST "/droplets/$id/actions" '{"type":"power_off"}' | python3 -c "import sys,json;print(json.load(sys.stdin)['action']['id'])")
|
|
_forge_wait_action "$aid" || return 1
|
|
snap="${_FORGE_SNAP_PREFIX}-$(date +%Y%m%d%H%M%S)"
|
|
echo "[3/4] snapshotting -> $snap"
|
|
aid=$(_forge_curl POST "/droplets/$id/actions" "{\"type\":\"snapshot\",\"name\":\"$snap\"}" | python3 -c "import sys,json;print(json.load(sys.stdin)['action']['id'])")
|
|
_forge_wait_action "$aid" || return 1
|
|
echo "[4/4] destroying droplet $id"
|
|
_forge_curl DELETE "/droplets/$id" >/dev/null
|
|
echo "forge down — snapshot $snap kept (~\$0.30/mo). './run forge:up' to restore."
|
|
}
|
|
|
|
cmd_forge_up() {
|
|
local snapid did ip code projid admin_user admin_pass
|
|
snapid=$(_forge_curl GET "/snapshots?resource_type=droplet" \
|
|
| python3 -c "import sys,json;s=[x for x in json.load(sys.stdin)['snapshots'] if x['name'].startswith('${_FORGE_SNAP_PREFIX}')];s.sort(key=lambda x:x['created_at']);print(s[-1]['id'] if s else '')")
|
|
[ -n "$snapid" ] || { echo "no ${_FORGE_SNAP_PREFIX:-cocotte-forge-snap}-* snapshot found" >&2; return 1; }
|
|
echo "[1/4] creating droplet from snapshot $snapid"
|
|
local key_id
|
|
key_id=$(_forge_key_id) || key_id=""
|
|
[ -n "$key_id" ] || { echo "ERROR: could not lookup SSH key '${_FORGE_KEY_NAME}' via DO API. Register ~/.ssh/id_cocotte_fleet.pub in DO as '${_FORGE_KEY_NAME}' first." >&2; return 1; }
|
|
did=$(_forge_curl POST "/droplets" "{\"name\":\"cocotte-forge\",\"region\":\"${_FORGE_REGION}\",\"size\":\"${_FORGE_SIZE}\",\"image\":${snapid},\"ssh_keys\":[${key_id}],\"tags\":[\"cocottetech\",\"${_FORGE_TAG}\"]}" \
|
|
| python3 -c "import sys,json;d=json.load(sys.stdin).get('droplet');print(d['id'] if d else '')")
|
|
[ -n "$did" ] || { echo "create failed" >&2; return 1; }
|
|
echo "[2/4] waiting for active + ip (droplet $did)"
|
|
for _ in $(seq 1 40); do
|
|
ip=$(_forge_curl GET "/droplets/$did" | python3 -c "import sys,json;d=json.load(sys.stdin)['droplet'];ips=[n['ip_address'] for n in d['networks']['v4'] if n['type']=='public'];print(ips[0] if ips and d['status']=='active' else '')")
|
|
[ -n "$ip" ] && break
|
|
sleep 8
|
|
done
|
|
[ -n "$ip" ] || { echo "droplet never reported an ip" >&2; return 1; }
|
|
echo "[3/4] waiting for forgejo http at $ip:3000"
|
|
for _ in $(seq 1 30); do code=$(curl -s -o /dev/null -m 5 -w "%{http_code}" "http://$ip:3000/" 2>/dev/null); [ "$code" = 200 ] && break; sleep 4; done
|
|
projid="$(_forge_project_id)"
|
|
[ -n "$projid" ] && _forge_curl POST "/projects/$projid/resources" "{\"resources\":[\"do:droplet:$did\"]}" >/dev/null
|
|
echo "[4/4] refreshing $_VAULT_CREDS with new ip"
|
|
admin_user=$(grep -E '^ADMIN_USER=' "$_VAULT_CREDS" 2>/dev/null | cut -d= -f2); admin_user=${admin_user:-cocotteadmin}
|
|
admin_pass=$(grep -E '^ADMIN_PASS=' "$_VAULT_CREDS" 2>/dev/null | cut -d= -f2)
|
|
umask 177
|
|
cat > "$_VAULT_CREDS" <<EOF
|
|
FORGE_IP=$ip
|
|
FORGE_URL=http://$ip:3000
|
|
ADMIN_USER=$admin_user
|
|
ADMIN_PASS=$admin_pass
|
|
SSH_KEY=~/.ssh/id_cocotte_fleet
|
|
EOF
|
|
echo "forge up at http://$ip:3000 (http $code). vault creds refreshed."
|
|
}
|