From 3dbb1d51e8fd5b01cdde6e3f23c642a57defed18 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 30 Jun 2025 22:57:36 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Complete=20automation=20pipeline?= =?UTF-8?q?=20with=20SSL,=20testing,=20and=20deployment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit โœจ Features: - Full SSL setup with Let's Encrypt for all environments - Automated CI/CD pipeline with GitHub Actions - Comprehensive smoke testing workflow - Auto-deploy to dev on main branch push - Manual staging/production deployments with confirmation - Istio + nginx SSL termination architecture ๐Ÿ”ง Infrastructure: - Migrated from Kourier to Istio for Knative ingress - nginx handles SSL termination and public traffic - Istio manages internal Knative service routing - Scale-to-zero configuration for all environments ๐Ÿงช Testing: - SSL certificate validation and expiry checks - Domain accessibility and content validation - Performance testing and redirect behavior validation - Automated smoke tests on every deployment ๐ŸŒ Domains: - Dev: https://2048-dev.wa.darknex.us - Staging: https://2048-staging.wa.darknex.us - Production: https://2048.wa.darknex.us ๐Ÿ“ฆ Deployment: - Uses latest GHCR images with imagePullPolicy: Always - Automated secret management across namespaces - Environment-specific Knative service configurations - Clean manifest structure with proper labeling --- .github/workflows/build-image.yml | 25 ++- .github/workflows/deploy-dev.yml | 117 ++++++++---- .github/workflows/deploy-prod.yml | 107 +++++++++-- .github/workflows/deploy-staging.yml | 88 +++++++-- .github/workflows/smoke-test.yml | 270 +++++++++++++++++++++++++++ README.md | 48 +++-- manifests/cluster-domain-claims.yaml | 20 -- manifests/dev/domain-mapping.yml | 15 -- manifests/dev/service.yml | 35 +--- manifests/istio-gateway.yaml | 20 ++ manifests/knative-domain-config.yaml | 10 +- manifests/kourier-ssl-config.yaml | 15 -- manifests/nginx-certificate.yaml | 40 ++++ manifests/nginx-to-istio-proxy.yaml | 119 ++++++++++++ manifests/prod/domain-mapping.yml | 15 -- manifests/prod/service.yml | 37 +--- manifests/ssl-certificate.yaml | 42 ----- manifests/staging/domain-mapping.yml | 15 -- manifests/staging/service.yml | 37 +--- scripts/deploy.sh | 124 ++++++------ scripts/setup-ssl.sh | 105 ----------- scripts/smoke-test.sh | 250 +++++++++++++++++++++++++ 22 files changed, 1094 insertions(+), 460 deletions(-) create mode 100644 .github/workflows/smoke-test.yml delete mode 100644 manifests/cluster-domain-claims.yaml delete mode 100644 manifests/dev/domain-mapping.yml create mode 100644 manifests/istio-gateway.yaml delete mode 100644 manifests/kourier-ssl-config.yaml create mode 100644 manifests/nginx-certificate.yaml create mode 100644 manifests/nginx-to-istio-proxy.yaml delete mode 100644 manifests/prod/domain-mapping.yml delete mode 100644 manifests/staging/domain-mapping.yml create mode 100644 scripts/smoke-test.sh diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 22708b6..6cd70b6 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -1,10 +1,10 @@ -name: Build and Push Image +name: Build and Push Container Image on: push: - branches: [ main, develop ] + branches: [ main ] pull_request: - branches: [ main, develop ] + branches: [ main ] env: REGISTRY: ghcr.io @@ -39,7 +39,7 @@ jobs: type=sha,prefix={{branch}}- type=raw,value=latest,enable={{is_default_branch}} - - name: Build and push Docker image + - name: Build and push container image uses: docker/build-push-action@v5 with: context: . @@ -47,5 +47,18 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - - name: Image digest - run: echo "Image pushed with digest ${{ steps.build.outputs.digest }}" + - name: Create build summary + run: | + echo "## ๐Ÿ“ฆ Container Image Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Registry | ${{ env.REGISTRY }} |" >> $GITHUB_STEP_SUMMARY + echo "| Image | ${{ env.IMAGE_NAME }} |" >> $GITHUB_STEP_SUMMARY + echo "| Tags | $(echo '${{ steps.meta.outputs.tags }}' | tr '\n' ', ') |" >> $GITHUB_STEP_SUMMARY + echo "| Trigger | ${{ github.event_name }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿงช Development deployment will trigger automatically" >> $GITHUB_STEP_SUMMARY + echo "- ๐ŸŽญ [Deploy to staging manually](https://github.com/${{ github.repository }}/actions/workflows/deploy-staging.yml)" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿš€ [Deploy to production manually](https://github.com/${{ github.repository }}/actions/workflows/deploy-prod.yml)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 634eddb..baf3f8a 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -1,53 +1,102 @@ name: Deploy to Development on: - push: - branches: [ develop ] - pull_request: - branches: [ develop ] + workflow_run: + workflows: ["Build and Push Container Image"] + types: + - completed + branches: [ main ] + workflow_dispatch: + inputs: + image_tag: + description: 'Image tag to deploy (default: latest)' + required: false + default: 'latest' env: REGISTRY: ghcr.io IMAGE_NAME: ghndrx/k8s-game-2048 jobs: - build-and-deploy: + deploy-dev: + name: Deploy to Development runs-on: ubuntu-latest - permissions: - contents: read - packages: write - + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + environment: development + steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Log in to Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=sha,prefix={{branch}}- - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - name: Set up kubectl uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Set image tag + run: | + IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}" + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG" + + - name: Deploy to development + run: | + echo "๐Ÿš€ Deploying to development environment..." + + # Apply namespace + kubectl apply -f manifests/dev/namespace.yml + + # Ensure GHCR secret exists + if kubectl get secret ghcr-secret -n default &>/dev/null; then + echo "๐Ÿ” Copying GHCR secret to dev namespace..." + kubectl get secret ghcr-secret -o yaml | \ + sed 's/namespace: default/namespace: game-2048-dev/' | \ + sed '/resourceVersion:/d' | \ + sed '/uid:/d' | \ + sed '/creationTimestamp:/d' | \ + kubectl apply -f - + fi + + # Update image in service and deploy + kubectl patch ksvc game-2048-dev -n game-2048-dev --type merge -p '{"spec":{"template":{"spec":{"containers":[{"image":"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}","imagePullPolicy":"Always"}]}}}}' + + echo "โณ Waiting for deployment to be ready..." + kubectl wait --for=condition=Ready ksvc/game-2048-dev -n game-2048-dev --timeout=300s || echo "โš ๏ธ Service may still be starting" + + - name: Verify deployment + run: | + echo "๐Ÿ“Š Deployment status:" + kubectl get ksvc -n game-2048-dev + + echo "" + echo "โœ… Development deployment completed!" + echo "๐ŸŒ Available at: https://2048-dev.wa.darknex.us" + + - name: Run smoke test + run: | + echo "๐Ÿงช Running smoke test..." + sleep 30 + + for i in {1..5}; do + echo "Attempt $i/5..." + if curl -s --max-time 30 https://2048-dev.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Smoke test passed!" + break + elif [ $i -eq 5 ]; then + echo "โš ๏ธ Smoke test failed after 5 attempts" + exit 1 + else + echo "Retrying in 30 seconds..." + sleep 30 + fi + done + uses: azure/setup-kubectl@v3 with: version: 'v1.28.0' diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 4956e9c..2f8e4b9 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -1,33 +1,114 @@ name: Deploy to Production on: - push: - branches: [ master ] - release: - types: [published] workflow_dispatch: inputs: - tag: - description: 'Tag to deploy' - required: true + image_tag: + description: 'Image tag to deploy (default: latest)' + required: false default: 'latest' + confirmation: + description: 'Type "DEPLOY" to confirm production deployment' + required: true env: REGISTRY: ghcr.io IMAGE_NAME: ghndrx/k8s-game-2048 jobs: - build-and-deploy: + deploy-prod: + name: Deploy to Production runs-on: ubuntu-latest - permissions: - contents: read - packages: write - + environment: production + if: ${{ github.event.inputs.confirmation == 'DEPLOY' }} + steps: - name: Checkout repository uses: actions/checkout@v4 + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 with: - ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} + version: 'latest' + + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Set image tag + run: | + IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}" + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG" + + - name: Deploy to production + run: | + echo "๐Ÿš€ Deploying to production environment..." + + # Apply namespace + kubectl apply -f manifests/prod/namespace.yml + + # Ensure GHCR secret exists + if kubectl get secret ghcr-secret -n default &>/dev/null; then + echo "๐Ÿ” Copying GHCR secret to prod namespace..." + kubectl get secret ghcr-secret -o yaml | \ + sed 's/namespace: default/namespace: game-2048-prod/' | \ + sed '/resourceVersion:/d' | \ + sed '/uid:/d' | \ + sed '/creationTimestamp:/d' | \ + kubectl apply -f - + fi + + # Update image in service and deploy + kubectl patch ksvc game-2048-prod -n game-2048-prod --type merge -p '{"spec":{"template":{"spec":{"containers":[{"image":"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}","imagePullPolicy":"Always"}]}}}}' + + echo "โณ Waiting for deployment to be ready..." + kubectl wait --for=condition=Ready ksvc/game-2048-prod -n game-2048-prod --timeout=300s || echo "โš ๏ธ Service may still be starting" + + - name: Verify deployment + run: | + echo "๐Ÿ“Š Deployment status:" + kubectl get ksvc -n game-2048-prod + + echo "" + echo "โœ… Production deployment completed!" + echo "๐ŸŒ Available at: https://2048.wa.darknex.us" + + - name: Run smoke test + run: | + echo "๐Ÿงช Running smoke test..." + sleep 30 + + for i in {1..5}; do + echo "Attempt $i/5..." + if curl -s --max-time 30 https://2048.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Smoke test passed!" + break + elif [ $i -eq 5 ]; then + echo "โš ๏ธ Smoke test failed after 5 attempts" + exit 1 + else + echo "Retrying in 30 seconds..." + sleep 30 + fi + done + + - name: Create production deployment summary + run: | + echo "## ๐Ÿš€ Production Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Environment | **Production** |" >> $GITHUB_STEP_SUMMARY + echo "| Image | \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Domain | https://2048.wa.darknex.us |" >> $GITHUB_STEP_SUMMARY + echo "| Status | โœ… **LIVE** |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ๐ŸŽ‰ Production is Live!" >> $GITHUB_STEP_SUMMARY + echo "- ๐ŸŽฎ [Play the game](https://2048.wa.darknex.us)" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿงช [Run smoke tests](https://github.com/${{ github.repository }}/actions/workflows/smoke-test.yml)" >> $GITHUB_STEP_SUMMARY - name: Log in to Container Registry uses: docker/login-action@v3 diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 1bf057a..b987ae8 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -1,33 +1,95 @@ name: Deploy to Staging on: - push: - branches: [ staging ] workflow_dispatch: + inputs: + image_tag: + description: 'Image tag to deploy (default: latest)' + required: false + default: 'latest' env: REGISTRY: ghcr.io IMAGE_NAME: ghndrx/k8s-game-2048 jobs: - build-and-deploy: + deploy-staging: + name: Deploy to Staging runs-on: ubuntu-latest - permissions: - contents: read - packages: write - + environment: staging + steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Log in to Container Registry - uses: docker/login-action@v3 + - name: Set up kubectl + uses: azure/setup-kubectl@v3 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + version: 'latest' - - name: Extract metadata + - name: Configure kubectl + run: | + mkdir -p ~/.kube + echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config + chmod 600 ~/.kube/config + + - name: Set image tag + run: | + IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}" + echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV + echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG" + + - name: Deploy to staging + run: | + echo "๐Ÿš€ Deploying to staging environment..." + + # Apply namespace + kubectl apply -f manifests/staging/namespace.yml + + # Ensure GHCR secret exists + if kubectl get secret ghcr-secret -n default &>/dev/null; then + echo "๐Ÿ” Copying GHCR secret to staging namespace..." + kubectl get secret ghcr-secret -o yaml | \ + sed 's/namespace: default/namespace: game-2048-staging/' | \ + sed '/resourceVersion:/d' | \ + sed '/uid:/d' | \ + sed '/creationTimestamp:/d' | \ + kubectl apply -f - + fi + + # Update image in service and deploy + kubectl patch ksvc game-2048-staging -n game-2048-staging --type merge -p '{"spec":{"template":{"spec":{"containers":[{"image":"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}","imagePullPolicy":"Always"}]}}}}' + + echo "โณ Waiting for deployment to be ready..." + kubectl wait --for=condition=Ready ksvc/game-2048-staging -n game-2048-staging --timeout=300s || echo "โš ๏ธ Service may still be starting" + + - name: Verify deployment + run: | + echo "๐Ÿ“Š Deployment status:" + kubectl get ksvc -n game-2048-staging + + echo "" + echo "โœ… Staging deployment completed!" + echo "๐ŸŒ Available at: https://2048-staging.wa.darknex.us" + + - name: Run smoke test + run: | + echo "๐Ÿงช Running smoke test..." + sleep 30 + + for i in {1..5}; do + echo "Attempt $i/5..." + if curl -s --max-time 30 https://2048-staging.wa.darknex.us/ | grep -q "2048"; then + echo "โœ… Smoke test passed!" + break + elif [ $i -eq 5 ]; then + echo "โš ๏ธ Smoke test failed after 5 attempts" + exit 1 + else + echo "Retrying in 30 seconds..." + sleep 30 + fi + done id: meta uses: docker/metadata-action@v5 with: diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 0000000..b8e2098 --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,270 @@ +name: Smoke Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + schedule: + # Run smoke tests every 6 hours + - cron: '0 */6 * * *' + workflow_dispatch: + inputs: + environment: + description: 'Environment to test (dev, staging, prod, all)' + required: false + default: 'all' + type: choice + options: + - all + - dev + - staging + - prod + +jobs: + smoke-tests: + name: Smoke Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + environment: + - ${{ github.event.inputs.environment == 'all' && 'dev' || github.event.inputs.environment || 'dev' }} + - ${{ github.event.inputs.environment == 'all' && 'staging' || '' }} + - ${{ github.event.inputs.environment == 'all' && 'prod' || '' }} + exclude: + - environment: '' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set environment variables + run: | + case "${{ matrix.environment }}" in + dev) + echo "DOMAIN=2048-dev.wa.darknex.us" >> $GITHUB_ENV + echo "CANONICAL_DOMAIN=game-2048-dev.game-2048-dev.dev.wa.darknex.us" >> $GITHUB_ENV + echo "ENV_NAME=development" >> $GITHUB_ENV + ;; + staging) + echo "DOMAIN=2048-staging.wa.darknex.us" >> $GITHUB_ENV + echo "CANONICAL_DOMAIN=game-2048-staging.game-2048-staging.staging.wa.darknex.us" >> $GITHUB_ENV + echo "ENV_NAME=staging" >> $GITHUB_ENV + ;; + prod) + echo "DOMAIN=2048.wa.darknex.us" >> $GITHUB_ENV + echo "CANONICAL_DOMAIN=game-2048-prod.game-2048-prod.wa.darknex.us" >> $GITHUB_ENV + echo "ENV_NAME=production" >> $GITHUB_ENV + ;; + esac + + - name: Test SSL Certificate + run: | + echo "๐Ÿ”’ Testing SSL certificate for ${{ env.DOMAIN }}" + + # Check SSL certificate validity + cert_info=$(echo | openssl s_client -servername ${{ env.DOMAIN }} -connect ${{ env.DOMAIN }}:443 2>/dev/null | openssl x509 -noout -dates) + echo "Certificate info: $cert_info" + + # Check if certificate is valid for at least 30 days + expiry_date=$(echo | openssl s_client -servername ${{ env.DOMAIN }} -connect ${{ env.DOMAIN }}:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2) + expiry_epoch=$(date -d "$expiry_date" +%s) + current_epoch=$(date +%s) + days_until_expiry=$(( (expiry_epoch - current_epoch) / 86400 )) + + echo "Days until certificate expiry: $days_until_expiry" + + if [ $days_until_expiry -lt 30 ]; then + echo "โŒ Certificate expires in less than 30 days!" + exit 1 + else + echo "โœ… Certificate is valid for $days_until_expiry days" + fi + + - name: Test Domain Accessibility + run: | + echo "๐ŸŒ Testing domain accessibility for ${{ env.DOMAIN }}" + + # Test HTTPS access + response_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://${{ env.DOMAIN }}/) + echo "HTTP response code: $response_code" + + if [ "$response_code" != "200" ]; then + echo "โŒ Domain ${{ env.DOMAIN }} returned HTTP $response_code" + exit 1 + else + echo "โœ… Domain ${{ env.DOMAIN }} is accessible" + fi + + - name: Test Content Validation + run: | + echo "๐Ÿ“„ Testing content validation for ${{ env.DOMAIN }}" + + # Download the page content + content=$(curl -s -L --max-time 30 https://${{ env.DOMAIN }}/) + + # Check if it contains expected 2048 game elements + if echo "$content" | grep -q "2048"; then + echo "โœ… Page contains '2048' title" + else + echo "โŒ Page does not contain '2048' title" + exit 1 + fi + + if echo "$content" | grep -q "HOW TO PLAY"; then + echo "โœ… Page contains game instructions" + else + echo "โŒ Page does not contain game instructions" + exit 1 + fi + + if echo "$content" | grep -q "Environment.*${{ env.ENV_NAME }}"; then + echo "โœ… Page shows correct environment: ${{ env.ENV_NAME }}" + else + echo "โš ๏ธ Environment indicator not found or incorrect" + fi + + # Check if CSS and JS files are referenced + if echo "$content" | grep -q "style.css"; then + echo "โœ… CSS file is referenced" + else + echo "โŒ CSS file is not referenced" + exit 1 + fi + + if echo "$content" | grep -q "script.js"; then + echo "โœ… JavaScript file is referenced" + else + echo "โŒ JavaScript file is not referenced" + exit 1 + fi + + - name: Test Redirect Behavior + run: | + echo "๐Ÿ”„ Testing redirect behavior for ${{ env.DOMAIN }}" + + # Test if custom domain redirects properly (allow redirects but capture them) + redirect_info=$(curl -s -I -L --max-time 30 https://${{ env.DOMAIN }}/ | grep -E "(HTTP|Location)") + echo "Redirect chain:" + echo "$redirect_info" + + # Check final destination + final_url=$(curl -s -o /dev/null -w "%{url_effective}" -L --max-time 30 https://${{ env.DOMAIN }}/) + echo "Final URL: $final_url" + + # Verify we can access the canonical domain directly + canonical_response=$(curl -s -o /dev/null -w "%{http_code}" --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/) + if [ "$canonical_response" = "200" ]; then + echo "โœ… Canonical domain ${{ env.CANONICAL_DOMAIN }} is accessible" + else + echo "โŒ Canonical domain ${{ env.CANONICAL_DOMAIN }} returned HTTP $canonical_response" + exit 1 + fi + + - name: Test Performance + run: | + echo "โšก Testing performance for ${{ env.DOMAIN }}" + + # Measure response time + response_time=$(curl -s -o /dev/null -w "%{time_total}" -L --max-time 30 https://${{ env.DOMAIN }}/) + echo "Response time: ${response_time}s" + + # Check if response time is reasonable (under 10 seconds) + if (( $(echo "$response_time < 10.0" | bc -l) )); then + echo "โœ… Response time is acceptable" + else + echo "โš ๏ธ Response time is slow: ${response_time}s" + fi + + # Check content size + content_size=$(curl -s -L --max-time 30 https://${{ env.DOMAIN }}/ | wc -c) + echo "Content size: $content_size bytes" + + if [ $content_size -gt 1000 ]; then + echo "โœ… Content size is reasonable" + else + echo "โŒ Content size is too small: $content_size bytes" + exit 1 + fi + + test-infrastructure: + name: Infrastructure Tests + runs-on: ubuntu-latest + if: github.event.inputs.environment == 'all' || github.event.inputs.environment == '' || github.event_name != 'workflow_dispatch' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Test DNS Resolution + run: | + echo "๐ŸŒ Testing DNS resolution" + + domains=("2048-dev.wa.darknex.us" "2048-staging.wa.darknex.us" "2048.wa.darknex.us") + + for domain in "${domains[@]}"; do + echo "Testing DNS for $domain" + ip=$(dig +short $domain) + if [ -n "$ip" ]; then + echo "โœ… $domain resolves to: $ip" + else + echo "โŒ $domain does not resolve" + exit 1 + fi + done + + - name: Test SSL Certificate Chain + run: | + echo "๐Ÿ” Testing SSL certificate chains" + + domains=("2048-dev.wa.darknex.us" "2048-staging.wa.darknex.us" "2048.wa.darknex.us") + + for domain in "${domains[@]}"; do + echo "Testing SSL chain for $domain" + + # Test certificate chain + chain_result=$(echo | openssl s_client -servername $domain -connect $domain:443 -verify_return_error 2>&1) + + if echo "$chain_result" | grep -q "Verify return code: 0"; then + echo "โœ… $domain has valid SSL certificate chain" + else + echo "โŒ $domain has invalid SSL certificate chain" + echo "$chain_result" + exit 1 + fi + done + + summary: + name: Test Summary + runs-on: ubuntu-latest + needs: [smoke-tests, test-infrastructure] + if: always() + + steps: + - name: Check test results + run: | + if [ "${{ needs.smoke-tests.result }}" = "success" ] && [ "${{ needs.test-infrastructure.result }}" = "success" ]; then + echo "โœ… All tests passed successfully!" + echo "๐ŸŽฎ 2048 game is working correctly across all environments" + else + echo "โŒ Some tests failed" + echo "Smoke tests: ${{ needs.smoke-tests.result }}" + echo "Infrastructure tests: ${{ needs.test-infrastructure.result }}" + exit 1 + fi + + - name: Post summary + if: always() + run: | + echo "## Test Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Test Type | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Smoke Tests | ${{ needs.smoke-tests.result == 'success' && 'โœ… Passed' || 'โŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Infrastructure Tests | ${{ needs.test-infrastructure.result == 'success' && 'โœ… Passed' || 'โŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Tested Domains" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿงช Development: https://2048-dev.wa.darknex.us" >> $GITHUB_STEP_SUMMARY + echo "- ๐ŸŽญ Staging: https://2048-staging.wa.darknex.us" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿš€ Production: https://2048.wa.darknex.us" >> $GITHUB_STEP_SUMMARY diff --git a/README.md b/README.md index bd525e2..ee889d0 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,49 @@ # K8s Game 2048 -A Kubernetes deployment of the classic 2048 game using Knative Serving with Kourier ingress controller. +A Kubernetes deployment of the classic 2048 game using Knative Serving with Istio service mesh and nginx ingress for SSL termination. ## Features - **Knative Serving**: Serverless deployment with scale-to-zero capability -- **Kourier Gateway**: Lightweight ingress controller for Knative +- **Istio Service Mesh**: Advanced traffic management and observability +- **nginx Ingress**: SSL termination and traffic routing - **Multi-environment**: Development, Staging, and Production deployments -- **Custom Domains**: Environment-specific domain configuration +- **Custom Domains with SSL**: Environment-specific HTTPS domains - **GitOps Workflow**: Complete CI/CD pipeline with GitHub Actions ## Environments -- **Development**: `2048-dev.wa.darknex.us` -- **Staging**: `2048-staging.wa.darknex.us` -- **Production**: `2048.wa.darknex.us` +- **Development**: `https://2048-dev.wa.darknex.us` +- **Staging**: `https://2048-staging.wa.darknex.us` +- **Production**: `https://2048.wa.darknex.us` ## Architecture ``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Kourier โ”‚ โ”‚ Knative โ”‚ โ”‚ 2048 Game โ”‚ -โ”‚ Gateway โ”‚โ”€โ”€โ”€โ–ถโ”‚ Service โ”‚โ”€โ”€โ”€โ–ถโ”‚ Container โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Internet โ”‚ โ”‚ nginx โ”‚ โ”‚ Istio โ”‚ โ”‚ Knative โ”‚ +โ”‚ โ”‚โ”€โ”€โ”€โ–ถโ”‚ Ingress โ”‚โ”€โ”€โ”€โ–ถโ”‚ Gateway โ”‚โ”€โ”€โ”€โ–ถโ”‚ Service โ”‚ +โ”‚ โ”‚ โ”‚ (SSL Term.) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ cert-managerโ”‚ โ”‚ 2048 Game โ”‚ + โ”‚ Let's Encryptโ”‚ โ”‚ Container โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ## Quick Start ### Prerequisites -- Kubernetes cluster (1.21+) +- Kubernetes cluster (1.21+) with k3s or similar - Knative Serving installed -- Kourier as the networking layer +- Istio service mesh installed +- nginx ingress controller installed +- cert-manager for SSL certificates - kubectl configured -- Domain DNS configured to point to Kourier LoadBalancer +- Domain DNS configured to point to your cluster IP ### Installation @@ -44,9 +53,16 @@ git clone https://github.com/ghndrx/k8s-game-2048.git cd k8s-game-2048 ``` -2. Deploy to development: +2. Deploy all environments: ```bash -kubectl apply -f manifests/dev/ +./scripts/deploy.sh all +``` + +3. Or deploy a specific environment: +```bash +./scripts/deploy.sh dev # Development only +./scripts/deploy.sh staging # Staging only +./scripts/deploy.sh prod # Production only ``` 3. Deploy to staging: diff --git a/manifests/cluster-domain-claims.yaml b/manifests/cluster-domain-claims.yaml deleted file mode 100644 index 34ad9ff..0000000 --- a/manifests/cluster-domain-claims.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.internal.knative.dev/v1alpha1 -kind: ClusterDomainClaim -metadata: - name: 2048-dev.wa.darknex.us -spec: - namespace: game-2048-dev ---- -apiVersion: networking.internal.knative.dev/v1alpha1 -kind: ClusterDomainClaim -metadata: - name: 2048-staging.wa.darknex.us -spec: - namespace: game-2048-staging ---- -apiVersion: networking.internal.knative.dev/v1alpha1 -kind: ClusterDomainClaim -metadata: - name: 2048.wa.darknex.us -spec: - namespace: game-2048-prod diff --git a/manifests/dev/domain-mapping.yml b/manifests/dev/domain-mapping.yml deleted file mode 100644 index d346199..0000000 --- a/manifests/dev/domain-mapping.yml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: serving.knative.dev/v1beta1 -kind: DomainMapping -metadata: - name: 2048-dev.wa.darknex.us - namespace: game-2048-dev - labels: - app: game-2048 - environment: development -spec: - ref: - name: game-2048-dev - kind: Service - apiVersion: serving.knative.dev/v1 - tls: - secretName: game-2048-dev-cert-secret diff --git a/manifests/dev/service.yml b/manifests/dev/service.yml index 1828d5e..c762bb3 100644 --- a/manifests/dev/service.yml +++ b/manifests/dev/service.yml @@ -9,45 +9,18 @@ metadata: spec: template: metadata: - labels: - app: game-2048 - environment: development annotations: - # Scale to zero configuration autoscaling.knative.dev/minScale: "0" autoscaling.knative.dev/maxScale: "10" - autoscaling.knative.dev/scaleDownDelay: "30s" autoscaling.knative.dev/target: "100" spec: containers: - - name: game-2048 - image: ghcr.io/ghndrx/k8s-game-2048:latest + - image: ghcr.io/ghndrx/k8s-game-2048:latest imagePullPolicy: Always ports: - - containerPort: 8080 - protocol: TCP + - containerPort: 80 env: - name: ENVIRONMENT value: "development" - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 256Mi - readinessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /health - port: 8080 - initialDelaySeconds: 15 - periodSeconds: 20 - traffic: - - percent: 100 - latestRevision: true + imagePullSecrets: + - name: ghcr-secret diff --git a/manifests/istio-gateway.yaml b/manifests/istio-gateway.yaml new file mode 100644 index 0000000..04724b7 --- /dev/null +++ b/manifests/istio-gateway.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: knative-ingress-gateway + namespace: knative-serving + labels: + app.kubernetes.io/component: net-istio + app.kubernetes.io/name: knative-serving + app.kubernetes.io/version: 1.12.0 + networking.knative.dev/ingress-provider: istio +spec: + selector: + istio: ingressgateway + servers: + - hosts: + - '*' + port: + name: http + number: 80 + protocol: HTTP diff --git a/manifests/knative-domain-config.yaml b/manifests/knative-domain-config.yaml index 147f164..bf31a33 100644 --- a/manifests/knative-domain-config.yaml +++ b/manifests/knative-domain-config.yaml @@ -4,5 +4,13 @@ metadata: name: config-domain namespace: knative-serving data: - wa.darknex.us: "" + dev.wa.darknex.us: | + selector: + environment: development + staging.wa.darknex.us: | + selector: + environment: staging + wa.darknex.us: | + selector: + environment: production autocreate-cluster-domain-claims: "true" diff --git a/manifests/kourier-ssl-config.yaml b/manifests/kourier-ssl-config.yaml deleted file mode 100644 index 292e181..0000000 --- a/manifests/kourier-ssl-config.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: config-kourier - namespace: knative-serving -data: - _example: | - ################################ - # # - # EXAMPLE CONFIGURATION # - # # - ################################ - enable-service-links: "false" - # Enable automatic HTTP to HTTPS redirect - ssl-redirect: "true" diff --git a/manifests/nginx-certificate.yaml b/manifests/nginx-certificate.yaml new file mode 100644 index 0000000..a648690 --- /dev/null +++ b/manifests/nginx-certificate.yaml @@ -0,0 +1,40 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: game-2048-dev-nginx-cert + namespace: default +spec: + secretName: game-2048-dev-nginx-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "2048-dev.wa.darknex.us" + +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: game-2048-staging-nginx-cert + namespace: default +spec: + secretName: game-2048-staging-nginx-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "2048-staging.wa.darknex.us" + +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: game-2048-prod-nginx-cert + namespace: default +spec: + secretName: game-2048-prod-nginx-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + dnsNames: + - "2048.wa.darknex.us" diff --git a/manifests/nginx-to-istio-proxy.yaml b/manifests/nginx-to-istio-proxy.yaml new file mode 100644 index 0000000..720556a --- /dev/null +++ b/manifests/nginx-to-istio-proxy.yaml @@ -0,0 +1,119 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: game-2048-dev-proxy + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header Host game-2048-dev.game-2048-dev.dev.wa.darknex.us; +spec: + ingressClassName: nginx + tls: + - hosts: + - 2048-dev.wa.darknex.us + secretName: game-2048-dev-nginx-tls + rules: + - host: 2048-dev.wa.darknex.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: istio-nodeport-service + port: + number: 80 + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: game-2048-staging-proxy + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header Host game-2048-staging.game-2048-staging.staging.wa.darknex.us; +spec: + ingressClassName: nginx + tls: + - hosts: + - 2048-staging.wa.darknex.us + secretName: game-2048-staging-nginx-tls + rules: + - host: 2048-staging.wa.darknex.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: istio-nodeport-service + port: + number: 80 + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: game-2048-prod-proxy + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + cert-manager.io/cluster-issuer: "letsencrypt-prod" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header Host game-2048-prod.game-2048-prod.wa.darknex.us; +spec: + ingressClassName: nginx + tls: + - hosts: + - 2048.wa.darknex.us + secretName: game-2048-prod-nginx-tls + rules: + - host: 2048.wa.darknex.us + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: istio-nodeport-service + port: + number: 80 + +--- +apiVersion: v1 +kind: Service +metadata: + name: istio-nodeport-service + namespace: default +spec: + ports: + - name: http + port: 80 + targetPort: 32135 + protocol: TCP + clusterIP: None + +--- +apiVersion: v1 +kind: Endpoints +metadata: + name: istio-nodeport-service + namespace: default +subsets: +- addresses: + - ip: 192.168.4.134 # Your k3s node IP + ports: + - name: http + port: 32135 diff --git a/manifests/prod/domain-mapping.yml b/manifests/prod/domain-mapping.yml deleted file mode 100644 index 7b32753..0000000 --- a/manifests/prod/domain-mapping.yml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: serving.knative.dev/v1beta1 -kind: DomainMapping -metadata: - name: 2048.wa.darknex.us - namespace: game-2048-prod - labels: - app: game-2048 - environment: production -spec: - ref: - name: game-2048-prod - kind: Service - apiVersion: serving.knative.dev/v1 - tls: - secretName: game-2048-prod-cert-secret diff --git a/manifests/prod/service.yml b/manifests/prod/service.yml index 2e5e594..2d063fe 100644 --- a/manifests/prod/service.yml +++ b/manifests/prod/service.yml @@ -9,45 +9,18 @@ metadata: spec: template: metadata: - labels: - app: game-2048 - environment: production annotations: - # Scale to zero configuration autoscaling.knative.dev/minScale: "0" - autoscaling.knative.dev/maxScale: "50" - autoscaling.knative.dev/scaleDownDelay: "300s" + autoscaling.knative.dev/maxScale: "10" autoscaling.knative.dev/target: "100" spec: containers: - - name: game-2048 - image: ghcr.io/ghndrx/k8s-game-2048:latest + - image: ghcr.io/ghndrx/k8s-game-2048:latest imagePullPolicy: Always ports: - - containerPort: 8080 - protocol: TCP + - containerPort: 80 env: - name: ENVIRONMENT value: "production" - resources: - requests: - cpu: 500m - memory: 512Mi - limits: - cpu: 2000m - memory: 1Gi - readinessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /health - port: 8080 - initialDelaySeconds: 15 - periodSeconds: 20 - traffic: - - percent: 100 - latestRevision: true + imagePullSecrets: + - name: ghcr-secret diff --git a/manifests/ssl-certificate.yaml b/manifests/ssl-certificate.yaml index d7c6d01..3daa342 100644 --- a/manifests/ssl-certificate.yaml +++ b/manifests/ssl-certificate.yaml @@ -12,45 +12,3 @@ spec: - http01: ingress: class: nginx - - http01: - ingress: - class: nginx ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: game-2048-dev-cert - namespace: knative-serving -spec: - secretName: game-2048-dev-cert-secret - issuerRef: - name: letsencrypt-prod - kind: ClusterIssuer - dnsNames: - - "2048-dev.wa.darknex.us" ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: game-2048-staging-cert - namespace: knative-serving -spec: - secretName: game-2048-staging-cert-secret - issuerRef: - name: letsencrypt-prod - kind: ClusterIssuer - dnsNames: - - "2048-staging.wa.darknex.us" ---- -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: game-2048-prod-cert - namespace: knative-serving -spec: - secretName: game-2048-prod-cert-secret - issuerRef: - name: letsencrypt-prod - kind: ClusterIssuer - dnsNames: - - "2048.wa.darknex.us" diff --git a/manifests/staging/domain-mapping.yml b/manifests/staging/domain-mapping.yml deleted file mode 100644 index 837a36c..0000000 --- a/manifests/staging/domain-mapping.yml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: serving.knative.dev/v1beta1 -kind: DomainMapping -metadata: - name: 2048-staging.wa.darknex.us - namespace: game-2048-staging - labels: - app: game-2048 - environment: staging -spec: - ref: - name: game-2048-staging - kind: Service - apiVersion: serving.knative.dev/v1 - tls: - secretName: game-2048-staging-cert-secret diff --git a/manifests/staging/service.yml b/manifests/staging/service.yml index 1126a63..d9ca382 100644 --- a/manifests/staging/service.yml +++ b/manifests/staging/service.yml @@ -9,45 +9,18 @@ metadata: spec: template: metadata: - labels: - app: game-2048 - environment: staging annotations: - # Scale to zero configuration autoscaling.knative.dev/minScale: "0" - autoscaling.knative.dev/maxScale: "20" - autoscaling.knative.dev/scaleDownDelay: "60s" + autoscaling.knative.dev/maxScale: "10" autoscaling.knative.dev/target: "100" spec: containers: - - name: game-2048 - image: ghcr.io/ghndrx/k8s-game-2048:latest + - image: ghcr.io/ghndrx/k8s-game-2048:latest imagePullPolicy: Always ports: - - containerPort: 8080 - protocol: TCP + - containerPort: 80 env: - name: ENVIRONMENT value: "staging" - resources: - requests: - cpu: 200m - memory: 256Mi - limits: - cpu: 1000m - memory: 512Mi - readinessProbe: - httpGet: - path: / - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /health - port: 8080 - initialDelaySeconds: 15 - periodSeconds: 20 - traffic: - - percent: 100 - latestRevision: true + imagePullSecrets: + - name: ghcr-secret diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 67040a4..c7ad294 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,87 +1,101 @@ #!/bin/bash -# Deployment script for 2048 game environments -# Usage: ./deploy.sh [dev|staging|prod] [image-tag] +# Deployment script for 2048 game with Istio + nginx SSL setup +# Usage: ./deploy.sh [env] where env = dev|staging|prod|all set -e -ENVIRONMENT=${1:-dev} -IMAGE_TAG=${2:-latest} +ENVIRONMENT=${1:-all} REGISTRY="ghcr.io/ghndrx/k8s-game-2048" -echo "๐Ÿš€ Deploying 2048 game to $ENVIRONMENT environment..." +echo "๐Ÿš€ Deploying 2048 game with Istio + nginx SSL..." +echo "Environment: $ENVIRONMENT" # Validate environment case $ENVIRONMENT in - dev|staging|prod) + dev|staging|prod|all) echo "โœ… Valid environment: $ENVIRONMENT" ;; *) - echo "โŒ Invalid environment. Use: dev, staging, or prod" + echo "โŒ Invalid environment. Use: dev, staging, prod, or all" exit 1 ;; esac -# Check if kubectl is available +# Check dependencies if ! command -v kubectl &> /dev/null; then - echo "โŒ kubectl is not installed. Please install kubectl first." + echo "โŒ kubectl is not installed" exit 1 fi -# Check if cluster is accessible if ! kubectl cluster-info &> /dev/null; then - echo "โŒ Cannot access Kubernetes cluster. Please check your kubeconfig." + echo "โŒ Cannot access Kubernetes cluster" exit 1 fi -# Update image tag in manifests -echo "๐Ÿ”ง Updating image tag to $IMAGE_TAG..." -if [ "$ENVIRONMENT" = "dev" ]; then - sed -i.bak "s|your-registry/game-2048:latest|$REGISTRY:$IMAGE_TAG|g" manifests/dev/service.yml -elif [ "$ENVIRONMENT" = "staging" ]; then - sed -i.bak "s|your-registry/game-2048:staging|$REGISTRY:$IMAGE_TAG|g" manifests/staging/service.yml +# Deploy function for a single environment +deploy_env() { + local env=$1 + echo "๐Ÿ“ฆ Deploying $env environment..." + + # Apply namespace + kubectl apply -f manifests/$env/namespace.yml + + # Ensure GHCR secret exists in the namespace + echo "๐Ÿ” Setting up GHCR secret for $env..." + if kubectl get secret ghcr-secret -n default &>/dev/null; then + kubectl get secret ghcr-secret -o yaml | \ + sed "s/namespace: default/namespace: game-2048-$env/" | \ + sed '/resourceVersion:/d' | \ + sed '/uid:/d' | \ + sed '/creationTimestamp:/d' | \ + kubectl apply -f - + else + echo "โš ๏ธ Warning: No GHCR secret found in default namespace" + fi + + # Apply service + kubectl apply -f manifests/$env/service.yml + + # Wait for service to be ready + echo "โณ Waiting for $env service to be ready..." + kubectl wait --for=condition=Ready ksvc/game-2048-$env -n game-2048-$env --timeout=300s || echo "Warning: Service may still be starting" +} + +# Deploy infrastructure (certificates, gateways, etc.) +echo "๐Ÿ—๏ธ Setting up infrastructure..." +kubectl apply -f manifests/ssl-certificate.yaml +kubectl apply -f manifests/nginx-certificate.yaml +kubectl apply -f manifests/knative-domain-config.yaml +kubectl apply -f manifests/istio-gateway.yaml +kubectl apply -f manifests/nginx-to-istio-proxy.yaml + +# Deploy environments +if [ "$ENVIRONMENT" = "all" ]; then + deploy_env "dev" + deploy_env "staging" + deploy_env "prod" else - sed -i.bak "s|your-registry/game-2048:v1.0.0|$REGISTRY:$IMAGE_TAG|g" manifests/prod/service.yml + deploy_env "$ENVIRONMENT" fi -# Deploy to the specified environment -echo "๐Ÿ“ฆ Deploying to $ENVIRONMENT..." -kubectl apply -f manifests/$ENVIRONMENT/ - -# Wait for deployment to be ready -echo "โณ Waiting for deployment to be ready..." -kubectl wait --for=condition=Ready ksvc/game-2048-$ENVIRONMENT -n game-2048-$ENVIRONMENT --timeout=300s - -# Get service details +echo "" echo "โœ… Deployment completed!" echo "" -echo "๐Ÿ” Service details:" -kubectl get ksvc game-2048-$ENVIRONMENT -n game-2048-$ENVIRONMENT -o wide - -echo "" -echo "๐ŸŒ Service URL:" -kubectl get ksvc game-2048-$ENVIRONMENT -n game-2048-$ENVIRONMENT -o jsonpath='{.status.url}' -echo "" - -echo "" -echo "๐ŸŽฏ Custom domain:" -case $ENVIRONMENT in - dev) - echo "https://2048-dev.wa.darknex.us" - ;; - staging) - echo "https://2048-staging.wa.darknex.us" - ;; - prod) - echo "https://2048.wa.darknex.us" - ;; -esac - -# Restore original manifests -echo "๐Ÿ”„ Restoring original manifests..." -if [ -f "manifests/$ENVIRONMENT/service.yml.bak" ]; then - mv manifests/$ENVIRONMENT/service.yml.bak manifests/$ENVIRONMENT/service.yml +echo "๏ฟฝ Your 2048 game is available at:" +if [ "$ENVIRONMENT" = "all" ] || [ "$ENVIRONMENT" = "dev" ]; then + echo " Development: https://2048-dev.wa.darknex.us" +fi +if [ "$ENVIRONMENT" = "all" ] || [ "$ENVIRONMENT" = "staging" ]; then + echo " Staging: https://2048-staging.wa.darknex.us" +fi +if [ "$ENVIRONMENT" = "all" ] || [ "$ENVIRONMENT" = "prod" ]; then + echo " Production: https://2048.wa.darknex.us" fi - echo "" -echo "๐ŸŽฎ Game deployed successfully! You can now access it at the custom domain." +echo "๐Ÿ”ง Check status with:" +echo " kubectl get ksvc -A" +echo " kubectl get certificates -A" +echo " kubectl get ingress -A" +echo "" +echo "๐Ÿ“ Architecture: Internet โ†’ nginx (SSL) โ†’ Istio โ†’ Knative" diff --git a/scripts/setup-ssl.sh b/scripts/setup-ssl.sh index 970417b..e69de29 100755 --- a/scripts/setup-ssl.sh +++ b/scripts/setup-ssl.sh @@ -1,105 +0,0 @@ -#!/bin/bash - -set -e - -echo "๐Ÿ”ง Setting up SSL for 2048 Game with Kourier..." - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -print_status() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Check if kubectl is available -if ! command -v kubectl &> /dev/null; then - print_error "kubectl is not installed or not in PATH" - exit 1 -fi - -# Check if cluster is accessible -if ! kubectl cluster-info &> /dev/null; then - print_error "Cannot connect to Kubernetes cluster" - exit 1 -fi - -print_status "Installing cert-manager..." -kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml - -print_status "Waiting for cert-manager to be ready..." -kubectl wait --for=condition=ready pod -l app=cert-manager -n cert-manager --timeout=120s -kubectl wait --for=condition=ready pod -l app=cainjector -n cert-manager --timeout=120s -kubectl wait --for=condition=ready pod -l app=webhook -n cert-manager --timeout=120s - -print_status "Applying SSL certificate configuration..." -kubectl apply -f manifests/ssl-certificate.yaml - -print_status "Configuring Knative domain..." -kubectl apply -f manifests/knative-domain-config.yaml - -print_status "Configuring Kourier for SSL..." -kubectl apply -f manifests/kourier-ssl-config.yaml - -print_status "Deploying all environments..." -kubectl apply -f manifests/dev/ -kubectl apply -f manifests/staging/ -kubectl apply -f manifests/prod/ - -print_status "Waiting for certificate to be issued..." -echo "This may take a few minutes..." - -# Wait for certificate to be ready -timeout=300 -counter=0 -while [ $counter -lt $timeout ]; do - if kubectl get certificate darknex-wildcard-cert -n knative-serving -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' | grep -q "True"; then - print_status "Certificate is ready!" - break - fi - echo -n "." - sleep 10 - counter=$((counter + 10)) -done - -if [ $counter -ge $timeout ]; then - print_warning "Certificate is taking longer than expected to be issued." - print_warning "You can check the status with: kubectl describe certificate darknex-wildcard-cert -n knative-serving" -fi - -print_status "Checking deployment status..." -echo "" -echo "=== Certificate Status ===" -kubectl get certificates -n knative-serving - -echo "" -echo "=== Domain Mappings ===" -kubectl get domainmappings --all-namespaces - -echo "" -echo "=== Knative Services ===" -kubectl get ksvc --all-namespaces - -echo "" -print_status "๐ŸŽ‰ SSL setup complete!" -echo "" -echo "Your 2048 game should be accessible at:" -echo " โ€ข Development: https://2048-dev.wa.darknex.us" -echo " โ€ข Staging: https://2048-staging.wa.darknex.us" -echo " โ€ข Production: https://2048.wa.darknex.us" -echo "" -echo "To test SSL is working:" -echo " curl -I https://2048-dev.wa.darknex.us" -echo " curl -I https://2048-staging.wa.darknex.us" -echo " curl -I https://2048.wa.darknex.us" diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100644 index 0000000..25fea26 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,250 @@ +#!/bin/bash + +# Smoke test script for 2048 game deployment +# Tests all environments and validates the complete flow + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test configuration +ENVIRONMENTS=("dev" "staging" "prod") +DOMAINS=("2048-dev.wa.darknex.us" "2048-staging.wa.darknex.us" "2048.wa.darknex.us") +CANONICAL_DOMAINS=("game-2048-dev.game-2048-dev.dev.wa.darknex.us" "game-2048-staging.game-2048-staging.staging.wa.darknex.us" "game-2048-prod.game-2048-prod.wa.darknex.us") +TIMEOUT=30 + +echo -e "${BLUE}๐Ÿงช Starting 2048 Game Smoke Tests${NC}" +echo "==================================" + +# Function to test HTTP response +test_http_response() { + local url=$1 + local expected_status=$2 + local test_name=$3 + + echo -n " Testing $test_name... " + + response=$(curl -s -w "%{http_code}" -o /tmp/response.html --max-time $TIMEOUT "$url" 2>/dev/null || echo "000") + + if [ "$response" = "$expected_status" ]; then + echo -e "${GREEN}โœ… PASS${NC} (HTTP $response)" + return 0 + else + echo -e "${RED}โŒ FAIL${NC} (HTTP $response, expected $expected_status)" + return 1 + fi +} + +# Function to test SSL certificate +test_ssl_cert() { + local domain=$1 + echo -n " Testing SSL certificate for $domain... " + + if echo | openssl s_client -servername "$domain" -connect "$domain:443" -verify_return_error &>/dev/null; then + echo -e "${GREEN}โœ… VALID${NC}" + return 0 + else + echo -e "${RED}โŒ INVALID${NC}" + return 1 + fi +} + +# Function to test content +test_content() { + local url=$1 + local expected_text=$2 + local test_name=$3 + + echo -n " Testing $test_name content... " + + if curl -s --max-time $TIMEOUT "$url" | grep -q "$expected_text"; then + echo -e "${GREEN}โœ… FOUND${NC}" + return 0 + else + echo -e "${RED}โŒ NOT FOUND${NC}" + return 1 + fi +} + +# Function to test Kubernetes resources +test_k8s_resources() { + local env=$1 + echo -e "${YELLOW}๐Ÿ“‹ Testing Kubernetes Resources for $env${NC}" + + # Test namespace + echo -n " Checking namespace game-2048-$env... " + if kubectl get namespace "game-2048-$env" &>/dev/null; then + echo -e "${GREEN}โœ… EXISTS${NC}" + else + echo -e "${RED}โŒ MISSING${NC}" + return 1 + fi + + # Test Knative service + echo -n " Checking Knative service... " + if kubectl get ksvc "game-2048-$env" -n "game-2048-$env" &>/dev/null; then + local status=$(kubectl get ksvc "game-2048-$env" -n "game-2048-$env" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}') + if [ "$status" = "True" ]; then + echo -e "${GREEN}โœ… READY${NC}" + else + echo -e "${YELLOW}โณ NOT READY${NC} (Status: $status)" + fi + else + echo -e "${RED}โŒ MISSING${NC}" + return 1 + fi + + # Test GHCR secret + echo -n " Checking GHCR secret... " + if kubectl get secret ghcr-secret -n "game-2048-$env" &>/dev/null; then + echo -e "${GREEN}โœ… EXISTS${NC}" + else + echo -e "${RED}โŒ MISSING${NC}" + return 1 + fi +} + +# Function to test ingress +test_ingress() { + echo -e "${YELLOW}๐ŸŒ Testing Ingress Configuration${NC}" + + # Test nginx ingress controller + echo -n " Checking nginx ingress controller... " + if kubectl get pods -n ingress-nginx | grep -q "ingress-nginx-controller.*Running"; then + echo -e "${GREEN}โœ… RUNNING${NC}" + else + echo -e "${RED}โŒ NOT RUNNING${NC}" + return 1 + fi + + # Test Istio ingress gateway + echo -n " Checking Istio ingress gateway... " + if kubectl get pods -n istio-system | grep -q "istio-ingressgateway.*Running"; then + echo -e "${GREEN}โœ… RUNNING${NC}" + else + echo -e "${RED}โŒ NOT RUNNING${NC}" + return 1 + fi + + # Test cert-manager + echo -n " Checking cert-manager... " + if kubectl get pods -n cert-manager | grep -q "cert-manager.*Running"; then + echo -e "${GREEN}โœ… RUNNING${NC}" + else + echo -e "${RED}โŒ NOT RUNNING${NC}" + return 1 + fi +} + +# Function to test certificates +test_certificates() { + echo -e "${YELLOW}๐Ÿ”’ Testing SSL Certificates${NC}" + + for i in "${!ENVIRONMENTS[@]}"; do + local env="${ENVIRONMENTS[$i]}" + local domain="${DOMAINS[$i]}" + + echo -n " Checking certificate for $domain... " + local cert_status=$(kubectl get certificate "game-2048-$env-nginx-cert" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "Unknown") + + if [ "$cert_status" = "True" ]; then + echo -e "${GREEN}โœ… READY${NC}" + test_ssl_cert "$domain" + else + echo -e "${RED}โŒ NOT READY${NC} (Status: $cert_status)" + fi + done +} + +# Main test execution +main() { + local total_tests=0 + local passed_tests=0 + + # Test infrastructure + test_ingress + test_certificates + + # Test each environment + for i in "${!ENVIRONMENTS[@]}"; do + local env="${ENVIRONMENTS[$i]}" + local domain="${DOMAINS[$i]}" + local canonical_domain="${CANONICAL_DOMAINS[$i]}" + + echo "" + echo -e "${BLUE}๐ŸŽฎ Testing $env Environment${NC}" + echo "Domain: https://$domain" + echo "Canonical: https://$canonical_domain" + echo "----------------------------------------" + + # Test Kubernetes resources + if test_k8s_resources "$env"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + + # Test custom domain accessibility + if test_http_response "https://$domain" "200\|301\|302" "custom domain"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + + # Test canonical domain accessibility + if test_http_response "https://$canonical_domain" "200" "canonical domain"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + + # Test content + if test_content "https://$canonical_domain" "2048" "game content"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + + # Test environment-specific content + local env_name="" + case $env in + "dev") env_name="development" ;; + "staging") env_name="staging" ;; + "prod") env_name="Production" ;; + esac + + if test_content "https://$canonical_domain" "$env_name" "environment detection"; then + ((total_tests++)) + ((passed_tests++)) + else + ((total_tests++)) + fi + done + + echo "" + echo "==================================" + echo -e "${BLUE}๐Ÿ“Š Test Summary${NC}" + echo "Total Tests: $total_tests" + echo -e "Passed: ${GREEN}$passed_tests${NC}" + echo -e "Failed: ${RED}$((total_tests - passed_tests))${NC}" + + if [ $passed_tests -eq $total_tests ]; then + echo -e "${GREEN}๐ŸŽ‰ All tests passed!${NC}" + exit 0 + else + echo -e "${RED}โŒ Some tests failed${NC}" + exit 1 + fi +} + +# Run main function +main "$@"