Files
3cloud-backend/app/__init__.py
T

225 lines
8.3 KiB
Python

"""
Entry point for the xCloudify Flask application.
* Loads environment variables from a `.env` file.
* Configures Flask, SQLAlchemy, Celery, and Cloudflare settings.
* Masks sensitive values in logs for security.
"""
print("-----In init----------")
import uuid
import os
import pymysql
from flask import Flask, g, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from celery import Celery, Task
from flasgger import Swagger
from logger import logger
from app.utils.standard_responses import api_response
from flask_cors import CORS
from runtime_urls import API_BASE_URL, APP_ENV, BASE_DOMAIN, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID, DATABASE_URL, JWT_SECRET_KEY, PING_HEARTBEAT_TIMEOUT_SECONDS, REDIS_BROKER_URL, REDIS_RESULT_BACKEND_URL, REDIS_URL, SCHEDULER_MAX_ALLOCATION_ATTEMPTS, SCHEDULER_RETRY_BACKOFF_FACTOR, SCHEDULER_RETRY_BASE_DELAY_SECONDS, SCHEDULER_RETRY_JITTER_SECONDS, SCHEDULER_RETRY_MAX_DELAY_SECONDS, TUNNEL_DOMAIN, VNC_BASE_URL, VNC_PROXY_HOST, VNC_PROXY_PORT, VNC_PROXY_WS_PATH, VNC_SECRET_KEY, WEBSOCKET_SERVER_URL
# --------------------------------------------------------------------------- #
# 1. Flask application & database #
# --------------------------------------------------------------------------- #
app: Flask = Flask(__name__)
CORS(app)
# Set APP_ENV=development in prod .env to disable /apidocs and /apispec.json.
_env = APP_ENV.lower()
_swagger_enabled = _env == "development"
if _swagger_enabled:
swagger = Swagger(app, config={
"headers": [],
"specs": [
{
"endpoint": "apispec",
"route": "/apispec.json",
"rule_filter": lambda rule: True,
"model_filter": lambda tag: True,
}
],
"static_url_path": "/flasgger_static",
"swagger_ui": True,
"specs_route": "/apidocs",
}, template={
"info": {
"title": "xCloudify API",
"description": "REST API for the xCloudify platform.",
"version": "1.0.0",
"contact": {
"name": "xCloudify",
},
},
"securityDefinitions": {
"BearerAuth": {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": "JWT bearer token. Format: `Bearer <token>`",
}
},
"security": [{"BearerAuth": []}],
"consumes": ["application/json"],
"produces": ["application/json"],
})
logger.info("Swagger UI enabled at /apidocs (APP_ENV=%s)", _env)
else:
logger.info("Swagger UI disabled (APP_ENV=%s)", _env)
app.config.update(SQLALCHEMY_DATABASE_URI=DATABASE_URL, WEBSOCKET_SERVER_URL=f"{WEBSOCKET_SERVER_URL}/api/create_task", broker_url=REDIS_BROKER_URL, result_backend=REDIS_RESULT_BACKEND_URL)
app.config["SCHEDULER_MAX_ALLOCATION_ATTEMPTS"] = SCHEDULER_MAX_ALLOCATION_ATTEMPTS
app.config["SCHEDULER_RETRY_BASE_DELAY_SECONDS"] = SCHEDULER_RETRY_BASE_DELAY_SECONDS
app.config["SCHEDULER_RETRY_BACKOFF_FACTOR"] = SCHEDULER_RETRY_BACKOFF_FACTOR
app.config["SCHEDULER_RETRY_JITTER_SECONDS"] = SCHEDULER_RETRY_JITTER_SECONDS
app.config["SCHEDULER_RETRY_MAX_DELAY_SECONDS"] = SCHEDULER_RETRY_MAX_DELAY_SECONDS
# --------------------------------------------------------------------------- #
# 2. Cloudflare configuration #
# --------------------------------------------------------------------------- #
def _mask(value: str, visible: int = 4) -> str:
"""
Return a partially masked version of a sensitive string.
Args:
value (str): The string to mask.
visible (int): Number of visible characters to keep at the start.
Returns:
str: Masked string suitable for logging.
"""
if not value:
return ""
return f"{value[:visible]}{'*' * (len(value) - visible)}"
app.config["CLOUDFLARE_API_TOKEN"] = CLOUDFLARE_API_TOKEN
app.config["CLOUDFLARE_ACCOUNT_ID"] = CLOUDFLARE_ACCOUNT_ID
app.config["CLOUDFLARE_ZONE_ID"] = CLOUDFLARE_ZONE_ID
app.config["PING_HEARTBEAT_TIMEOUT_SECONDS"] = PING_HEARTBEAT_TIMEOUT_SECONDS # How long before a Websocket PING\PONG is classed as a failure and triggers a worker offline event
app.config["REDIS_URL"] = REDIS_URL # Redis Database 2 for operational tasks like websocket server and PING logging
app.config["VNC_SECRET_KEY"] = VNC_SECRET_KEY
app.config["API_BASE_URL"] = API_BASE_URL
app.config["BASE_DOMAIN"] = BASE_DOMAIN
app.config["TUNNEL_DOMAIN"] = TUNNEL_DOMAIN
app.config["VNC_BASE_URL"] = VNC_BASE_URL
app.config["VNC_PROXY_HOST"] = VNC_PROXY_HOST
app.config["VNC_PROXY_PORT"] = VNC_PROXY_PORT
app.config["VNC_PROXY_WS_PATH"] = VNC_PROXY_WS_PATH
# JWT secret used for decoding Authorization bearer tokens for audit attribution
app.config["JWT_SECRET_KEY"] = JWT_SECRET_KEY
logger.info(
"Cloudflare configuration set "
f"(token={_mask(app.config['CLOUDFLARE_API_TOKEN'])}, "
f"account_id={_mask(app.config['CLOUDFLARE_ACCOUNT_ID'])})"
f"zone_id={_mask(app.config['CLOUDFLARE_ZONE_ID'])})"
)
pymysql.install_as_MySQLdb()
db: SQLAlchemy = SQLAlchemy(app)
migrate: Migrate = Migrate(app, db)
app.secret_key = "your_secret_key_here"
# --------------------------------------------------------------------------- #
# 2. Celery initialisation #
# --------------------------------------------------------------------------- #
def _make_celery(flask_app: Flask) -> Celery:
"""
Create a Celery instance that shares the Flask application context.
Every task will inherit `current_app` and can use the database session
without manual `app.app_context()` juggling.
"""
celery = Celery(
flask_app.import_name,
broker=flask_app.config["broker_url"],
backend=flask_app.config["result_backend"],
)
celery.conf.update(flask_app.config)
class ContextTask(Task):
"""Celery Task base-class that wraps task execution in app_context."""
abstract = True
def __call__(self, *args, **kwargs):
with flask_app.app_context():
return super().__call__(*args, **kwargs)
celery.Task = ContextTask
return celery
celery_app: Celery = _make_celery(app)
celery_app.autodiscover_tasks(["app.tasks"], force=True)
app.extensions["celery"] = celery_app
# --------------------------------------------------------------------------- #
# 3. Blueprints and standard middleware #
# --------------------------------------------------------------------------- #
from app.controller import api_bp, ui_bp, celery_bp # noqa: E402 circular OK
app.register_blueprint(api_bp)
app.register_blueprint(ui_bp)
app.register_blueprint(celery_bp)
# ---------- Error-handlers, request IDs, context processors --------------- #
@app.errorhandler(500)
def handle_internal_server_error(error):
logger.error(
"Unhandled exception for %s %s (request_id=%s)",
request.method,
request.path,
getattr(g, "request_id", "n/a"),
exc_info=True,
)
return api_response(
success=False,
status=500,
message="Internal server error",
error_type="INTERNAL_SERVER_ERROR",
error_details={"path": request.path},
)
@app.errorhandler(404)
def not_found(e):
return api_response(
success=False, status=404, message=str(e), error_type="RESOURCE_NOT_FOUND"
)
@app.before_request
def assign_request_id():
g.request_id = str(uuid.uuid4())
@app.after_request
def log_request_id(response):
response.headers["X-Request-ID"] = g.request_id
return response
@app.context_processor
def inject_user_roles():
try:
user_details = {"name": "user.name", "roles": ["user_roles"]}
if "current_user" in g:
user_details = {
"id": str(g.current_user.id),
"friendly_id": g.current_user.friendly_id,
"email": g.current_user.email,
"name": g.current_user.name,
"roles": g.current_user.roles_text,
}
return dict(user_details=user_details)
except Exception as exc:
logger.error("inject_user_roles - %s", exc)
return {}
logger.debug("__init__ complete")