diff --git a/backend/app_new.py b/backend/app_new.py deleted file mode 100644 index af41d7f..0000000 --- a/backend/app_new.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Main application entry point.""" -from flask import Flask -from flask_cors import CORS -from flask_socketio import SocketIO - -from config import logger -from routes.auth import auth_bp -from routes.containers import containers_bp -from routes.health import health_bp -from handlers.terminal import register_terminal_handlers -from utils.docker_client import diagnose_docker_environment, get_docker_client - -# Initialize Flask app -app = Flask(__name__) -CORS(app, resources={r"/*": {"origins": "*"}}) - -# Initialize SocketIO -socketio = SocketIO( - app, - cors_allowed_origins="*", - async_mode='threading', - ping_timeout=60, - ping_interval=25, - logger=True, - engineio_logger=True -) - -# Register blueprints -app.register_blueprint(auth_bp) -app.register_blueprint(containers_bp) -app.register_blueprint(health_bp) - -# Register WebSocket handlers -register_terminal_handlers(socketio) - - -if __name__ == '__main__': - # Run diagnostics on startup - logger.info("Backend server starting...") - diagnose_docker_environment() - - # Try to get Docker client and log result - test_client = get_docker_client() - if test_client: - logger.info("✓ Docker connection verified on startup") - else: - logger.error("✗ Docker connection FAILED on startup - check logs above for details") - - socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True) diff --git a/backend/app_old.py b/backend/app_old.py deleted file mode 100644 index 8d044c8..0000000 --- a/backend/app_old.py +++ /dev/null @@ -1,631 +0,0 @@ -from flask import Flask, jsonify, request -from flask_cors import CORS -from flask_socketio import SocketIO, emit, disconnect -import docker -import os -import sys -import logging -import threading -import select -from datetime import datetime, timedelta - -# Configure logging -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler(sys.stdout) - ] -) -logger = logging.getLogger(__name__) - -app = Flask(__name__) -CORS(app, resources={r"/*": {"origins": "*"}}) -socketio = SocketIO( - app, - cors_allowed_origins="*", - async_mode='threading', - ping_timeout=60, - ping_interval=25, - logger=True, - engineio_logger=True -) - -# Simple in-memory session storage (in production, use proper session management) -sessions = {} -# Track working directory per session -session_workdirs = {} - -# Default credentials (should be environment variables in production) -ADMIN_USERNAME = os.getenv('ADMIN_USERNAME', 'admin') -ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin123') - -def diagnose_docker_environment(): - """Diagnose Docker environment and configuration""" - logger.info("=== Docker Environment Diagnosis ===") - - # Check environment variables - docker_host = os.getenv('DOCKER_HOST', 'Not set') - docker_cert_path = os.getenv('DOCKER_CERT_PATH', 'Not set') - docker_tls_verify = os.getenv('DOCKER_TLS_VERIFY', 'Not set') - - logger.info(f"DOCKER_HOST: {docker_host}") - logger.info(f"DOCKER_CERT_PATH: {docker_cert_path}") - logger.info(f"DOCKER_TLS_VERIFY: {docker_tls_verify}") - - # Check what's in /var/run - logger.info("Checking /var/run directory contents:") - try: - if os.path.exists('/var/run'): - var_run_contents = os.listdir('/var/run') - logger.info(f" /var/run contains: {var_run_contents}") - - # Check for any Docker-related files - docker_related = [f for f in var_run_contents if 'docker' in f.lower()] - if docker_related: - logger.info(f" Docker-related files/dirs found: {docker_related}") - else: - logger.warning(" /var/run directory doesn't exist") - except Exception as e: - logger.error(f" Error reading /var/run: {e}") - - # Check Docker socket - socket_path = '/var/run/docker.sock' - logger.info(f"Checking Docker socket at {socket_path}") - - if os.path.exists(socket_path): - logger.info(f"✓ Docker socket exists at {socket_path}") - - # Check permissions - import stat - st = os.stat(socket_path) - logger.info(f" Socket permissions: {oct(st.st_mode)}") - logger.info(f" Socket owner UID: {st.st_uid}") - logger.info(f" Socket owner GID: {st.st_gid}") - - # Check if readable/writable - readable = os.access(socket_path, os.R_OK) - writable = os.access(socket_path, os.W_OK) - logger.info(f" Readable: {readable}") - logger.info(f" Writable: {writable}") - - if not (readable and writable): - logger.warning(f"⚠ Socket exists but lacks proper permissions!") - else: - logger.error(f"✗ Docker socket NOT found at {socket_path}") - logger.error(f" This means the Docker socket mount is NOT configured in CapRover") - logger.error(f" The serviceUpdateOverride in captain-definition may not be applied") - - # Check current user - import pwd - try: - current_uid = os.getuid() - current_gid = os.getgid() - user_info = pwd.getpwuid(current_uid) - logger.info(f"Current user: {user_info.pw_name} (UID: {current_uid}, GID: {current_gid})") - - # Check groups - import grp - groups = os.getgroups() - logger.info(f"User groups (GIDs): {groups}") - - for gid in groups: - try: - group_info = grp.getgrgid(gid) - logger.info(f" - {group_info.gr_name} (GID: {gid})") - except: - logger.info(f" - Unknown group (GID: {gid})") - except Exception as e: - logger.error(f"Error checking user info: {e}") - - logger.info("=== End Diagnosis ===") - -def get_docker_client(): - """Get Docker client with enhanced error reporting""" - try: - logger.info("Attempting to connect to Docker...") - - # Try default connection first - try: - client = docker.from_env() - # Test the connection - client.ping() - logger.info("✓ Successfully connected to Docker using docker.from_env()") - return client - except Exception as e: - logger.warning(f"docker.from_env() failed: {e}") - - # Try explicit Unix socket connection - try: - logger.info("Trying explicit Unix socket connection...") - client = docker.DockerClient(base_url='unix:///var/run/docker.sock') - client.ping() - logger.info("✓ Successfully connected to Docker using Unix socket") - return client - except Exception as e: - logger.warning(f"Unix socket connection failed: {e}") - - # If all fails, run diagnostics and return None - logger.error("All Docker connection attempts failed!") - diagnose_docker_environment() - return None - - except Exception as e: - logger.error(f"Unexpected error in get_docker_client: {e}", exc_info=True) - return None - -def format_uptime(created_at): - """Format container uptime""" - created = datetime.fromisoformat(created_at.replace('Z', '+00:00')) - now = datetime.now(created.tzinfo) - delta = now - created - - days = delta.days - hours = delta.seconds // 3600 - minutes = (delta.seconds % 3600) // 60 - - if days > 0: - return f"{days}d {hours}h" - elif hours > 0: - return f"{hours}h {minutes}m" - else: - return f"{minutes}m" - -@app.route('/api/auth/login', methods=['POST']) -def login(): - """Authenticate user""" - data = request.get_json() - username = data.get('username') - password = data.get('password') - - if username == ADMIN_USERNAME and password == ADMIN_PASSWORD: - # Create a simple session token (in production, use JWT or proper session management) - session_token = f"session_{username}_{datetime.now().timestamp()}" - sessions[session_token] = { - 'username': username, - 'created_at': datetime.now() - } - return jsonify({ - 'success': True, - 'token': session_token, - 'username': username - }) - - return jsonify({ - 'success': False, - 'message': 'Invalid credentials' - }), 401 - -@app.route('/api/auth/logout', methods=['POST']) -def logout(): - """Logout user""" - auth_header = request.headers.get('Authorization') - if auth_header and auth_header.startswith('Bearer '): - token = auth_header.split(' ')[1] - if token in sessions: - del sessions[token] - - return jsonify({'success': True}) - -@app.route('/api/containers', methods=['GET']) -def get_containers(): - """Get list of all containers""" - auth_header = request.headers.get('Authorization') - if not auth_header or not auth_header.startswith('Bearer '): - return jsonify({'error': 'Unauthorized'}), 401 - - token = auth_header.split(' ')[1] - if token not in sessions: - return jsonify({'error': 'Invalid session'}), 401 - - client = get_docker_client() - if not client: - return jsonify({'error': 'Cannot connect to Docker'}), 500 - - try: - containers = client.containers.list(all=True) - container_list = [] - - for container in containers: - container_list.append({ - 'id': container.short_id, - 'name': container.name, - 'image': container.image.tags[0] if container.image.tags else 'unknown', - 'status': container.status, - 'uptime': format_uptime(container.attrs['Created']) if container.status == 'running' else 'N/A' - }) - - return jsonify({'containers': container_list}) - except Exception as e: - return jsonify({'error': str(e)}), 500 - -@app.route('/api/containers//exec', methods=['POST']) -def exec_container(container_id): - """Execute command in container""" - auth_header = request.headers.get('Authorization') - if not auth_header or not auth_header.startswith('Bearer '): - return jsonify({'error': 'Unauthorized'}), 401 - - token = auth_header.split(' ')[1] - if token not in sessions: - return jsonify({'error': 'Invalid session'}), 401 - - data = request.get_json() - user_command = data.get('command', 'echo "No command provided"') - - client = get_docker_client() - if not client: - return jsonify({'error': 'Cannot connect to Docker'}), 500 - - try: - container = client.containers.get(container_id) - - # Get or initialize session working directory - session_key = f"{token}_{container_id}" - if session_key not in session_workdirs: - # Get container's default working directory or use root - session_workdirs[session_key] = '/' - - current_workdir = session_workdirs[session_key] - - # Check if this is a cd command - cd_match = user_command.strip() - is_cd_command = cd_match.startswith('cd ') - - # If it's a cd command, handle it specially - if is_cd_command: - target_dir = cd_match[3:].strip() or '~' - # Resolve the new directory and update session - resolve_command = f'cd "{current_workdir}" && cd {target_dir} && pwd' - bash_command = [ - '/bin/bash', - '-c', - f'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; {resolve_command}' - ] - else: - # Regular command - execute in current working directory - bash_command = [ - '/bin/bash', - '-c', - f'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; cd "{current_workdir}" && {user_command}; echo "::WORKDIR::$(pwd)"' - ] - - # Try bash first, fallback to sh if bash doesn't exist - try: - exec_instance = container.exec_run( - bash_command, - stdout=True, - stderr=True, - stdin=False, - tty=True, - environment={'TERM': 'xterm-256color', 'LANG': 'C.UTF-8'} - ) - except Exception as bash_error: - logger.warning(f"Bash execution failed, trying sh: {bash_error}") - # Fallback to sh - if is_cd_command: - target_dir = cd_match[3:].strip() or '~' - resolve_command = f'cd "{current_workdir}" && cd {target_dir} && pwd' - sh_command = ['/bin/sh', '-c', f'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; {resolve_command}'] - else: - sh_command = ['/bin/sh', '-c', f'export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; cd "{current_workdir}" && {user_command}; echo "::WORKDIR::$(pwd)"'] - - exec_instance = container.exec_run( - sh_command, - stdout=True, - stderr=True, - stdin=False, - tty=True, - environment={'TERM': 'xterm-256color', 'LANG': 'C.UTF-8'} - ) - - # Decode output with error handling - output = '' - if exec_instance.output: - try: - output = exec_instance.output.decode('utf-8') - except UnicodeDecodeError: - # Try latin-1 as fallback - output = exec_instance.output.decode('latin-1', errors='replace') - - # Extract and update working directory from output - new_workdir = current_workdir - if is_cd_command: - # For cd commands, the output is the new pwd - new_workdir = output.strip() - session_workdirs[session_key] = new_workdir - output = '' # Don't show the pwd output for cd - else: - # Extract workdir marker from output - if '::WORKDIR::' in output: - parts = output.rsplit('::WORKDIR::', 1) - output = parts[0] - new_workdir = parts[1].strip() - session_workdirs[session_key] = new_workdir - - return jsonify({ - 'output': output, - 'exit_code': exec_instance.exit_code, - 'workdir': new_workdir - }) - except Exception as e: - logger.error(f"Error executing command: {e}", exc_info=True) - return jsonify({'error': str(e)}), 500 - -@app.route('/api/containers//start', methods=['POST']) -def start_container(container_id): - """Start a stopped container""" - auth_header = request.headers.get('Authorization') - if not auth_header or not auth_header.startswith('Bearer '): - return jsonify({'error': 'Unauthorized'}), 401 - - token = auth_header.split(' ')[1] - if token not in sessions: - return jsonify({'error': 'Invalid session'}), 401 - - client = get_docker_client() - if not client: - return jsonify({'error': 'Cannot connect to Docker'}), 500 - - try: - container = client.containers.get(container_id) - container.start() - logger.info(f"Started container {container_id}") - return jsonify({'success': True, 'message': f'Container {container_id} started'}) - except Exception as e: - logger.error(f"Error starting container: {e}", exc_info=True) - return jsonify({'error': str(e)}), 500 - -@app.route('/api/containers//stop', methods=['POST']) -def stop_container(container_id): - """Stop a running container""" - auth_header = request.headers.get('Authorization') - if not auth_header or not auth_header.startswith('Bearer '): - return jsonify({'error': 'Unauthorized'}), 401 - - token = auth_header.split(' ')[1] - if token not in sessions: - return jsonify({'error': 'Invalid session'}), 401 - - client = get_docker_client() - if not client: - return jsonify({'error': 'Cannot connect to Docker'}), 500 - - try: - container = client.containers.get(container_id) - container.stop() - logger.info(f"Stopped container {container_id}") - return jsonify({'success': True, 'message': f'Container {container_id} stopped'}) - except Exception as e: - logger.error(f"Error stopping container: {e}", exc_info=True) - return jsonify({'error': str(e)}), 500 - -@app.route('/api/containers//restart', methods=['POST']) -def restart_container(container_id): - """Restart a container""" - auth_header = request.headers.get('Authorization') - if not auth_header or not auth_header.startswith('Bearer '): - return jsonify({'error': 'Unauthorized'}), 401 - - token = auth_header.split(' ')[1] - if token not in sessions: - return jsonify({'error': 'Invalid session'}), 401 - - client = get_docker_client() - if not client: - return jsonify({'error': 'Cannot connect to Docker'}), 500 - - try: - container = client.containers.get(container_id) - container.restart() - logger.info(f"Restarted container {container_id}") - return jsonify({'success': True, 'message': f'Container {container_id} restarted'}) - except Exception as e: - logger.error(f"Error restarting container: {e}", exc_info=True) - return jsonify({'error': str(e)}), 500 - -@app.route('/api/containers/', methods=['DELETE']) -def remove_container(container_id): - """Remove a container""" - auth_header = request.headers.get('Authorization') - if not auth_header or not auth_header.startswith('Bearer '): - return jsonify({'error': 'Unauthorized'}), 401 - - token = auth_header.split(' ')[1] - if token not in sessions: - return jsonify({'error': 'Invalid session'}), 401 - - client = get_docker_client() - if not client: - return jsonify({'error': 'Cannot connect to Docker'}), 500 - - try: - container = client.containers.get(container_id) - # Force remove (including if running) - container.remove(force=True) - logger.info(f"Removed container {container_id}") - return jsonify({'success': True, 'message': f'Container {container_id} removed'}) - except Exception as e: - logger.error(f"Error removing container: {e}", exc_info=True) - return jsonify({'error': str(e)}), 500 - -@app.route('/api/health', methods=['GET']) -def health(): - """Health check endpoint""" - return jsonify({'status': 'healthy'}) - -# WebSocket handlers for interactive terminal -active_terminals = {} - -@socketio.on('connect', namespace='/terminal') -def handle_connect(): - """Handle WebSocket connection""" - logger.info(f"Client connected to terminal WebSocket: {request.sid}") - -@socketio.on('disconnect', namespace='/terminal') -def handle_disconnect(): - """Handle WebSocket disconnection""" - logger.info(f"Client disconnected from terminal WebSocket: {request.sid}") - # Clean up any active terminal sessions - if request.sid in active_terminals: - try: - exec_instance = active_terminals[request.sid]['exec'] - # Try to stop the exec instance - if hasattr(exec_instance, 'kill'): - exec_instance.kill() - except: - pass - del active_terminals[request.sid] - -@socketio.on('start_terminal', namespace='/terminal') -def handle_start_terminal(data): - """Start an interactive terminal session""" - try: - container_id = data.get('container_id') - token = data.get('token') - cols = data.get('cols', 80) - rows = data.get('rows', 24) - - # Validate token - if not token or token not in sessions: - emit('error', {'error': 'Unauthorized'}) - disconnect() - return - - # Get Docker client and container - client = get_docker_client() - if not client: - emit('error', {'error': 'Cannot connect to Docker'}) - return - - container = client.containers.get(container_id) - - # Create an interactive bash session with PTY - exec_instance = container.exec_run( - ['/bin/bash'], - stdin=True, - stdout=True, - stderr=True, - tty=True, - socket=True, - environment={ - 'TERM': 'xterm-256color', - 'COLUMNS': str(cols), - 'LINES': str(rows), - 'LANG': 'C.UTF-8' - } - ) - - # Store the exec instance - active_terminals[request.sid] = { - 'exec': exec_instance, - 'container_id': container_id - } - - # Capture request.sid before starting thread to avoid context issues - sid = request.sid - - # Start a thread to read from the container and send to client - def read_output(): - sock = exec_instance.output - try: - while True: - # Check if socket is still connected - if sid not in active_terminals: - break - - try: - # Read data from container - data = sock.recv(4096) - if not data: - break - - # Send to client - try: - decoded_data = data.decode('utf-8') - except UnicodeDecodeError: - decoded_data = data.decode('latin-1', errors='replace') - - socketio.emit('output', {'data': decoded_data}, - namespace='/terminal', room=sid) - except Exception as e: - logger.error(f"Error reading from container: {e}") - break - finally: - # Clean up - if sid in active_terminals: - del active_terminals[sid] - try: - sock.close() - except: - pass - socketio.emit('exit', {'code': 0}, - namespace='/terminal', room=sid) - - # Start the output reader thread - output_thread = threading.Thread(target=read_output, daemon=True) - output_thread.start() - - emit('started', {'message': 'Terminal started'}) - - except Exception as e: - logger.error(f"Error starting terminal: {e}", exc_info=True) - emit('error', {'error': str(e)}) - -@socketio.on('input', namespace='/terminal') -def handle_input(data): - """Handle input from the client""" - try: - if request.sid not in active_terminals: - emit('error', {'error': 'No active terminal session'}) - return - - terminal_data = active_terminals[request.sid] - exec_instance = terminal_data['exec'] - input_data = data.get('data', '') - - # Send input to the container - sock = exec_instance.output - # Access the underlying socket for sendall method - if hasattr(sock, '_sock'): - sock._sock.sendall(input_data.encode('utf-8')) - else: - # Fallback for direct socket objects - sock.sendall(input_data.encode('utf-8')) - - except Exception as e: - logger.error(f"Error sending input: {e}", exc_info=True) - emit('error', {'error': str(e)}) - -@socketio.on('resize', namespace='/terminal') -def handle_resize(data): - """Handle terminal resize""" - try: - cols = data.get('cols', 80) - rows = data.get('rows', 24) - - if request.sid in active_terminals: - terminal_data = active_terminals[request.sid] - exec_instance = terminal_data['exec'] - - # Note: Docker exec_run doesn't support resizing after creation - # This is a limitation of the Docker API - # We acknowledge the resize but can't actually resize the PTY - logger.info(f"Terminal resize requested: {cols}x{rows}") - - except Exception as e: - logger.error(f"Error resizing terminal: {e}", exc_info=True) - -if __name__ == '__main__': - # Run diagnostics on startup - logger.info("Backend server starting...") - diagnose_docker_environment() - - # Try to get Docker client and log result - test_client = get_docker_client() - if test_client: - logger.info("✓ Docker connection verified on startup") - else: - logger.error("✗ Docker connection FAILED on startup - check logs above for details") - - socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)