mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 17:21:58 -06:00
Bridge legacy API registration through Engine
This commit is contained in:
@@ -27,10 +27,10 @@ Lastly, everytime that you complete a stage, you will create a pull request name
|
|||||||
- [x] Provide fixtures that mirror the legacy SQLite schema and seed data.
|
- [x] Provide fixtures that mirror the legacy SQLite schema and seed data.
|
||||||
- [x] Assert HTTP status codes, payloads, and side effects for parity.
|
- [x] Assert HTTP status codes, payloads, and side effects for parity.
|
||||||
- [x] Integrate Engine API tests into CI/local workflows.
|
- [x] Integrate Engine API tests into CI/local workflows.
|
||||||
- [ ] **Stage 5 — Bridge the legacy server to Engine APIs**
|
- [x] **Stage 5 — Bridge the legacy server to Engine APIs**
|
||||||
- [ ] Delegate API blueprint registration to the Engine factory from the legacy server.
|
- [x] Delegate API blueprint registration to the Engine factory from the legacy server.
|
||||||
- [ ] Replace legacy API routes with Engine-provided blueprints gated by a flag.
|
- [x] Replace legacy API routes with Engine-provided blueprints gated by a flag.
|
||||||
- [ ] Emit transitional logging when Engine handles requests.
|
- [x] Emit transitional logging when Engine handles requests.
|
||||||
- [ ] **Stage 6 — Plan WebUI migration**
|
- [ ] **Stage 6 — Plan WebUI migration**
|
||||||
- [ ] Move static/template handling into Data/Engine/services/WebUI.
|
- [ ] Move static/template handling into Data/Engine/services/WebUI.
|
||||||
- [ ] Preserve TLS-aware URL generation and caching.
|
- [ ] Preserve TLS-aware URL generation and caching.
|
||||||
@@ -43,5 +43,5 @@ Lastly, everytime that you complete a stage, you will create a pull request name
|
|||||||
- [ ] Update legacy server to consume Engine WebSocket registration.
|
- [ ] Update legacy server to consume Engine WebSocket registration.
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
- **Stage:** Stage 4 — Build unit and smoke tests for Engine APIs (completed)
|
- **Stage:** Stage 5 — Bridge the legacy server to Engine APIs (completed)
|
||||||
- **Active Task:** Awaiting next stage instructions.
|
- **Active Task:** Awaiting next stage instructions.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Mapping, Optional, Sequence, Tuple
|
from typing import Any, Mapping, Optional, Sequence, Tuple
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
from flask import Flask
|
from flask import Flask, request
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
@@ -106,7 +106,46 @@ class EngineContext:
|
|||||||
api_groups: Sequence[str]
|
api_groups: Sequence[str]
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["EngineContext", "create_app"]
|
__all__ = ["EngineContext", "create_app", "register_engine_api"]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_engine_context(settings: EngineSettings, logger: logging.Logger) -> EngineContext:
|
||||||
|
return EngineContext(
|
||||||
|
database_path=settings.database_path,
|
||||||
|
logger=logger,
|
||||||
|
scheduler=None,
|
||||||
|
tls_cert_path=settings.tls_cert_path,
|
||||||
|
tls_key_path=settings.tls_key_path,
|
||||||
|
tls_bundle_path=settings.tls_bundle_path,
|
||||||
|
config=settings.as_dict(),
|
||||||
|
api_groups=settings.api_groups,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_transition_logging(app: Flask, context: EngineContext, logger: logging.Logger) -> None:
|
||||||
|
tracked = {group.strip().lower() for group in context.api_groups if group}
|
||||||
|
if not tracked:
|
||||||
|
tracked = {"tokens", "enrollment"}
|
||||||
|
|
||||||
|
existing = getattr(app, "_engine_api_tracked_blueprints", set())
|
||||||
|
if existing:
|
||||||
|
tracked.update(existing)
|
||||||
|
setattr(app, "_engine_api_tracked_blueprints", tracked)
|
||||||
|
|
||||||
|
if getattr(app, "_engine_api_logging_installed", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def _log_engine_api_bridge() -> None: # pragma: no cover - integration behaviour exercised in higher-level tests
|
||||||
|
blueprint = (request.blueprint or "").lower()
|
||||||
|
if blueprint and blueprint in getattr(app, "_engine_api_tracked_blueprints", tracked):
|
||||||
|
logger.info(
|
||||||
|
"Engine handling API request via legacy bridge: %s %s",
|
||||||
|
request.method,
|
||||||
|
request.path,
|
||||||
|
)
|
||||||
|
|
||||||
|
setattr(app, "_engine_api_logging_installed", True)
|
||||||
|
|
||||||
|
|
||||||
def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, SocketIO, EngineContext]:
|
def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, SocketIO, EngineContext]:
|
||||||
@@ -115,8 +154,6 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke
|
|||||||
settings: EngineSettings = load_runtime_config(config)
|
settings: EngineSettings = load_runtime_config(config)
|
||||||
logger = initialise_engine_logger(settings)
|
logger = initialise_engine_logger(settings)
|
||||||
|
|
||||||
database_path = settings.database_path
|
|
||||||
|
|
||||||
static_folder = settings.static_folder
|
static_folder = settings.static_folder
|
||||||
app = Flask(__name__, static_folder=static_folder, static_url_path="")
|
app = Flask(__name__, static_folder=static_folder, static_url_path="")
|
||||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
|
||||||
@@ -141,22 +178,7 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
tls_cert_path, tls_key_path, tls_bundle_path = (
|
context = _build_engine_context(settings, logger)
|
||||||
settings.tls_cert_path,
|
|
||||||
settings.tls_key_path,
|
|
||||||
settings.tls_bundle_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
context = EngineContext(
|
|
||||||
database_path=database_path,
|
|
||||||
logger=logger,
|
|
||||||
scheduler=None,
|
|
||||||
tls_cert_path=tls_cert_path,
|
|
||||||
tls_key_path=tls_key_path,
|
|
||||||
tls_bundle_path=tls_bundle_path,
|
|
||||||
config=settings.as_dict(),
|
|
||||||
api_groups=settings.api_groups,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .services import API, WebSocket, WebUI # Local import to avoid circular deps during bootstrap
|
from .services import API, WebSocket, WebUI # Local import to avoid circular deps during bootstrap
|
||||||
|
|
||||||
@@ -167,3 +189,21 @@ def create_app(config: Optional[Mapping[str, Any]] = None) -> Tuple[Flask, Socke
|
|||||||
logger.debug("Engine application factory completed initialisation.")
|
logger.debug("Engine application factory completed initialisation.")
|
||||||
|
|
||||||
return app, socketio, context
|
return app, socketio, context
|
||||||
|
|
||||||
|
|
||||||
|
def register_engine_api(app: Flask, *, config: Optional[Mapping[str, Any]] = None) -> EngineContext:
|
||||||
|
"""Register Engine-managed API blueprints onto an existing Flask app."""
|
||||||
|
|
||||||
|
settings: EngineSettings = load_runtime_config(config)
|
||||||
|
logger = initialise_engine_logger(settings)
|
||||||
|
context = _build_engine_context(settings, logger)
|
||||||
|
|
||||||
|
from .services import API # Local import avoids circular dependency at module import time
|
||||||
|
|
||||||
|
API.register_api(app, context)
|
||||||
|
_attach_transition_logging(app, context, logger)
|
||||||
|
|
||||||
|
groups_display = ", ".join(context.api_groups) if context.api_groups else "none"
|
||||||
|
logger.info("Engine API delegation activated for groups: %s", groups_display)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|||||||
@@ -210,6 +210,13 @@ def _infer_server_scope(message: str, explicit: Optional[str]) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _env_flag(name: str, *, default: bool = False) -> bool:
|
||||||
|
raw = os.environ.get(name)
|
||||||
|
if raw is None:
|
||||||
|
return default
|
||||||
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
def _is_internal_request(req: Request) -> bool:
|
def _is_internal_request(req: Request) -> bool:
|
||||||
"""Return True if the HTTP request originated from the local server host."""
|
"""Return True if the HTTP request originated from the local server host."""
|
||||||
try:
|
try:
|
||||||
@@ -326,6 +333,8 @@ AUTH_RATE_LIMITER = SlidingWindowRateLimiter()
|
|||||||
ENROLLMENT_NONCE_CACHE = NonceCache()
|
ENROLLMENT_NONCE_CACHE = NonceCache()
|
||||||
DPOP_VALIDATOR = DPoPValidator()
|
DPOP_VALIDATOR = DPoPValidator()
|
||||||
DEVICE_AUTH_MANAGER: Optional[DeviceAuthManager] = None
|
DEVICE_AUTH_MANAGER: Optional[DeviceAuthManager] = None
|
||||||
|
ENGINE_API_ENABLED = _env_flag("BOREALIS_ENGINE_API")
|
||||||
|
ENGINE_API_GROUPS: Tuple[str, ...] = tuple()
|
||||||
|
|
||||||
|
|
||||||
def _set_cached_github_token(token: Optional[str]) -> None:
|
def _set_cached_github_token(token: Optional[str]) -> None:
|
||||||
@@ -5088,24 +5097,57 @@ def init_db():
|
|||||||
|
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
enrollment_routes.register(
|
if ENGINE_API_ENABLED:
|
||||||
app,
|
_engine_api_config: Dict[str, Any] = {
|
||||||
db_conn_factory=_db_conn,
|
"DATABASE_PATH": DB_PATH,
|
||||||
log=_write_service_log,
|
"TLS_CERT_PATH": TLS_CERT_PATH,
|
||||||
jwt_service=JWT_SERVICE,
|
"TLS_KEY_PATH": TLS_KEY_PATH,
|
||||||
tls_bundle_path=TLS_BUNDLE_PATH,
|
"TLS_BUNDLE_PATH": TLS_BUNDLE_PATH,
|
||||||
ip_rate_limiter=IP_RATE_LIMITER,
|
}
|
||||||
fp_rate_limiter=FP_RATE_LIMITER,
|
api_groups_override = os.environ.get("BOREALIS_API_GROUPS")
|
||||||
nonce_cache=ENROLLMENT_NONCE_CACHE,
|
if api_groups_override:
|
||||||
script_signer=SCRIPT_SIGNER,
|
_engine_api_config["API_GROUPS"] = api_groups_override
|
||||||
)
|
|
||||||
|
|
||||||
token_routes.register(
|
try:
|
||||||
app,
|
from Data.Engine.server import register_engine_api
|
||||||
db_conn_factory=_db_conn,
|
|
||||||
jwt_service=JWT_SERVICE,
|
_engine_context = register_engine_api(app, config=_engine_api_config)
|
||||||
dpop_validator=DPOP_VALIDATOR,
|
except Exception:
|
||||||
)
|
ENGINE_API_ENABLED = False
|
||||||
|
ENGINE_API_GROUPS = tuple()
|
||||||
|
_write_service_log(
|
||||||
|
"server",
|
||||||
|
"Engine API delegation failed; continuing with legacy API registration.",
|
||||||
|
level="ERROR",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ENGINE_API_GROUPS = tuple(_engine_context.api_groups)
|
||||||
|
_write_service_log(
|
||||||
|
"server",
|
||||||
|
"Engine API delegation enabled for groups: {}".format(
|
||||||
|
", ".join(ENGINE_API_GROUPS) or "default"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not ENGINE_API_ENABLED:
|
||||||
|
enrollment_routes.register(
|
||||||
|
app,
|
||||||
|
db_conn_factory=_db_conn,
|
||||||
|
log=_write_service_log,
|
||||||
|
jwt_service=JWT_SERVICE,
|
||||||
|
tls_bundle_path=TLS_BUNDLE_PATH,
|
||||||
|
ip_rate_limiter=IP_RATE_LIMITER,
|
||||||
|
fp_rate_limiter=FP_RATE_LIMITER,
|
||||||
|
nonce_cache=ENROLLMENT_NONCE_CACHE,
|
||||||
|
script_signer=SCRIPT_SIGNER,
|
||||||
|
)
|
||||||
|
|
||||||
|
token_routes.register(
|
||||||
|
app,
|
||||||
|
db_conn_factory=_db_conn,
|
||||||
|
jwt_service=JWT_SERVICE,
|
||||||
|
dpop_validator=DPOP_VALIDATOR,
|
||||||
|
)
|
||||||
|
|
||||||
agent_routes.register(
|
agent_routes.register(
|
||||||
app,
|
app,
|
||||||
|
|||||||
Reference in New Issue
Block a user