diff --git a/.env.staging.example b/.env.staging.example new file mode 100644 index 0000000..7946cc9 --- /dev/null +++ b/.env.staging.example @@ -0,0 +1,11 @@ +# Staging Environment Configuration +# Copy this file to .env.staging and fill in real values +# +# Setup steps: +# 1. wrangler d1 create tunnel-db-staging +# 2. Copy the database_id from output below +# 3. Set CLOUDFLARE_API_TOKEN (same token used for CI is fine) + +CLOUDFLARE_API_TOKEN=your-api-token-here +STAGING_D1_ID=your-staging-d1-id-here +STAGING_WORKER_URL=https://key-server-staging..workers.dev diff --git a/.gitignore b/.gitignore index 341a3ac..4596e9d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ yarn-error.log* .env.development.local .env.test.local .env.production.local +.env.staging *.pem xpose.yaml **/xpose.yaml diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..df92db0 --- /dev/null +++ b/dev.sh @@ -0,0 +1,262 @@ +#!/bin/bash +set -e + +show_usage() { + echo "Usage: ./dev.sh [command]" + echo "" + echo "Local Development Helper โ€” Run key-server + CLI locally" + echo "" + echo "Local Commands:" + echo " up Start key-server-dev (port 8787) in background" + echo " down Stop all dev services" + echo " logs Follow key-server-dev logs" + echo " status Show service status and health" + echo " test-api Run quick smoke test against local key-server" + echo " cli [args] Build & run xpose CLI pointed at local key-server" + echo " shell Open a bash shell in the dev container" + echo "" + echo "Staging Commands (Cloudflare):" + echo " staging-setup Create D1 database + deploy staging worker (one-time)" + echo " staging-deploy Build & deploy key-server to staging" + echo " staging-test Smoke test against staging worker" + echo " staging-cli Run CLI pointed at staging worker" + echo "" + echo " help Show this help message" +} + +COMMAND=${1:-help} + +case "$COMMAND" in + up) + echo "๐Ÿš€ Starting key-server-dev..." + docker compose up key-server-dev -d --build + echo "" + echo "โณ Waiting for key-server to be healthy (first build may take 2-5 min)..." + echo " Run './dev.sh logs' in another terminal to watch progress." + echo "" + # Wait for healthy + MAX_WAIT=300 + ELAPSED=0 + while [ $ELAPSED -lt $MAX_WAIT ]; do + STATUS=$(docker compose ps key-server-dev --format json 2>/dev/null | grep -o '"Health":"[^"]*"' | cut -d'"' -f4 || echo "unknown") + if [ "$STATUS" = "healthy" ]; then + echo "โœ… key-server-dev is healthy!" + echo "" + echo " API: http://localhost:8787" + echo " CLI: ./dev.sh cli 3000" + echo " Logs: ./dev.sh logs" + exit 0 + fi + sleep 5 + ELAPSED=$((ELAPSED + 5)) + echo " ... waiting ($ELAPSED/${MAX_WAIT}s) status=$STATUS" + done + echo "โš ๏ธ Timed out waiting for healthy status. Check logs:" + echo " ./dev.sh logs" + exit 1 + ;; + down) + echo "๐Ÿ›‘ Stopping dev services..." + docker compose down + echo "โœ… Done." + ;; + logs) + docker compose logs -f key-server-dev + ;; + status) + docker compose ps key-server-dev + echo "" + echo "Quick API check:" + curl -sf http://localhost:8787/api/config 2>/dev/null && echo "" || echo "โŒ Key server not responding" + ;; + test-api) + echo "๐Ÿงช Running smoke tests against http://localhost:8787..." + echo "" + + # Test /api/config + echo " [1/4] GET /api/config" + CONFIG=$(curl -sf http://localhost:8787/api/config) + echo "$CONFIG" | grep -q "min_cli_version" && echo " โœ… Config OK" || { echo " โŒ Config failed"; exit 1; } + + # Test /api/stats + echo " [2/4] GET /api/stats" + STATS=$(curl -sf http://localhost:8787/api/stats) + echo "$STATS" | grep -q "total" && echo " โœ… Stats OK" || { echo " โŒ Stats failed"; exit 1; } + + # Test admin add tunnel + echo " [3/4] POST /admin/tunnels (add test tunnel)" + ADD=$(curl -sf -X POST -H "Authorization: Bearer my-secret-token" \ + -H "Content-Type: application/json" \ + -d '{"id": "dev-t1", "name": "dev-test", "token": "dev-tok1"}' \ + http://localhost:8787/admin/tunnels) + echo "$ADD" | grep -q '"success":true' && echo " โœ… Add tunnel OK" || { echo " โŒ Add tunnel failed: $ADD"; exit 1; } + + # Test stats updated + echo " [4/4] GET /api/stats (verify tunnel added)" + STATS2=$(curl -sf http://localhost:8787/api/stats) + echo "$STATS2" | grep -q '"available":1' && echo " โœ… Stats updated OK" || echo " โš ๏ธ Stats: $STATS2" + + echo "" + echo "๐ŸŽ‰ All smoke tests passed!" + ;; + cli) + shift + echo "๐Ÿ”จ Building and running xpose CLI โ†’ http://localhost:8787" + docker compose run --rm \ + -e XPOSE_SERVER_URL=http://key-server-dev:8787 \ + dev bash -c "cd packages/cli && cargo run -- $*" + ;; + shell) + docker compose run --rm dev bash + ;; + staging-setup) + echo "๐Ÿ”ง Setting up staging environment..." + echo "" + + # Check for .env.staging + if [ ! -f .env.staging ]; then + echo "๐Ÿ“ Creating .env.staging from template..." + cp .env.staging.example .env.staging + echo "โš ๏ธ Please edit .env.staging and set CLOUDFLARE_API_TOKEN first." + echo " Then re-run: ./dev.sh staging-setup" + exit 1 + fi + + source .env.staging + + if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ "$CLOUDFLARE_API_TOKEN" = "your-api-token-here" ]; then + echo "โŒ CLOUDFLARE_API_TOKEN not set in .env.staging" + exit 1 + fi + + # Create D1 database if not already done + if [ -z "$STAGING_D1_ID" ] || [ "$STAGING_D1_ID" = "your-staging-d1-id-here" ]; then + echo "๐Ÿ—„๏ธ Creating D1 database: tunnel-db-staging..." + D1_OUTPUT=$(docker compose run --rm \ + -e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \ + dev bash -c "cd packages/key-server && wrangler d1 create tunnel-db-staging" 2>&1) + echo "$D1_OUTPUT" + NEW_ID=$(echo "$D1_OUTPUT" | grep -o 'database_id.*=.*"[^"]*"' | grep -o '"[^"]*"' | tr -d '"' || true) + if [ -z "$NEW_ID" ]; then + NEW_ID=$(echo "$D1_OUTPUT" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1 || true) + fi + if [ -n "$NEW_ID" ]; then + echo "" + echo "โœ… D1 database created! ID: $NEW_ID" + sed -i "s/STAGING_D1_ID=.*/STAGING_D1_ID=$NEW_ID/" .env.staging + sed -i "s/__STAGING_D1_ID__/$NEW_ID/" packages/key-server/wrangler.jsonc + echo "โœ… Updated .env.staging and wrangler.jsonc" + else + echo "โš ๏ธ Could not auto-extract D1 ID. Please update .env.staging manually." + exit 1 + fi + else + echo "โœ… D1 database already configured: $STAGING_D1_ID" + sed -i "s/__STAGING_D1_ID__/$STAGING_D1_ID/" packages/key-server/wrangler.jsonc 2>/dev/null || true + fi + + source .env.staging + + # Run migrations + deploy inside Docker + echo "" + echo "๐Ÿ“ฆ Running D1 migrations + deploying to staging..." + docker compose run --rm \ + -e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \ + dev bash -c " + cd packages/key-server && + wrangler d1 migrations apply tunnel-db-staging --remote --env staging && + echo '' && + echo '๐Ÿš€ Deploying key-server-staging...' && + wrangler deploy --env staging + " + + echo "" + echo "๐ŸŽ‰ Staging setup complete!" + echo "" + echo " Worker URL: Check output above or Cloudflare dashboard" + echo " Update STAGING_WORKER_URL in .env.staging" + echo "" + echo " Next: ./dev.sh staging-test" + ;; + staging-deploy) + echo "๐Ÿš€ Deploying key-server to staging..." + + if [ ! -f .env.staging ]; then + echo "โŒ .env.staging not found. Run './dev.sh staging-setup' first." + exit 1 + fi + source .env.staging + + docker compose run --rm \ + -e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \ + dev bash -c " + cd packages/key-server && + wrangler d1 migrations apply tunnel-db-staging --remote --env staging && + wrangler deploy --env staging + " + + echo "" + echo "โœ… Staging deploy complete!" + ;; + staging-test) + if [ ! -f .env.staging ]; then + echo "โŒ .env.staging not found. Run './dev.sh staging-setup' first." + exit 1 + fi + source .env.staging + + if [ -z "$STAGING_WORKER_URL" ] || echo "$STAGING_WORKER_URL" | grep -q 'your-subdomain'; then + echo "โŒ STAGING_WORKER_URL not configured in .env.staging" + echo " Set it to your staging worker URL, e.g.: https://key-server-staging.xxx.workers.dev" + exit 1 + fi + + URL="$STAGING_WORKER_URL" + echo "๐Ÿงช Running smoke tests against staging: $URL" + echo "" + + echo " [1/3] GET /api/config" + CONFIG=$(curl -sf "$URL/api/config") + echo "$CONFIG" | grep -q "min_cli_version" && echo " โœ… Config OK" || { echo " โŒ Config failed"; exit 1; } + + echo " [2/3] GET /api/stats" + STATS=$(curl -sf "$URL/api/stats") + echo "$STATS" | grep -q "total" && echo " โœ… Stats OK" || { echo " โŒ Stats failed"; exit 1; } + + echo " [3/3] POST /admin/tunnels" + ADD=$(curl -sf -X POST -H "Authorization: Bearer staging-secret-token" \ + -H "Content-Type: application/json" \ + -d '{"id": "staging-t1", "name": "staging-test", "token": "staging-tok1"}' \ + "$URL/admin/tunnels") + echo "$ADD" | grep -q '"success":true' && echo " โœ… Add tunnel OK" || { echo " โŒ Add tunnel failed: $ADD"; exit 1; } + + echo "" + echo "๐ŸŽ‰ All staging smoke tests passed!" + ;; + staging-cli) + if [ ! -f .env.staging ]; then + echo "โŒ .env.staging not found. Run './dev.sh staging-setup' first." + exit 1 + fi + source .env.staging + + if [ -z "$STAGING_WORKER_URL" ] || echo "$STAGING_WORKER_URL" | grep -q 'your-subdomain'; then + echo "โŒ STAGING_WORKER_URL not configured in .env.staging" + exit 1 + fi + + shift + echo "๐Ÿ”จ Building and running xpose CLI โ†’ $STAGING_WORKER_URL" + docker compose run --rm \ + -e XPOSE_SERVER_URL="$STAGING_WORKER_URL" \ + dev bash -c "cd packages/cli && cargo run -- $*" + ;; + help|--help|-h) + show_usage + ;; + *) + echo "Unknown command: $COMMAND" + show_usage + exit 1 + ;; +esac diff --git a/docker-compose.yml b/docker-compose.yml index 602e92f..de2f161 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,42 @@ services: retries: 3 start_period: 10s + # Local development: Key Server (wrangler dev --local) + # Usage: docker compose up key-server-dev + key-server-dev: + build: + context: . + target: dev + volumes: + - .:/workspace + - cargo-registry:/usr/local/cargo/registry + - cargo-git:/usr/local/cargo/git + tmpfs: + - /workspace/.sccache:size=512m,mode=1777 + - /tmp:size=1g,mode=1777 + working_dir: /workspace/packages/key-server + command: > + bash -c " + echo '๐Ÿ”จ Building key-server worker...' && + worker-build --release && + echo '๐Ÿš€ Starting wrangler dev on port 8787...' && + wrangler dev --config wrangler.jsonc --local --persist --d1 DB --port 8787 --non-interactive --ip 0.0.0.0 + " + ports: + - "8787:8787" + environment: + - CARGO_HOME=/usr/local/cargo + - RUSTC_WRAPPER=sccache + - SCCACHE_DIR=/workspace/.sccache + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8787/api/config"] + interval: 10s + timeout: 5s + retries: 15 + start_period: 120s + mem_limit: 4g + shm_size: 1gb + volumes: cargo-registry: cargo-git: diff --git a/packages/key-server/wrangler.jsonc b/packages/key-server/wrangler.jsonc index 6649249..da7f959 100644 --- a/packages/key-server/wrangler.jsonc +++ b/packages/key-server/wrangler.jsonc @@ -27,5 +27,21 @@ }, "observability": { "enabled": true + }, + "env": { + "staging": { + "name": "key-server-staging", + "d1_databases": [ + { + "binding": "DB", + "database_name": "tunnel-db-staging", + "database_id": "__STAGING_D1_ID__", + "migrations_dir": "./migrations" + } + ], + "vars": { + "ADMIN_SECRET": "staging-secret-token" + } + } } } \ No newline at end of file