mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
hotfix: avoid Docker smoke port collisions (#1412)
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user