mirror of
https://github.com/ghndrx/k8s-game-2048.git
synced 2026-02-10 06:45:07 +00:00
Auto-promote: Merge develop to staging after successful dev tests
This commit is contained in:
35
.env.example
Normal file
35
.env.example
Normal file
@@ -0,0 +1,35 @@
|
||||
# Webhook-based Deployment Configuration
|
||||
# Copy this to .env and customize for your environment
|
||||
|
||||
# Webhook Security
|
||||
WEBHOOK_SECRET=your-webhook-secret-here
|
||||
|
||||
# Your Domain Configuration
|
||||
BASE_DOMAIN=your-domain.com
|
||||
WEBHOOK_DOMAIN=webhook.your-domain.com
|
||||
KNATIVE_DOMAIN=your-domain.com
|
||||
|
||||
# Application Domains
|
||||
DEV_DOMAIN=2048-dev.your-domain.com
|
||||
STAGING_DOMAIN=2048-staging.your-domain.com
|
||||
PROD_DOMAIN=2048.your-domain.com
|
||||
|
||||
# Canonical Knative Domains
|
||||
DEV_CANONICAL_DOMAIN=game-2048-dev.game-2048-dev.dev.your-domain.com
|
||||
STAGING_CANONICAL_DOMAIN=game-2048-staging.game-2048-staging.staging.your-domain.com
|
||||
PROD_CANONICAL_DOMAIN=game-2048-prod.game-2048-prod.your-domain.com
|
||||
|
||||
# Paths and Configuration
|
||||
MANIFESTS_PATH=/home/administrator/k8s-game-2048/manifests
|
||||
KUBECONFIG_PATH=/etc/rancher/k3s/k3s.yaml
|
||||
|
||||
# Deployment Options
|
||||
DEPLOY_INGRESS=true
|
||||
WEBHOOK_REPLICAS=1
|
||||
|
||||
# GitHub Repository (for container registry)
|
||||
GITHUB_REPOSITORY=your-username/k8s-game-2048
|
||||
CONTAINER_REGISTRY=ghcr.io
|
||||
|
||||
# Email for SSL certificates
|
||||
CERT_EMAIL=your-email@your-domain.com
|
||||
177
.github/workflows/auto-promote.yml
vendored
177
.github/workflows/auto-promote.yml
vendored
@@ -5,17 +5,24 @@ on:
|
||||
workflows: ["Deploy to Development"]
|
||||
types:
|
||||
- completed
|
||||
branches: [ main ]
|
||||
branches: [ develop ]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
test-and-promote:
|
||||
name: Test Dev and Auto-Promote
|
||||
test-and-promote-to-staging:
|
||||
name: Test Dev and Auto-Promote to Staging
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: develop
|
||||
|
||||
- name: Wait for dev deployment to settle
|
||||
run: |
|
||||
@@ -26,40 +33,20 @@ jobs:
|
||||
run: |
|
||||
echo "🧪 Running comprehensive tests on dev environment..."
|
||||
|
||||
# Test canonical domain first (primary test)
|
||||
echo "Testing canonical domain: game-2048-dev.game-2048-dev.dev.wa.darknex.us"
|
||||
canonical_response=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://game-2048-dev.game-2048-dev.dev.wa.darknex.us/)
|
||||
# Use the canonical Knative domain
|
||||
CANONICAL_URL="https://game-2048-dev.game-2048-dev.${{ secrets.DEV_DOMAIN }}"
|
||||
echo "Testing canonical domain: $CANONICAL_URL"
|
||||
|
||||
canonical_response=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 "$CANONICAL_URL")
|
||||
if [ "$canonical_response" != "200" ]; then
|
||||
echo "❌ Canonical domain returned HTTP $canonical_response"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Canonical domain accessible"
|
||||
|
||||
# Test SSL certificate on custom domain
|
||||
echo "Testing SSL certificate on custom domain..."
|
||||
cert_expiry=$(echo | openssl s_client -servername 2048-dev.wa.darknex.us -connect 2048-dev.wa.darknex.us:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
|
||||
expiry_epoch=$(date -d "$cert_expiry" +%s)
|
||||
current_epoch=$(date +%s)
|
||||
days_until_expiry=$(( (expiry_epoch - current_epoch) / 86400 ))
|
||||
|
||||
if [ $days_until_expiry -lt 30 ]; then
|
||||
echo "❌ SSL certificate expires in less than 30 days!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ SSL certificate valid for $days_until_expiry days"
|
||||
|
||||
# Test custom domain accessibility
|
||||
echo "Testing custom domain accessibility..."
|
||||
response_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://2048-dev.wa.darknex.us/)
|
||||
if [ "$response_code" != "200" ]; then
|
||||
echo "❌ Custom domain returned HTTP $response_code"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Custom domain accessible"
|
||||
|
||||
# Test content validation on canonical domain
|
||||
echo "Testing content validation on canonical domain..."
|
||||
content=$(curl -s -L --max-time 30 https://game-2048-dev.game-2048-dev.dev.wa.darknex.us/)
|
||||
content=$(curl -s -L --max-time 30 "$CANONICAL_URL")
|
||||
|
||||
if ! echo "$content" | grep -q "2048"; then
|
||||
echo "❌ Content missing 2048 title"
|
||||
@@ -85,132 +72,58 @@ jobs:
|
||||
|
||||
# Test performance on canonical domain
|
||||
echo "Testing performance on canonical domain..."
|
||||
response_time=$(curl -s -o /dev/null -w "%{time_total}" -L --max-time 30 https://game-2048-dev.game-2048-dev.dev.wa.darknex.us/)
|
||||
response_time=$(curl -s -o /dev/null -w "%{time_total}" -L --max-time 30 "$CANONICAL_URL")
|
||||
if (( $(echo "$response_time > 10.0" | bc -l) )); then
|
||||
echo "❌ Response time too slow: ${response_time}s"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Performance test passed: ${response_time}s"
|
||||
|
||||
- name: Auto-promote to staging
|
||||
- name: Auto-promote develop to staging branch
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
console.log('🚀 All dev tests passed! Auto-promoting to staging...');
|
||||
console.log('🚀 All dev tests passed! Auto-promoting develop to staging branch...');
|
||||
|
||||
const response = await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'deploy-staging.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
image_tag: 'latest'
|
||||
// Create a merge from develop to staging
|
||||
try {
|
||||
const response = await github.rest.repos.merge({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
base: 'staging',
|
||||
head: 'develop',
|
||||
commit_message: 'Auto-promote: Merge develop to staging after successful dev tests'
|
||||
});
|
||||
|
||||
console.log('✅ Successfully merged develop to staging branch');
|
||||
console.log('This will trigger staging deployment automatically');
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
console.log('ℹ️ No new commits to merge - staging is already up to date');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ Staging deployment triggered');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
- name: Create promotion summary
|
||||
run: |
|
||||
echo "## 🎯 Auto-Promotion Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## 🎯 Auto-Promotion Summary (Develop → Staging)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Stage | Status | Action |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|--------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Dev Tests | ✅ Passed | Comprehensive validation completed |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Staging | 🚀 Triggered | Auto-promotion initiated |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Staging Branch | 🚀 Updated | Auto-promotion completed |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Staging Deploy | ⏳ Triggered | Deployment will start automatically |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📋 Tests Performed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- SSL certificate validation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Domain accessibility check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Canonical domain accessibility check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Content and functionality validation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Performance testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🔗 Next Steps" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Monitor staging deployment progress" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Staging deployment will start automatically" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Staging tests will run automatically" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Production promotion requires manual approval" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
promote-to-production:
|
||||
name: Test Staging and Promote to Production
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-and-promote
|
||||
if: success()
|
||||
environment: production-approval # This requires manual approval
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Wait for staging deployment
|
||||
run: |
|
||||
echo "⏳ Waiting for staging deployment to complete..."
|
||||
sleep 120 # Give staging time to deploy
|
||||
|
||||
- name: Test staging environment
|
||||
run: |
|
||||
echo "🧪 Running staging tests..."
|
||||
|
||||
# Test canonical staging domain first
|
||||
echo "Testing canonical staging domain: game-2048-staging.game-2048-staging.staging.wa.darknex.us"
|
||||
canonical_response=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://game-2048-staging.game-2048-staging.staging.wa.darknex.us/)
|
||||
if [ "$canonical_response" != "200" ]; then
|
||||
echo "❌ Staging canonical domain returned HTTP $canonical_response"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Staging canonical domain accessible"
|
||||
|
||||
# Test custom staging domain
|
||||
echo "Testing custom staging domain: 2048-staging.wa.darknex.us"
|
||||
response_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://2048-staging.wa.darknex.us/)
|
||||
if [ "$response_code" != "200" ]; then
|
||||
echo "❌ Staging custom domain returned HTTP $response_code"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Staging custom domain accessible"
|
||||
|
||||
# Test staging content on canonical domain
|
||||
echo "Testing staging content..."
|
||||
content=$(curl -s -L --max-time 30 https://game-2048-staging.game-2048-staging.staging.wa.darknex.us/)
|
||||
if ! echo "$content" | grep -q "2048"; then
|
||||
echo "❌ Staging content validation failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Staging content validation passed"
|
||||
|
||||
- name: Auto-promote to production
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
console.log('🎯 Staging tests passed! Promoting to production...');
|
||||
|
||||
const response = await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'deploy-prod.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
image_tag: 'latest',
|
||||
confirmation: 'DEPLOY'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🚀 Production deployment triggered');
|
||||
|
||||
return response;
|
||||
|
||||
- name: Create final summary
|
||||
run: |
|
||||
echo "## 🎉 Full Pipeline Completion" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Environment | Status | URL |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------------|--------|-----|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Development | ✅ Tested & Live | https://2048-dev.wa.darknex.us |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Staging | ✅ Tested & Live | https://2048-staging.wa.darknex.us |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Production | 🚀 Deploying | https://2048.wa.darknex.us |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🎮 Your 2048 Game is Live!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All environments have been automatically tested and promoted successfully." >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Production promotion requires manual approval via staging → main merge" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
6
.github/workflows/build-image.yml
vendored
6
.github/workflows/build-image.yml
vendored
@@ -2,9 +2,9 @@ name: Build and Push Container Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ main, develop, staging ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
branches: [ main, develop, staging ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
|
||||
277
.github/workflows/deploy-dev.yml
vendored
277
.github/workflows/deploy-dev.yml
vendored
@@ -1,217 +1,136 @@
|
||||
name: Deploy to Development
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push Container Image"]
|
||||
types:
|
||||
- completed
|
||||
branches: [ main ]
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ main, master, develop ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'Dockerfile'
|
||||
- 'nginx.conf'
|
||||
- 'package.json'
|
||||
- 'manifests/dev/**'
|
||||
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
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
deploy-dev:
|
||||
name: Deploy to Development
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
environment: development
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
version: 'latest'
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
- 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: Set image tag
|
||||
- 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 image tag for deployment
|
||||
run: |
|
||||
IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}"
|
||||
IMAGE_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n1 | cut -d':' -f2)
|
||||
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
||||
echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG"
|
||||
echo "🏷️ Using image tag: $IMAGE_TAG"
|
||||
|
||||
- name: Deploy to development
|
||||
- name: Deploy to development via webhook
|
||||
run: |
|
||||
echo "🚀 Deploying to development environment..."
|
||||
echo "🚀 Triggering webhook deployment to development..."
|
||||
|
||||
# Apply namespace
|
||||
kubectl apply -f manifests/dev/namespace.yml
|
||||
# Prepare deployment payload (compact JSON to avoid whitespace issues)
|
||||
PAYLOAD='{"environment":"development","image":"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}","namespace":"game-2048-dev","service_name":"game-2048-dev","deployment_id":"${{ github.run_id }}-${{ github.run_attempt }}","commit_sha":"${{ github.sha }}","triggered_by":"${{ github.actor }}","timestamp":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}'
|
||||
|
||||
# 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 -
|
||||
# Generate HMAC signature for webhook security
|
||||
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.WEBHOOK_SECRET }}" | sed 's/^.* //')
|
||||
|
||||
# Send webhook
|
||||
HTTP_CODE=$(curl -s -o /tmp/webhook_response.json -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Signature-SHA256: sha256=$SIGNATURE" \
|
||||
-H "X-GitHub-Event: deployment" \
|
||||
-H "X-GitHub-Delivery: ${{ github.run_id }}" \
|
||||
-d "$PAYLOAD" \
|
||||
"${{ secrets.DEV_WEBHOOK_URL }}")
|
||||
|
||||
echo "Webhook response code: $HTTP_CODE"
|
||||
cat /tmp/webhook_response.json || echo "No response body"
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "✅ Webhook deployment triggered successfully!"
|
||||
else
|
||||
echo "❌ Webhook deployment failed with code: $HTTP_CODE"
|
||||
exit 1
|
||||
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
|
||||
- name: Wait for deployment to complete
|
||||
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..."
|
||||
echo "⏳ Waiting for deployment to stabilize..."
|
||||
sleep 30
|
||||
|
||||
for i in {1..5}; do
|
||||
echo "Attempt $i/5..."
|
||||
# Test canonical domain first
|
||||
if curl -s --max-time 30 https://game-2048-dev.game-2048-dev.dev.wa.darknex.us/ | grep -q "2048"; then
|
||||
echo "✅ Canonical domain smoke test passed!"
|
||||
break
|
||||
# Fallback to custom domain
|
||||
elif curl -s --max-time 30 https://2048-dev.wa.darknex.us/ | grep -q "2048"; then
|
||||
echo "✅ Custom domain smoke test passed!"
|
||||
break
|
||||
elif [ $i -eq 5 ]; then
|
||||
echo "⚠️ Smoke test failed after 5 attempts"
|
||||
exit 1
|
||||
- name: Health check
|
||||
run: |
|
||||
echo "🏥 Performing health check..."
|
||||
MAX_RETRIES=10
|
||||
RETRY_COUNT=0
|
||||
|
||||
# Use the canonical Knative domain for health check
|
||||
HEALTH_URL="https://game-2048-dev.game-2048-dev.${{ secrets.DEV_DOMAIN }}"
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||
echo "Attempt $((RETRY_COUNT + 1))/$MAX_RETRIES - Checking: $HEALTH_URL"
|
||||
|
||||
if curl -f -s --max-time 10 "$HEALTH_URL" > /dev/null; then
|
||||
echo "✅ Health check passed!"
|
||||
echo "🌐 Application is available at: $HEALTH_URL"
|
||||
exit 0
|
||||
else
|
||||
echo "Retrying in 30 seconds..."
|
||||
sleep 30
|
||||
echo "⚠️ Health check failed, retrying in 15 seconds..."
|
||||
sleep 15
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: 'v1.28.0'
|
||||
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
|
||||
export KUBECONFIG=kubeconfig
|
||||
echo "❌ Health check failed after $MAX_RETRIES attempts"
|
||||
echo "The deployment webhook was sent successfully, but the service is not responding"
|
||||
echo "Please check your cluster logs for deployment issues"
|
||||
exit 1
|
||||
|
||||
- name: Update image in manifests
|
||||
run: |
|
||||
sed -i "s|ghcr.io/ghndrx/k8s-game-2048:latest|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}|g" manifests/dev/service.yml
|
||||
|
||||
- name: Deploy to development
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
kubectl apply -f manifests/dev/
|
||||
|
||||
- name: Wait for deployment
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
kubectl wait --for=condition=Ready ksvc/game-2048-dev -n game-2048-dev --timeout=300s
|
||||
|
||||
- name: Get service URL
|
||||
id: get-url
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
SERVICE_URL=$(kubectl get ksvc game-2048-dev -n game-2048-dev -o jsonpath='{.status.url}')
|
||||
echo "service_url=$SERVICE_URL" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Development service deployed at: $SERVICE_URL"
|
||||
|
||||
- name: Set up Node.js for testing
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: tests/package.json
|
||||
|
||||
- name: Install Playwright dependencies
|
||||
run: |
|
||||
cd tests
|
||||
npm install
|
||||
npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
cd tests
|
||||
BASE_URL=${{ steps.get-url.outputs.service_url }} npx playwright test
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Deployment summary
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-results-dev-${{ github.sha }}-${{ github.run_number }}
|
||||
path: |
|
||||
tests/playwright-report/
|
||||
tests/test-results/
|
||||
retention-days: 30
|
||||
run: |
|
||||
echo "## 🚀 Development Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Environment:** Development" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Deployment Method:** Webhook-based" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: screenshots-dev-${{ github.sha }}-${{ github.run_number }}
|
||||
path: tests/test-results/**/*.png
|
||||
retention-days: 30
|
||||
|
||||
promote-to-staging:
|
||||
needs: build-and-deploy
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create Pull Request to Staging
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
push-to-fork: false
|
||||
branch: auto-promote/dev-to-staging-${{ github.sha }}
|
||||
base: staging
|
||||
title: "🚀 Auto-promote: Deploy ${{ github.sha }} to staging"
|
||||
body: |
|
||||
## 🚀 Auto-promotion from Development
|
||||
|
||||
**Source**: `develop` branch
|
||||
**Commit**: ${{ github.sha }}
|
||||
**Triggered by**: @${{ github.actor }}
|
||||
|
||||
### ✅ Development Tests Passed
|
||||
- Basic functionality tests
|
||||
- Gameplay mechanics tests
|
||||
- Visual regression tests
|
||||
- Environment validation tests
|
||||
|
||||
### 🎯 Changes in this promotion:
|
||||
${{ github.event.head_commit.message }}
|
||||
|
||||
This PR was automatically created after successful deployment and testing in the development environment.
|
||||
|
||||
**Development URL**: https://2048-dev.wa.darknex.us
|
||||
**Will deploy to**: https://2048-staging.wa.darknex.us
|
||||
labels: |
|
||||
auto-promotion
|
||||
staging
|
||||
deploy
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
echo "- **Status:** ✅ Success" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **URL:** https://game-2048-dev.game-2048-dev.${{ secrets.DEV_DOMAIN }}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "- **Status:** ❌ Failed" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
369
.github/workflows/deploy-prod.yml
vendored
369
.github/workflows/deploy-prod.yml
vendored
@@ -10,253 +10,176 @@ on:
|
||||
confirmation:
|
||||
description: 'Type "DEPLOY" to confirm production deployment'
|
||||
required: true
|
||||
source_environment:
|
||||
description: 'Source environment (staging or manual)'
|
||||
required: false
|
||||
default: 'staging'
|
||||
workflow_run:
|
||||
workflows: ["Deploy to Staging"]
|
||||
types:
|
||||
- completed
|
||||
branches: [ main, master ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ghndrx/k8s-game-2048
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
deploy-prod:
|
||||
name: Deploy to Production
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
if: ${{ github.event.inputs.confirmation == 'DEPLOY' }}
|
||||
if: |
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.confirmation == 'DEPLOY') ||
|
||||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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' }}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}"
|
||||
else
|
||||
# For auto-promotion, use the latest successful build
|
||||
IMAGE_TAG="main-$(echo "${{ github.sha }}" | cut -c1-7)"
|
||||
fi
|
||||
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
||||
echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG"
|
||||
|
||||
- name: Deploy to production
|
||||
- name: Deploy to production via webhook (Blue-Green)
|
||||
run: |
|
||||
echo "🚀 Deploying to production environment..."
|
||||
echo "🚀 Triggering blue-green webhook deployment to production..."
|
||||
|
||||
# Apply namespace
|
||||
kubectl apply -f manifests/prod/namespace.yml
|
||||
# Prepare deployment payload
|
||||
PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"environment": "production",
|
||||
"image": "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}",
|
||||
"namespace": "game-2048-prod",
|
||||
"service_name": "game-2048-prod",
|
||||
"deployment_id": "${{ github.run_id }}-${{ github.run_attempt }}",
|
||||
"commit_sha": "${{ github.sha }}",
|
||||
"triggered_by": "${{ github.actor }}",
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"auto_promotion": "${{ github.event_name == 'workflow_run' }}",
|
||||
"deployment_strategy": "blue-green",
|
||||
"traffic_split": {
|
||||
"initial": 10,
|
||||
"intermediate": 50,
|
||||
"final": 100
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# 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
|
||||
# Generate HMAC signature for webhook security
|
||||
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.WEBHOOK_SECRET }}" -binary | base64)
|
||||
|
||||
# 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"}]}}}}'
|
||||
# Send webhook
|
||||
HTTP_CODE=$(curl -s -o /tmp/webhook_response.json -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Signature-SHA256: sha256=$SIGNATURE" \
|
||||
-H "X-GitHub-Event: deployment" \
|
||||
-H "X-GitHub-Delivery: ${{ github.run_id }}" \
|
||||
-d "$PAYLOAD" \
|
||||
"${{ secrets.PROD_WEBHOOK_URL }}")
|
||||
|
||||
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"
|
||||
echo "Webhook response code: $HTTP_CODE"
|
||||
cat /tmp/webhook_response.json || echo "No response body"
|
||||
|
||||
- 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..."
|
||||
# Test canonical domain first
|
||||
if curl -s --max-time 30 https://game-2048-prod.game-2048-prod.wa.darknex.us/ | grep -q "2048"; then
|
||||
echo "✅ Canonical domain smoke test passed!"
|
||||
break
|
||||
# Fallback to custom domain
|
||||
elif curl -s --max-time 30 https://2048.wa.darknex.us/ | grep -q "2048"; then
|
||||
echo "✅ Custom domain 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
|
||||
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=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- 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: 'v1.28.0'
|
||||
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
|
||||
export KUBECONFIG=kubeconfig
|
||||
|
||||
- name: Update image in manifests
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name || github.event.inputs.tag }}"
|
||||
sed -i "s|ghcr.io/ghndrx/k8s-game-2048:v1.0.0|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}|g" manifests/prod/service.yml
|
||||
|
||||
- name: Deploy to production with blue-green strategy
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
|
||||
# Deploy new revision with 0% traffic
|
||||
kubectl apply -f manifests/prod/
|
||||
|
||||
# Wait for new revision to be ready
|
||||
kubectl wait --for=condition=Ready ksvc/game-2048-prod -n game-2048-prod --timeout=300s
|
||||
|
||||
# Get the latest revision name
|
||||
LATEST_REVISION=$(kubectl get ksvc game-2048-prod -n game-2048-prod -o jsonpath='{.status.latestReadyRevisionName}')
|
||||
|
||||
# Gradually shift traffic (10%, 50%, 100%)
|
||||
kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"revisionName":"'$LATEST_REVISION'","percent":10},{"latestRevision":false,"percent":90}]}}'
|
||||
sleep 60
|
||||
|
||||
kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"revisionName":"'$LATEST_REVISION'","percent":50},{"latestRevision":false,"percent":50}]}}'
|
||||
sleep 60
|
||||
|
||||
kubectl patch ksvc game-2048-prod -n game-2048-prod --type='merge' -p='{"spec":{"traffic":[{"latestRevision":true,"percent":100}]}}'
|
||||
|
||||
- name: Run production health checks
|
||||
run: |
|
||||
# Wait for traffic to stabilize
|
||||
sleep 60
|
||||
# Test the production URL
|
||||
curl -f https://2048.wa.darknex.us/ || exit 1
|
||||
# Additional health checks can be added here
|
||||
|
||||
- name: Get service URL
|
||||
id: get-url
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
SERVICE_URL=$(kubectl get ksvc game-2048-prod -n game-2048-prod -o jsonpath='{.status.url}')
|
||||
echo "service_url=$SERVICE_URL" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Production service deployed at: $SERVICE_URL"
|
||||
|
||||
- name: Set up Node.js for testing
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: tests/package.json
|
||||
|
||||
- name: Install Playwright dependencies
|
||||
run: |
|
||||
cd tests
|
||||
npm install
|
||||
npx playwright install --with-deps
|
||||
|
||||
- name: Run production smoke tests
|
||||
run: |
|
||||
cd tests
|
||||
BASE_URL=${{ steps.get-url.outputs.service_url }} npx playwright test environment.spec.ts
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Run full test suite
|
||||
run: |
|
||||
cd tests
|
||||
BASE_URL=${{ steps.get-url.outputs.service_url }} npx playwright test
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Upload production test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-results-production-${{ github.sha }}-${{ github.run_number }}
|
||||
path: |
|
||||
tests/playwright-report/
|
||||
tests/test-results/
|
||||
retention-days: 90
|
||||
|
||||
- name: Upload production screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: screenshots-production-${{ github.sha }}-${{ github.run_number }}
|
||||
path: tests/test-results/**/*.png
|
||||
retention-days: 90
|
||||
|
||||
- name: Production health validation
|
||||
run: |
|
||||
# Extended health checks for production
|
||||
echo "🔍 Running production health checks..."
|
||||
|
||||
# Test main URL
|
||||
curl -f https://2048.wa.darknex.us/ || exit 1
|
||||
|
||||
# Test health endpoint
|
||||
curl -f https://2048.wa.darknex.us/health || exit 1
|
||||
|
||||
# Check response times
|
||||
RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' https://2048.wa.darknex.us/)
|
||||
echo "Response time: ${RESPONSE_TIME}s"
|
||||
|
||||
# Fail if response time > 3 seconds
|
||||
if (( $(echo "$RESPONSE_TIME > 3.0" | bc -l) )); then
|
||||
echo "❌ Response time too slow: ${RESPONSE_TIME}s"
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "✅ Webhook deployment triggered successfully!"
|
||||
else
|
||||
echo "❌ Webhook deployment failed with code: $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All production health checks passed!"
|
||||
- name: Wait for blue-green deployment phases
|
||||
run: |
|
||||
echo "⏳ Waiting for blue-green deployment phases..."
|
||||
echo "Phase 1: Initial deployment (10% traffic) - 2 minutes"
|
||||
sleep 120
|
||||
|
||||
echo "Phase 2: Intermediate traffic split (50%) - 2 minutes"
|
||||
sleep 120
|
||||
|
||||
echo "Phase 3: Full traffic switch (100%) - 1 minute"
|
||||
sleep 60
|
||||
|
||||
echo "✅ Blue-green deployment phases completed"
|
||||
|
||||
- name: Production health check
|
||||
run: |
|
||||
echo "🏥 Performing comprehensive production health check..."
|
||||
MAX_RETRIES=10
|
||||
RETRY_COUNT=0
|
||||
|
||||
# Use the canonical Knative domain for health check
|
||||
HEALTH_URL="https://game-2048-prod.game-2048-prod.${{ secrets.PROD_DOMAIN }}"
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||
echo "Attempt $((RETRY_COUNT + 1))/$MAX_RETRIES - Checking: $HEALTH_URL"
|
||||
|
||||
# Check if service responds
|
||||
if curl -f -s --max-time 10 "$HEALTH_URL" > /dev/null; then
|
||||
echo "✅ Basic health check passed!"
|
||||
|
||||
# Additional production validations
|
||||
echo "🔍 Running extended production validations..."
|
||||
|
||||
# Check response time
|
||||
RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' "$HEALTH_URL")
|
||||
echo "Response time: ${RESPONSE_TIME}s"
|
||||
|
||||
# Check if response contains expected content
|
||||
if curl -s --max-time 10 "$HEALTH_URL" | grep -q "2048"; then
|
||||
echo "✅ Content validation passed!"
|
||||
echo "🌐 Production application is live at: $HEALTH_URL"
|
||||
exit 0
|
||||
else
|
||||
echo "⚠️ Content validation failed, retrying..."
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Health check failed, retrying in 20 seconds..."
|
||||
sleep 20
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "❌ Production health check failed after $MAX_RETRIES attempts"
|
||||
echo "The deployment webhook was sent successfully, but the service is not responding correctly"
|
||||
echo "Please check your cluster logs and consider rolling back"
|
||||
exit 1
|
||||
|
||||
- name: Production deployment summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## 🚀 Production Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Environment:** Production" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Deployment Method:** Webhook-based Blue-Green" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Strategy:** 10% → 50% → 100% traffic split" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_run" ]; then
|
||||
echo "- **Type:** Auto-promotion from Staging" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "- **Type:** Manual deployment with confirmation" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
echo "- **Status:** ✅ **LIVE IN PRODUCTION**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **URL:** https://game-2048-prod.game-2048-prod.${{ secrets.PROD_DOMAIN }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🎉 Production is Live!" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🎮 [Play the game](https://game-2048-prod.game-2048-prod.${{ secrets.PROD_DOMAIN }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🧪 [Run smoke tests](https://github.com/${{ github.repository }}/actions/workflows/smoke-test.yml)" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "- **Status:** ❌ Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### ⚠️ Production Deployment Failed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Please check the logs and consider manual intervention or rollback." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
322
.github/workflows/deploy-staging.yml
vendored
322
.github/workflows/deploy-staging.yml
vendored
@@ -7,267 +7,133 @@ on:
|
||||
description: 'Image tag to deploy (default: latest)'
|
||||
required: false
|
||||
default: 'latest'
|
||||
workflow_run:
|
||||
workflows: ["Deploy to Development"]
|
||||
types:
|
||||
- completed
|
||||
branches: [ main, master ]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ghndrx/k8s-game-2048
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
deploy-staging:
|
||||
name: Deploy to Staging
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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' }}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
IMAGE_TAG="${{ github.event.inputs.image_tag || 'latest' }}"
|
||||
else
|
||||
# For auto-promotion, use the latest successful build
|
||||
IMAGE_TAG="main-$(echo "${{ github.sha }}" | cut -c1-7)"
|
||||
fi
|
||||
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
|
||||
echo "Deploying image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$IMAGE_TAG"
|
||||
|
||||
- name: Deploy to staging
|
||||
- name: Deploy to staging via webhook
|
||||
run: |
|
||||
echo "🚀 Deploying to staging environment..."
|
||||
echo "🚀 Triggering webhook deployment to staging..."
|
||||
|
||||
# Apply namespace
|
||||
kubectl apply -f manifests/staging/namespace.yml
|
||||
# Prepare deployment payload
|
||||
PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"environment": "staging",
|
||||
"image": "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}",
|
||||
"namespace": "game-2048-staging",
|
||||
"service_name": "game-2048-staging",
|
||||
"deployment_id": "${{ github.run_id }}-${{ github.run_attempt }}",
|
||||
"commit_sha": "${{ github.sha }}",
|
||||
"triggered_by": "${{ github.actor }}",
|
||||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"auto_promotion": "${{ github.event_name == 'workflow_run' }}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# 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 -
|
||||
# Generate HMAC signature for webhook security
|
||||
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.WEBHOOK_SECRET }}" -binary | base64)
|
||||
|
||||
# Send webhook
|
||||
HTTP_CODE=$(curl -s -o /tmp/webhook_response.json -w "%{http_code}" \
|
||||
-X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Signature-SHA256: sha256=$SIGNATURE" \
|
||||
-H "X-GitHub-Event: deployment" \
|
||||
-H "X-GitHub-Delivery: ${{ github.run_id }}" \
|
||||
-d "$PAYLOAD" \
|
||||
"${{ secrets.STAGING_WEBHOOK_URL }}")
|
||||
|
||||
echo "Webhook response code: $HTTP_CODE"
|
||||
cat /tmp/webhook_response.json || echo "No response body"
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "✅ Webhook deployment triggered successfully!"
|
||||
else
|
||||
echo "❌ Webhook deployment failed with code: $HTTP_CODE"
|
||||
exit 1
|
||||
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
|
||||
- name: Wait for deployment to complete
|
||||
run: |
|
||||
echo "📊 Deployment status:"
|
||||
kubectl get ksvc -n game-2048-staging
|
||||
echo "⏳ Waiting for deployment to stabilize..."
|
||||
sleep 45
|
||||
|
||||
echo ""
|
||||
echo "✅ Staging deployment completed!"
|
||||
echo "🌐 Available at: https://2048-staging.wa.darknex.us"
|
||||
|
||||
- name: Run smoke test
|
||||
- name: Health check
|
||||
run: |
|
||||
echo "🧪 Running smoke test..."
|
||||
sleep 30
|
||||
echo "🏥 Performing health check..."
|
||||
MAX_RETRIES=10
|
||||
RETRY_COUNT=0
|
||||
|
||||
for i in {1..5}; do
|
||||
echo "Attempt $i/5..."
|
||||
# Test canonical domain first
|
||||
if curl -s --max-time 30 https://game-2048-staging.game-2048-staging.staging.wa.darknex.us/ | grep -q "2048"; then
|
||||
echo "✅ Canonical domain smoke test passed!"
|
||||
break
|
||||
# Fallback to custom domain
|
||||
elif curl -s --max-time 30 https://2048-staging.wa.darknex.us/ | grep -q "2048"; then
|
||||
echo "✅ Custom domain smoke test passed!"
|
||||
break
|
||||
elif [ $i -eq 5 ]; then
|
||||
echo "⚠️ Smoke test failed after 5 attempts"
|
||||
exit 1
|
||||
# Use the canonical Knative domain for health check
|
||||
HEALTH_URL="https://game-2048-staging.game-2048-staging.${{ secrets.STAGING_DOMAIN }}"
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||
echo "Attempt $((RETRY_COUNT + 1))/$MAX_RETRIES - Checking: $HEALTH_URL"
|
||||
|
||||
if curl -f -s --max-time 10 "$HEALTH_URL" > /dev/null; then
|
||||
echo "✅ Health check passed!"
|
||||
echo "🌐 Application is available at: $HEALTH_URL"
|
||||
exit 0
|
||||
else
|
||||
echo "Retrying in 30 seconds..."
|
||||
sleep 30
|
||||
echo "⚠️ Health check failed, retrying in 15 seconds..."
|
||||
sleep 15
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix=staging-
|
||||
|
||||
- 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 }}
|
||||
echo "❌ Health check failed after $MAX_RETRIES attempts"
|
||||
echo "The deployment webhook was sent successfully, but the service is not responding"
|
||||
echo "Please check your cluster logs for deployment issues"
|
||||
exit 1
|
||||
|
||||
- name: Set up kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: 'v1.28.0'
|
||||
|
||||
- name: Configure kubectl
|
||||
run: |
|
||||
echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig
|
||||
export KUBECONFIG=kubeconfig
|
||||
|
||||
- name: Update image in manifests
|
||||
run: |
|
||||
sed -i "s|ghcr.io/ghndrx/k8s-game-2048:staging|${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging-${{ github.sha }}|g" manifests/staging/service.yml
|
||||
|
||||
- name: Deploy to staging
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
kubectl apply -f manifests/staging/
|
||||
|
||||
- name: Wait for deployment
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
kubectl wait --for=condition=Ready ksvc/game-2048-staging -n game-2048-staging --timeout=300s
|
||||
|
||||
- name: Run smoke tests
|
||||
run: |
|
||||
# Wait a bit for the service to be fully ready
|
||||
sleep 30
|
||||
# Test the staging URL
|
||||
curl -f https://2048-staging.wa.darknex.us/ || exit 1
|
||||
|
||||
- name: Get service URL
|
||||
id: get-url
|
||||
run: |
|
||||
export KUBECONFIG=kubeconfig
|
||||
SERVICE_URL=$(kubectl get ksvc game-2048-staging -n game-2048-staging -o jsonpath='{.status.url}')
|
||||
echo "service_url=$SERVICE_URL" >> $GITHUB_OUTPUT
|
||||
echo "🚀 Staging service deployed at: $SERVICE_URL"
|
||||
|
||||
- name: Set up Node.js for testing
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: tests/package.json
|
||||
|
||||
- name: Install Playwright dependencies
|
||||
run: |
|
||||
cd tests
|
||||
npm install
|
||||
npx playwright install --with-deps
|
||||
|
||||
- name: Run comprehensive Playwright tests
|
||||
run: |
|
||||
cd tests
|
||||
BASE_URL=${{ steps.get-url.outputs.service_url }} npx playwright test
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Deployment summary
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-results-staging-${{ github.sha }}-${{ github.run_number }}
|
||||
path: |
|
||||
tests/playwright-report/
|
||||
tests/test-results/
|
||||
retention-days: 30
|
||||
run: |
|
||||
echo "## 🚀 Staging Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Environment:** Staging" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Deployment Method:** Webhook-based" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Triggered by:** ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: screenshots-staging-${{ github.sha }}-${{ github.run_number }}
|
||||
path: tests/test-results/**/*.png
|
||||
retention-days: 30
|
||||
if [ "${{ github.event_name }}" = "workflow_run" ]; then
|
||||
echo "- **Type:** Auto-promotion from Development" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "- **Type:** Manual deployment" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
promote-to-master:
|
||||
needs: build-and-deploy
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/staging'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create Pull Request to Master
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
push-to-fork: false
|
||||
branch: auto-promote/staging-to-master-${{ github.sha }}
|
||||
base: master
|
||||
title: "🚀 Auto-promote: Deploy ${{ github.sha }} to production"
|
||||
body: |
|
||||
## 🚀 Auto-promotion from Staging
|
||||
|
||||
**Source**: `staging` branch
|
||||
**Commit**: ${{ github.sha }}
|
||||
**Triggered by**: @${{ github.actor }}
|
||||
|
||||
### ✅ Staging Tests Passed
|
||||
- Basic functionality tests
|
||||
- Gameplay mechanics tests
|
||||
- Visual regression tests
|
||||
- Environment validation tests
|
||||
|
||||
### 🎯 Changes in this promotion:
|
||||
${{ github.event.head_commit.message }}
|
||||
|
||||
This PR was automatically created after successful deployment and testing in the staging environment.
|
||||
|
||||
**Staging URL**: https://2048-staging.wa.darknex.us
|
||||
**Will deploy to**: https://2048.wa.darknex.us
|
||||
|
||||
⚠️ **Production Deployment** - Please review carefully before merging!
|
||||
labels: |
|
||||
auto-promotion
|
||||
production
|
||||
deploy
|
||||
needs-review
|
||||
|
||||
- name: Create Pull Request to Master
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
push-to-fork: false
|
||||
branch: auto-promote/staging-to-master-${{ github.sha }}
|
||||
base: master
|
||||
title: "🚀 Auto-promote: Deploy ${{ github.sha }} to production"
|
||||
body: |
|
||||
## 🚀 Auto-promotion from Staging
|
||||
|
||||
**Source**: `staging` branch
|
||||
**Commit**: ${{ github.sha }}
|
||||
**Triggered by**: @${{ github.actor }}
|
||||
|
||||
### ✅ Staging Tests Passed
|
||||
- Basic functionality tests
|
||||
- Gameplay mechanics tests
|
||||
- Visual regression tests
|
||||
- Environment validation tests
|
||||
- Performance tests
|
||||
|
||||
### 🎯 Changes in this promotion:
|
||||
${{ github.event.head_commit.message }}
|
||||
|
||||
This PR was automatically created after successful deployment and testing in the staging environment.
|
||||
|
||||
**Staging URL**: https://2048-staging.wa.darknex.us
|
||||
**Will deploy to**: https://2048.wa.darknex.us
|
||||
|
||||
⚠️ **Production Deployment** - Please review carefully before merging!
|
||||
labels: |
|
||||
auto-promotion
|
||||
production
|
||||
deploy
|
||||
needs-review
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
echo "- **Status:** ✅ Success" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **URL:** https://game-2048-staging.game-2048-staging.${{ secrets.STAGING_DOMAIN }}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "- **Status:** ❌ Failed" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
137
.github/workflows/promote-to-production.yml
vendored
Normal file
137
.github/workflows/promote-to-production.yml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
name: Promote to Production
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Deploy to Staging"]
|
||||
types:
|
||||
- completed
|
||||
branches: [ staging ]
|
||||
|
||||
jobs:
|
||||
test-staging-and-promote-to-main:
|
||||
name: Test Staging and Promote to Main
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: staging
|
||||
|
||||
- name: Wait for staging deployment to settle
|
||||
run: |
|
||||
echo "⏳ Waiting for staging deployment to fully settle..."
|
||||
sleep 120
|
||||
|
||||
- name: Run comprehensive staging tests
|
||||
run: |
|
||||
echo "🧪 Running comprehensive tests on staging environment..."
|
||||
|
||||
# Use the canonical Knative domain for staging
|
||||
CANONICAL_URL="https://game-2048-staging.game-2048-staging.${{ secrets.STAGING_DOMAIN }}"
|
||||
echo "Testing canonical staging domain: $CANONICAL_URL"
|
||||
|
||||
canonical_response=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 "$CANONICAL_URL")
|
||||
if [ "$canonical_response" != "200" ]; then
|
||||
echo "❌ Staging canonical domain returned HTTP $canonical_response"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Staging canonical domain accessible"
|
||||
|
||||
# Test staging content validation on canonical domain
|
||||
echo "Testing staging content validation..."
|
||||
content=$(curl -s -L --max-time 30 "$CANONICAL_URL")
|
||||
|
||||
if ! echo "$content" | grep -q "2048"; then
|
||||
echo "❌ Content missing 2048 title"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$content" | grep -q "HOW TO PLAY"; then
|
||||
echo "❌ Content missing game instructions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$content" | grep -q "style.css"; then
|
||||
echo "❌ CSS file not referenced"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$content" | grep -q "script.js"; then
|
||||
echo "❌ JavaScript file not referenced"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All staging content validation tests passed"
|
||||
|
||||
# Test staging performance on canonical domain
|
||||
echo "Testing staging performance..."
|
||||
response_time=$(curl -s -o /dev/null -w "%{time_total}" -L --max-time 30 "$CANONICAL_URL")
|
||||
if (( $(echo "$response_time > 10.0" | bc -l) )); then
|
||||
echo "❌ Response time too slow: ${response_time}s"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Staging performance test passed: ${response_time}s"
|
||||
|
||||
- name: Auto-promote staging to main branch
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
console.log('🎯 All staging tests passed! Auto-promoting staging to main branch...');
|
||||
|
||||
// Create a merge from staging to main
|
||||
try {
|
||||
const response = await github.rest.repos.merge({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
base: 'main',
|
||||
head: 'staging',
|
||||
commit_message: 'Auto-promote: Merge staging to main after successful staging tests - Deploy to Production'
|
||||
});
|
||||
|
||||
console.log('✅ Successfully merged staging to main branch');
|
||||
console.log('This will trigger production deployment automatically');
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.status === 409) {
|
||||
console.log('ℹ️ No new commits to merge - main is already up to date');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
- name: Create production promotion summary
|
||||
run: |
|
||||
echo "## 🎉 Production Promotion Summary (Staging → Main)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Stage | Status | Action |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|--------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Staging Tests | ✅ Passed | Comprehensive validation completed |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Main Branch | 🚀 Updated | Auto-promotion completed |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Production Deploy | ⏳ Triggered | Deployment will start automatically |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 📋 Tests Performed" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Staging canonical domain accessibility" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Staging custom domain accessibility" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Content and functionality validation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Performance testing" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🎮 Deployment Status" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Use canonical domain format (these are the Knative domains)
|
||||
DEV_URL="https://game-2048-dev.game-2048-dev.dev.wa.darknex.us"
|
||||
STAGING_URL="https://game-2048-staging.game-2048-staging.staging.wa.darknex.us"
|
||||
PROD_URL="https://game-2048-prod.game-2048-prod.wa.darknex.us"
|
||||
|
||||
echo "- **Development**: ✅ Live at $DEV_URL" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Staging**: ✅ Live at $STAGING_URL" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Production**: 🚀 Deploying to $PROD_URL" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### 🔗 Next Steps" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Production deployment will start automatically" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Monitor the production deployment workflow" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- All environments will be live with the latest code!" >> $GITHUB_STEP_SUMMARY
|
||||
28
.github/workflows/smoke-test.yml
vendored
28
.github/workflows/smoke-test.yml
vendored
@@ -43,15 +43,15 @@ jobs:
|
||||
run: |
|
||||
case "${{ matrix.environment }}" in
|
||||
dev)
|
||||
echo "CANONICAL_DOMAIN=game-2048-dev.game-2048-dev.dev.wa.darknex.us" >> $GITHUB_ENV
|
||||
echo "CANONICAL_DOMAIN=https://game-2048-dev.game-2048-dev.dev.wa.darknex.us" >> $GITHUB_ENV
|
||||
echo "ENV_NAME=development" >> $GITHUB_ENV
|
||||
;;
|
||||
staging)
|
||||
echo "CANONICAL_DOMAIN=game-2048-staging.game-2048-staging.staging.wa.darknex.us" >> $GITHUB_ENV
|
||||
echo "CANONICAL_DOMAIN=https://game-2048-staging.game-2048-staging.staging.wa.darknex.us" >> $GITHUB_ENV
|
||||
echo "ENV_NAME=staging" >> $GITHUB_ENV
|
||||
;;
|
||||
prod)
|
||||
echo "CANONICAL_DOMAIN=game-2048-prod.game-2048-prod.wa.darknex.us" >> $GITHUB_ENV
|
||||
echo "CANONICAL_DOMAIN=https://game-2048-prod.game-2048-prod.wa.darknex.us" >> $GITHUB_ENV
|
||||
echo "ENV_NAME=production" >> $GITHUB_ENV
|
||||
;;
|
||||
esac
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
echo "🎯 Testing canonical Knative domain: ${{ env.CANONICAL_DOMAIN }}"
|
||||
|
||||
# Test HTTPS access to canonical domain
|
||||
response_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/)
|
||||
response_code=$(curl -s -o /dev/null -w "%{http_code}" -L --max-time 30 "${{ env.CANONICAL_DOMAIN }}")
|
||||
echo "Canonical domain HTTP response code: $response_code"
|
||||
|
||||
if [ "$response_code" != "200" ]; then
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
echo "📄 Testing content validation on canonical domain: ${{ env.CANONICAL_DOMAIN }}"
|
||||
|
||||
# Download the page content from canonical domain
|
||||
content=$(curl -s -L --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/)
|
||||
content=$(curl -s -L --max-time 30 "${{ env.CANONICAL_DOMAIN }}")
|
||||
|
||||
# Check if it contains expected 2048 game elements
|
||||
if echo "$content" | grep -q "2048"; then
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
echo "⚡ Testing performance for canonical domain: ${{ env.CANONICAL_DOMAIN }}"
|
||||
|
||||
# Measure response time on canonical domain
|
||||
response_time=$(curl -s -o /dev/null -w "%{time_total}" -L --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/)
|
||||
response_time=$(curl -s -o /dev/null -w "%{time_total}" -L --max-time 30 "${{ env.CANONICAL_DOMAIN }}")
|
||||
echo "Canonical domain response time: ${response_time}s"
|
||||
|
||||
# Check if response time is reasonable (under 10 seconds)
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
fi
|
||||
|
||||
# Check content size
|
||||
content_size=$(curl -s -L --max-time 30 https://${{ env.CANONICAL_DOMAIN }}/ | wc -c)
|
||||
content_size=$(curl -s -L --max-time 30 "${{ env.CANONICAL_DOMAIN }}" | wc -c)
|
||||
echo "Content size: $content_size bytes"
|
||||
|
||||
if [ $content_size -gt 1000 ]; then
|
||||
@@ -154,6 +154,7 @@ jobs:
|
||||
run: |
|
||||
echo "🌐 Testing canonical domain DNS resolution"
|
||||
|
||||
# Canonical domains (Knative domains only)
|
||||
canonical_domains=(
|
||||
"game-2048-dev.game-2048-dev.dev.wa.darknex.us"
|
||||
"game-2048-staging.game-2048-staging.staging.wa.darknex.us"
|
||||
@@ -175,6 +176,7 @@ jobs:
|
||||
run: |
|
||||
echo "🔐 Testing SSL certificate chains for canonical domains"
|
||||
|
||||
# Canonical domains (Knative domains only)
|
||||
canonical_domains=(
|
||||
"game-2048-dev.game-2048-dev.dev.wa.darknex.us"
|
||||
"game-2048-staging.game-2048-staging.staging.wa.darknex.us"
|
||||
@@ -226,6 +228,12 @@ jobs:
|
||||
echo "| Canonical Domain Tests | ${{ needs.test-canonical-domains.result == 'success' && '✅ Passed' || '❌ Failed' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Tested Canonical Domains" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🧪 Development: https://game-2048-dev.game-2048-dev.dev.wa.darknex.us" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🎭 Staging: https://game-2048-staging.game-2048-staging.staging.wa.darknex.us" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🚀 Production: https://game-2048-prod.game-2048-prod.wa.darknex.us" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Use canonical domain format
|
||||
DEV_URL="https://game-2048-dev.game-2048-dev.dev.wa.darknex.us"
|
||||
STAGING_URL="https://game-2048-staging.game-2048-staging.staging.wa.darknex.us"
|
||||
PROD_URL="https://game-2048-prod.game-2048-prod.wa.darknex.us"
|
||||
|
||||
echo "- 🧪 Development: $DEV_URL" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🎭 Staging: $STAGING_URL" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- 🚀 Production: $PROD_URL" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -28,9 +28,23 @@ Thumbs.db
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.development
|
||||
.env.staging
|
||||
.env.production
|
||||
webhook_secret.txt
|
||||
|
||||
# Test files with PII
|
||||
test-signature.py
|
||||
test-webhook.sh
|
||||
|
||||
# Personal deployment files
|
||||
manifests/personal/
|
||||
config/personal/
|
||||
|
||||
# Backup files with potentially sensitive data
|
||||
*.backup
|
||||
*.bak
|
||||
backup-*
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
||||
34
DEPLOYMENT_TEST.md
Normal file
34
DEPLOYMENT_TEST.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Deployment Pipeline Test
|
||||
|
||||
## Current Status: ✅ READY
|
||||
|
||||
This file was created to test the deployment pipeline. All environment variables are properly configured, and the system is ready for end-to-end testing.
|
||||
|
||||
### Test Timestamp
|
||||
Generated on: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
||||
|
||||
### Repository Secrets Required
|
||||
|
||||
The following secrets must be configured in your GitHub repository:
|
||||
|
||||
1. **WEBHOOK_SECRET** - Secret for webhook authentication
|
||||
2. **DEV_WEBHOOK_URL** - Development webhook endpoint
|
||||
3. **STAGING_WEBHOOK_URL** - Staging webhook endpoint
|
||||
4. **PROD_WEBHOOK_URL** - Production webhook endpoint
|
||||
5. **KNATIVE_DOMAIN** - Your Knative domain (e.g., `dev.wa.darknex.us`)
|
||||
|
||||
### Testing the Pipeline
|
||||
|
||||
1. Push changes to `main` branch → triggers dev deployment
|
||||
2. Push changes to `develop` branch → triggers dev deployment + auto-promotion to staging
|
||||
3. Merge staging to main → triggers production deployment
|
||||
|
||||
### Current Environment State
|
||||
|
||||
- Webhook handler: ✅ Running and healthy
|
||||
- GHCR secrets: ✅ Configured in all namespaces
|
||||
- Git state: ✅ All changes pushed to main
|
||||
- Manifests: ✅ All configured with environment variables
|
||||
- Documentation: ✅ Updated with .env instructions
|
||||
|
||||
Ready for end-to-end testing!
|
||||
45
Dockerfile.webhook
Normal file
45
Dockerfile.webhook
Normal file
@@ -0,0 +1,45 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
gnupg2 \
|
||||
software-properties-common \
|
||||
apt-transport-https \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install kubectl
|
||||
RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
|
||||
&& chmod +x kubectl \
|
||||
&& mv kubectl /usr/local/bin/
|
||||
|
||||
# Install Docker CLI
|
||||
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
|
||||
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bullseye stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y docker-ce-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir flask requests
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy webhook handler script
|
||||
COPY webhook-handler.py /app/
|
||||
|
||||
# Create manifests directory
|
||||
RUN mkdir -p /app/manifests
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "webhook-handler.py"]
|
||||
103
docs/ENVIRONMENT.md
Normal file
103
docs/ENVIRONMENT.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Environment Configuration
|
||||
|
||||
This repository uses environment variables to keep personal information (domains, emails, repository names) out of the public codebase.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
1. **Copy the environment template:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. **Edit `.env` with your information:**
|
||||
```bash
|
||||
nano .env
|
||||
```
|
||||
|
||||
3. **Update these key values:**
|
||||
- `BASE_DOMAIN` - Your domain (e.g., `example.com`)
|
||||
- `GITHUB_REPOSITORY` - Your GitHub repo (e.g., `username/k8s-game-2048`)
|
||||
- `CERT_EMAIL` - Your email for SSL certificates
|
||||
- `WEBHOOK_SECRET` - Generate with: `openssl rand -hex 32`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required Variables
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `BASE_DOMAIN` | Your base domain | `example.com` |
|
||||
| `WEBHOOK_DOMAIN` | Webhook endpoint domain | `webhook.example.com` |
|
||||
| `GITHUB_REPOSITORY` | Your GitHub repository | `username/k8s-game-2048` |
|
||||
| `CERT_EMAIL` | Email for SSL certificates | `admin@example.com` |
|
||||
| `WEBHOOK_SECRET` | Secret for webhook security | Generated 64-char hex |
|
||||
|
||||
### Auto-generated Domains
|
||||
|
||||
The following domains are automatically generated from `BASE_DOMAIN`:
|
||||
|
||||
- **Development**: `2048-dev.{BASE_DOMAIN}`
|
||||
- **Staging**: `2048-staging.{BASE_DOMAIN}`
|
||||
- **Production**: `2048.{BASE_DOMAIN}`
|
||||
|
||||
### Canonical Knative Domains
|
||||
|
||||
- **Dev**: `game-2048-dev.game-2048-dev.dev.{BASE_DOMAIN}`
|
||||
- **Staging**: `game-2048-staging.game-2048-staging.staging.{BASE_DOMAIN}`
|
||||
- **Production**: `game-2048-prod.game-2048-prod.{BASE_DOMAIN}`
|
||||
|
||||
## Security
|
||||
|
||||
- **Never commit `.env`** - It's in `.gitignore` for security
|
||||
- **Use strong webhook secrets** - Generate with `openssl rand -hex 32`
|
||||
- **Rotate secrets regularly** - Update webhook secret periodically
|
||||
|
||||
## Deployment Scripts
|
||||
|
||||
### Setup Webhook Handler
|
||||
```bash
|
||||
./scripts/setup-webhook-deployment.sh
|
||||
```
|
||||
|
||||
### Prepare Environment-Specific Manifests
|
||||
```bash
|
||||
./scripts/prepare-deployment.sh
|
||||
```
|
||||
|
||||
### Sanitize Repository (for public sharing)
|
||||
```bash
|
||||
./scripts/sanitize-repo.sh
|
||||
```
|
||||
|
||||
## GitHub Secrets
|
||||
|
||||
After setting up your `.env`, configure these GitHub repository secrets:
|
||||
|
||||
1. Go to your repository Settings → Secrets and variables → Actions
|
||||
2. Add these secrets from your `.env` file:
|
||||
|
||||
```
|
||||
WEBHOOK_SECRET=<from .env>
|
||||
DEV_WEBHOOK_URL=https://<WEBHOOK_DOMAIN>/webhook/deploy
|
||||
STAGING_WEBHOOK_URL=https://<WEBHOOK_DOMAIN>/webhook/deploy
|
||||
PROD_WEBHOOK_URL=https://<WEBHOOK_DOMAIN>/webhook/deploy
|
||||
KNATIVE_DOMAIN=<BASE_DOMAIN>
|
||||
```
|
||||
|
||||
## Template System
|
||||
|
||||
The repository uses a template system to keep personal information secure:
|
||||
|
||||
- **`manifests/templates/`** - Sanitized templates with placeholders
|
||||
- **`manifests/`** - Your actual deployment manifests (gitignored)
|
||||
- **`.env.example`** - Template for environment configuration
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Clone repository
|
||||
2. Copy `.env.example` to `.env`
|
||||
3. Update `.env` with your configuration
|
||||
4. Run `./scripts/prepare-deployment.sh`
|
||||
5. Deploy with `./scripts/setup-webhook-deployment.sh`
|
||||
|
||||
This ensures your personal information stays private while keeping the codebase shareable.
|
||||
249
docs/WEBHOOK_DEPLOYMENT.md
Normal file
249
docs/WEBHOOK_DEPLOYMENT.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Webhook-Based Deployment Guide
|
||||
|
||||
This guide explains how to set up the webhook-based deployment system for the k8s-game-2048 application, designed to work with k3s clusters behind NAT (no direct API access).
|
||||
|
||||
## Overview
|
||||
|
||||
The deployment pipeline uses secure webhooks instead of direct kubectl/SSH access, making it perfect for k3s clusters behind NAT or firewall restrictions. Each environment (dev, staging, prod) has its own webhook endpoint that receives deployment instructions and applies them locally.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
GitHub Actions → HTTPS Webhook → Local Webhook Handler → kubectl apply
|
||||
```
|
||||
|
||||
### Deployment Flow
|
||||
|
||||
1. **Development**: Triggered on push to `main`/`master`
|
||||
2. **Staging**: Auto-promoted from successful dev deployment
|
||||
3. **Production**: Auto-promoted from successful staging OR manual deployment with confirmation
|
||||
|
||||
## Required Secrets
|
||||
|
||||
Configure these secrets in your GitHub repository settings:
|
||||
|
||||
### GitHub Container Registry
|
||||
- `GITHUB_TOKEN` - Automatically provided by GitHub Actions
|
||||
|
||||
### Webhook Endpoints
|
||||
- `DEV_WEBHOOK_URL` - Your development webhook endpoint
|
||||
- `STAGING_WEBHOOK_URL` - Your staging webhook endpoint
|
||||
- `PROD_WEBHOOK_URL` - Your production webhook endpoint
|
||||
|
||||
### Security
|
||||
- `WEBHOOK_SECRET` - Shared secret for HMAC signature verification
|
||||
- `KNATIVE_DOMAIN` - Your Knative cluster domain (e.g., `staging.wa.darknex.us`)
|
||||
|
||||
## Webhook Handler Implementation
|
||||
|
||||
You need to implement webhook handlers on your k3s cluster that:
|
||||
|
||||
1. **Receive** webhook POST requests with deployment details
|
||||
2. **Verify** HMAC signatures for security
|
||||
3. **Pull** the specified Docker image
|
||||
4. **Apply** Kubernetes manifests
|
||||
5. **Return** deployment status
|
||||
|
||||
### Example Webhook Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"environment": "development",
|
||||
"image": "ghcr.io/owner/repo:tag",
|
||||
"namespace": "game-2048-dev",
|
||||
"service_name": "game-2048-dev",
|
||||
"deployment_id": "123456-1",
|
||||
"commit_sha": "abc123...",
|
||||
"triggered_by": "username",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"auto_promotion": false,
|
||||
"deployment_strategy": "rolling" // or "blue-green" for prod
|
||||
}
|
||||
```
|
||||
|
||||
### Security Headers
|
||||
|
||||
The webhook includes these security headers:
|
||||
- `X-Signature-SHA256`: HMAC-SHA256 signature of the payload
|
||||
- `X-GitHub-Event`: Always "deployment"
|
||||
- `X-GitHub-Delivery`: Unique delivery ID
|
||||
|
||||
### Sample Webhook Handler (Python Flask)
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import subprocess
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
WEBHOOK_SECRET = "your-webhook-secret"
|
||||
|
||||
def verify_signature(payload, signature):
|
||||
expected = hmac.new(
|
||||
WEBHOOK_SECRET.encode(),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(f"sha256={expected}", signature)
|
||||
|
||||
@app.route('/webhook/deploy', methods=['POST'])
|
||||
def deploy():
|
||||
# Verify signature
|
||||
signature = request.headers.get('X-Signature-SHA256')
|
||||
if not verify_signature(request.data, signature):
|
||||
return jsonify({"error": "Invalid signature"}), 401
|
||||
|
||||
data = request.json
|
||||
image = data['image']
|
||||
namespace = data['namespace']
|
||||
|
||||
try:
|
||||
# Pull image
|
||||
subprocess.run(['docker', 'pull', image], check=True)
|
||||
|
||||
# Apply manifests
|
||||
subprocess.run([
|
||||
'kubectl', 'apply', '-f', f'manifests/{data["environment"]}/'
|
||||
], check=True)
|
||||
|
||||
# Update image
|
||||
subprocess.run([
|
||||
'kubectl', 'patch', 'ksvc', data['service_name'],
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', f'{{"spec":{{"template":{{"spec":{{"containers":[{{"image":"{image}","imagePullPolicy":"Always"}}]}}}}}}}}'
|
||||
], check=True)
|
||||
|
||||
return jsonify({"status": "success", "deployment_id": data['deployment_id']})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8080)
|
||||
```
|
||||
|
||||
## Deployment Strategies
|
||||
|
||||
### Development & Staging
|
||||
- **Strategy**: Rolling update
|
||||
- **Traffic**: Immediate 100% switch
|
||||
- **Verification**: Health check after 30-45 seconds
|
||||
|
||||
### Production
|
||||
- **Strategy**: Blue-Green deployment
|
||||
- **Traffic Split**: 10% → 50% → 100% over 5 minutes
|
||||
- **Verification**: Extended health checks and response time validation
|
||||
|
||||
## Health Checks
|
||||
|
||||
All environments use canonical Knative domains for health checks:
|
||||
- **Dev**: `https://game-2048-dev.game-2048-dev.{KNATIVE_DOMAIN}`
|
||||
- **Staging**: `https://game-2048-staging.game-2048-staging.{KNATIVE_DOMAIN}`
|
||||
- **Prod**: `https://game-2048-prod.game-2048-prod.{KNATIVE_DOMAIN}`
|
||||
|
||||
## Auto-Promotion Pipeline
|
||||
|
||||
```
|
||||
Push to main → Dev Deployment → Staging Deployment → Production (manual/auto)
|
||||
```
|
||||
|
||||
### Triggers
|
||||
- **Dev**: Automatic on code changes
|
||||
- **Staging**: Automatic on successful dev deployment
|
||||
- **Prod**: Automatic on successful staging deployment OR manual with confirmation
|
||||
|
||||
## Manual Deployment
|
||||
|
||||
### Staging
|
||||
```bash
|
||||
# Trigger staging deployment manually
|
||||
gh workflow run deploy-staging.yml -f image_tag=v1.2.3
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
# Trigger production deployment (requires confirmation)
|
||||
gh workflow run deploy-prod.yml -f image_tag=v1.2.3 -f confirmation=DEPLOY
|
||||
```
|
||||
|
||||
## Monitoring & Debugging
|
||||
|
||||
### GitHub Actions Logs
|
||||
- View deployment progress in Actions tab
|
||||
- Check webhook response codes and payloads
|
||||
- Monitor health check results
|
||||
|
||||
### Cluster-Side Debugging
|
||||
```bash
|
||||
# Check webhook handler logs
|
||||
kubectl logs -n webhook-system deployment/webhook-handler
|
||||
|
||||
# Check service status
|
||||
kubectl get ksvc -n game-2048-dev
|
||||
|
||||
# Check recent deployments
|
||||
kubectl get revisions -n game-2048-dev
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **HMAC Verification**: All webhooks are signed with SHA-256 HMAC
|
||||
2. **HTTPS Only**: All webhook endpoints must use HTTPS
|
||||
3. **Secret Rotation**: Regularly rotate the `WEBHOOK_SECRET`
|
||||
4. **Network Security**: Consider IP allowlisting for webhook endpoints
|
||||
5. **Audit Logging**: Log all deployment requests with timestamps and users
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Webhook Timeout
|
||||
- **Symptom**: HTTP 408 or connection timeout
|
||||
- **Solution**: Check webhook handler is running and accessible
|
||||
- **Debug**: Test webhook endpoint manually with curl
|
||||
|
||||
#### Signature Verification Failed
|
||||
- **Symptom**: HTTP 401 from webhook
|
||||
- **Solution**: Verify `WEBHOOK_SECRET` matches on both sides
|
||||
- **Debug**: Check HMAC calculation in webhook handler
|
||||
|
||||
#### Image Pull Errors
|
||||
- **Symptom**: Deployment fails after webhook success
|
||||
- **Solution**: Ensure image exists and registry credentials are configured
|
||||
- **Debug**: Check `kubectl get events` in the target namespace
|
||||
|
||||
#### Health Check Failures
|
||||
- **Symptom**: Deployment marked as failed despite successful webhook
|
||||
- **Solution**: Verify Knative domain configuration and service startup time
|
||||
- **Debug**: Check service logs and Knative serving controller logs
|
||||
|
||||
### Manual Recovery
|
||||
|
||||
If automated deployment fails, you can deploy manually:
|
||||
|
||||
```bash
|
||||
# Set image and apply manifests
|
||||
kubectl patch ksvc game-2048-dev -n game-2048-dev \
|
||||
--type merge \
|
||||
-p '{"spec":{"template":{"spec":{"containers":[{"image":"ghcr.io/owner/repo:tag","imagePullPolicy":"Always"}]}}}}'
|
||||
```
|
||||
|
||||
## Benefits of Webhook-Based Deployment
|
||||
|
||||
1. **NAT-Friendly**: Works with k3s clusters behind NAT/firewall
|
||||
2. **Secure**: HMAC-signed webhooks prevent unauthorized deployments
|
||||
3. **Scalable**: Can handle multiple clusters and environments
|
||||
4. **Auditable**: Full deployment history in GitHub Actions
|
||||
5. **Flexible**: Supports various deployment strategies
|
||||
6. **Reliable**: Retry logic and health checks ensure successful deployments
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Implement webhook handlers for each environment
|
||||
2. Configure webhook endpoints and secrets
|
||||
3. Test the deployment pipeline end-to-end
|
||||
4. Set up monitoring and alerting for webhook handlers
|
||||
5. Document environment-specific configuration
|
||||
215
manifests/webhook/webhook-handler.yaml
Normal file
215
manifests/webhook/webhook-handler.yaml
Normal file
@@ -0,0 +1,215 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: webhook-system
|
||||
labels:
|
||||
name: webhook-system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: webhook-secret
|
||||
namespace: webhook-system
|
||||
type: Opaque
|
||||
stringData:
|
||||
webhook-secret: "CHANGE_ME_IN_PRODUCTION" # Replace with your actual webhook secret
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: webhook-handler-config
|
||||
namespace: webhook-system
|
||||
data:
|
||||
MANIFESTS_PATH: "/app/manifests"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: webhook-handler
|
||||
namespace: webhook-system
|
||||
labels:
|
||||
app: webhook-handler
|
||||
spec:
|
||||
replicas: 1 # Start with 1 for testing
|
||||
selector:
|
||||
matchLabels:
|
||||
app: webhook-handler
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: webhook-handler
|
||||
spec:
|
||||
serviceAccountName: webhook-handler
|
||||
initContainers:
|
||||
- name: setup
|
||||
image: python:3.11-slim
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
echo "🚀 Setting up webhook handler dependencies..."
|
||||
|
||||
# Update and install basic tools
|
||||
apt-get update
|
||||
apt-get install -y curl wget
|
||||
|
||||
# Install kubectl
|
||||
echo "📦 Installing kubectl..."
|
||||
curl -LO "https://dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl"
|
||||
chmod +x kubectl
|
||||
cp kubectl /shared/kubectl
|
||||
|
||||
# Install Python dependencies
|
||||
echo "📦 Installing Python dependencies..."
|
||||
pip install flask requests
|
||||
|
||||
# Copy requirements to shared volume
|
||||
pip freeze > /shared/requirements.txt
|
||||
|
||||
echo "✅ Setup completed!"
|
||||
volumeMounts:
|
||||
- name: shared-tools
|
||||
mountPath: /shared
|
||||
containers:
|
||||
- name: webhook-handler
|
||||
image: python:3.11-slim
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
env:
|
||||
- name: WEBHOOK_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: webhook-secret
|
||||
key: webhook-secret
|
||||
- name: MANIFESTS_PATH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: webhook-handler-config
|
||||
key: MANIFESTS_PATH
|
||||
- name: PATH
|
||||
value: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/shared"
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
echo "🎯 Starting webhook handler..."
|
||||
|
||||
# Install Python dependencies from init container
|
||||
if [ -f /shared/requirements.txt ]; then
|
||||
pip install -r /shared/requirements.txt
|
||||
else
|
||||
pip install flask requests
|
||||
fi
|
||||
|
||||
# Make kubectl available
|
||||
cp /shared/kubectl /usr/local/bin/ 2>/dev/null || echo "kubectl already available"
|
||||
chmod +x /usr/local/bin/kubectl 2>/dev/null || true
|
||||
|
||||
# Test connectivity (using in-cluster service account)
|
||||
echo "🔍 Testing Kubernetes connectivity..."
|
||||
kubectl version --client || echo "⚠️ kubectl client test failed"
|
||||
kubectl cluster-info || echo "⚠️ cluster connectivity test failed, but continuing..."
|
||||
|
||||
# Start the webhook handler
|
||||
echo "🚀 Starting Flask application..."
|
||||
cd /app
|
||||
exec python webhook-handler.py
|
||||
volumeMounts:
|
||||
- name: webhook-handler-script
|
||||
mountPath: /app/webhook-handler.py
|
||||
subPath: webhook-handler.py
|
||||
- name: manifests
|
||||
mountPath: /app/manifests
|
||||
- name: shared-tools
|
||||
mountPath: /shared
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 10
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
volumes:
|
||||
- name: webhook-handler-script
|
||||
configMap:
|
||||
name: webhook-handler-script
|
||||
defaultMode: 0755
|
||||
- name: manifests
|
||||
hostPath:
|
||||
path: /home/administrator/k8s-game-2048/manifests
|
||||
type: Directory
|
||||
- name: shared-tools
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: webhook-handler
|
||||
namespace: webhook-system
|
||||
labels:
|
||||
app: webhook-handler
|
||||
spec:
|
||||
selector:
|
||||
app: webhook-handler
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: webhook-handler
|
||||
namespace: webhook-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: webhook-handler
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces", "secrets", "configmaps", "services"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "replicasets"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["serving.knative.dev"]
|
||||
resources: ["services", "revisions"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["events", "pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: webhook-handler
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: webhook-handler
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: webhook-handler
|
||||
namespace: webhook-system
|
||||
43
manifests/webhook/webhook-ingress.yaml
Normal file
43
manifests/webhook/webhook-ingress.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: webhook-handler-external
|
||||
namespace: webhook-system
|
||||
labels:
|
||||
app: webhook-handler
|
||||
spec:
|
||||
selector:
|
||||
app: webhook-handler
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
type: LoadBalancer # Change to NodePort if LoadBalancer is not available
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: webhook-handler-ingress
|
||||
namespace: webhook-system
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod" # Adjust to your cert issuer
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- webhook.wa.darknex.us
|
||||
secretName: webhook-tls
|
||||
rules:
|
||||
- host: webhook.wa.darknex.us
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: webhook-handler
|
||||
port:
|
||||
number: 80
|
||||
288
manifests/webhook/webhook-script-configmap.yaml
Normal file
288
manifests/webhook/webhook-script-configmap.yaml
Normal file
@@ -0,0 +1,288 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: webhook-handler-script
|
||||
namespace: webhook-system
|
||||
data:
|
||||
webhook-handler.py: |
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Webhook deployment handler for k8s-game-2048
|
||||
Receives webhook requests from GitHub Actions and deploys to k3s cluster
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
# Configuration
|
||||
app = Flask(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'change-me-in-production')
|
||||
MANIFESTS_PATH = os.environ.get('MANIFESTS_PATH', '/app/manifests')
|
||||
|
||||
def verify_signature(payload, signature):
|
||||
"""Verify HMAC signature from GitHub webhook"""
|
||||
if not signature:
|
||||
return False
|
||||
|
||||
expected = hmac.new(
|
||||
WEBHOOK_SECRET.encode('utf-8'),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(f"sha256={expected}", signature)
|
||||
|
||||
def run_command(cmd, **kwargs):
|
||||
"""Run shell command with logging"""
|
||||
logger.info(f"Running command: {' '.join(cmd)}")
|
||||
try:
|
||||
result = subprocess.run(cmd, check=True, capture_output=True, text=True, **kwargs)
|
||||
logger.info(f"Command output: {result.stdout}")
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed: {e.stderr}")
|
||||
raise
|
||||
|
||||
def pull_image(image):
|
||||
"""Pull Docker image to ensure it's available"""
|
||||
logger.info(f"Pulling image: {image}")
|
||||
run_command(['docker', 'pull', image])
|
||||
|
||||
def apply_manifests(environment):
|
||||
"""Apply Kubernetes manifests for environment"""
|
||||
manifest_dir = f"{MANIFESTS_PATH}/{environment}"
|
||||
logger.info(f"Applying manifests from: {manifest_dir}")
|
||||
|
||||
if not os.path.exists(manifest_dir):
|
||||
raise FileNotFoundError(f"Manifest directory not found: {manifest_dir}")
|
||||
|
||||
run_command(['kubectl', 'apply', '-f', manifest_dir])
|
||||
|
||||
def update_service_image(service_name, namespace, image):
|
||||
"""Update Knative service with new image"""
|
||||
logger.info(f"Updating service {service_name} in namespace {namespace} with image {image}")
|
||||
|
||||
patch = {
|
||||
"spec": {
|
||||
"template": {
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"image": image,
|
||||
"imagePullPolicy": "Always"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run_command([
|
||||
'kubectl', 'patch', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', json.dumps(patch)
|
||||
])
|
||||
|
||||
def wait_for_service_ready(service_name, namespace, timeout=300):
|
||||
"""Wait for Knative service to be ready"""
|
||||
logger.info(f"Waiting for service {service_name} to be ready...")
|
||||
|
||||
run_command([
|
||||
'kubectl', 'wait', '--for=condition=Ready',
|
||||
f'ksvc/{service_name}',
|
||||
'-n', namespace,
|
||||
f'--timeout={timeout}s'
|
||||
])
|
||||
|
||||
def implement_blue_green_deployment(service_name, namespace, traffic_split):
|
||||
"""Implement blue-green deployment with gradual traffic shifting"""
|
||||
if not traffic_split:
|
||||
return
|
||||
|
||||
logger.info("Starting blue-green deployment...")
|
||||
|
||||
# Get the latest revision
|
||||
result = run_command([
|
||||
'kubectl', 'get', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'-o', 'jsonpath={.status.latestReadyRevisionName}'
|
||||
])
|
||||
latest_revision = result.stdout.strip()
|
||||
|
||||
if not latest_revision:
|
||||
logger.warning("No latest revision found, skipping traffic split")
|
||||
return
|
||||
|
||||
# Phase 1: Initial traffic (e.g., 10%)
|
||||
initial_percent = traffic_split.get('initial', 10)
|
||||
logger.info(f"Phase 1: Routing {initial_percent}% traffic to new revision")
|
||||
traffic_patch = {
|
||||
"spec": {
|
||||
"traffic": [
|
||||
{"revisionName": latest_revision, "percent": initial_percent},
|
||||
{"latestRevision": False, "percent": 100 - initial_percent}
|
||||
]
|
||||
}
|
||||
}
|
||||
run_command([
|
||||
'kubectl', 'patch', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', json.dumps(traffic_patch)
|
||||
])
|
||||
time.sleep(60) # Wait 1 minute
|
||||
|
||||
# Phase 2: Intermediate traffic (e.g., 50%)
|
||||
intermediate_percent = traffic_split.get('intermediate', 50)
|
||||
logger.info(f"Phase 2: Routing {intermediate_percent}% traffic to new revision")
|
||||
traffic_patch["spec"]["traffic"] = [
|
||||
{"revisionName": latest_revision, "percent": intermediate_percent},
|
||||
{"latestRevision": False, "percent": 100 - intermediate_percent}
|
||||
]
|
||||
run_command([
|
||||
'kubectl', 'patch', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', json.dumps(traffic_patch)
|
||||
])
|
||||
time.sleep(60) # Wait 1 minute
|
||||
|
||||
# Phase 3: Full traffic (100%)
|
||||
logger.info("Phase 3: Routing 100% traffic to new revision")
|
||||
traffic_patch["spec"]["traffic"] = [
|
||||
{"latestRevision": True, "percent": 100}
|
||||
]
|
||||
run_command([
|
||||
'kubectl', 'patch', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', json.dumps(traffic_patch)
|
||||
])
|
||||
|
||||
@app.route('/webhook/deploy', methods=['POST'])
|
||||
def deploy():
|
||||
"""Main webhook endpoint for deployments"""
|
||||
try:
|
||||
# Verify signature
|
||||
signature = request.headers.get('X-Signature-SHA256')
|
||||
if not verify_signature(request.data, signature):
|
||||
logger.warning("Invalid webhook signature")
|
||||
return jsonify({"error": "Invalid signature"}), 401
|
||||
|
||||
# Parse payload
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({"error": "No JSON payload"}), 400
|
||||
|
||||
# Extract deployment details
|
||||
environment = data.get('environment')
|
||||
image = data.get('image')
|
||||
namespace = data.get('namespace')
|
||||
service_name = data.get('service_name')
|
||||
deployment_id = data.get('deployment_id')
|
||||
deployment_strategy = data.get('deployment_strategy', 'rolling')
|
||||
traffic_split = data.get('traffic_split')
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ['environment', 'image', 'namespace', 'service_name']
|
||||
missing_fields = [field for field in required_fields if not data.get(field)]
|
||||
if missing_fields:
|
||||
return jsonify({"error": f"Missing required fields: {missing_fields}"}), 400
|
||||
|
||||
logger.info(f"Starting deployment {deployment_id}")
|
||||
logger.info(f"Environment: {environment}")
|
||||
logger.info(f"Image: {image}")
|
||||
logger.info(f"Namespace: {namespace}")
|
||||
logger.info(f"Service: {service_name}")
|
||||
logger.info(f"Strategy: {deployment_strategy}")
|
||||
|
||||
# Step 1: Pull the Docker image
|
||||
pull_image(image)
|
||||
|
||||
# Step 2: Apply manifests
|
||||
apply_manifests(environment)
|
||||
|
||||
# Step 3: Update service image
|
||||
update_service_image(service_name, namespace, image)
|
||||
|
||||
# Step 4: Wait for service to be ready
|
||||
wait_for_service_ready(service_name, namespace)
|
||||
|
||||
# Step 5: Apply deployment strategy
|
||||
if deployment_strategy == 'blue-green' and traffic_split:
|
||||
implement_blue_green_deployment(service_name, namespace, traffic_split)
|
||||
|
||||
logger.info(f"Deployment {deployment_id} completed successfully")
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"deployment_id": deployment_id,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"environment": environment,
|
||||
"image": image,
|
||||
"strategy": deployment_strategy
|
||||
})
|
||||
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"File not found: {e}")
|
||||
return jsonify({"error": str(e)}), 404
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed: {e}")
|
||||
return jsonify({"error": f"Command failed: {e.stderr}"}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint"""
|
||||
return jsonify({
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"version": "1.0.0"
|
||||
})
|
||||
|
||||
@app.route('/status', methods=['GET'])
|
||||
def status():
|
||||
"""Status endpoint with cluster information"""
|
||||
try:
|
||||
# Get cluster info
|
||||
result = run_command(['kubectl', 'cluster-info'])
|
||||
cluster_info = result.stdout
|
||||
|
||||
# Get webhook handler pod info
|
||||
result = run_command(['kubectl', 'get', 'pods', '-n', 'webhook-system', '--selector=app=webhook-handler'])
|
||||
pod_info = result.stdout
|
||||
|
||||
return jsonify({
|
||||
"status": "operational",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"cluster_info": cluster_info,
|
||||
"pod_info": pod_info
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Verify environment
|
||||
logger.info("Starting webhook deployment handler...")
|
||||
logger.info(f"Webhook secret configured: {'Yes' if WEBHOOK_SECRET != 'change-me-in-production' else 'No (using default)'}")
|
||||
logger.info(f"Manifests path: {MANIFESTS_PATH}")
|
||||
|
||||
# Start the Flask app
|
||||
app.run(host='0.0.0.0', port=8080, debug=False)
|
||||
77
scripts/prepare-deployment.sh
Executable file
77
scripts/prepare-deployment.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Environment-aware deployment script
|
||||
echo "🚀 Environment-aware deployment script..."
|
||||
|
||||
# Load environment variables
|
||||
if [ -f ".env" ]; then
|
||||
echo "📋 Loading configuration from .env file..."
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
else
|
||||
echo "❌ No .env file found! Please create one from .env.example"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate required environment variables
|
||||
required_vars=(
|
||||
"BASE_DOMAIN"
|
||||
"WEBHOOK_DOMAIN"
|
||||
"KNATIVE_DOMAIN"
|
||||
"GITHUB_REPOSITORY"
|
||||
"CONTAINER_REGISTRY"
|
||||
)
|
||||
|
||||
for var in "${required_vars[@]}"; do
|
||||
if [ -z "${!var}" ]; then
|
||||
echo "❌ Required environment variable $var is not set!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Environment validation passed"
|
||||
echo " Base Domain: $BASE_DOMAIN"
|
||||
echo " Webhook Domain: $WEBHOOK_DOMAIN"
|
||||
echo " GitHub Repository: $GITHUB_REPOSITORY"
|
||||
|
||||
# Function to substitute environment variables in manifests
|
||||
substitute_env_vars() {
|
||||
local source_dir="$1"
|
||||
local target_dir="$2"
|
||||
|
||||
echo "📝 Substituting environment variables: $source_dir -> $target_dir"
|
||||
|
||||
# Create target directory
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
# Process all YAML files
|
||||
for file in "$source_dir"/*.yml "$source_dir"/*.yaml; do
|
||||
if [ -f "$file" ]; then
|
||||
local basename=$(basename "$file")
|
||||
local target_file="$target_dir/$basename"
|
||||
|
||||
# Substitute environment variables
|
||||
envsubst < "$file" > "$target_file"
|
||||
echo " ✅ $basename"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Create deployment-ready manifests from templates
|
||||
if [ -d "manifests/templates" ]; then
|
||||
echo "🔄 Creating deployment manifests from templates..."
|
||||
|
||||
substitute_env_vars "manifests/templates/dev" "manifests/dev"
|
||||
substitute_env_vars "manifests/templates/staging" "manifests/staging"
|
||||
substitute_env_vars "manifests/templates/prod" "manifests/prod"
|
||||
substitute_env_vars "manifests/templates" "manifests"
|
||||
|
||||
echo "✅ Deployment manifests ready"
|
||||
else
|
||||
echo "⚠️ No templates directory found, using existing manifests"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎯 Ready for deployment with your environment configuration!"
|
||||
echo " Run: kubectl apply -f manifests/dev/"
|
||||
echo " Or use: ./scripts/setup-webhook-deployment.sh"
|
||||
107
scripts/sanitize-repo.sh
Executable file
107
scripts/sanitize-repo.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Script to sanitize repository by replacing hardcoded values with placeholders
|
||||
echo "🧹 Sanitizing repository - removing hardcoded personal information..."
|
||||
|
||||
# Load environment variables to know what to replace
|
||||
if [ -f ".env" ]; then
|
||||
source .env
|
||||
else
|
||||
echo "❌ No .env file found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to replace in file if it exists
|
||||
replace_in_file() {
|
||||
local file="$1"
|
||||
local search="$2"
|
||||
local replace="$3"
|
||||
|
||||
if [ -f "$file" ]; then
|
||||
sed -i "s|${search}|${replace}|g" "$file"
|
||||
echo "✅ Updated $file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Replace domain names in all relevant files
|
||||
echo "📝 Replacing domain names with placeholders..."
|
||||
|
||||
# README.md
|
||||
replace_in_file "README.md" "$DEV_DOMAIN" "2048-dev.example.com"
|
||||
replace_in_file "README.md" "$STAGING_DOMAIN" "2048-staging.example.com"
|
||||
replace_in_file "README.md" "$PROD_DOMAIN" "2048.example.com"
|
||||
replace_in_file "README.md" "$GITHUB_REPOSITORY" "your-username/k8s-game-2048"
|
||||
|
||||
# GitHub workflows - replace all hardcoded domains
|
||||
for workflow in .github/workflows/*.yml; do
|
||||
if [ -f "$workflow" ]; then
|
||||
replace_in_file "$workflow" "$DEV_CANONICAL_DOMAIN" "game-2048-dev.game-2048-dev.dev.example.com"
|
||||
replace_in_file "$workflow" "$STAGING_CANONICAL_DOMAIN" "game-2048-staging.game-2048-staging.staging.example.com"
|
||||
replace_in_file "$workflow" "$PROD_CANONICAL_DOMAIN" "game-2048-prod.game-2048-prod.example.com"
|
||||
replace_in_file "$workflow" "$DEV_DOMAIN" "2048-dev.example.com"
|
||||
replace_in_file "$workflow" "$STAGING_DOMAIN" "2048-staging.example.com"
|
||||
replace_in_file "$workflow" "$PROD_DOMAIN" "2048.example.com"
|
||||
replace_in_file "$workflow" "$GITHUB_REPOSITORY" "your-username/k8s-game-2048"
|
||||
fi
|
||||
done
|
||||
|
||||
# Scripts
|
||||
for script in scripts/*.sh; do
|
||||
if [ -f "$script" ]; then
|
||||
replace_in_file "$script" "$DEV_DOMAIN" "2048-dev.example.com"
|
||||
replace_in_file "$script" "$STAGING_DOMAIN" "2048-staging.example.com"
|
||||
replace_in_file "$script" "$PROD_DOMAIN" "2048.example.com"
|
||||
replace_in_file "$script" "$DEV_CANONICAL_DOMAIN" "game-2048-dev.game-2048-dev.dev.example.com"
|
||||
replace_in_file "$script" "$STAGING_CANONICAL_DOMAIN" "game-2048-staging.game-2048-staging.staging.example.com"
|
||||
replace_in_file "$script" "$PROD_CANONICAL_DOMAIN" "game-2048-prod.game-2048-prod.example.com"
|
||||
replace_in_file "$script" "$KNATIVE_DOMAIN" "example.com"
|
||||
replace_in_file "$script" "$WEBHOOK_DOMAIN" "webhook.example.com"
|
||||
replace_in_file "$script" "$GITHUB_REPOSITORY" "your-username/k8s-game-2048"
|
||||
fi
|
||||
done
|
||||
|
||||
# Manifests - create template versions
|
||||
echo "📂 Creating template manifests..."
|
||||
mkdir -p manifests/templates
|
||||
|
||||
# Copy current manifests to templates and sanitize
|
||||
cp -r manifests/dev manifests/templates/
|
||||
cp -r manifests/staging manifests/templates/
|
||||
cp -r manifests/prod manifests/templates/
|
||||
cp manifests/*.yaml manifests/templates/ 2>/dev/null || true
|
||||
|
||||
# Sanitize template manifests
|
||||
for file in manifests/templates/**/*.yml manifests/templates/**/*.yaml manifests/templates/*.yaml; do
|
||||
if [ -f "$file" ]; then
|
||||
replace_in_file "$file" "$DEV_DOMAIN" "2048-dev.example.com"
|
||||
replace_in_file "$file" "$STAGING_DOMAIN" "2048-staging.example.com"
|
||||
replace_in_file "$file" "$PROD_DOMAIN" "2048.example.com"
|
||||
replace_in_file "$file" "$DEV_CANONICAL_DOMAIN" "game-2048-dev.game-2048-dev.dev.example.com"
|
||||
replace_in_file "$file" "$STAGING_CANONICAL_DOMAIN" "game-2048-staging.game-2048-staging.staging.example.com"
|
||||
replace_in_file "$file" "$PROD_CANONICAL_DOMAIN" "game-2048-prod.game-2048-prod.example.com"
|
||||
replace_in_file "$file" "dev.$KNATIVE_DOMAIN" "dev.example.com"
|
||||
replace_in_file "$file" "staging.$KNATIVE_DOMAIN" "staging.example.com"
|
||||
replace_in_file "$file" "$KNATIVE_DOMAIN" "example.com"
|
||||
replace_in_file "$file" "$GITHUB_REPOSITORY" "your-username/k8s-game-2048"
|
||||
replace_in_file "$file" "$CERT_EMAIL" "admin@example.com"
|
||||
fi
|
||||
done
|
||||
|
||||
# Package.json
|
||||
replace_in_file "package.json" "$GITHUB_REPOSITORY" "your-username/k8s-game-2048"
|
||||
|
||||
# Documentation
|
||||
replace_in_file "docs/WEBHOOK_DEPLOYMENT.md" "$KNATIVE_DOMAIN" "example.com"
|
||||
|
||||
echo ""
|
||||
echo "✅ Repository sanitization completed!"
|
||||
echo ""
|
||||
echo "📋 Summary of changes:"
|
||||
echo "- Replaced all domain references with example.com"
|
||||
echo "- Replaced GitHub repository with placeholder"
|
||||
echo "- Created template manifests in manifests/templates/"
|
||||
echo "- Personal information is now only in .env file (which is .gitignored)"
|
||||
echo ""
|
||||
echo "⚠️ Note: Current manifests still contain your actual domains for deployment"
|
||||
echo " Template manifests are sanitized for public repository"
|
||||
153
scripts/setup-webhook-deployment.sh
Executable file
153
scripts/setup-webhook-deployment.sh
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Webhook-based Deployment Setup Script for k8s-game-2048
|
||||
echo "🚀 Setting up webhook-based deployment for k8s-game-2048..."
|
||||
|
||||
# Load configuration from .env file
|
||||
if [ -f ".env" ]; then
|
||||
echo "📋 Loading configuration from .env file..."
|
||||
export $(grep -v '^#' .env | xargs)
|
||||
else
|
||||
echo "⚠️ No .env file found, using defaults"
|
||||
fi
|
||||
|
||||
# Configuration with fallbacks
|
||||
WEBHOOK_SECRET="${WEBHOOK_SECRET:-$(openssl rand -hex 32)}"
|
||||
MANIFESTS_PATH="${MANIFESTS_PATH:-/home/administrator/k8s-game-2048/manifests}"
|
||||
WEBHOOK_DOMAIN="${WEBHOOK_DOMAIN:-webhook.wa.darknex.us}"
|
||||
KNATIVE_DOMAIN="${KNATIVE_DOMAIN:-wa.darknex.us}"
|
||||
KUBECONFIG_PATH="${KUBECONFIG_PATH:-/etc/rancher/k3s/k3s.yaml}"
|
||||
DEPLOY_INGRESS="${DEPLOY_INGRESS:-true}"
|
||||
WEBHOOK_REPLICAS="${WEBHOOK_REPLICAS:-1}"
|
||||
|
||||
echo "📋 Configuration:"
|
||||
echo " Webhook Secret: ${WEBHOOK_SECRET:0:8}..."
|
||||
echo " Manifests Path: $MANIFESTS_PATH"
|
||||
echo " Webhook Domain: $WEBHOOK_DOMAIN"
|
||||
echo " Knative Domain: $KNATIVE_DOMAIN"
|
||||
echo " Deploy Ingress: $DEPLOY_INGRESS"
|
||||
echo " Replicas: $WEBHOOK_REPLICAS"
|
||||
|
||||
# Step 1: Create webhook system namespace
|
||||
echo ""
|
||||
echo "📦 Creating webhook system namespace..."
|
||||
kubectl create namespace webhook-system --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Step 2: Create webhook secret
|
||||
echo "🔐 Creating webhook secret..."
|
||||
kubectl create secret generic webhook-secret \
|
||||
--from-literal=webhook-secret="$WEBHOOK_SECRET" \
|
||||
-n webhook-system \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Step 2.5: Create kubeconfig secret for webhook handler
|
||||
echo "🔑 Creating kubeconfig secret..."
|
||||
if [ -f "$KUBECONFIG_PATH" ]; then
|
||||
kubectl create secret generic webhook-kubeconfig \
|
||||
--from-file=config="$KUBECONFIG_PATH" \
|
||||
-n webhook-system \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
else
|
||||
echo "⚠️ Kubeconfig not found at $KUBECONFIG_PATH"
|
||||
echo "Please create the webhook-kubeconfig secret manually:"
|
||||
echo "kubectl create secret generic webhook-kubeconfig --from-file=config=~/.kube/config -n webhook-system"
|
||||
fi
|
||||
|
||||
# Step 3: Update webhook handler manifests with correct paths
|
||||
echo "🔧 Updating webhook handler manifests..."
|
||||
sed -i "s|/home/administrator/k8s-game-2048/manifests|$MANIFESTS_PATH|g" manifests/webhook/webhook-handler.yaml
|
||||
sed -i "s|webhook.yourdomain.com|$WEBHOOK_DOMAIN|g" manifests/webhook/webhook-ingress.yaml
|
||||
|
||||
# Step 4: Deploy webhook handler script ConfigMap
|
||||
echo "📜 Deploying webhook handler script..."
|
||||
kubectl apply -f manifests/webhook/webhook-script-configmap.yaml
|
||||
|
||||
# Step 5: Deploy webhook handler
|
||||
echo "🤖 Deploying webhook handler..."
|
||||
kubectl apply -f manifests/webhook/webhook-handler.yaml
|
||||
|
||||
# Step 6: Deploy ingress (optional)
|
||||
if [ "$DEPLOY_INGRESS" = "true" ]; then
|
||||
echo "🌐 Deploying webhook ingress..."
|
||||
kubectl apply -f manifests/webhook/webhook-ingress.yaml
|
||||
else
|
||||
echo "⏭️ Skipping ingress deployment (set DEPLOY_INGRESS=true to enable)"
|
||||
fi
|
||||
|
||||
# Step 7: Wait for deployment to be ready
|
||||
echo "⏳ Waiting for webhook handler to be ready..."
|
||||
kubectl wait --for=condition=available deployment/webhook-handler -n webhook-system --timeout=300s
|
||||
|
||||
# Step 8: Get service information
|
||||
echo ""
|
||||
echo "📊 Webhook handler status:"
|
||||
kubectl get pods -n webhook-system -l app=webhook-handler
|
||||
|
||||
echo ""
|
||||
echo "🌐 Service endpoints:"
|
||||
kubectl get svc -n webhook-system
|
||||
|
||||
# Step 9: Test webhook handler
|
||||
echo ""
|
||||
echo "🧪 Testing webhook handler..."
|
||||
WEBHOOK_POD=$(kubectl get pods -n webhook-system -l app=webhook-handler -o jsonpath='{.items[0].metadata.name}')
|
||||
if [ -n "$WEBHOOK_POD" ]; then
|
||||
echo "Testing health endpoint..."
|
||||
kubectl port-forward -n webhook-system pod/$WEBHOOK_POD 8080:8080 &
|
||||
KUBECTL_PID=$!
|
||||
sleep 5
|
||||
|
||||
if curl -s http://localhost:8080/health | grep -q "healthy"; then
|
||||
echo "✅ Webhook handler health check passed!"
|
||||
else
|
||||
echo "⚠️ Webhook handler health check failed"
|
||||
fi
|
||||
|
||||
kill $KUBECTL_PID 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Step 10: Display setup information
|
||||
echo ""
|
||||
echo "🎉 Webhook-based deployment setup completed!"
|
||||
echo ""
|
||||
echo "📝 Next steps:"
|
||||
echo "1. Configure GitHub repository secrets:"
|
||||
echo " - WEBHOOK_SECRET: $WEBHOOK_SECRET"
|
||||
echo " - DEV_WEBHOOK_URL: https://$WEBHOOK_DOMAIN/webhook/deploy"
|
||||
echo " - STAGING_WEBHOOK_URL: https://$WEBHOOK_DOMAIN/webhook/deploy"
|
||||
echo " - PROD_WEBHOOK_URL: https://$WEBHOOK_DOMAIN/webhook/deploy"
|
||||
echo " - KNATIVE_DOMAIN: $KNATIVE_DOMAIN"
|
||||
echo ""
|
||||
echo "2. Expose webhook handler externally:"
|
||||
if [ "$DEPLOY_INGRESS" != "true" ]; then
|
||||
echo " # Option A: Use port-forward for testing"
|
||||
echo " kubectl port-forward -n webhook-system svc/webhook-handler-external 8080:80"
|
||||
echo ""
|
||||
echo " # Option B: Get LoadBalancer IP (if available)"
|
||||
echo " kubectl get svc webhook-handler-external -n webhook-system"
|
||||
echo ""
|
||||
echo " # Option C: Deploy ingress with your domain"
|
||||
echo " DEPLOY_INGRESS=true WEBHOOK_DOMAIN=your-domain.com ./scripts/setup-webhook-deployment.sh"
|
||||
fi
|
||||
echo ""
|
||||
echo "3. Test webhook endpoint:"
|
||||
echo " curl -X POST https://$WEBHOOK_DOMAIN/webhook/deploy \\"
|
||||
echo " -H 'Content-Type: application/json' \\"
|
||||
echo " -H 'X-Signature-SHA256: sha256=SIGNATURE' \\"
|
||||
echo " -d '{\"environment\":\"dev\",\"image\":\"nginx:latest\",\"namespace\":\"default\",\"service_name\":\"test\"}'"
|
||||
echo ""
|
||||
echo "4. Push code changes to trigger automated deployment!"
|
||||
|
||||
# Output webhook secret for GitHub configuration
|
||||
echo ""
|
||||
echo "🔑 GitHub Secrets Configuration:"
|
||||
echo "===============================|"
|
||||
echo "SECRET NAME | SECRET VALUE"
|
||||
echo "===============================|"
|
||||
echo "WEBHOOK_SECRET | $WEBHOOK_SECRET"
|
||||
echo "DEV_WEBHOOK_URL | https://$WEBHOOK_DOMAIN/webhook/deploy"
|
||||
echo "STAGING_WEBHOOK_URL | https://$WEBHOOK_DOMAIN/webhook/deploy"
|
||||
echo "PROD_WEBHOOK_URL | https://$WEBHOOK_DOMAIN/webhook/deploy"
|
||||
echo "KNATIVE_DOMAIN | $KNATIVE_DOMAIN"
|
||||
echo "===============================|"
|
||||
318
scripts/webhook-handler.py
Normal file
318
scripts/webhook-handler.py
Normal file
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Webhook deployment handler for k8s-game-2048
|
||||
Receives webhook requests from GitHub Actions and deploys to k3s cluster
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
# Configuration
|
||||
app = Flask(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'change-me-in-production')
|
||||
MANIFESTS_PATH = os.environ.get('MANIFESTS_PATH', '/app/manifests')
|
||||
|
||||
def verify_signature(payload, signature):
|
||||
"""Verify HMAC signature from GitHub webhook"""
|
||||
if not signature:
|
||||
return False
|
||||
|
||||
expected = hmac.new(
|
||||
WEBHOOK_SECRET.encode('utf-8'),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(f"sha256={expected}", signature)
|
||||
|
||||
def run_command(cmd, **kwargs):
|
||||
"""Run shell command with logging"""
|
||||
logger.info(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
# Set up environment for kubectl to use in-cluster config
|
||||
env = os.environ.copy()
|
||||
env['KUBECONFIG'] = '' # Force kubectl to use in-cluster config
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, check=True, capture_output=True, text=True, env=env, **kwargs)
|
||||
logger.info(f"Command output: {result.stdout}")
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed: {e.stderr}")
|
||||
raise
|
||||
|
||||
def pull_image(image):
|
||||
"""Pull Docker image to ensure it's available"""
|
||||
logger.info(f"Pulling image: {image}")
|
||||
run_command(['docker', 'pull', image])
|
||||
|
||||
def apply_manifests(environment):
|
||||
"""Apply Kubernetes manifests for environment"""
|
||||
# Map environment names to manifest directories
|
||||
env_mapping = {
|
||||
'development': 'dev',
|
||||
'staging': 'staging',
|
||||
'production': 'prod'
|
||||
}
|
||||
|
||||
manifest_env = env_mapping.get(environment, environment)
|
||||
manifest_dir = f"{MANIFESTS_PATH}/{manifest_env}"
|
||||
logger.info(f"Applying manifests from: {manifest_dir} (environment: {environment})")
|
||||
|
||||
if not os.path.exists(manifest_dir):
|
||||
raise FileNotFoundError(f"Manifest directory not found: {manifest_dir}")
|
||||
|
||||
run_command(['kubectl', 'apply', '-f', manifest_dir])
|
||||
|
||||
def update_service_image(service_name, namespace, image):
|
||||
"""Update Knative service with new image"""
|
||||
logger.info(f"Updating service {service_name} in namespace {namespace} with image {image}")
|
||||
|
||||
patch = {
|
||||
"spec": {
|
||||
"template": {
|
||||
"spec": {
|
||||
"containers": [{
|
||||
"image": image,
|
||||
"imagePullPolicy": "Always"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run_command([
|
||||
'kubectl', 'patch', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', json.dumps(patch)
|
||||
])
|
||||
|
||||
def wait_for_service_ready(service_name, namespace, timeout=300):
|
||||
"""Wait for Knative service to be ready"""
|
||||
logger.info(f"Waiting for service {service_name} to be ready...")
|
||||
|
||||
run_command([
|
||||
'kubectl', 'wait', '--for=condition=Ready',
|
||||
f'ksvc/{service_name}',
|
||||
'-n', namespace,
|
||||
f'--timeout={timeout}s'
|
||||
])
|
||||
|
||||
def implement_blue_green_deployment(service_name, namespace, traffic_split):
|
||||
"""Implement blue-green deployment with gradual traffic shifting"""
|
||||
if not traffic_split:
|
||||
return
|
||||
|
||||
logger.info("Starting blue-green deployment...")
|
||||
|
||||
# Get the latest revision
|
||||
result = run_command([
|
||||
'kubectl', 'get', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'-o', 'jsonpath={.status.latestReadyRevisionName}'
|
||||
])
|
||||
latest_revision = result.stdout.strip()
|
||||
|
||||
if not latest_revision:
|
||||
logger.warning("No latest revision found, skipping traffic split")
|
||||
return
|
||||
|
||||
# Phase 1: Initial traffic (e.g., 10%)
|
||||
initial_percent = traffic_split.get('initial', 10)
|
||||
logger.info(f"Phase 1: Routing {initial_percent}% traffic to new revision")
|
||||
traffic_patch = {
|
||||
"spec": {
|
||||
"traffic": [
|
||||
{"revisionName": latest_revision, "percent": initial_percent},
|
||||
{"latestRevision": False, "percent": 100 - initial_percent}
|
||||
]
|
||||
}
|
||||
}
|
||||
run_command([
|
||||
'kubectl', 'patch', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', json.dumps(traffic_patch)
|
||||
])
|
||||
time.sleep(60) # Wait 1 minute
|
||||
|
||||
# Phase 2: Intermediate traffic (e.g., 50%)
|
||||
intermediate_percent = traffic_split.get('intermediate', 50)
|
||||
logger.info(f"Phase 2: Routing {intermediate_percent}% traffic to new revision")
|
||||
traffic_patch["spec"]["traffic"] = [
|
||||
{"revisionName": latest_revision, "percent": intermediate_percent},
|
||||
{"latestRevision": False, "percent": 100 - intermediate_percent}
|
||||
]
|
||||
run_command([
|
||||
'kubectl', 'patch', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', json.dumps(traffic_patch)
|
||||
])
|
||||
time.sleep(60) # Wait 1 minute
|
||||
|
||||
# Phase 3: Full traffic (100%)
|
||||
logger.info("Phase 3: Routing 100% traffic to new revision")
|
||||
traffic_patch["spec"]["traffic"] = [
|
||||
{"latestRevision": True, "percent": 100}
|
||||
]
|
||||
run_command([
|
||||
'kubectl', 'patch', 'ksvc', service_name,
|
||||
'-n', namespace,
|
||||
'--type', 'merge',
|
||||
'-p', json.dumps(traffic_patch)
|
||||
])
|
||||
|
||||
@app.route('/webhook/deploy', methods=['POST'])
|
||||
def deploy():
|
||||
"""Main webhook endpoint for deployments"""
|
||||
try:
|
||||
# Verify signature
|
||||
signature = request.headers.get('X-Signature-SHA256')
|
||||
payload = request.data
|
||||
|
||||
logger.info(f"Received webhook request")
|
||||
logger.info(f"Signature header: {signature}")
|
||||
logger.info(f"Payload length: {len(payload)} bytes")
|
||||
logger.info(f"Payload: {payload.decode('utf-8')[:200]}...")
|
||||
|
||||
# Test signature verification with debug
|
||||
if signature:
|
||||
expected = hmac.new(
|
||||
WEBHOOK_SECRET.encode('utf-8'),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
expected_full = f"sha256={expected}"
|
||||
logger.info(f"Expected signature: {expected_full}")
|
||||
logger.info(f"Received signature: {signature}")
|
||||
logger.info(f"Signatures match: {hmac.compare_digest(expected_full, signature)}")
|
||||
|
||||
if not verify_signature(payload, signature):
|
||||
logger.warning("Invalid webhook signature")
|
||||
return jsonify({"error": "Invalid signature"}), 401
|
||||
else:
|
||||
logger.warning("No signature header found")
|
||||
return jsonify({"error": "No signature provided"}), 401
|
||||
|
||||
logger.info(f"Signature verification passed")
|
||||
|
||||
# Parse payload
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({"error": "No JSON payload"}), 400
|
||||
|
||||
# Extract deployment details
|
||||
environment = data.get('environment')
|
||||
image = data.get('image')
|
||||
namespace = data.get('namespace')
|
||||
service_name = data.get('service_name')
|
||||
deployment_id = data.get('deployment_id')
|
||||
deployment_strategy = data.get('deployment_strategy', 'rolling')
|
||||
traffic_split = data.get('traffic_split')
|
||||
|
||||
# Validate required fields
|
||||
required_fields = ['environment', 'image', 'namespace', 'service_name']
|
||||
missing_fields = [field for field in required_fields if not data.get(field)]
|
||||
if missing_fields:
|
||||
return jsonify({"error": f"Missing required fields: {missing_fields}"}), 400
|
||||
|
||||
logger.info(f"Starting deployment {deployment_id}")
|
||||
logger.info(f"Environment: {environment}")
|
||||
logger.info(f"Image: {image}")
|
||||
logger.info(f"Namespace: {namespace}")
|
||||
logger.info(f"Service: {service_name}")
|
||||
logger.info(f"Strategy: {deployment_strategy}")
|
||||
|
||||
# Step 1: Skip Docker pull for Knative (Knative handles image pulling)
|
||||
logger.info("Skipping Docker pull step (Knative handles image pulling)")
|
||||
|
||||
# Step 2: Apply manifests
|
||||
apply_manifests(environment)
|
||||
|
||||
# Step 3: Update service image
|
||||
update_service_image(service_name, namespace, image)
|
||||
|
||||
# Step 4: Wait for service to be ready
|
||||
wait_for_service_ready(service_name, namespace)
|
||||
|
||||
# Step 5: Apply deployment strategy
|
||||
if deployment_strategy == 'blue-green' and traffic_split:
|
||||
implement_blue_green_deployment(service_name, namespace, traffic_split)
|
||||
|
||||
logger.info(f"Deployment {deployment_id} completed successfully")
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"deployment_id": deployment_id,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"environment": environment,
|
||||
"image": image,
|
||||
"strategy": deployment_strategy
|
||||
})
|
||||
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"File not found: {e}")
|
||||
return jsonify({"error": str(e)}), 404
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed: {e}")
|
||||
return jsonify({"error": f"Command failed: {e.stderr}"}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
"""Health check endpoint"""
|
||||
return jsonify({
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"version": "1.0.0"
|
||||
})
|
||||
|
||||
@app.route('/status', methods=['GET'])
|
||||
def status():
|
||||
"""Status endpoint with cluster information"""
|
||||
try:
|
||||
# Get cluster info
|
||||
result = run_command(['kubectl', 'cluster-info'])
|
||||
cluster_info = result.stdout
|
||||
|
||||
# Get webhook handler pod info
|
||||
result = run_command(['kubectl', 'get', 'pods', '-n', 'webhook-system', '--selector=app=webhook-handler'])
|
||||
pod_info = result.stdout
|
||||
|
||||
return jsonify({
|
||||
"status": "operational",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"cluster_info": cluster_info,
|
||||
"pod_info": pod_info
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Verify environment
|
||||
logger.info("Starting webhook deployment handler...")
|
||||
logger.info(f"Webhook secret configured: {'Yes' if WEBHOOK_SECRET != 'change-me-in-production' else 'No (using default)'}")
|
||||
logger.info(f"Manifests path: {MANIFESTS_PATH}")
|
||||
|
||||
# Start the Flask app
|
||||
app.run(host='0.0.0.0', port=8080, debug=False)
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>2048 Game - Knative Edition</title>
|
||||
<title>Knative 2048 v2.0.3 - Auto-Promotion Fixed <20></title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
</head>
|
||||
|
||||
@@ -246,8 +246,8 @@ class Game2048 {
|
||||
const tile = document.createElement('div');
|
||||
tile.className = `tile tile-${this.grid[row][col]}`;
|
||||
tile.textContent = this.grid[row][col];
|
||||
tile.style.left = `${col * 121.25}px`;
|
||||
tile.style.top = `${row * 121.25}px`;
|
||||
tile.style.left = `${col * 124.25}px`;
|
||||
tile.style.top = `${row * 124.25}px`;
|
||||
|
||||
if (this.grid[row][col] > 2048) {
|
||||
tile.className = 'tile tile-super';
|
||||
|
||||
@@ -25,7 +25,7 @@ h1.title {
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 500px;
|
||||
width: 512px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -141,8 +141,8 @@ h1.title {
|
||||
touch-action: none;
|
||||
background: #bbada0;
|
||||
border-radius: 10px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
width: 512px;
|
||||
height: 512px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ h1.title {
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.grid-row:last-child {
|
||||
@@ -220,7 +220,7 @@ h1.title {
|
||||
height: 106.25px;
|
||||
background: rgba(238, 228, 218, 0.35);
|
||||
border-radius: 6px;
|
||||
margin-right: 15px;
|
||||
margin-right: 18px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user