#!/bin/bash # scripts/deploy/deploy_walter.sh # Deployment script: karl (development) → walter (OpenBSD staging) # # Usage: # ./scripts/deploy/deploy_walter.sh [--dry-run] [--rollback] [--force] # # Dragons@Work - Furt API-Gateway Deployment # Version: 1.0 set -euo pipefail # Exit on error, undefined vars, pipe failures # ============================================================================= # CONFIGURATION # ============================================================================= # Source (karl development) SOURCE_DIR="/home/michael/Develop/DAW/furt/furt-lua" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" # scripts/deploy/ -> scripts/ -> furt/ # Target (walter OpenBSD staging) WALTER_HOST="walter" # Assumes SSH config entry (as michael user) TARGET_DIR="/usr/local/furt/furt-lua" SERVICE_USER="_furt" SERVICE_GROUP="_furt" # Backup configuration BACKUP_DIR="/usr/local/furt/backups" BACKUP_RETENTION=3 # Keep last 3 backups # Health check configuration HEALTH_URL="http://localhost:8080/health" HEALTH_TIMEOUT=10 HEALTH_RETRIES=3 # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ============================================================================= # LOGGING FUNCTIONS # ============================================================================= log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_step() { echo -e "\n${BLUE}==>${NC} $1" } # ============================================================================= # UTILITY FUNCTIONS # ============================================================================= usage() { cat << EOF Usage: $0 [OPTIONS] Deployment script for furt-lua: karl (development) → walter (OpenBSD staging) OPTIONS: --dry-run Show what would be deployed without making changes --rollback Rollback to previous deployment --force Skip confirmation prompts --help Show this help message EXAMPLES: $0 # Normal deployment with confirmation $0 --dry-run # Preview deployment without changes $0 --force # Deploy without confirmation $0 --rollback # Rollback to previous version EOF } check_dependencies() { log_step "Checking dependencies" # Check if source directory exists if [[ ! -d "$SOURCE_DIR" ]]; then log_error "Source directory not found: $SOURCE_DIR" exit 1 fi # Check SSH connectivity to walter if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$WALTER_HOST" exit 2>/dev/null; then log_error "Cannot connect to walter via SSH" log_info "Please ensure SSH key is set up for $WALTER_HOST" exit 1 fi # Check rsync availability if ! command -v rsync &> /dev/null; then log_error "rsync is required but not installed" exit 1 fi log_success "All dependencies OK" } get_backup_timestamp() { date +"%Y%m%d_%H%M%S" } # ============================================================================= # SSH REMOTE EXECUTION FUNCTIONS # ============================================================================= walter_exec() { ssh "$WALTER_HOST" "$@" } walter_exec_as_root() { ssh "$WALTER_HOST" "doas $@" } walter_exec_as_furt() { ssh "$WALTER_HOST" "doas -u $SERVICE_USER $@" } # ============================================================================= # BACKUP FUNCTIONS # ============================================================================= create_backup() { local timestamp=$(get_backup_timestamp) local backup_path="$BACKUP_DIR/furt-lua_$timestamp" log_step "Creating backup" # Create backup directory if it doesn't exist walter_exec_as_root "mkdir -p $BACKUP_DIR" walter_exec_as_root "chown $SERVICE_USER:$SERVICE_GROUP $BACKUP_DIR" # Check if target directory exists if walter_exec "test -d $TARGET_DIR"; then log_info "Backing up current deployment to: $backup_path" walter_exec_as_root "cp -r $TARGET_DIR $backup_path" walter_exec_as_root "chown -R $SERVICE_USER:$SERVICE_GROUP $backup_path" # Set backup metadata (fix shell redirect issue) walter_exec_as_root "sh -c \"echo 'Backup created: \$(date)' > $backup_path/.backup_info\"" walter_exec_as_root "sh -c \"echo 'Original path: $TARGET_DIR' >> $backup_path/.backup_info\"" walter_exec_as_root "chown $SERVICE_USER:$SERVICE_GROUP $backup_path/.backup_info" log_success "Backup created: $backup_path" echo "$backup_path" # Return backup path else log_warning "No existing deployment found, skipping backup" echo "" # Return empty string fi } cleanup_old_backups() { log_step "Cleaning up old backups" local backup_count=$(walter_exec "ls -1 $BACKUP_DIR/furt-lua_* 2>/dev/null | wc -l" || echo "0") if [[ $backup_count -gt $BACKUP_RETENTION ]]; then log_info "Found $backup_count backups, keeping last $BACKUP_RETENTION" walter_exec_as_root "ls -1t $BACKUP_DIR/furt-lua_* | tail -n +$((BACKUP_RETENTION + 1)) | xargs rm -rf" log_success "Old backups cleaned up" else log_info "Found $backup_count backups, no cleanup needed" fi } list_backups() { log_step "Available backups" if walter_exec "ls -1 $BACKUP_DIR/furt-lua_* 2>/dev/null"; then walter_exec "ls -1t $BACKUP_DIR/furt-lua_* | head -n 5 | while read backup; do echo \" \$backup\" if [ -f \"\$backup/.backup_info\" ]; then cat \"\$backup/.backup_info\" | sed 's/^/ /' fi echo done" else log_warning "No backups found" fi } rollback_deployment() { log_step "Rolling back deployment" # List available backups local latest_backup=$(walter_exec "ls -1t $BACKUP_DIR/furt-lua_* 2>/dev/null | head -n 1" || echo "") if [[ -z "$latest_backup" ]]; then log_error "No backups available for rollback" exit 1 fi log_info "Latest backup: $latest_backup" if [[ "$FORCE" != "true" ]]; then echo -n "Rollback to this version? [y/N]: " read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then log_info "Rollback cancelled" exit 0 fi fi # Stop service stop_service # Backup current version (before rollback) local rollback_backup=$(create_backup) if [[ -n "$rollback_backup" ]]; then walter_exec_as_root "mv $rollback_backup ${rollback_backup}_pre_rollback" fi # Restore from backup walter_exec_as_root "rm -rf $TARGET_DIR" walter_exec_as_root "cp -r $latest_backup $TARGET_DIR" # Fix permissions fix_permissions # Start service start_service # Health check if health_check; then log_success "Rollback completed successfully" else log_error "Rollback completed but health check failed" exit 1 fi } # ============================================================================= # SERVICE MANAGEMENT FUNCTIONS # ============================================================================= get_service_status() { # Check if furt process is actually running (regardless of rcctl status) if walter_exec "ps aux | grep -v grep | grep -q '_furt.*lua.*main.lua'"; then echo "running" else echo "stopped" fi } get_rcctl_status() { # Check OpenBSD service status specifically if walter_exec "rcctl check furt >/dev/null 2>&1"; then echo "running" else echo "stopped" fi } stop_service() { log_step "Stopping furt service" local process_status=$(get_service_status) local rcctl_status=$(get_rcctl_status) if [[ "$process_status" == "running" ]]; then log_info "Furt process is running (rcctl status: $rcctl_status)" # Try rcctl stop first if service is managed by rcctl if [[ "$rcctl_status" == "running" ]]; then log_info "Stopping via rcctl..." walter_exec_as_root "rcctl stop furt" || true sleep 2 fi # Check if still running (manual process or rcctl didn't work) if [[ $(get_service_status) == "running" ]]; then log_info "Process still running, stopping manually..." walter_exec_as_root "pkill -f -U $SERVICE_USER 'lua.*main.lua'" || true sleep 2 fi # Final check local final_status=$(get_service_status) if [[ "$final_status" == "stopped" ]]; then log_success "Service stopped" else log_error "Could not stop service" return 1 fi else log_info "Service already stopped" fi } start_service() { log_step "Starting furt service" local status=$(get_service_status) if [[ "$status" == "stopped" ]]; then # Check port availability before starting if ! check_port_availability; then log_error "Cannot start service - port 8080 is occupied" return 1 fi log_info "Starting furt service via rcctl..." walter_exec_as_root "rcctl start furt" # Wait and check if service actually started sleep 5 local process_status=$(get_service_status) local rcctl_status=$(get_rcctl_status) if [[ "$process_status" == "running" ]]; then log_success "Service started successfully (rcctl: $rcctl_status, process: $process_status)" elif [[ "$rcctl_status" == "running" ]]; then log_warning "rcctl reports running but process not detected - checking port..." if walter_exec "netstat -an | grep -q ':8080.*LISTEN'"; then log_success "Service appears to be running (port 8080 active)" else log_error "Service failed to start properly" # Show service logs for debugging walter_exec "tail -10 /var/log/daemon" || true return 1 fi else log_error "Failed to start service" # Show service logs for debugging walter_exec "tail -10 /var/log/daemon" || true return 1 fi else log_info "Service already running" fi } # ============================================================================= # DEPLOYMENT FUNCTIONS # ============================================================================= prepare_source() { log_step "Preparing source files" # Check source directory structure local required_dirs=("src" "config" "scripts") for dir in "${required_dirs[@]}"; do if [[ ! -d "$SOURCE_DIR/$dir" ]]; then log_error "Required directory missing: $SOURCE_DIR/$dir" exit 1 fi done # Check required files local required_files=("src/main.lua" "scripts/start.sh") for file in "${required_files[@]}"; do if [[ ! -f "$SOURCE_DIR/$file" ]]; then log_error "Required file missing: $SOURCE_DIR/$file" exit 1 fi done log_success "Source files validated" } sync_files() { log_step "Syncing files to walter" # Prepare rsync excludes local excludes=( "--exclude=.env" "--exclude=.env.*" "--exclude=*.backup" "--exclude=*.orig" "--exclude=*.tmp" "--exclude=.git/" "--exclude=.DS_Store" "--exclude=logs/" "--exclude=*.log" ) if [[ "$DRY_RUN" == "true" ]]; then log_info "DRY RUN: Would sync the following files:" rsync -avz --dry-run "${excludes[@]}" "$SOURCE_DIR/" "$WALTER_HOST:$TARGET_DIR/" return 0 fi # Create target directory and set initial ownership walter_exec_as_root "mkdir -p $TARGET_DIR" walter_exec_as_root "chown -R $SERVICE_USER:$SERVICE_GROUP $TARGET_DIR" # Sync files using rsync with _furt user log_info "Syncing files..." rsync -avz "${excludes[@]}" \ -e "ssh" \ --rsync-path="doas -u $SERVICE_USER rsync" \ "$SOURCE_DIR/" "$WALTER_HOST:$TARGET_DIR/" log_success "Files synced" } fix_service_file() { log_step "Updating OpenBSD service file" if [[ "$DRY_RUN" == "true" ]]; then log_info "DRY RUN: Would update /etc/rc.d/furt" return 0 fi log_info "Creating correct service file..." walter_exec_as_root "sh -c 'cat > /etc/rc.d/furt << \"EOF\" #!/bin/ksh daemon=\"$TARGET_DIR/scripts/start.sh\" daemon_user=\"$SERVICE_USER\" daemon_cwd=\"$TARGET_DIR\" daemon_flags=\"start\" pexp=\"lua.*main.lua\" . /etc/rc.d/rc.subr rc_bg=YES rc_cmd \$1 EOF'" # Make service file executable walter_exec_as_root "chmod +x /etc/rc.d/furt" # Enable service if not already enabled if ! walter_exec "rcctl ls on | grep -q furt"; then log_info "Enabling furt service..." walter_exec_as_root "rcctl enable furt" log_success "Service enabled" else log_info "Service already enabled" fi log_success "Service file updated" } fix_permissions() { log_step "Fixing permissions" if [[ "$DRY_RUN" == "true" ]]; then log_info "DRY RUN: Would set ownership to $SERVICE_USER:$SERVICE_GROUP" return 0 fi # Set ownership (already done in sync_files, but ensure it's correct) walter_exec_as_root "chown -R $SERVICE_USER:$SERVICE_GROUP $TARGET_DIR" # Set executable permissions for scripts walter_exec_as_root "chmod +x $TARGET_DIR/scripts/*.sh" log_success "Permissions fixed" } # ============================================================================= # HEALTH CHECK FUNCTIONS # ============================================================================= check_port_availability() { log_step "Checking port availability" local port="8080" # Default furt port if walter_exec "netstat -an | grep -q ':$port'"; then log_warning "Port $port is already in use" walter_exec "netstat -an | grep ':$port'" || true # Try to identify what's using the port log_info "Checking what's using port $port..." walter_exec "fstat 2>/dev/null | grep ':$port'" || walter_exec "netstat -anp 2>/dev/null | grep ':$port'" || true return 1 else log_success "Port $port is available" return 0 fi } health_check() { log_step "Running health check" local retries=$HEALTH_RETRIES while [[ $retries -gt 0 ]]; do log_info "Health check attempt $((HEALTH_RETRIES - retries + 1))/$HEALTH_RETRIES" if walter_exec "curl -s --max-time $HEALTH_TIMEOUT $HEALTH_URL >/dev/null 2>&1"; then log_success "Health check passed" # Get health check response local health_response=$(walter_exec "curl -s --max-time $HEALTH_TIMEOUT $HEALTH_URL" || echo "") if [[ -n "$health_response" ]]; then log_info "Health response: $health_response" fi # Additional status info local process_status=$(get_service_status) local rcctl_status=$(get_rcctl_status) log_info "Service status - Process: $process_status, rcctl: $rcctl_status" return 0 fi ((retries--)) if [[ $retries -gt 0 ]]; then log_info "Health check failed, retrying in 5 seconds..." sleep 5 fi done log_error "Health check failed after $HEALTH_RETRIES attempts" log_info "Debugging service status..." local process_status=$(get_service_status) local rcctl_status=$(get_rcctl_status) log_info "Process status: $process_status" log_info "rcctl status: $rcctl_status" # Check if port is at least listening if walter_exec "netstat -an | grep -q ':8080.*LISTEN'"; then log_warning "Port 8080 is listening but health check failed - possible service issue" else log_error "Port 8080 is not listening - service not running" fi return 1 } # ============================================================================= # MAIN DEPLOYMENT FUNCTION # ============================================================================= deploy() { log_step "Starting deployment: karl → walter" # Pre-deployment checks check_dependencies prepare_source # Show deployment summary log_info "Deployment Summary:" log_info " Source: $SOURCE_DIR" log_info " Target: $WALTER_HOST:$TARGET_DIR" log_info " Service User: $SERVICE_USER" log_info " Dry Run: $DRY_RUN" if [[ "$DRY_RUN" == "true" ]]; then log_warning "DRY RUN MODE - No changes will be made" fi # Confirmation prompt if [[ "$FORCE" != "true" && "$DRY_RUN" != "true" ]]; then echo -n "Continue with deployment? [y/N]: " read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then log_info "Deployment cancelled" exit 0 fi fi # Create backup before deployment local backup_path="" if [[ "$DRY_RUN" != "true" ]]; then backup_path=$(create_backup) fi # Stop service if [[ "$DRY_RUN" != "true" ]]; then stop_service fi # Deploy files sync_files fix_permissions # Update service file for correct paths fix_service_file # Start service if [[ "$DRY_RUN" != "true" ]]; then # Check if port is available before starting if ! check_port_availability; then log_error "Cannot start service - port conflict detected" log_info "Try: ssh walter \"doas pkill -f 'lua.*main.lua'\" to kill conflicting processes" exit 1 fi if start_service; then # Health check if health_check; then log_success "Deployment completed successfully!" # Cleanup old backups cleanup_old_backups else log_error "Deployment failed health check" # Offer rollback if [[ -n "$backup_path" ]]; then echo -n "Rollback to previous version? [y/N]: " read -r response if [[ "$response" =~ ^[Yy]$ ]]; then log_info "Rolling back..." walter_exec_as_root "rm -rf $TARGET_DIR" walter_exec_as_root "cp -r $backup_path $TARGET_DIR" fix_permissions start_service health_check fi fi exit 1 fi else log_error "Failed to start service after deployment" exit 1 fi else log_success "Dry run completed - no changes made" fi } # ============================================================================= # MAIN SCRIPT LOGIC # ============================================================================= # Parse command line arguments DRY_RUN=false ROLLBACK=false FORCE=false while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true shift ;; --rollback) ROLLBACK=true shift ;; --force) FORCE=true shift ;; --help) usage exit 0 ;; *) log_error "Unknown option: $1" usage exit 1 ;; esac done # Main execution main() { log_info "Furt Deployment Script - karl → walter" log_info "$(date)" if [[ "$ROLLBACK" == "true" ]]; then rollback_deployment else deploy fi } # Trap for cleanup on exit cleanup() { if [[ $? -ne 0 ]]; then log_error "Deployment failed" fi } trap cleanup EXIT # Run main function main "$@"