Files
archy/scripts/dev-container-test.sh
Dorian c917814d32
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled
refactor: migrate container registry from 80.71.235.15:3000 to git.tx1138.com/lfg2025
All hardcoded references to the old IP-based registry replaced across
Rust backend, Vue frontend, shell scripts, Dockerfiles, CI, and docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 09:33:10 -04:00

260 lines
9.2 KiB
Bash
Executable File

#!/bin/bash
#
# Container Orchestration Dev Loop
# Fast edit-build-test cycle against real containers on .228
#
# Usage:
# ./scripts/dev-container-test.sh # Interactive loop
# ./scripts/dev-container-test.sh --once # Single run (for CI)
#
# Workflow: edit locally → rsync → build on server → restart → smoke test
#
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}"
SSH_HOST="${ARCHIPELAGO_SSH_HOST:-archipelago@192.168.1.228}"
SSH_OPTS="-o StrictHostKeyChecking=no -o ServerAliveInterval=15 -i $SSH_KEY"
REMOTE_DIR="/home/archipelago/archy"
RPC_URL="http://192.168.1.228/rpc/v1"
COOKIE=""
ONCE=false
[ "$1" = "--once" ] && ONCE=true
# ── Colors ──────────────────────────────────────────────────────────────
RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m'
CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m'
pass() { echo -e " ${GREEN}${NC} $*"; }
fail() { echo -e " ${RED}${NC} $*"; FAILURES=$((FAILURES + 1)); }
info() { echo -e " ${CYAN}${NC} $*"; }
header() { echo -e "\n${BOLD}$*${NC}"; }
TESTS=0
FAILURES=0
# ── Helpers ─────────────────────────────────────────────────────────────
rpc() {
local method="$1"
local params="${2:-{}}"
local result
result=$(curl -sf -b "$COOKIE" -X POST "$RPC_URL" \
-H "Content-Type: application/json" \
-d "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":$params,\"id\":1}" \
--connect-timeout 10 --max-time 30 2>/dev/null)
echo "$result"
}
login() {
# Get session cookie
COOKIE=$(mktemp)
local resp
resp=$(curl -sf -c "$COOKIE" -X POST "$RPC_URL" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"auth.login","params":{"password":"password123"},"id":1}' \
--connect-timeout 10 2>/dev/null)
if echo "$resp" | grep -q '"result"'; then
return 0
fi
return 1
}
wait_for_health() {
local timeout=${1:-30}
for i in $(seq 1 "$timeout"); do
if curl -sf "http://192.168.1.228/health" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
# ── Sync & Build ────────────────────────────────────────────────────────
sync_and_build() {
header "Step 1: Sync code to .228"
rsync -az --delete \
--exclude='.git' --exclude='target' --exclude='node_modules' \
--exclude='dist' --exclude='*.iso' --exclude='.claude' \
-e "ssh $SSH_OPTS" \
"$PROJECT_ROOT/" "$SSH_HOST:$REMOTE_DIR/" 2>&1
pass "Code synced"
header "Step 2: Build backend (incremental)"
local build_start=$(date +%s)
if ssh $SSH_OPTS "$SSH_HOST" "cd $REMOTE_DIR/core && cargo build --release -p archipelago 2>&1 | tail -3"; then
local elapsed=$(( $(date +%s) - build_start ))
pass "Built in ${elapsed}s"
else
fail "Build failed"
return 1
fi
header "Step 3: Restart service"
ssh $SSH_OPTS "$SSH_HOST" "sudo systemctl restart archipelago"
info "Waiting for health..."
if wait_for_health 30; then
pass "Backend healthy"
else
fail "Backend failed to start (30s timeout)"
ssh $SSH_OPTS "$SSH_HOST" "journalctl -u archipelago --since '30 sec ago' --no-pager | tail -20"
return 1
fi
}
# ── Smoke Tests ─────────────────────────────────────────────────────────
run_smoke_tests() {
header "Step 4: Container Orchestration Smoke Tests"
TESTS=0
FAILURES=0
# Login
if login; then
pass "Authenticated"
else
fail "Login failed"
return 1
fi
# Test 1: Container list
TESTS=$((TESTS + 1))
local list
list=$(rpc "container.list")
if echo "$list" | grep -q '"result"'; then
local count
count=$(echo "$list" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('result',{}).get('containers',[])))" 2>/dev/null || echo "?")
pass "container.list: $count containers"
else
fail "container.list failed"
fi
# Test 2: Health status
TESTS=$((TESTS + 1))
local health
health=$(rpc "container.health")
if echo "$health" | grep -q '"result"'; then
pass "container.health: OK"
else
fail "container.health failed"
fi
# Test 3: Install a lightweight container (filebrowser — small, fast, no deps)
TESTS=$((TESTS + 1))
local install_img="git.tx1138.com/lfg2025/filebrowser:v2.27.0"
# Check if already installed
local fb_state
fb_state=$(ssh $SSH_OPTS "$SSH_HOST" "podman inspect filebrowser --format '{{.State.Status}}' 2>/dev/null || echo 'none'")
if [ "$fb_state" = "none" ]; then
info "Installing filebrowser..."
local install_result
install_result=$(rpc "package.install" "{\"id\":\"filebrowser\",\"dockerImage\":\"$install_img\"}")
if echo "$install_result" | grep -q '"success"'; then
pass "package.install filebrowser: success"
else
fail "package.install filebrowser: $(echo "$install_result" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("error",{}).get("message","unknown"))' 2>/dev/null)"
fi
else
pass "filebrowser already installed ($fb_state)"
fi
# Test 4: Stop with grace period
TESTS=$((TESTS + 1))
local stop_result
stop_result=$(rpc "package.stop" '{"id":"filebrowser"}')
sleep 2
fb_state=$(ssh $SSH_OPTS "$SSH_HOST" "podman inspect filebrowser --format '{{.State.Status}}' 2>/dev/null || echo 'unknown'")
if [ "$fb_state" = "exited" ] || [ "$fb_state" = "stopped" ]; then
pass "package.stop: filebrowser → $fb_state"
else
fail "package.stop: expected stopped, got $fb_state"
fi
# Test 5: Start
TESTS=$((TESTS + 1))
rpc "package.start" '{"id":"filebrowser"}' >/dev/null
sleep 3
fb_state=$(ssh $SSH_OPTS "$SSH_HOST" "podman inspect filebrowser --format '{{.State.Status}}' 2>/dev/null || echo 'unknown'")
if [ "$fb_state" = "running" ]; then
pass "package.start: filebrowser → running"
else
fail "package.start: expected running, got $fb_state"
fi
# Test 6: Restart tracker persisted
TESTS=$((TESTS + 1))
local tracker
tracker=$(ssh $SSH_OPTS "$SSH_HOST" "cat /var/lib/archipelago/restart-tracker.json 2>/dev/null")
if [ -n "$tracker" ] && echo "$tracker" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
pass "restart-tracker.json: valid JSON"
else
pass "restart-tracker.json: empty (no failures — healthy)"
fi
# Test 7: Systemd timers active
TESTS=$((TESTS + 1))
local timers
timers=$(ssh $SSH_OPTS "$SSH_HOST" "systemctl list-timers --no-pager 2>/dev/null | grep -c archipelago")
if [ "${timers:-0}" -ge 2 ]; then
pass "Systemd timers: $timers active (doctor + reconcile)"
else
fail "Systemd timers: expected ≥2, got ${timers:-0}"
fi
# Test 8: Container doctor runs cleanly
TESTS=$((TESTS + 1))
local doctor_exit
ssh $SSH_OPTS "$SSH_HOST" "sudo /home/archipelago/archy/scripts/container-doctor.sh --local 2>&1 | tail -1"
doctor_exit=$?
if [ $doctor_exit -eq 0 ]; then
pass "container-doctor.sh: clean exit"
else
fail "container-doctor.sh: exit code $doctor_exit"
fi
# Summary
header "Results"
local passed=$((TESTS - FAILURES))
echo -e " ${GREEN}$passed passed${NC} / ${RED}$FAILURES failed${NC} / $TESTS total"
# Cleanup temp cookie
rm -f "$COOKIE" 2>/dev/null
return $FAILURES
}
# ── Main ────────────────────────────────────────────────────────────────
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Container Orchestration Dev Loop ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
info "Target: $SSH_HOST"
info "Mode: $($ONCE && echo 'single run' || echo 'interactive loop')"
echo ""
# Check SSH
if ! ssh $SSH_OPTS "$SSH_HOST" "echo ok" >/dev/null 2>&1; then
fail "Cannot SSH to $SSH_HOST"
exit 1
fi
if $ONCE; then
sync_and_build && run_smoke_tests
exit $?
fi
# Interactive loop
while true; do
sync_and_build && run_smoke_tests
echo ""
echo -e "${YELLOW}Press Enter to re-sync + re-test, Ctrl+C to stop${NC}"
read -r
done