Demo images / Build & push demo images (push) Failing after 2m22s
Replaces the registry host across 86 files: 309 references, covering all 40 app manifests, the orchestrator and container crates, the release and catalog scripts, both demo-images workflows, the ISO builder, demo-deploy, and the frontend marketplace data. Verified the domain actually serves the registry before rewriting anything, rather than assuming the web host implies the registry: - TLS verifies clean, HTTP/2 on the web root - an anonymous token grants a manifest fetch (HTTP 200) with no credentials - skopeo inspect --no-creds resolves an image and lists its tags That last check is the one that matters: an outside developer with no account can now pull, which was the functional blocker for publishing at all. Plain-HTTP references become HTTPS in the same pass, so OTA downloads stop crossing the network in the clear. Deliberately NOT rewritten: - The public FIPS anchor on port 8444. It is a functional network endpoint every node dials to bootstrap the mesh — closer to Bitcoin Core's hardcoded seeds than to leaked infrastructure. The domain does resolve to the same host, so it could become a hostname, but that adds a DNS dependency to the path used precisely when things are broken. Worth a deliberate decision, not a side effect of this change. - The companion APK on port 2100. The domain returns 404 for that path, so rewriting it would swap a working URL for a broken one. The Releases page does serve (200), which is where the plan already wants those binaries. - releases/app-catalog.json, releases/manifest.json and release-manifest.json. These carry `signature` and `signed_by`; editing their contents invalidates the signature and the fleet refuses artifacts that fail verification. They were rewritten in a first pass and reverted — they must be regenerated and re-signed through the signing ceremony instead, which needs the mnemonic. So the catalog still advertises the old host until that ceremony runs. Nodes resolve images through the signed catalog, not the on-disk manifests, so this commit alone does not change what a node pulls. Verified: archipelago-container 75/75; every manifest still parses with a top-level app block; no signed artifact modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
265 lines
9.3 KiB
Bash
Executable File
265 lines
9.3 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:-}"
|
|
if [ -z "$SSH_HOST" ]; then
|
|
echo "ARCHIPELAGO_SSH_HOST must be set, e.g. archipelago@<node-host>" >&2
|
|
exit 2
|
|
fi
|
|
HOST_ONLY="${SSH_HOST#*@}"
|
|
SSH_OPTS="-o StrictHostKeyChecking=no -o ServerAliveInterval=15 -i $SSH_KEY"
|
|
REMOTE_DIR="/home/archipelago/archy"
|
|
RPC_URL="http://${HOST_ONLY}/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://${HOST_ONLY}/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="source.archipelago-foundation.org/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
|