#!/usr/bin/env bash # tests/docker/network-contract.sh # # Verifies the stable ccs-net Docker network contract: # - Network name: ccs-net # - Service DNS: ccs # - CLIProxy: http://ccs:8317 # - Dashboard: http://ccs:3000 # # Requires: Docker with compose plugin, internet access to pull curlimages/curl # Usage: bash tests/docker/network-contract.sh [compose-file] [image-ref] # compose-file Path to compose file (default: docker/compose.yaml) # image-ref Override the image used in the compose file (optional). # When set, the compose stack is run with that image instead # of whatever is pinned in the compose file. # Called from repo root so the default path resolves. # set -euo pipefail COMPOSE_FILE="${1:-docker/compose.yaml}" IMAGE_OVERRIDE="${2:-}" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- log() { printf '[i] %s\n' "$*"; } ok() { printf '[OK] %s\n' "$*"; } err() { printf '[X] %s\n' "$*" >&2; } # --------------------------------------------------------------------------- # Bring stack up; register teardown on any exit # --------------------------------------------------------------------------- log "Bringing CCS stack up: $COMPOSE_FILE" if [[ -n "$IMAGE_OVERRIDE" ]]; then log "Overriding image with: $IMAGE_OVERRIDE" CCS_IMAGE="$IMAGE_OVERRIDE" docker compose -f "$COMPOSE_FILE" up -d else docker compose -f "$COMPOSE_FILE" up -d fi cleanup() { log "Tearing down stack..." docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true } trap cleanup EXIT # --------------------------------------------------------------------------- # Wait for healthcheck (max 90s) — use jq instead of python3 for CI portability # --------------------------------------------------------------------------- log "Waiting for healthcheck to pass (max 90s)..." WAIT_MAX=45 # 45 x 2s = 90s HEALTHY=0 for _i in $(seq 1 "$WAIT_MAX"); do STATUS=$( docker compose -f "$COMPOSE_FILE" ps --format json 2>/dev/null \ | jq -r 'if type == "array" then .[] else . end | select(.Service != null and (.Service | contains("ccs"))) | .Health // "unknown"' \ 2>/dev/null | head -1 || echo "unknown" ) STATUS="${STATUS:-unknown}" if [ "$STATUS" = "healthy" ]; then HEALTHY=1 break fi log "Health: $STATUS (attempt ${_i}/${WAIT_MAX})" sleep 2 done if [ "$HEALTHY" -ne 1 ]; then err "Container did not become healthy within 90s" exit 1 fi ok "Container is healthy" # --------------------------------------------------------------------------- # Verify ccs-net network exists on the host # --------------------------------------------------------------------------- log "Inspecting ccs-net network..." docker network inspect ccs-net >/dev/null ok "ccs-net network exists" # --------------------------------------------------------------------------- # Verify DNS resolution from a sibling container on ccs-net # --------------------------------------------------------------------------- log "Testing http://ccs:8317 from sibling container..." docker run --rm \ --network ccs-net \ curlimages/curl:latest \ -fsS --max-time 10 \ http://ccs:8317/ \ >/dev/null ok "CLIProxy reachable at http://ccs:8317" log "Testing http://ccs:3000 from sibling container..." docker run --rm \ --network ccs-net \ curlimages/curl:latest \ -fsS --max-time 10 \ http://ccs:3000/ \ >/dev/null ok "Dashboard reachable at http://ccs:3000" # --------------------------------------------------------------------------- # Done # --------------------------------------------------------------------------- ok "network contract verified"