- Don't fail if git pull doesn't work (SSH keys not configured) - Show warning but continue with deployment - Useful for first-time deployment Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
509 lines
17 KiB
Bash
Executable file
509 lines
17 KiB
Bash
Executable file
#!/bin/bash
|
|
#
|
|
# Oliver Metadata Tool v4.0 - Production Deployment Script
|
|
# Idempotent deployment for Ubuntu server at /opt/solventum-image-metadata/
|
|
#
|
|
# Usage: sudo ./deploy.sh
|
|
#
|
|
# Prerequisites:
|
|
# - Configure Apache/Nginx reverse proxy separately
|
|
# - Ensure .env file is configured
|
|
# - Git repository must be clean (no uncommitted changes)
|
|
|
|
set -e
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Logging functions
|
|
log_info() {
|
|
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${BLUE}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_success() {
|
|
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${GREEN}[SUCCESS]${NC} $1"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
log_step() {
|
|
echo ""
|
|
echo -e "${CYAN}▶ $1${NC}"
|
|
echo "=============================================="
|
|
}
|
|
|
|
# Error handler
|
|
error_exit() {
|
|
log_error "$1"
|
|
log_error "Deployment failed! Check logs above for details."
|
|
exit 1
|
|
}
|
|
|
|
# Configuration
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
FRONTEND_DEPLOY_PATH="/var/www/html/solventum-image-metadata"
|
|
|
|
# Load environment variables to get BACKEND_PORT
|
|
if [[ -f "$SCRIPT_DIR/.env" ]]; then
|
|
source "$SCRIPT_DIR/.env"
|
|
fi
|
|
|
|
BACKEND_PORT="${BACKEND_PORT:-5001}"
|
|
REDIS_PORT=6379
|
|
HEALTH_CHECK_RETRIES=30
|
|
HEALTH_CHECK_INTERVAL=2
|
|
COMPOSE_FILE="docker-compose.fastapi.yml"
|
|
|
|
# Banner
|
|
echo ""
|
|
echo -e "${CYAN}╔════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${CYAN}║ Oliver Metadata Tool v4.0 Deployment ║${NC}"
|
|
echo -e "${CYAN}║ FastAPI + React + Redis ║${NC}"
|
|
echo -e "${CYAN}╚════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
log_info "Starting deployment..."
|
|
log_info "Working directory: $SCRIPT_DIR"
|
|
log_info "Frontend deploy path: $FRONTEND_DEPLOY_PATH"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Pre-flight checks
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Pre-flight Checks"
|
|
|
|
# Check if running as root
|
|
if [[ $EUID -ne 0 ]]; then
|
|
error_exit "This script must be run as root (use sudo)"
|
|
fi
|
|
log_info "✓ Running as root"
|
|
|
|
# Check Docker
|
|
if ! command -v docker &> /dev/null; then
|
|
error_exit "Docker is not installed"
|
|
fi
|
|
log_info "✓ Docker: $(docker --version)"
|
|
|
|
# Check docker-compose (try both v1 and v2 syntax)
|
|
if command -v docker-compose &> /dev/null; then
|
|
DOCKER_COMPOSE="docker-compose"
|
|
elif docker compose version &> /dev/null; then
|
|
DOCKER_COMPOSE="docker compose"
|
|
else
|
|
error_exit "docker-compose is not installed"
|
|
fi
|
|
log_info "✓ Docker Compose: $($DOCKER_COMPOSE version --short 2>/dev/null || $DOCKER_COMPOSE version)"
|
|
|
|
# Check Node.js
|
|
if ! command -v node &> /dev/null; then
|
|
error_exit "Node.js is not installed"
|
|
fi
|
|
NODE_VERSION=$(node --version)
|
|
log_info "✓ Node.js: $NODE_VERSION"
|
|
|
|
# Verify Node.js version (need 18+)
|
|
NODE_MAJOR_VERSION=$(echo "$NODE_VERSION" | sed 's/v\([0-9]*\).*/\1/')
|
|
if [[ "$NODE_MAJOR_VERSION" -lt 18 ]]; then
|
|
log_warn "Node.js version $NODE_VERSION detected. Version 18+ recommended."
|
|
fi
|
|
|
|
# Check npm
|
|
if ! command -v npm &> /dev/null; then
|
|
error_exit "npm is not installed"
|
|
fi
|
|
log_info "✓ npm: $(npm --version)"
|
|
|
|
# Check git
|
|
if ! command -v git &> /dev/null; then
|
|
log_warn "git is not installed - manual code updates required"
|
|
else
|
|
log_info "✓ git: $(git --version)"
|
|
fi
|
|
|
|
# Check .env file
|
|
if [[ ! -f "$SCRIPT_DIR/.env" ]]; then
|
|
error_exit "Environment file not found at $SCRIPT_DIR/.env"
|
|
fi
|
|
log_info "✓ .env file found"
|
|
|
|
# Validate required environment variables
|
|
log_info "Validating environment variables..."
|
|
source "$SCRIPT_DIR/.env"
|
|
|
|
if [[ -z "$SECRET_KEY" ]] || [[ "$SECRET_KEY" == *"change"* ]]; then
|
|
log_warn "SECRET_KEY not properly set - using default (NOT SECURE FOR PRODUCTION)"
|
|
fi
|
|
|
|
if [[ -z "$OPENAI_API_KEY" ]]; then
|
|
log_warn "OPENAI_API_KEY not set - AI features will not work"
|
|
fi
|
|
|
|
if [[ -n "$AZURE_CLIENT_ID" ]]; then
|
|
log_info "✓ Azure AD SSO configured"
|
|
fi
|
|
|
|
# Verify compose file exists
|
|
if [[ ! -f "$SCRIPT_DIR/$COMPOSE_FILE" ]]; then
|
|
error_exit "$COMPOSE_FILE not found"
|
|
fi
|
|
log_info "✓ Docker Compose file: $COMPOSE_FILE"
|
|
|
|
# Check frontend directory
|
|
if [[ ! -d "$SCRIPT_DIR/frontend" ]]; then
|
|
error_exit "Frontend directory not found"
|
|
fi
|
|
log_info "✓ Frontend directory exists"
|
|
|
|
# Check backend directory
|
|
if [[ ! -d "$SCRIPT_DIR/backend" ]]; then
|
|
error_exit "Backend directory not found"
|
|
fi
|
|
log_info "✓ Backend directory exists"
|
|
|
|
log_success "All pre-flight checks passed"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Pull latest code from Git
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Pulling Latest Code"
|
|
|
|
if command -v git &> /dev/null && [[ -d "$SCRIPT_DIR/.git" ]]; then
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# Get current commit before pull
|
|
COMMIT_BEFORE=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
|
|
# Check for uncommitted changes
|
|
if [[ -n $(git status --porcelain 2>/dev/null) ]]; then
|
|
log_warn "Uncommitted changes detected:"
|
|
git status --short
|
|
read -p "Continue with deployment? [y/N] " -n 1 -r
|
|
echo
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
error_exit "Deployment cancelled by user"
|
|
fi
|
|
fi
|
|
|
|
# Stash any local changes (just in case)
|
|
log_info "Stashing local changes (if any)..."
|
|
git stash push -m "Auto-stash before deployment $(date +%Y%m%d-%H%M%S)" || true
|
|
|
|
# Pull latest code
|
|
log_info "Pulling from origin/main..."
|
|
if git pull origin main; then
|
|
log_success "Git pull successful"
|
|
else
|
|
log_warn "Git pull failed - continuing with existing code"
|
|
log_warn "This is OK for first deployment or if SSH keys not configured"
|
|
log_warn "For updates, ensure git credentials are set up"
|
|
fi
|
|
|
|
# Get new commit info
|
|
COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "unknown")
|
|
COMMIT_DATE=$(git log -1 --pretty=format:"%ci" 2>/dev/null || echo "unknown")
|
|
|
|
if [[ "$COMMIT_BEFORE" != "$COMMIT_HASH" ]]; then
|
|
log_success "Code updated: $COMMIT_BEFORE → $COMMIT_HASH"
|
|
else
|
|
log_info "Already up to date at commit: $COMMIT_HASH"
|
|
fi
|
|
|
|
log_info "Commit message: $COMMIT_MSG"
|
|
log_info "Commit date: $COMMIT_DATE"
|
|
else
|
|
log_warn "Git not available or not a git repository"
|
|
COMMIT_HASH="unknown"
|
|
COMMIT_MSG="unknown"
|
|
COMMIT_DATE="unknown"
|
|
fi
|
|
|
|
log_success "Code ready for deployment"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Clean old Docker resources
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Cleaning Old Docker Resources"
|
|
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# Stop old containers
|
|
log_info "Stopping old containers..."
|
|
$DOCKER_COMPOSE -f "$COMPOSE_FILE" down --remove-orphans || log_warn "No containers to stop"
|
|
|
|
# Remove old images for this project (keep base images)
|
|
log_info "Removing old project images..."
|
|
OLD_IMAGES=$(docker images --filter "reference=solventum-image-metadata*" --filter "reference=*oliver*" -q 2>/dev/null || true)
|
|
if [[ -n "$OLD_IMAGES" ]]; then
|
|
docker rmi -f $OLD_IMAGES 2>/dev/null || log_warn "Some images could not be removed (may be in use)"
|
|
log_success "Old images removed"
|
|
else
|
|
log_info "No old images to remove"
|
|
fi
|
|
|
|
# Clean build cache (keep last 24 hours)
|
|
log_info "Cleaning Docker build cache..."
|
|
docker builder prune -f --filter "until=24h" > /dev/null 2>&1 || true
|
|
|
|
# Remove unused networks
|
|
log_info "Removing unused networks..."
|
|
docker network prune -f > /dev/null 2>&1 || true
|
|
|
|
# Show disk space saved
|
|
log_info "Docker cleanup complete"
|
|
|
|
log_success "Old resources cleaned"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Build Docker containers
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Building Docker Containers"
|
|
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# Pull latest base images and build (use cache for efficiency)
|
|
log_info "Building containers with latest base images..."
|
|
$DOCKER_COMPOSE -f "$COMPOSE_FILE" build --pull || error_exit "Docker build failed"
|
|
|
|
log_success "Docker containers built successfully"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Start Docker services
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Starting Docker Services"
|
|
|
|
log_info "Starting backend and Redis..."
|
|
$DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d || error_exit "Failed to start Docker services"
|
|
|
|
# Wait for Redis to be ready (inside Docker network)
|
|
log_info "Waiting for Redis to be ready..."
|
|
sleep 5 # Give Redis time to start
|
|
log_success "Redis container started"
|
|
|
|
# Wait for backend to start
|
|
log_info "Waiting for backend to start..."
|
|
sleep 5
|
|
|
|
log_success "Docker services started"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Database initialization (if needed)
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Database Setup"
|
|
|
|
# Check if database exists
|
|
if [[ -f "$SCRIPT_DIR/backend/data/oliver_metadata.db" ]]; then
|
|
log_info "Database file exists - skipping initialization"
|
|
else
|
|
log_info "First run detected - database will be initialized automatically"
|
|
fi
|
|
|
|
# Note: Alembic migrations would go here if we add them
|
|
# For now, FastAPI initializes DB on first run via init_db()
|
|
|
|
log_success "Database setup complete"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Build frontend
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Building Frontend"
|
|
|
|
cd "$SCRIPT_DIR/frontend"
|
|
|
|
# Check if node_modules exists and package.json changed
|
|
if [[ ! -d "node_modules" ]] || [[ "package.json" -nt "node_modules" ]]; then
|
|
log_info "Installing frontend dependencies..."
|
|
npm ci || error_exit "npm ci failed"
|
|
log_success "Dependencies installed"
|
|
else
|
|
log_info "Dependencies up to date (skipping install)"
|
|
fi
|
|
|
|
# Build production bundle
|
|
log_info "Creating production build with Vite..."
|
|
npm run build || error_exit "Frontend build failed"
|
|
|
|
# Verify dist directory was created
|
|
if [[ ! -d "$SCRIPT_DIR/frontend/dist" ]]; then
|
|
error_exit "Frontend dist directory not found (build failed)"
|
|
fi
|
|
|
|
# Verify index.html exists
|
|
if [[ ! -f "$SCRIPT_DIR/frontend/dist/index.html" ]]; then
|
|
error_exit "Frontend index.html not found in dist/"
|
|
fi
|
|
|
|
# Get build size
|
|
BUILD_SIZE=$(du -sh "$SCRIPT_DIR/frontend/dist" | cut -f1)
|
|
log_info "Build size: $BUILD_SIZE"
|
|
|
|
log_success "Frontend built successfully"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Deploy frontend to Apache/Nginx
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Deploying Frontend"
|
|
|
|
# Create deployment directory if it doesn't exist
|
|
log_info "Creating deployment directory..."
|
|
mkdir -p "$FRONTEND_DEPLOY_PATH"
|
|
|
|
# Backup existing files (optional)
|
|
if [[ -d "$FRONTEND_DEPLOY_PATH" ]] && [[ "$(ls -A $FRONTEND_DEPLOY_PATH)" ]]; then
|
|
BACKUP_DIR="/tmp/oliver-metadata-backup-$(date +%Y%m%d-%H%M%S)"
|
|
log_info "Backing up existing files to $BACKUP_DIR"
|
|
mkdir -p "$BACKUP_DIR"
|
|
cp -r "$FRONTEND_DEPLOY_PATH"/* "$BACKUP_DIR/" || log_warn "Backup failed (non-critical)"
|
|
fi
|
|
|
|
# Clear existing files
|
|
log_info "Removing old frontend files..."
|
|
rm -rf "${FRONTEND_DEPLOY_PATH:?}"/*
|
|
|
|
# Copy new build
|
|
log_info "Copying new build to web directory..."
|
|
cp -r "$SCRIPT_DIR/frontend/dist/"* "$FRONTEND_DEPLOY_PATH/"
|
|
|
|
# Set proper ownership for web server
|
|
log_info "Setting permissions..."
|
|
chown -R www-data:www-data "$FRONTEND_DEPLOY_PATH"
|
|
chmod -R 755 "$FRONTEND_DEPLOY_PATH"
|
|
|
|
# Verify deployment
|
|
if [[ ! -f "$FRONTEND_DEPLOY_PATH/index.html" ]]; then
|
|
error_exit "Frontend deployment verification failed - index.html not found"
|
|
fi
|
|
|
|
log_success "Frontend deployed to $FRONTEND_DEPLOY_PATH"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Verification & Health Checks
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Running Health Checks"
|
|
|
|
# Wait for backend API to be ready
|
|
log_info "Checking backend API health..."
|
|
BACKEND_READY=false
|
|
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
|
|
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$BACKEND_PORT/health" 2>/dev/null || echo "000")
|
|
if [[ "$HTTP_STATUS" == "200" ]]; then
|
|
BACKEND_READY=true
|
|
break
|
|
fi
|
|
log_info "Waiting for backend... (attempt $i/$HEALTH_CHECK_RETRIES, status: $HTTP_STATUS)"
|
|
sleep $HEALTH_CHECK_INTERVAL
|
|
done
|
|
|
|
if [[ "$BACKEND_READY" != "true" ]]; then
|
|
log_warn "Backend health check failed - service may still be starting"
|
|
log_info "Backend logs:"
|
|
cd "$SCRIPT_DIR"
|
|
$DOCKER_COMPOSE -f "$COMPOSE_FILE" logs --tail=50 backend
|
|
else
|
|
log_success "Backend health check passed (HTTP 200)"
|
|
fi
|
|
|
|
# Check API documentation endpoint
|
|
API_DOCS_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$BACKEND_PORT/docs" 2>/dev/null || echo "000")
|
|
if [[ "$API_DOCS_STATUS" == "200" ]]; then
|
|
log_success "API docs accessible at http://localhost:$BACKEND_PORT/docs"
|
|
else
|
|
log_warn "API docs check failed (status: $API_DOCS_STATUS)"
|
|
fi
|
|
|
|
# Verify Redis (check if container is running)
|
|
log_info "Verifying Redis..."
|
|
if docker ps | grep -q oliver-redis; then
|
|
log_success "Redis container is running"
|
|
else
|
|
log_warn "Redis container not found"
|
|
fi
|
|
|
|
# Check Docker container status
|
|
log_info "Docker container status:"
|
|
cd "$SCRIPT_DIR"
|
|
$DOCKER_COMPOSE -f "$COMPOSE_FILE" ps
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Cleanup
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Cleanup"
|
|
|
|
# Remove old Docker images
|
|
log_info "Removing unused Docker images..."
|
|
docker image prune -f > /dev/null 2>&1 || log_warn "Image cleanup failed (non-critical)"
|
|
|
|
# Remove old backups (keep last 7 days)
|
|
if [[ -d "/tmp" ]]; then
|
|
log_info "Removing old backup files (>7 days)..."
|
|
find /tmp -name "oliver-metadata-backup-*" -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true
|
|
fi
|
|
|
|
log_success "Cleanup complete"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Summary
|
|
# -----------------------------------------------------------------------------
|
|
echo ""
|
|
echo -e "${GREEN}╔════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ 🎉 Deployment Successful! ║${NC}"
|
|
echo -e "${GREEN}╚════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
if [[ -n "$COMMIT_HASH" ]]; then
|
|
log_info "Deployed commit: $COMMIT_HASH - $COMMIT_MSG"
|
|
fi
|
|
|
|
echo ""
|
|
log_info "📍 Access Points:"
|
|
echo " Frontend: https://ai-sandbox.oliver.solutions/solventum-image-metadata/"
|
|
echo " Backend API: https://ai-sandbox.oliver.solutions/solventum-image-metadata/api/"
|
|
echo " API Docs: http://localhost:$BACKEND_PORT/docs"
|
|
echo ""
|
|
|
|
log_info "🐳 Docker Services:"
|
|
echo " Backend: http://localhost:$BACKEND_PORT"
|
|
echo " Redis: localhost:$REDIS_PORT"
|
|
echo ""
|
|
|
|
log_info "📂 File Locations:"
|
|
echo " Frontend: $FRONTEND_DEPLOY_PATH"
|
|
echo " Backend: $SCRIPT_DIR/backend"
|
|
echo " Database: $SCRIPT_DIR/backend/data/oliver_metadata.db"
|
|
echo " Uploads: $SCRIPT_DIR/backend/uploads"
|
|
echo ""
|
|
|
|
log_info "🔧 Useful Commands:"
|
|
echo " View logs: $DOCKER_COMPOSE -f $COMPOSE_FILE logs -f"
|
|
echo " Stop services: $DOCKER_COMPOSE -f $COMPOSE_FILE down"
|
|
echo " Restart backend: $DOCKER_COMPOSE -f $COMPOSE_FILE restart backend"
|
|
echo " Redis CLI: docker exec -it oliver-redis redis-cli"
|
|
echo ""
|
|
|
|
if [[ "$BACKEND_READY" != "true" ]]; then
|
|
log_warn "⚠️ Backend health check did not pass - verify services manually"
|
|
echo " Check logs: $DOCKER_COMPOSE -f $COMPOSE_FILE logs backend"
|
|
else
|
|
log_success "✓ All health checks passed"
|
|
fi
|
|
|
|
echo ""
|
|
log_info "🔐 Next Steps:"
|
|
echo " 1. Configure Apache reverse proxy (see apache-config.conf)"
|
|
echo " 2. Test frontend: https://ai-sandbox.oliver.solutions/solventum-image-metadata/"
|
|
echo " 3. Verify SSO redirect (Azure AD)"
|
|
echo " 4. Upload test files and verify metadata updates"
|
|
echo ""
|
|
|
|
log_success "Deployment complete! 🚀"
|
|
echo "=============================================="
|