#!/usr/bin/env bash # Two containerised SSH remotes for manual vev testing from the host terminal. # # scripts/demo/test-remotes.sh up|status|down|broker-restart|shell # # Everything vev-side runs with VEV_ENV=test, so config, state, runtime and # hosts live in .dev/test/ of this checkout. SSH material lives in # .dev/test/ssh/; ~/.ssh is never read or written. vev runs plain `ssh` from # PATH, so .dev/test/bin/ssh wraps the real ssh with `-F .dev/test/ssh/config`. # # Environment: # VEV_TEST_SSH_PORT_A / VEV_TEST_SSH_PORT_B loopback sshd ports (2222/2223) # VEV_TEST_TAG image tag (test) # VEV_TEST_ENV VEV_ENV name (test) # DOCKER_HOST honoured as-is set -euo pipefail cd "$(dirname "$0")/../.." root=$(pwd -P) export VEV_TEST_TAG=${VEV_TEST_TAG:-test} env_name=${VEV_TEST_ENV:-test} # Exported so compose.test.yaml's `name:` interpolates the same resolved # value: a distinct VEV_ENV name must get a distinct compose project, or two # sandboxes running side by side under different VEV_TEST_ENV values collide # on the same containers/network. export VEV_TEST_ENV="$env_name" export VEV_TEST_SSH_PORT_A=${VEV_TEST_SSH_PORT_A:-2222} export VEV_TEST_SSH_PORT_B=${VEV_TEST_SSH_PORT_B:-2223} image="vev-sandbox:${VEV_TEST_TAG}" compose=(docker compose -f scripts/demo/compose.test.yaml) hosts=(remote-a remote-b) env_root="$root/.dev" test_dir="$env_root/$env_name" ssh_dir="$test_dir/ssh" bin_dir="$test_dir/bin" vev_bin="$root/build/vev" die() { echo "test-remotes: error: $*" >&2; exit 1; } log() { echo "test-remotes: $*" >&2; } port_for() { case "$1" in remote-a) echo "$VEV_TEST_SSH_PORT_A" ;; remote-b) echo "$VEV_TEST_SSH_PORT_B" ;; *) die "unknown host $1" ;; esac } # vev_env runs a command with the sandbox vev environment. VEV is unset so a # command launched from inside a real vev pane does not look nested. vev_env() { env -u VEV -u VEV_ENV_ROOT \ VEV_ENV="$env_name" VEV_ENV_ROOT="$env_root" VEV_REMOTE_TRANSPORT=stdio \ PATH="$bin_dir:$PATH" "$@" } need_docker() { command -v docker >/dev/null || die "docker not found" docker info >/dev/null 2>&1 || die "docker daemon unreachable (DOCKER_HOST=${DOCKER_HOST:-unset})" } real_ssh() { local path path=$(PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vxF "$bin_dir" | paste -sd:) command -v ssh) \ || die "ssh client not found" echo "$path" } write_ssh_material() { local dir for dir in "$env_root" "$test_dir" "$ssh_dir" "$bin_dir"; do [[ -d $dir ]] || mkdir -m 700 "$dir" done if [[ ! -f "$ssh_dir/id_ed25519" ]]; then ssh-keygen -q -t ed25519 -N '' -C vev-test-remotes -f "$ssh_dir/id_ed25519" fi { echo "# Generated by scripts/demo/test-remotes.sh. Sandbox only." local host for host in "${hosts[@]}"; do cat <"$ssh_dir/config" chmod 600 "$ssh_dir/config" cat >"$bin_dir/ssh" <"$ssh_dir/known_hosts.tmp" for host in "${hosts[@]}"; do cid=$(container_for "$host") [[ -n $cid ]] || die "$host container is not running" docker exec -i "$cid" sh -c 'umask 077; cat > /home/demo/.ssh/authorized_keys && chown demo:demo /home/demo/.ssh/authorized_keys' <<<"$pub" printf '%s ' "$host" >>"$ssh_dir/known_hosts.tmp" docker exec "$cid" cut -d' ' -f1-2 /etc/ssh/ssh_host_ed25519_key.pub >>"$ssh_dir/known_hosts.tmp" done mv "$ssh_dir/known_hosts.tmp" "$ssh_dir/known_hosts" chmod 600 "$ssh_dir/known_hosts" } wait_ssh() { local host for host in "${hosts[@]}"; do local ok= for _ in $(seq 1 40); do if timeout 5 "$bin_dir/ssh" -o ConnectTimeout=2 -o BatchMode=yes "$host" true >/dev/null 2>&1; then ok=1; break fi sleep 0.25 done [[ -n $ok ]] || die "SSH readiness timeout for $host (try: $bin_dir/ssh -v $host true)" timeout 10 "$bin_dir/ssh" -o BatchMode=yes "$host" 'vev --version' >/dev/null \ || die "vev is not runnable on $host" log "ssh ready: $host" done } register_hosts() { local host listed listed=$(vev_env timeout 30 "$vev_bin" host list 2>&1) || die "vev host list failed: $listed" for host in "${hosts[@]}"; do if grep -qw -- "$host" <<<"$listed"; then log "host already registered: $host" continue fi vev_env timeout 30 "$vev_bin" host add "$host" || die "vev host add $host failed" log "host registered: $host" done } cmd_up() { need_docker log "building build/vev" go build -o build/vev . log "building $image" docker build -q --label vev.sandbox.binary-sha256="$(sha256sum build/vev | cut -d' ' -f1)" \ -t "$image" -f scripts/demo/Dockerfile . >/dev/null local stale stale=$(vev_pids | while read -r pid; do exe=$(readlink "/proc/$pid/exe" 2>/dev/null) || continue [[ $exe == "$vev_bin" ]] || echo "$pid" done | paste -sd' ') if [[ -n $stale ]]; then log "warning: VEV_ENV=$env_name broker/daemon from an older build is still running (pids $stale)." log "warning: it keeps serving until stopped; run '$0 down' then '$0 up' to test the new build." fi write_ssh_material # --force-recreate picks up a rebuilt image and restarts remote daemons. local out out=$("${compose[@]}" up -d --force-recreate --remove-orphans remote-a remote-b 2>&1) \ || die "compose up failed: $out" install_keys wait_ssh register_hosts echo cmd_shell_hint } cmd_status() { need_docker "${compose[@]}" ps --format 'table {{.Service}}\t{{.State}}\t{{.Ports}}' echo if [[ ! -x $vev_bin ]]; then echo "build/vev missing; run: $0 up" return 0 fi echo "== vev host list" vev_env timeout 15 "$vev_bin" host list || echo "(host list failed: $?)" echo "== vev ls --all" vev_env timeout 30 "$vev_bin" ls --all || echo "(ls --all failed: $?)" } # vev_pids prints vev processes of this checkout's sandbox environment: the # executable lives inside this checkout, and the environment carries both # VEV_ENV= and VEV_ENV_ROOT=/.dev. The owner's installed # vev and other VEV_ENV names never match. Pane shells are not signalled; they # end with their daemon. vev_pids() { local environ pid exe vars for environ in /proc/[0-9]*/environ; do [[ -r $environ ]] || continue pid=${environ#/proc/}; pid=${pid%/environ} exe=$(readlink "/proc/$pid/exe" 2>/dev/null) || continue [[ ${exe% (deleted)} == "$root"/* ]] || continue vars=$(tr '\0' '\n' 2>/dev/null <"$environ") || continue if grep -qxF "VEV_ENV=$env_name" <<<"$vars" && grep -qxF "VEV_ENV_ROOT=$env_root" <<<"$vars"; then echo "$pid" fi done return 0 } cmd_down() { if [[ -x $vev_bin && -d $test_dir ]]; then local host # Forget the registrations so a later plain VEV_ENV=test client does not # keep dialing hosts that no longer exist; `up` re-adds them. for host in "${hosts[@]}"; do vev_env timeout 15 "$vev_bin" host rm "$host" >/dev/null 2>&1 || true done vev_env timeout 10 "$vev_bin" _daemon-stop >/dev/null 2>&1 || true fi # `kill --all` would stop every vev of this user: TERM the sandbox broker and any leftover # sandbox roles, then KILL whatever ignores it. local pids pids=$(vev_pids | paste -sd' ') if [[ -n $pids ]]; then log "stopping VEV_ENV=$env_name vev processes: $pids" # shellcheck disable=SC2086 kill -TERM $pids 2>/dev/null || true for _ in $(seq 1 20); do [[ -z $(vev_pids) ]] && break sleep 0.1 done pids=$(vev_pids | paste -sd' ') # shellcheck disable=SC2086 [[ -z $pids ]] || kill -KILL $pids 2>/dev/null || true fi if command -v docker >/dev/null && docker info >/dev/null 2>&1; then "${compose[@]}" down -v --remove-orphans --timeout 5 >/dev/null 2>&1 || true else log "docker unreachable; containers were not removed (DOCKER_HOST=${DOCKER_HOST:-unset})" fi rm -rf "$ssh_dir" "$bin_dir" log "down" } # broker_restart stops only this sandbox's broker. The daemon and its sessions # stay up; the next vev invocation respawns the broker. cmd_broker_restart() { local pid pids=() for pid in $(vev_pids); do if tr '\0' ' ' 2>/dev/null /dev/null || break sleep 0.1 done vev_env timeout 30 "$vev_bin" host list } cmd_shell_hint() { cat <&2; exit 2 ;; esac