#!/bin/bash # # Golf Game Installer # # This script provides a menu-driven installation for the Golf card game. # Run with: ./scripts/install.sh # set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Get the directory where this script lives SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" echo -e "${BLUE}" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ Golf Game Installer ║" echo "╚══════════════════════════════════════════════════════════════╝" echo -e "${NC}" show_menu() { echo "" echo "Select an option:" echo "" echo " 1) Development Setup" echo " - Start Docker services (PostgreSQL, Redis)" echo " - Create Python virtual environment" echo " - Install dependencies" echo " - Create .env from template" echo "" echo " 2) Production Install to /opt/golfgame" echo " - Install application to /opt/golfgame" echo " - Create production .env" echo " - Set up systemd service" echo "" echo " 3) Docker Services Only" echo " - Start PostgreSQL and Redis containers" echo "" echo " 4) Create/Update Systemd Service" echo " - Create or update the systemd service file" echo "" echo " 5) Uninstall Production" echo " - Stop and remove systemd service" echo " - Optionally remove /opt/golfgame" echo "" echo " 6) Show Status" echo " - Check Docker containers" echo " - Check systemd service" echo " - Test endpoints" echo "" echo " q) Quit" echo "" } check_requirements() { local missing=() if ! command -v python3 &> /dev/null; then missing+=("python3") fi if ! command -v docker &> /dev/null; then missing+=("docker") fi if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then missing+=("docker-compose") fi if [ ${#missing[@]} -gt 0 ]; then echo -e "${RED}Missing required tools: ${missing[*]}${NC}" echo "Please install them before continuing." return 1 fi return 0 } start_docker_services() { echo -e "${BLUE}Starting Docker services...${NC}" cd "$PROJECT_DIR" if docker compose version &> /dev/null; then docker compose -f docker-compose.dev.yml up -d else docker-compose -f docker-compose.dev.yml up -d fi echo -e "${GREEN}Docker services started.${NC}" echo "" echo "Services:" echo " - PostgreSQL: localhost:5432 (user: golf, password: devpassword, db: golf)" echo " - Redis: localhost:6379" } setup_dev_venv() { echo -e "${BLUE}Setting up Python virtual environment...${NC}" cd "$PROJECT_DIR" # Check Python version PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') echo "Using Python $PYTHON_VERSION" # Remove old venv if it exists and is broken if [ -f "pyvenv.cfg" ]; then if [ -L "bin/python" ] && [ ! -e "bin/python" ]; then echo -e "${YELLOW}Removing broken virtual environment...${NC}" rm -rf bin lib lib64 pyvenv.cfg include share 2>/dev/null || true fi fi # Create venv if it doesn't exist if [ ! -f "pyvenv.cfg" ]; then echo "Creating virtual environment..." python3 -m venv . fi # Install dependencies echo "Installing dependencies..." ./bin/pip install --upgrade pip ./bin/pip install -e ".[dev]" echo -e "${GREEN}Virtual environment ready.${NC}" } setup_dev_env() { echo -e "${BLUE}Setting up .env file...${NC}" cd "$PROJECT_DIR" if [ -f ".env" ]; then echo -e "${YELLOW}.env file already exists. Overwrite? (y/N)${NC}" read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then echo "Keeping existing .env" return fi fi cat > .env << 'EOF' # Golf Game Development Configuration # Generated by install.sh HOST=0.0.0.0 PORT=8000 DEBUG=true LOG_LEVEL=DEBUG ENVIRONMENT=development # PostgreSQL (from docker-compose.dev.yml) DATABASE_URL=postgresql://golf:devpassword@localhost:5432/golf POSTGRES_URL=postgresql://golf:devpassword@localhost:5432/golf # Room Settings MAX_PLAYERS_PER_ROOM=6 ROOM_TIMEOUT_MINUTES=60 ROOM_CODE_LENGTH=4 # Game Defaults DEFAULT_ROUNDS=9 DEFAULT_INITIAL_FLIPS=2 DEFAULT_USE_JOKERS=false DEFAULT_FLIP_ON_DISCARD=false EOF echo -e "${GREEN}.env file created.${NC}" } dev_setup() { echo -e "${BLUE}=== Development Setup ===${NC}" echo "" if ! check_requirements; then return 1 fi start_docker_services echo "" setup_dev_venv echo "" setup_dev_env echo "" echo -e "${GREEN}=== Development Setup Complete ===${NC}" echo "" echo "To start the development server:" echo "" echo " cd $PROJECT_DIR/server" echo " ../bin/uvicorn main:app --reload --host 0.0.0.0 --port 8000" echo "" echo "Or use the helper script:" echo "" echo " $PROJECT_DIR/scripts/dev-server.sh" echo "" } prod_install() { echo -e "${BLUE}=== Production Installation ===${NC}" echo "" INSTALL_DIR="/opt/golfgame" # Check if running as root or with sudo available if [ "$EUID" -ne 0 ]; then if ! command -v sudo &> /dev/null; then echo -e "${RED}This option requires root privileges. Run with sudo or as root.${NC}" return 1 fi SUDO="sudo" else SUDO="" fi echo "This will install Golf Game to $INSTALL_DIR" echo -e "${YELLOW}Continue? (y/N)${NC}" read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then echo "Aborted." return fi # Create directory echo "Creating $INSTALL_DIR..." $SUDO mkdir -p "$INSTALL_DIR" # Copy files echo "Copying application files..." $SUDO cp -r "$PROJECT_DIR/server" "$INSTALL_DIR/" $SUDO cp -r "$PROJECT_DIR/client" "$INSTALL_DIR/" $SUDO cp "$PROJECT_DIR/pyproject.toml" "$INSTALL_DIR/" $SUDO cp "$PROJECT_DIR/README.md" "$INSTALL_DIR/" $SUDO cp "$PROJECT_DIR/INSTALL.md" "$INSTALL_DIR/" $SUDO cp "$PROJECT_DIR/.env.example" "$INSTALL_DIR/" $SUDO cp -r "$PROJECT_DIR/scripts" "$INSTALL_DIR/" # Create venv echo "Creating virtual environment..." $SUDO python3 -m venv "$INSTALL_DIR" $SUDO "$INSTALL_DIR/bin/pip" install --upgrade pip $SUDO "$INSTALL_DIR/bin/pip" install "$INSTALL_DIR" # Create production .env if it doesn't exist if [ ! -f "$INSTALL_DIR/.env" ]; then echo "Creating production .env..." # Generate a secret key SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") $SUDO tee "$INSTALL_DIR/.env" > /dev/null << EOF # Golf Game Production Configuration # Generated by install.sh HOST=0.0.0.0 PORT=8000 DEBUG=false LOG_LEVEL=INFO ENVIRONMENT=production # PostgreSQL - UPDATE THESE VALUES DATABASE_URL=postgresql://golf:CHANGE_ME@localhost:5432/golf POSTGRES_URL=postgresql://golf:CHANGE_ME@localhost:5432/golf # Security SECRET_KEY=$SECRET_KEY # Room Settings MAX_PLAYERS_PER_ROOM=6 ROOM_TIMEOUT_MINUTES=60 ROOM_CODE_LENGTH=4 # Game Defaults DEFAULT_ROUNDS=9 DEFAULT_INITIAL_FLIPS=2 DEFAULT_USE_JOKERS=false DEFAULT_FLIP_ON_DISCARD=false # Optional: Sentry error tracking # SENTRY_DSN=https://your-sentry-dsn EOF $SUDO chmod 600 "$INSTALL_DIR/.env" fi # Set ownership echo "Setting permissions..." $SUDO chown -R www-data:www-data "$INSTALL_DIR" echo -e "${GREEN}Application installed to $INSTALL_DIR${NC}" echo "" echo -e "${YELLOW}IMPORTANT: Edit $INSTALL_DIR/.env and update:${NC}" echo " - DATABASE_URL / POSTGRES_URL with your PostgreSQL credentials" echo " - Any other settings as needed" echo "" # Offer to set up systemd echo "Set up systemd service now? (Y/n)" read -r response if [[ ! "$response" =~ ^[Nn]$ ]]; then setup_systemd fi } setup_systemd() { echo -e "${BLUE}=== Systemd Service Setup ===${NC}" echo "" INSTALL_DIR="/opt/golfgame" SERVICE_FILE="/etc/systemd/system/golfgame.service" if [ "$EUID" -ne 0 ]; then if ! command -v sudo &> /dev/null; then echo -e "${RED}This option requires root privileges.${NC}" return 1 fi SUDO="sudo" else SUDO="" fi if [ ! -d "$INSTALL_DIR" ]; then echo -e "${RED}$INSTALL_DIR does not exist. Run production install first.${NC}" return 1 fi echo "Creating systemd service..." $SUDO tee "$SERVICE_FILE" > /dev/null << 'EOF' [Unit] Description=Golf Card Game Server Documentation=https://github.com/alee/golfgame After=network.target postgresql.service redis.service Wants=postgresql.service redis.service [Service] Type=simple User=www-data Group=www-data WorkingDirectory=/opt/golfgame/server Environment="PATH=/opt/golfgame/bin:/usr/local/bin:/usr/bin:/bin" EnvironmentFile=/opt/golfgame/.env ExecStart=/opt/golfgame/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 ExecReload=/bin/kill -HUP $MAINPID Restart=always RestartSec=5 StandardOutput=journal StandardError=journal # Security hardening NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/opt/golfgame [Install] WantedBy=multi-user.target EOF echo "Reloading systemd..." $SUDO systemctl daemon-reload echo "Enabling service..." $SUDO systemctl enable golfgame echo -e "${GREEN}Systemd service created.${NC}" echo "" echo "Commands:" echo " sudo systemctl start golfgame # Start the service" echo " sudo systemctl stop golfgame # Stop the service" echo " sudo systemctl restart golfgame # Restart the service" echo " sudo systemctl status golfgame # Check status" echo " journalctl -u golfgame -f # View logs" echo "" echo "Start the service now? (Y/n)" read -r response if [[ ! "$response" =~ ^[Nn]$ ]]; then $SUDO systemctl start golfgame sleep 2 $SUDO systemctl status golfgame --no-pager fi } uninstall_prod() { echo -e "${BLUE}=== Production Uninstall ===${NC}" echo "" if [ "$EUID" -ne 0 ]; then if ! command -v sudo &> /dev/null; then echo -e "${RED}This option requires root privileges.${NC}" return 1 fi SUDO="sudo" else SUDO="" fi echo -e "${YELLOW}This will stop and remove the systemd service.${NC}" echo "Continue? (y/N)" read -r response if [[ ! "$response" =~ ^[Yy]$ ]]; then echo "Aborted." return fi # Stop and disable service if [ -f "/etc/systemd/system/golfgame.service" ]; then echo "Stopping service..." $SUDO systemctl stop golfgame 2>/dev/null || true $SUDO systemctl disable golfgame 2>/dev/null || true $SUDO rm -f /etc/systemd/system/golfgame.service $SUDO systemctl daemon-reload echo "Service removed." else echo "No systemd service found." fi # Optionally remove installation directory if [ -d "/opt/golfgame" ]; then echo "" echo -e "${YELLOW}Remove /opt/golfgame directory? (y/N)${NC}" read -r response if [[ "$response" =~ ^[Yy]$ ]]; then $SUDO rm -rf /opt/golfgame echo "Directory removed." else echo "Directory kept." fi fi echo -e "${GREEN}Uninstall complete.${NC}" } show_status() { echo -e "${BLUE}=== Status Check ===${NC}" echo "" # Docker containers echo "Docker Containers:" if command -v docker &> /dev/null; then docker ps --filter "name=golfgame" --format " {{.Names}}: {{.Status}}" 2>/dev/null || echo " (none running)" echo "" else echo " Docker not installed" echo "" fi # Systemd service echo "Systemd Service:" if [ -f "/etc/systemd/system/golfgame.service" ]; then systemctl status golfgame --no-pager 2>/dev/null | head -5 || echo " Service not running" else echo " Not installed" fi echo "" # Health check echo "Health Check:" for port in 8000; do if curl -s "http://localhost:$port/health" > /dev/null 2>&1; then response=$(curl -s "http://localhost:$port/health") echo -e " Port $port: ${GREEN}OK${NC} - $response" else echo -e " Port $port: ${RED}Not responding${NC}" fi done echo "" # Database echo "PostgreSQL:" if command -v pg_isready &> /dev/null; then if pg_isready -h localhost -p 5432 > /dev/null 2>&1; then echo -e " ${GREEN}Running${NC} on localhost:5432" else echo -e " ${RED}Not responding${NC}" fi else if docker ps --filter "name=postgres" --format "{{.Names}}" 2>/dev/null | grep -q postgres; then echo -e " ${GREEN}Running${NC} (Docker)" else echo " Unable to check (pg_isready not installed)" fi fi echo "" # Redis echo "Redis:" if command -v redis-cli &> /dev/null; then if redis-cli ping > /dev/null 2>&1; then echo -e " ${GREEN}Running${NC} on localhost:6379" else echo -e " ${RED}Not responding${NC}" fi else if docker ps --filter "name=redis" --format "{{.Names}}" 2>/dev/null | grep -q redis; then echo -e " ${GREEN}Running${NC} (Docker)" else echo " Unable to check (redis-cli not installed)" fi fi } # Main loop while true; do show_menu echo -n "Enter choice: " read -r choice case $choice in 1) dev_setup ;; 2) prod_install ;; 3) start_docker_services ;; 4) setup_systemd ;; 5) uninstall_prod ;; 6) show_status ;; q|Q) echo "Goodbye!"; exit 0 ;; *) echo -e "${RED}Invalid option${NC}" ;; esac echo "" echo "Press Enter to continue..." read -r done