From 15aad2eabee6017321ed35ea7afd9a8232f1a77f Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 29 Jun 2026 23:12:47 -0400 Subject: [PATCH] uvlava: add ./run task runner for the services tier One runner (cf. @applications/prospector/run): services/deploy/status/logs/ restart over the services/ tree (each a dir with deploy.sh + compose.yml, shipped to /opt/), plus a `tf` passthrough to terraform/do with the vault token. Auto-discovers services; target from services//.target else the forge droplet; health from services//.health. Fleet SSH uses a dedicated known_hosts and self-heals a changed host key after a droplet rebuild. Co-Authored-By: Claude Opus 4.8 (1M context) --- run | 151 +++++++++++++++++++++++++++++++++++ services/dns-updater/.health | 1 + 2 files changed, 152 insertions(+) create mode 100755 run create mode 100644 services/dns-updater/.health diff --git a/run b/run new file mode 100755 index 0000000..685f3db --- /dev/null +++ b/run @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# uvlava — infranet Task Runner +# Usage: ./run [args...] +# +# Manages the services under services/ (each a dir with deploy.sh + compose.yml, +# deployed to /opt/ on its target droplet) and wraps the DO terraform. +# +# ./run services List discovered services + their targets. +# ./run deploy [user@host] Run services//deploy.sh (build+ship+wire). +# ./run status [target] docker compose ps (+ health probe if defined). +# ./run logs [target] [-- ] +# ./run restart [target] docker compose restart on the droplet. +# ./run tf terraform in terraform/do/ (token from vault). +# +# A service's target host resolves from services//.target (one line, +# user@host) else DEFAULT_TARGET. Health probe: services//.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 (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 [args...]" + echo "" + echo -e "${YELLOW}Services (services//)${NC}" + echo " services List services + resolved targets." + echo " deploy [target] Build + ship + wire (delegates to its deploy.sh)." + echo " status [target] docker compose ps (+ health probe if defined)." + echo " logs [target] docker compose logs (pass extra args after --)." + echo " restart [target] docker compose restart on the droplet." + echo "" + echo -e "${YELLOW}Infrastructure${NC}" + echo " tf 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 diff --git a/services/dns-updater/.health b/services/dns-updater/.health new file mode 100644 index 0000000..8eb8db5 --- /dev/null +++ b/services/dns-updater/.health @@ -0,0 +1 @@ +http://127.0.0.1:8090/healthz