hotfix: avoid Docker smoke port collisions (#1412)

This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-29 11:49:25 -04:00
committed by GitHub
parent 5e8cd1035f
commit c3d8dcbdd9
4 changed files with 143 additions and 12 deletions
+2 -2
View File
@@ -25,8 +25,8 @@ services:
CCS_DOCKER_LEGACY_KEY_GRACE_DAYS: "${CCS_DOCKER_LEGACY_KEY_GRACE_DAYS:-}"
CCS_DOCKER_RESTORE_LEGACY_API_KEY: "${CCS_DOCKER_RESTORE_LEGACY_API_KEY:-}"
ports:
- "3000:3000"
- "8317:8317"
- "${CCS_DASHBOARD_PORT:-3000}:3000"
- "${CCS_CLIPROXY_PORT:-8317}:8317"
volumes:
# /root/.ccs matches the HOME used inside the integrated image.
# entrypoint-integrated.sh runs as root (supervisord user=root) and
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Unit test for network-contract.sh Docker Compose env and args.
# Uses a fake docker binary; no Docker daemon required.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT="${SCRIPT_DIR}/network-contract.sh"
PASS=0
FAIL=0
MOCK_DIR="$(mktemp -d)"
LOG_FILE="${MOCK_DIR}/docker.log"
COMPOSE_FILE="${MOCK_DIR}/compose.yaml"
trap 'rm -rf "$MOCK_DIR"' EXIT
cat > "$COMPOSE_FILE" <<'YAML'
services:
ccs:
image: ${CCS_IMAGE:-ghcr.io/kaitranntt/ccs:latest}
YAML
cat > "${MOCK_DIR}/docker" <<'MOCK'
#!/usr/bin/env bash
set -euo pipefail
printf 'cmd=%s env_image=%s env_dashboard=%s env_cliproxy=%s args=%s\n' \
"$1" "${CCS_IMAGE:-}" "${CCS_DASHBOARD_PORT:-}" "${CCS_CLIPROXY_PORT:-}" "$*" \
>> "$DOCKER_MOCK_LOG"
if [[ "$1" == "compose" ]]; then
if printf '%s\n' "$*" | grep -q 'ps --format json'; then
printf '[{"Service":"ccs","Health":"healthy"}]\n'
fi
exit 0
fi
if [[ "$1" == "network" && "$2" == "inspect" && "$3" == "ccs-net" ]]; then
exit 0
fi
if [[ "$1" == "run" ]]; then
exit 0
fi
exit 1
MOCK
chmod +x "${MOCK_DIR}/docker"
run_test() {
local name="$1"
shift
if "$@"; then
echo "[OK] ${name}"
(( PASS++ )) || true
else
echo "[X] ${name}"
(( FAIL++ )) || true
fi
}
run_contract() {
PATH="${MOCK_DIR}:${PATH}" \
DOCKER_MOCK_LOG="$LOG_FILE" \
bash "$SCRIPT" "$COMPOSE_FILE" "ghcr.io/kaitranntt/ccs:test" >/dev/null
}
echo ""
echo "Running network-contract.sh env tests..."
echo ""
run_contract
run_test "compose up receives safe host ports and image override" \
grep -q 'env_image=ghcr.io/kaitranntt/ccs:test env_dashboard=13001 env_cliproxy=18318 args=compose -f .* up -d --remove-orphans' "$LOG_FILE"
run_test "compose ps receives the same safe host ports" \
grep -q 'env_image=ghcr.io/kaitranntt/ccs:test env_dashboard=13001 env_cliproxy=18318 args=compose -f .* ps --format json' "$LOG_FILE"
run_test "compose down removes volumes and orphans through cleanup trap" \
grep -q 'env_image=ghcr.io/kaitranntt/ccs:test env_dashboard=13001 env_cliproxy=18318 args=compose -f .* down -v --remove-orphans' "$LOG_FILE"
run_test "sibling probes keep the ccs-net DNS contract" \
grep -q 'args=run --rm --network ccs-net curlimages/curl:latest -fsS --max-time 10 http://ccs:8317/' "$LOG_FILE"
run_test "dashboard sibling probe keeps internal port 3000" \
grep -q 'args=run --rm --network ccs-net curlimages/curl:latest -fsS --max-time 10 http://ccs:3000/' "$LOG_FILE"
echo ""
echo "Results: ${PASS} passed, ${FAIL} failed"
echo ""
if [[ "$FAIL" -gt 0 ]]; then
exit 1
fi
+25 -10
View File
@@ -19,6 +19,8 @@ set -euo pipefail
COMPOSE_FILE="${1:-docker/compose.yaml}"
IMAGE_OVERRIDE="${2:-}"
DASHBOARD_HOST_PORT="${CCS_NETWORK_CONTRACT_DASHBOARD_PORT:-${CCS_DASHBOARD_PORT:-13001}}"
CLIPROXY_HOST_PORT="${CCS_NETWORK_CONTRACT_CLIPROXY_PORT:-${CCS_CLIPROXY_PORT:-18318}}"
# ---------------------------------------------------------------------------
# Helpers
@@ -27,22 +29,35 @@ log() { printf '[i] %s\n' "$*"; }
ok() { printf '[OK] %s\n' "$*"; }
err() { printf '[X] %s\n' "$*" >&2; }
compose() {
if [[ -n "$IMAGE_OVERRIDE" ]]; then
CCS_IMAGE="$IMAGE_OVERRIDE" \
CCS_DASHBOARD_PORT="$DASHBOARD_HOST_PORT" \
CCS_CLIPROXY_PORT="$CLIPROXY_HOST_PORT" \
docker compose -f "$COMPOSE_FILE" "$@"
return
fi
CCS_DASHBOARD_PORT="$DASHBOARD_HOST_PORT" \
CCS_CLIPROXY_PORT="$CLIPROXY_HOST_PORT" \
docker compose -f "$COMPOSE_FILE" "$@"
}
cleanup() {
log "Tearing down stack..."
compose down -v --remove-orphans 2>/dev/null || true
}
trap cleanup EXIT
# ---------------------------------------------------------------------------
# Bring stack up; register teardown on any exit
# ---------------------------------------------------------------------------
log "Bringing CCS stack up: $COMPOSE_FILE"
log "Using host ports: dashboard=${DASHBOARD_HOST_PORT}, cliproxy=${CLIPROXY_HOST_PORT}"
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
compose up -d --remove-orphans
# ---------------------------------------------------------------------------
# Wait for healthcheck (max 90s) — use jq instead of python3 for CI portability
@@ -52,7 +67,7 @@ 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 \
compose 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"
)
@@ -1,4 +1,5 @@
import { describe, expect, test } from 'bun:test';
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
@@ -43,4 +44,24 @@ describe('docker release workflow context', () => {
expect(dockerfile).toContain('127.0.0.1:3000');
expect(dockerfile).toContain('127.0.0.1:8317');
});
test('lets network-contract smoke tests avoid fixed host port collisions', () => {
const compose = readFileSync(join(repoRoot, 'docker/compose.yaml'), 'utf8');
const contractScript = readFileSync(join(repoRoot, 'tests/docker/network-contract.sh'), 'utf8');
expect(compose).toContain('${CCS_DASHBOARD_PORT:-3000}:3000');
expect(compose).toContain('${CCS_CLIPROXY_PORT:-8317}:8317');
expect(contractScript).toContain('CCS_NETWORK_CONTRACT_DASHBOARD_PORT:-${CCS_DASHBOARD_PORT:-13001}');
expect(contractScript).toContain('CCS_NETWORK_CONTRACT_CLIPROXY_PORT:-${CCS_CLIPROXY_PORT:-18318}');
expect(contractScript).toContain('up -d --remove-orphans');
});
test('passes collision-safe compose env through network-contract calls', () => {
const result = spawnSync('bash', ['tests/docker/network-contract-env.test.sh'], {
cwd: repoRoot,
encoding: 'utf8',
});
expect(result.status, result.stdout + result.stderr).toBe(0);
});
});