Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 124 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,131 @@ jobs:
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=max

- name: Verify pushed image
- name: Verify pushed image with docker-compose
env:
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
AZURE_KEY_VAULT_URL: ${{ secrets.AZURE_KEY_VAULT_URL }}
AZURE_APP_CONFIG_URL: ${{ secrets.AZURE_APP_CONFIG_URL }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
run: |
# Pull the image we just pushed using branch name tag (guaranteed to exist)
# The metadata action creates tags: branch-name, sha-<short>, and latest (for main)
# Pull the image we just pushed
IMAGE_TAG="${{ env.IMAGE_NAME }}:${{ github.ref_name }}"
docker pull ${IMAGE_TAG}
docker run -d --name test-container -p 8000:8000 ${IMAGE_TAG}
sleep 5
curl --fail http://localhost:8000/health || exit 1
docker stop test-container
docker rm test-container

# Tag it as latest for docker compose
docker tag ${IMAGE_TAG} harinypatel/nerospatial-backend:latest

# Create docker-compose file for verification
cat > docker-compose.verify.yml << 'EOF'
version: '3.8'

services:
postgres:
image: postgres:16-alpine
container_name: verify-postgres
environment:
POSTGRES_DB: nerospatial
POSTGRES_USER: nerospatial
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nerospatial -d nerospatial"]
interval: 5s
timeout: 5s
retries: 5
start_period: 5s
networks:
- verify-network

redis:
image: redis:7-alpine
container_name: verify-redis
command: redis-server --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 5s
timeout: 5s
retries: 5
start_period: 5s
networks:
- verify-network

backend:
image: harinypatel/nerospatial-backend:latest
container_name: verify-backend
ports:
- "8000:8000"
environment:
- ENVIRONMENT=production
- AZURE_KEY_VAULT_URL=${AZURE_KEY_VAULT_URL}
- AZURE_APP_CONFIG_URL=${AZURE_APP_CONFIG_URL}
- AZURE_TENANT_ID=${AZURE_TENANT_ID}
- AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
- AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- verify-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 10s
timeout: 10s
retries: 3
start_period: 30s

networks:
verify-network:
driver: bridge
EOF

# Start services
docker compose -f docker-compose.verify.yml up -d

# Wait for backend to be healthy (up to 60 seconds)
echo "Waiting for backend to start and connect to Azure services..."
timeout=60
elapsed=0
while [ $elapsed -lt $timeout ]; do
if docker ps | grep -q verify-backend; then
# Check if container is still running (not crashed)
if ! docker ps | grep verify-backend | grep -q Up; then
echo "Backend container stopped unexpectedly"
docker logs verify-backend
docker compose -f docker-compose.verify.yml down
exit 1
fi

# Try health check
if curl -f -s http://localhost:8000/health > /dev/null 2>&1; then
echo "Backend health check passed!"
break
fi
fi
sleep 2
elapsed=$((elapsed + 2))
done

# Final health check
if ! curl -f http://localhost:8000/health; then
echo "Health check failed after $timeout seconds"
echo "Backend logs:"
docker logs verify-backend
echo "Postgres logs:"
docker logs verify-postgres
echo "Redis logs:"
docker logs verify-redis
docker compose -f docker-compose.verify.yml down
exit 1
fi

# Cleanup
docker compose -f docker-compose.verify.yml down
echo "Image verification successful"

- name: Workflow Summary
if: always()
Expand All @@ -153,9 +267,9 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Build Status" >> $GITHUB_STEP_SUMMARY
if [ "${{ job.status }}" == "success" ]; then
echo "**Build Successful**" >> $GITHUB_STEP_SUMMARY
echo "**Build Successful**" >> $GITHUB_STEP_SUMMARY
else
echo "**Build Failed**" >> $GITHUB_STEP_SUMMARY
echo "**Build Failed**" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Image Tags" >> $GITHUB_STEP_SUMMARY
Expand Down
15 changes: 11 additions & 4 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
3. .env file (fallback for development, bootstrap credentials)
"""

from urllib.parse import quote_plus

from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

Expand Down Expand Up @@ -103,14 +105,17 @@ def is_development(self) -> bool:

@property
def postgres_url(self) -> str:
"""Build PostgreSQL connection URL."""
"""Build PostgreSQL connection URL with URL-encoded password."""
if not self.postgres_password:
return f"postgresql://{self.postgres_user}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
return f"postgresql://{self.postgres_user}:{self.postgres_password}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
# URL-encode password to handle special characters (@, :, /, etc.)
encoded_password = quote_plus(self.postgres_password)
encoded_user = quote_plus(self.postgres_user)
return f"postgresql://{encoded_user}:{encoded_password}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"

@property
def redis_url(self) -> str:
"""Build Redis connection URL."""
"""Build Redis connection URL with URL-encoded password."""
# Check for explicit REDIS_URL environment variable first (useful for Docker Compose)
import os

Expand All @@ -120,7 +125,9 @@ def redis_url(self) -> str:

# Otherwise, build from components
if self.redis_password:
return f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}"
# URL-encode password to handle special characters (@, :, /, %, etc.)
encoded_password = quote_plus(self.redis_password)
return f"redis://:{encoded_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}"
return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}"


Expand Down
79 changes: 79 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
version: '3.8'

# Production Docker Compose for NeroSpatial Backend
# This file should be deployed to Azure VM via Jenkins
# Only bootstrap credentials needed - rest loaded from Azure App Config & Key Vault

services:
postgres:
image: postgres:16-alpine
container_name: nerospatial-postgres
environment:
POSTGRES_DB: ${POSTGRES_DB:-nerospatial}
POSTGRES_USER: ${POSTGRES_USER:-nerospatial}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped
networks:
- nerospatial-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-nerospatial} -d ${POSTGRES_DB:-nerospatial}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

redis:
image: redis:7-alpine
container_name: nerospatial-redis
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
restart: unless-stopped
networks:
- nerospatial-network
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s

backend:
image: harinypatel/nerospatial-backend:latest
container_name: nerospatial-backend
ports:
- "8000:8000"
environment:
# Bootstrap credentials only - app will load rest from Azure App Config & Key Vault
- ENVIRONMENT=production
- AZURE_KEY_VAULT_URL=${AZURE_KEY_VAULT_URL}
- AZURE_APP_CONFIG_URL=${AZURE_APP_CONFIG_URL}
- AZURE_TENANT_ID=${AZURE_TENANT_ID}
- AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
- AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- nerospatial-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s # Give app time to load config from Azure

volumes:
postgres-data:
driver: local
redis-data:
driver: local

networks:
nerospatial-network:
driver: bridge