test-remotes.sh RawHistoryFolder
1#!/usr/bin/env bash
2# Two containerised SSH remotes for manual vev testing from the host terminal.
3#
4# scripts/demo/test-remotes.sh up|status|down|broker-restart|shell
5#
6# Everything vev-side runs with VEV_ENV=test, so config, state, runtime and
7# hosts live in .dev/test/ of this checkout. SSH material lives in
8# .dev/test/ssh/; ~/.ssh is never read or written. vev runs plain `ssh` from
9# PATH, so .dev/test/bin/ssh wraps the real ssh with `-F .dev/test/ssh/config`.
10#
11# Environment:
12# VEV_TEST_SSH_PORT_A / VEV_TEST_SSH_PORT_B loopback sshd ports (2222/2223)
13# VEV_TEST_TAG image tag (test)
14# VEV_TEST_ENV VEV_ENV name (test)
15# DOCKER_HOST honoured as-is
16set -euo pipefail
17
18cd "$(dirname "$0")/../.."
19root=$(pwd -P)
20
21export VEV_TEST_TAG=${VEV_TEST_TAG:-test}
22env_name=${VEV_TEST_ENV:-test}
23# Exported so compose.test.yaml's `name:` interpolates the same resolved
24# value: a distinct VEV_ENV name must get a distinct compose project, or two
25# sandboxes running side by side under different VEV_TEST_ENV values collide
26# on the same containers/network.
27export VEV_TEST_ENV="$env_name"
28export VEV_TEST_SSH_PORT_A=${VEV_TEST_SSH_PORT_A:-2222}
29export VEV_TEST_SSH_PORT_B=${VEV_TEST_SSH_PORT_B:-2223}
30
31image="vev-sandbox:${VEV_TEST_TAG}"
32compose=(docker compose -f scripts/demo/compose.test.yaml)
33hosts=(remote-a remote-b)
34env_root="$root/.dev"
35test_dir="$env_root/$env_name"
36ssh_dir="$test_dir/ssh"
37bin_dir="$test_dir/bin"
38vev_bin="$root/build/vev"
39
40die() { echo "test-remotes: error: $*" >&2; exit 1; }
41log() { echo "test-remotes: $*" >&2; }
42
43port_for() {
44 case "$1" in
45 remote-a) echo "$VEV_TEST_SSH_PORT_A" ;;
46 remote-b) echo "$VEV_TEST_SSH_PORT_B" ;;
47 *) die "unknown host $1" ;;
48 esac
49}
50
51# vev_env runs a command with the sandbox vev environment. VEV is unset so a
52# command launched from inside a real vev pane does not look nested.
53vev_env() {
54 env -u VEV -u VEV_ENV_ROOT \
55 VEV_ENV="$env_name" VEV_ENV_ROOT="$env_root" VEV_REMOTE_TRANSPORT=stdio \
56 PATH="$bin_dir:$PATH" "$@"
57}
58
59need_docker() {
60 command -v docker >/dev/null || die "docker not found"
61 docker info >/dev/null 2>&1 || die "docker daemon unreachable (DOCKER_HOST=${DOCKER_HOST:-unset})"
62}
63
64real_ssh() {
65 local path
66 path=$(PATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vxF "$bin_dir" | paste -sd:) command -v ssh) \
67 || die "ssh client not found"
68 echo "$path"
69}
70
71write_ssh_material() {
72 local dir
73 for dir in "$env_root" "$test_dir" "$ssh_dir" "$bin_dir"; do
74 [[ -d $dir ]] || mkdir -m 700 "$dir"
75 done
76 if [[ ! -f "$ssh_dir/id_ed25519" ]]; then
77 ssh-keygen -q -t ed25519 -N '' -C vev-test-remotes -f "$ssh_dir/id_ed25519"
78 fi
79 {
80 echo "# Generated by scripts/demo/test-remotes.sh. Sandbox only."
81 local host
82 for host in "${hosts[@]}"; do
83 cat <<EOF
84
85Host $host
86 HostName 127.0.0.1
87 Port $(port_for "$host")
88 User demo
89 HostKeyAlias $host
90 IdentityFile $ssh_dir/id_ed25519
91 IdentitiesOnly yes
92 IdentityAgent none
93 UserKnownHostsFile $ssh_dir/known_hosts
94 GlobalKnownHostsFile /dev/null
95 StrictHostKeyChecking yes
96 ControlMaster no
97 ControlPath none
98 LogLevel ERROR
99EOF
100 done
101 } >"$ssh_dir/config"
102 chmod 600 "$ssh_dir/config"
103 cat >"$bin_dir/ssh" <<EOF
104#!/bin/sh
105# Sandbox ssh: vev runs \`ssh\` from PATH; force the sandbox config.
106exec $(real_ssh) -F "$ssh_dir/config" "\$@"
107EOF
108 chmod 700 "$bin_dir/ssh"
109}
110
111container_for() {
112 "${compose[@]}" ps -q "$1"
113}
114
115install_keys() {
116 local host cid pub
117 pub=$(cat "$ssh_dir/id_ed25519.pub")
118 : >"$ssh_dir/known_hosts.tmp"
119 for host in "${hosts[@]}"; do
120 cid=$(container_for "$host")
121 [[ -n $cid ]] || die "$host container is not running"
122 docker exec -i "$cid" sh -c 'umask 077; cat > /home/demo/.ssh/authorized_keys && chown demo:demo /home/demo/.ssh/authorized_keys' <<<"$pub"
123 printf '%s ' "$host" >>"$ssh_dir/known_hosts.tmp"
124 docker exec "$cid" cut -d' ' -f1-2 /etc/ssh/ssh_host_ed25519_key.pub >>"$ssh_dir/known_hosts.tmp"
125 done
126 mv "$ssh_dir/known_hosts.tmp" "$ssh_dir/known_hosts"
127 chmod 600 "$ssh_dir/known_hosts"
128}
129
130wait_ssh() {
131 local host
132 for host in "${hosts[@]}"; do
133 local ok=
134 for _ in $(seq 1 40); do
135 if timeout 5 "$bin_dir/ssh" -o ConnectTimeout=2 -o BatchMode=yes "$host" true >/dev/null 2>&1; then
136 ok=1; break
137 fi
138 sleep 0.25
139 done
140 [[ -n $ok ]] || die "SSH readiness timeout for $host (try: $bin_dir/ssh -v $host true)"
141 timeout 10 "$bin_dir/ssh" -o BatchMode=yes "$host" 'vev --version' >/dev/null \
142 || die "vev is not runnable on $host"
143 log "ssh ready: $host"
144 done
145}
146
147register_hosts() {
148 local host listed
149 listed=$(vev_env timeout 30 "$vev_bin" host list 2>&1) || die "vev host list failed: $listed"
150 for host in "${hosts[@]}"; do
151 if grep -qw -- "$host" <<<"$listed"; then
152 log "host already registered: $host"
153 continue
154 fi
155 vev_env timeout 30 "$vev_bin" host add "$host" || die "vev host add $host failed"
156 log "host registered: $host"
157 done
158}
159
160cmd_up() {
161 need_docker
162 log "building build/vev"
163 go build -o build/vev .
164 log "building $image"
165 docker build -q --label vev.sandbox.binary-sha256="$(sha256sum build/vev | cut -d' ' -f1)" \
166 -t "$image" -f scripts/demo/Dockerfile . >/dev/null
167 local stale
168 stale=$(vev_pids | while read -r pid; do
169 exe=$(readlink "/proc/$pid/exe" 2>/dev/null) || continue
170 [[ $exe == "$vev_bin" ]] || echo "$pid"
171 done | paste -sd' ')
172 if [[ -n $stale ]]; then
173 log "warning: VEV_ENV=$env_name broker/daemon from an older build is still running (pids $stale)."
174 log "warning: it keeps serving until stopped; run '$0 down' then '$0 up' to test the new build."
175 fi
176 write_ssh_material
177 # --force-recreate picks up a rebuilt image and restarts remote daemons.
178 local out
179 out=$("${compose[@]}" up -d --force-recreate --remove-orphans remote-a remote-b 2>&1) \
180 || die "compose up failed: $out"
181 install_keys
182 wait_ssh
183 register_hosts
184 echo
185 cmd_shell_hint
186}
187
188cmd_status() {
189 need_docker
190 "${compose[@]}" ps --format 'table {{.Service}}\t{{.State}}\t{{.Ports}}'
191 echo
192 if [[ ! -x $vev_bin ]]; then
193 echo "build/vev missing; run: $0 up"
194 return 0
195 fi
196 echo "== vev host list"
197 vev_env timeout 15 "$vev_bin" host list || echo "(host list failed: $?)"
198 echo "== vev ls --all"
199 vev_env timeout 30 "$vev_bin" ls --all || echo "(ls --all failed: $?)"
200}
201
202# vev_pids prints vev processes of this checkout's sandbox environment: the
203# executable lives inside this checkout, and the environment carries both
204# VEV_ENV=<name> and VEV_ENV_ROOT=<this checkout>/.dev. The owner's installed
205# vev and other VEV_ENV names never match. Pane shells are not signalled; they
206# end with their daemon.
207vev_pids() {
208 local environ pid exe vars
209 for environ in /proc/[0-9]*/environ; do
210 [[ -r $environ ]] || continue
211 pid=${environ#/proc/}; pid=${pid%/environ}
212 exe=$(readlink "/proc/$pid/exe" 2>/dev/null) || continue
213 [[ ${exe% (deleted)} == "$root"/* ]] || continue
214 vars=$(tr '\0' '\n' 2>/dev/null <"$environ") || continue
215 if grep -qxF "VEV_ENV=$env_name" <<<"$vars" && grep -qxF "VEV_ENV_ROOT=$env_root" <<<"$vars"; then
216 echo "$pid"
217 fi
218 done
219 return 0
220}
221
222cmd_down() {
223 if [[ -x $vev_bin && -d $test_dir ]]; then
224 local host
225 # Forget the registrations so a later plain VEV_ENV=test client does not
226 # keep dialing hosts that no longer exist; `up` re-adds them.
227 for host in "${hosts[@]}"; do
228 vev_env timeout 15 "$vev_bin" host rm "$host" >/dev/null 2>&1 || true
229 done
230 vev_env timeout 10 "$vev_bin" _daemon-stop >/dev/null 2>&1 || true
231 fi
232 # `kill --all` would stop every vev of this user: TERM the sandbox broker and any leftover
233 # sandbox roles, then KILL whatever ignores it.
234 local pids
235 pids=$(vev_pids | paste -sd' ')
236 if [[ -n $pids ]]; then
237 log "stopping VEV_ENV=$env_name vev processes: $pids"
238 # shellcheck disable=SC2086
239 kill -TERM $pids 2>/dev/null || true
240 for _ in $(seq 1 20); do
241 [[ -z $(vev_pids) ]] && break
242 sleep 0.1
243 done
244 pids=$(vev_pids | paste -sd' ')
245 # shellcheck disable=SC2086
246 [[ -z $pids ]] || kill -KILL $pids 2>/dev/null || true
247 fi
248 if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
249 "${compose[@]}" down -v --remove-orphans --timeout 5 >/dev/null 2>&1 || true
250 else
251 log "docker unreachable; containers were not removed (DOCKER_HOST=${DOCKER_HOST:-unset})"
252 fi
253 rm -rf "$ssh_dir" "$bin_dir"
254 log "down"
255}
256
257# broker_restart stops only this sandbox's broker. The daemon and its sessions
258# stay up; the next vev invocation respawns the broker.
259cmd_broker_restart() {
260 local pid pids=()
261 for pid in $(vev_pids); do
262 if tr '\0' ' ' 2>/dev/null </proc/"$pid"/cmdline | grep -q '_broker-production-serve'; then
263 pids+=("$pid")
264 fi
265 done
266 [[ ${#pids[@]} -gt 0 ]] || die "no VEV_ENV=$env_name broker is running"
267 log "stopping broker: ${pids[*]}"
268 kill -TERM "${pids[@]}"
269 for _ in $(seq 1 30); do
270 kill -0 "${pids[@]}" 2>/dev/null || break
271 sleep 0.1
272 done
273 vev_env timeout 30 "$vev_bin" host list
274}
275
276cmd_shell_hint() {
277 cat <<EOF
278Sandbox ready. Launch the client from any terminal with:
279
280 cd $root && env -u VEV VEV_ENV=$env_name VEV_ENV_ROOT=$env_root VEV_REMOTE_TRANSPORT=stdio PATH=$bin_dir:\$PATH ./build/vev
281
282or: $0 shell --exec
283Plain ssh: $bin_dir/ssh remote-a
284EOF
285}
286
287cmd_shell() {
288 [[ -x $bin_dir/ssh ]] || die "sandbox is not up; run: $0 up"
289 if [[ ${1:-} == --exec ]]; then
290 shift
291 exec env -u VEV -u VEV_ENV_ROOT VEV_ENV="$env_name" VEV_ENV_ROOT="$env_root" VEV_REMOTE_TRANSPORT=stdio \
292 PATH="$bin_dir:$PATH" "$vev_bin" "$@"
293 fi
294 cmd_shell_hint
295}
296
297case "${1:-}" in
298 up) cmd_up ;;
299 status) cmd_status ;;
300 down) cmd_down ;;
301 broker-restart) cmd_broker_restart ;;
302 shell) shift; cmd_shell "$@" ;;
303 *) echo "usage: $0 up|status|down|broker-restart|shell [--exec [vev args]]" >&2; exit 2 ;;
304esac