uvlava/run

152 lines
6.1 KiB
Text
Raw Permalink Normal View History

#!/usr/bin/env bash
# uvlava — infranet Task Runner
# Usage: ./run <command> [args...]
#
# Manages the services under services/ (each a dir with deploy.sh + compose.yml,
# deployed to /opt/<service> on its target droplet) and wraps the DO terraform.
#
# ./run services List discovered services + their targets.
# ./run deploy <svc> [user@host] Run services/<svc>/deploy.sh (build+ship+wire).
# ./run status <svc> [target] docker compose ps (+ health probe if defined).
# ./run logs <svc> [target] [-- <compose logs args>]
# ./run restart <svc> [target] docker compose restart on the droplet.
# ./run tf <args...> terraform in terraform/do/ (token from vault).
#
# A service's target host resolves from services/<svc>/.target (one line,
# user@host) else DEFAULT_TARGET. Health probe: services/<svc>/.health (one line,
# a URL curl'd on the droplet, e.g. http://127.0.0.1:8090/healthz).
#
# Follows the ./run task-runner convention (cf. @applications/prospector/run).
set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVICES_DIR="$REPO_ROOT/services"
DEFAULT_TARGET="root@134.199.243.61" # com.uvlava.quinn.artifacts (forge droplet)
RED=$'\e[31m' YELLOW=$'\e[33m' BLUE=$'\e[34m' GREEN=$'\e[32m' NC=$'\e[0m' || true
die() { echo -e "${RED}uvlava: $*${NC}" >&2; exit 1; }
# Fleet SSH. Cloud droplets get rebuilt (IP reused, host key changes), so we keep
# a DEDICATED known_hosts for the fleet (never conflicts with the user's main
# file), TOFU-accept new keys, and self-heal a changed key (rebuilt host) by
# dropping the stale entry and retrying once. Uses the fleet key when present.
FLEET_KH="$HOME/.ssh/known_hosts_uvlava"
SSH="ssh -o StrictHostKeyChecking=accept-new -o CheckHostIP=no -o UserKnownHostsFile=$FLEET_KH"
[ -f "$HOME/.ssh/id_ed25519_1984" ] && SSH="$SSH -i $HOME/.ssh/id_ed25519_1984"
fssh() { # fssh user@host "remote command..."
local target="$1"; shift
local host="${target#*@}" err
err="$(mktemp)"
if $SSH "$target" "$@" 2>"$err"; then rm -f "$err"; return 0; fi
if grep -qiE "host key.*changed|REMOTE HOST IDENTIFICATION HAS CHANGED" "$err"; then
ssh-keygen -R "$host" -f "$FLEET_KH" >/dev/null 2>&1 || true
rm -f "$err"; $SSH "$target" "$@"; return $?
fi
cat "$err" >&2; rm -f "$err"; return 1
}
# A service = a dir under services/ that has a deploy.sh.
list_services() {
[ -d "$SERVICES_DIR" ] || return 0
for d in "$SERVICES_DIR"/*/; do
[ -f "${d}deploy.sh" ] && basename "$d"
done
}
require_service() {
local svc="$1"
[ -n "$svc" ] || die "missing <service> (see: ./run services)"
[ -f "$SERVICES_DIR/$svc/deploy.sh" ] || die "unknown service '$svc' (see: ./run services)"
}
service_target() { # resolves a service's deploy target
local svc="$1" f="$SERVICES_DIR/$1/.target"
[ -f "$f" ] && head -1 "$f" || echo "$DEFAULT_TARGET"
}
usage() {
echo -e "${BLUE}uvlava${NC} — infranet Task Runner"
echo ""
echo "Usage: ./run <command> [args...]"
echo ""
echo -e "${YELLOW}Services (services/<name>/)${NC}"
echo " services List services + resolved targets."
echo " deploy <svc> [target] Build + ship + wire (delegates to its deploy.sh)."
echo " status <svc> [target] docker compose ps (+ health probe if defined)."
echo " logs <svc> [target] docker compose logs (pass extra args after --)."
echo " restart <svc> [target] docker compose restart on the droplet."
echo ""
echo -e "${YELLOW}Infrastructure${NC}"
echo " tf <args...> terraform in terraform/do/ (do_token from ~/.vault)."
echo ""
if [ -n "$(list_services)" ]; then
echo -e "${YELLOW}Discovered services${NC}"
while read -r s; do printf ' %-16s -> %s\n' "$s" "$(service_target "$s")"; done < <(list_services)
fi
}
cmd_services() {
local any=0
while read -r s; do any=1; printf '%-16s -> %s\n' "$s" "$(service_target "$s")"; done < <(list_services)
[ "$any" = 1 ] || echo "(no services under $SERVICES_DIR — a service is a dir with deploy.sh)"
}
cmd_deploy() {
local svc="${1:-}"; require_service "$svc"; shift
echo -e "${GREEN}==> deploy $svc${NC}"
exec "$SERVICES_DIR/$svc/deploy.sh" "$@"
}
cmd_status() {
local svc="${1:-}"; require_service "$svc"
local target="${2:-$(service_target "$svc")}"
echo -e "${GREEN}==> $svc @ $target${NC}"
fssh "$target" "cd /opt/$svc && docker compose ps" || die "compose ps failed on $target"
local health="$SERVICES_DIR/$svc/.health"
if [ -f "$health" ]; then
local url; url="$(head -1 "$health")"
echo "--- health: $url ---"
fssh "$target" "curl -fsS '$url'" && echo || echo -e "${RED}(health probe failed)${NC}"
fi
}
cmd_logs() {
local svc="${1:-}"; require_service "$svc"
local target="${2:-$(service_target "$svc")}"; shift 2>/dev/null || true; shift 2>/dev/null || true
# everything after `--` is forwarded to `docker compose logs`
[ "${1:-}" = "--" ] && shift
local extra="${*:---tail=100}"
fssh "$target" "cd /opt/$svc && docker compose logs $extra"
}
cmd_restart() {
local svc="${1:-}"; require_service "$svc"
local target="${2:-$(service_target "$svc")}"
echo -e "${GREEN}==> restart $svc @ $target${NC}"
fssh "$target" "cd /opt/$svc && docker compose restart"
}
cmd_tf() {
local tfdir="$REPO_ROOT/terraform/do"
[ -d "$tfdir" ] || die "no terraform dir at $tfdir"
local tok=""
for c in "$HOME/.vault/do-pat-ct.token" "$HOME/.vault/do_pat_ct" "$HOME/.vault/do_pat_cocotte"; do
[ -f "$c" ] && { tok="$(cat "$c")"; break; }
done
[ -n "$tok" ] || die "no DO token found in ~/.vault (do-pat-ct.token / do_pat_cocotte)"
(cd "$tfdir" && TF_VAR_do_token="$tok" terraform "$@")
}
COMMAND="${1:-}"; shift 2>/dev/null || true
case "$COMMAND" in
help|--help|-h|"") usage; exit 0 ;;
services|ls) cmd_services "$@" ;;
deploy) cmd_deploy "$@" ;;
status) cmd_status "$@" ;;
logs) cmd_logs "$@" ;;
restart) cmd_restart "$@" ;;
tf|terraform) cmd_tf "$@" ;;
*) echo -e "${RED}Unknown command: $COMMAND${NC}" >&2; usage; exit 1 ;;
esac