152 lines
6.1 KiB
Text
152 lines
6.1 KiB
Text
|
|
#!/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
|