Add Traefik Rate Limits #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Production Stack Test | |
| # Enable Buildkit and let compose use it to speed up image building | |
| env: | |
| DOCKER_BUILDKIT: 1 | |
| COMPOSE_DOCKER_CLI_BUILD: 1 | |
| defaults: | |
| run: | |
| working-directory: ./ | |
| on: | |
| pull_request: | |
| branches: [ "master", "main" ] | |
| paths: | |
| - "compose/production/**" | |
| - "production.yml" | |
| - ".github/workflows/production-stack.yml" | |
| push: | |
| branches: [ "master", "main" ] | |
| paths: | |
| - "compose/production/**" | |
| - "production.yml" | |
| - ".github/workflows/production-stack.yml" | |
| concurrency: | |
| group: production-stack-${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| production-stack: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout Code Repository | |
| uses: actions/checkout@v4.2.2 | |
| - name: Create Required Environment Files | |
| run: | | |
| mkdir -p .envs/.production | |
| # Create minimal Django env file based on sample | |
| cat > .envs/.production/.django << 'EOF' | |
| # Core Django Security Settings | |
| DJANGO_SETTINGS_MODULE=config.settings.production | |
| DJANGO_SECRET_KEY=test-secret-key-for-ci-only-not-secure | |
| DJANGO_ADMIN_URL=admin/ | |
| DJANGO_SECURE_SSL_REDIRECT=false | |
| # Django Username and Password for Initial Admin Login | |
| DJANGO_SUPERUSER_PASSWORD=Openc0ntracts_test | |
| DJANGO_SUPERUSER_EMAIL=test@opensource.legal | |
| DJANGO_SUPERUSER_USERNAME=admin | |
| # General | |
| USE_DOCKER=yes | |
| WEB_CONCURRENCY=4 | |
| IPYTHONDIR=/app/.ipython | |
| DJANGO_ALLOWED_HOSTS=django,127.0.0.1,localhost,0.0.0.0,opencontracts.opensource.legal | |
| DJANGO_WORKER_TIMEOUT=3600 | |
| # Application Configuration | |
| USE_ANALYZER=false | |
| CALLBACK_ROOT_URL_FOR_ANALYZER=http://django:8000 | |
| # AWS - disabled for testing | |
| USE_AWS=false | |
| AWS_ACCESS_KEY_ID=test-key | |
| AWS_SECRET_ACCESS_KEY=test-secret | |
| AWS_STORAGE_BUCKET_NAME=test-bucket | |
| AWS_S3_REGION_NAME=us-east-1 | |
| # Redis | |
| REDIS_URL=redis://redis:6379/0 | |
| # Celery | |
| # Flower | |
| CELERY_FLOWER_USER=test | |
| CELERY_FLOWER_PASSWORD=test | |
| # NLM Parser | |
| NLM_INGESTOR_ACTIVE=false | |
| # LLM SETTINGS | |
| OPENAI_API_KEY=test-api-key-not-real | |
| OPENAI_MODEL=gpt-4o | |
| # AUTH0 - disabled for testing | |
| USE_AUTH0=false | |
| AUTH0_CLIENT_ID=test-client-id | |
| AUTH0_API_AUDIENCE=https://opensource.legal/contracts | |
| AUTH0_DOMAIN=test.auth0.com | |
| AUTH0_M2M_MANAGEMENT_API_SECRET=test-secret | |
| AUTH0_M2M_MANAGEMENT_API_ID=test-id | |
| AUTH0_M2M_MANAGEMENT_GRANT_TYPE=client_credentials | |
| # Docling | |
| DOCLING_MODELS_PATH=/models/docling | |
| SENTENCE_TRANSFORMER_MODELS_PATH=/models/sentence-transformers | |
| EOF | |
| # Create minimal Postgres env file | |
| cat > .envs/.production/.postgres << 'EOF' | |
| POSTGRES_HOST=postgres | |
| POSTGRES_PORT=5432 | |
| POSTGRES_DB=opencontracts | |
| POSTGRES_USER=postgres | |
| POSTGRES_PASSWORD=postgres | |
| EOF | |
| # Create minimal Frontend env file based on sample | |
| cat > .envs/.production/.frontend << 'EOF' | |
| OPEN_CONTRACTS_REACT_APP_APPLICATION_DOMAIN=opencontracts.opensource.legal | |
| OPEN_CONTRACTS_REACT_APP_APPLICATION_CLIENT_ID=test-client-id | |
| OPEN_CONTRACTS_REACT_APP_AUDIENCE=http://localhost:3000 | |
| OPEN_CONTRACTS_REACT_APP_API_ROOT_URL=http://django:8000 | |
| # Disabled for testing | |
| OPEN_CONTRACTS_REACT_APP_USE_AUTH0=false | |
| OPEN_CONTRACTS_REACT_APP_USE_ANALYZERS=false | |
| OPEN_CONTRACTS_REACT_APP_ALLOW_IMPORTS=false | |
| EOF | |
| - name: Build Production Stack (Test Mode) | |
| run: docker compose -f production.yml -f compose/test-production.yml build | |
| - name: Start Production Stack (Test Mode) | |
| run: | | |
| docker compose -f production.yml -f compose/test-production.yml up -d | |
| sleep 30 # Give services time to start | |
| - name: Verify Stack Health | |
| run: | | |
| echo "=== Docker containers status ===" | |
| docker compose -f production.yml -f compose/test-production.yml ps | |
| echo "=== Container health checks ===" | |
| for container in $(docker compose -f production.yml -f compose/test-production.yml ps -q); do | |
| name=$(docker inspect -f '{{.Name}}' $container | sed 's/^\/*//') | |
| echo "--- Health for $name ---" | |
| docker logs --tail 10 $container 2>&1 || true | |
| done | |
| - name: Test Traefik Configuration | |
| run: | | |
| echo "=== Testing Traefik config parsing ===" | |
| docker compose -f production.yml -f compose/test-production.yml logs traefik | grep -i "rate" || echo "No rate limiting logs yet" | |
| echo "=== Testing basic connectivity ===" | |
| # Test if traefik responds - through its exposed port 80 | |
| docker compose -f production.yml -f compose/test-production.yml exec -T django wget -qO- --header="Host: opencontracts.opensource.legal" http://traefik:80/ || echo "Expected - backend may not be ready" | |
| - name: Test Rate Limiting | |
| run: | | |
| echo "=== Testing Frontend Rate Limits ===" | |
| # Test frontend rate limiting through Traefik (port 80) from django container | |
| success_count=0 | |
| rate_limited_count=0 | |
| for i in {1..250}; do | |
| response=$(docker compose -f production.yml -f compose/test-production.yml exec -T django wget -qS --header="Host: opencontracts.opensource.legal" http://traefik:80/ -O /dev/null 2>&1 | grep -o "HTTP/1.1 [0-9]*" | cut -d' ' -f2 | head -1 || echo "000") | |
| if [ "$response" = "200" ] || [ "$response" = "404" ] || [ "$response" = "502" ] || [ "$response" = "503" ]; then | |
| success_count=$((success_count + 1)) | |
| elif [ "$response" = "429" ]; then | |
| rate_limited_count=$((rate_limited_count + 1)) | |
| fi | |
| # Small delay to avoid overwhelming | |
| sleep 0.01 | |
| done | |
| echo "Successful requests: $success_count" | |
| echo "Rate limited requests: $rate_limited_count" | |
| # We expect some rate limiting after burst is exhausted | |
| if [ $rate_limited_count -gt 0 ]; then | |
| echo "✅ Rate limiting is working - got $rate_limited_count rate limited responses" | |
| else | |
| echo "⚠️ No rate limiting detected - this might indicate an issue" | |
| fi | |
| echo "=== Testing API Rate Limits (lower threshold) ===" | |
| api_success=0 | |
| api_rate_limited=0 | |
| # Wait a moment for rate limits to reset | |
| sleep 5 | |
| for i in {1..100}; do | |
| response=$(docker compose -f production.yml -f compose/test-production.yml exec -T django wget -qS --header="Host: opencontracts.opensource.legal" http://traefik:80/graphql -O /dev/null 2>&1 | grep -o "HTTP/1.1 [0-9]*" | cut -d' ' -f2 | head -1 || echo "000") | |
| if [ "$response" = "200" ] || [ "$response" = "404" ] || [ "$response" = "502" ] || [ "$response" = "503" ]; then | |
| api_success=$((api_success + 1)) | |
| elif [ "$response" = "429" ]; then | |
| api_rate_limited=$((api_rate_limited + 1)) | |
| fi | |
| sleep 0.01 | |
| done | |
| echo "API successful requests: $api_success" | |
| echo "API rate limited requests: $api_rate_limited" | |
| - name: Check Redis Rate Limit Storage | |
| run: | | |
| echo "=== Checking Redis for rate limit data ===" | |
| # Check if rate limiting data is being stored in Redis | |
| docker compose -f production.yml -f compose/test-production.yml exec -T redis redis-cli -n 1 keys "*" | head -20 | |
| docker compose -f production.yml -f compose/test-production.yml exec -T redis redis-cli -n 1 info keyspace | |
| - name: Capture Logs on Failure | |
| if: failure() | |
| run: | | |
| echo "=== Capturing logs for debugging ===" | |
| docker compose -f production.yml -f compose/test-production.yml logs --no-color > production-stack-logs.txt | |
| - name: Upload Logs on Failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: production-stack-logs | |
| path: production-stack-logs.txt | |
| - name: Tear Down Stack | |
| if: always() | |
| run: docker compose -f production.yml -f compose/test-production.yml down -v |