spawner/app.py
XPS\Micro c5c2678b65 fix: use spawn_multi_container instead of spawn_container for multi-container support
CRITICAL FIX for container routing bug:
- Replace all spawn_container() calls with spawn_multi_container()
- spawn_container() was overwriting primary container ID with single ID
- This caused all containers to route to same container-id
- Now each container_type gets its own route:
  - spawner.wieland.org/slug-template-01
  - spawner.wieland.org/slug-template-02
  - spawner.wieland.org/slug-template-next
- Affects: Signup, Login, Container Restart endpoints
- Fixes: #CONTAINER-ROUTING
2026-02-02 22:47:48 +01:00

215 lines
6.8 KiB
Python

from flask import Flask, render_template, redirect, url_for, jsonify
from flask_login import LoginManager, login_required, current_user
from flask_jwt_extended import JWTManager
from flask_cors import CORS
from flasgger import Swagger
from sqlalchemy import text
from models import db, User, AdminTakeoverSession
from auth import auth_bp
from api import api_bp, check_if_token_revoked
from admin_api import admin_bp
from config import Config
from container_manager import ContainerManager
import logging
from logging.handlers import RotatingFileHandler
import os
# Flask-App initialisieren
app = Flask(__name__)
app.config.from_object(Config)
# Datenbank initialisieren
db.init_app(app)
# CORS initialisieren
CORS(app, resources={
r"/api/*": {
"origins": app.config.get('CORS_ORIGINS', ['http://localhost:3000']),
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"],
"supports_credentials": True
}
})
# JWT initialisieren
jwt = JWTManager(app)
# Swagger/OpenAPI initialisieren
swagger_config = {
"headers": [],
"specs": [
{
"endpoint": 'openapi',
"route": '/openapi.json',
"rule_filter": lambda rule: True,
"model_filter": lambda tag: True,
}
],
"static_url_path": "/flasgger_static",
"swagger_ui": True,
"specs_route": "/swagger",
"title": "Container Spawner API",
"description": "API für Container-Spawner mit Admin-Debug-Endpoints",
"version": "2.0.0",
"termsOfService": "",
"contact": {
"name": "API Support"
}
}
swagger = Swagger(app, config=swagger_config)
@jwt.token_in_blocklist_loader
def check_if_token_in_blocklist(jwt_header, jwt_payload):
return check_if_token_revoked(jwt_header, jwt_payload)
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return jsonify({'error': 'Token abgelaufen'}), 401
@jwt.invalid_token_loader
def invalid_token_callback(error):
return jsonify({'error': 'Ungültiger Token'}), 401
@jwt.unauthorized_loader
def missing_token_callback(error):
return jsonify({'error': 'Authentifizierung erforderlich'}), 401
# ========================================
# Logging konfigurieren
# ========================================
log_file = app.config.get('LOG_FILE', '/app/logs/spawner.log')
log_dir = os.path.dirname(log_file)
# Erstelle Log-Verzeichnis falls nicht vorhanden
if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
# Rotating File Handler (max 10MB pro Datei, 5 Backups)
if log_file:
file_handler = RotatingFileHandler(
log_file,
maxBytes=10485760, # 10MB
backupCount=5
)
file_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
# Flask-Login initialisieren
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Bitte melde dich an, um auf diese Seite zuzugreifen.'
login_manager.login_message_category = 'error'
# Blueprints registrieren
app.register_blueprint(auth_bp)
app.register_blueprint(api_bp)
app.register_blueprint(admin_bp)
@login_manager.user_loader
def load_user(user_id):
"""Lädt User für Flask-Login"""
return User.query.get(int(user_id))
@app.route('/')
def index():
"""Startseite - Redirect zu Dashboard oder Login"""
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
return redirect(url_for('auth.login'))
@app.route('/dashboard')
@login_required
def dashboard():
"""Dashboard - zeigt Container-Status und Service-URL"""
container_mgr = ContainerManager()
container_status = 'unknown'
if current_user.container_id:
container_status = container_mgr.get_container_status(current_user.container_id)
# Service-URL für den User (pfad-basiert)
scheme = app.config['PREFERRED_URL_SCHEME']
spawner_domain = f"{app.config['SPAWNER_SUBDOMAIN']}.{app.config['BASE_DOMAIN']}"
service_url = f"{scheme}://{spawner_domain}/{current_user.slug}"
return render_template('dashboard.html',
user=current_user,
service_url=service_url,
container_status=container_status)
@app.route('/container/restart')
@login_required
def restart_container():
"""Startet Container des Users neu"""
container_mgr = ContainerManager()
# Alten Container stoppen falls vorhanden
if current_user.container_id:
container_mgr.stop_container(current_user.container_id)
container_mgr.remove_container(current_user.container_id)
# Neuen Container starten - Multi-Container kompatibel
try:
# Nutze spawn_multi_container für Primary Container
default_template = list(app.config['CONTAINER_TEMPLATES'].keys())[0]
container_id, port = container_mgr.spawn_multi_container(current_user.id, current_user.slug, default_template)
if current_user.containers:
current_user.containers[0].container_id = container_id
current_user.containers[0].container_port = port
db.session.commit()
except Exception as e:
app.logger.error(f"Container-Restart fehlgeschlagen: {str(e)}")
return redirect(url_for('dashboard'))
@app.route('/health')
def health():
"""Health-Check für Docker und Monitoring"""
db_status = 'ok'
docker_status = 'warning'
try:
# DB-Check (KRITISCH)
db.session.execute(text('SELECT 1'))
except Exception as e:
db_status = f'error: {str(e)}'
app.logger.error(f"Database health check failed: {str(e)}")
try:
# Docker-Check (OPTIONAL)
container_mgr = ContainerManager()
container_mgr._get_client().ping()
docker_status = 'ok'
except Exception as e:
docker_status = f'warning: {str(e)}'
app.logger.warning(f"Docker health check failed (non-critical): {str(e)}")
# Status 503 nur wenn DATABASE down ist, nicht wenn Docker down ist
status_code = 200 if db_status == 'ok' else 503
response = {
'status': 'healthy' if status_code == 200 else 'unhealthy',
'database': db_status,
'docker': docker_status,
'version': '1.0.0'
}
if status_code != 200:
app.logger.error(f"Health check CRITICAL: {response}")
else:
app.logger.info(f"Health check OK")
return response, status_code
# Datenbank-Tabellen erstellen beim ersten Start
with app.app_context():
db.create_all()
app.logger.info('Datenbank-Tabellen erstellt')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)