- Repo scaffolding: .gitignore, .env.example, Makefile, docker-compose.yml, README.md, CHANGELOG.md, pre-commit config. - Three-service stack: api (Flask 3), db (postgres:16-alpine), front (nginx serving the Vite bundle). Named volumes metamorph_db + metamorph_evidence. - Backend skeleton: Flask app factory, JSON structured logging on stdout, GET /api/v1/health, multi-stage Dockerfile, pyproject.toml driven by uv, Pydantic Settings with secret guard rails (refuses to boot in non-dev with placeholders), APP_ENV gating. - Frontend skeleton: Vite + React 18 + TypeScript strict + TailwindCSS, RTOps design tokens from tasks/design.md, self-hosted JetBrains Mono / IBM Plex Sans via @fontsource, base UI primitives (Card/Tag/SectionHeader/FlowNode/ Button), home page wired to /api/v1/health. - Engine-agnostic Makefile: auto-detects docker or podman, picks the matching compose driver. Targets: up/down/build/rebuild/dev/lint/fmt/test/migrate/ seed-mitre/print-install-token/e2e/inspect-health. - Playwright suite: e2e/tests/m0-smoke.spec.ts (8 tests) + HTML + JUnit reports + traces on retry. - Docs: tasks/spec.md (finalized after Q&A), tasks/design.md, tasks/todo.md (14 milestones), tasks/testing-m0.md, tasks/lessons.md. DoD: make up + make health + make e2e all pass on podman 5.x (Fedora) and docker. TLS terminated by external reverse proxy (spec §6 NF-network). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.0 KiB
Python
73 lines
2.0 KiB
Python
"""Flask application factory and WSGI entry point."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from flask import Flask
|
|
from flask_cors import CORS
|
|
|
|
from app.api.v1 import bp as v1_bp
|
|
from app.core.config import settings
|
|
from app.core.install_token import (
|
|
ensure_install_token,
|
|
log_install_token_banner,
|
|
)
|
|
from app.core.logging import configure_logging
|
|
from app.core.rate_limit import limiter
|
|
from app.services.bootstrap import ensure_system_groups
|
|
from app.services.permissions_seed import seed_all as seed_permissions_and_bindings
|
|
|
|
|
|
def _try_bootstrap_at_boot(log: logging.Logger) -> None:
|
|
"""Best-effort: seed system groups + mint an install token if needed.
|
|
|
|
Wrapped in try/except because the DB may not be ready (or schema not
|
|
migrated yet) at the very first boot — gunicorn must still come up so the
|
|
operator can run `make migrate` and curl /setup afterwards.
|
|
"""
|
|
try:
|
|
ensure_system_groups()
|
|
seed_permissions_and_bindings()
|
|
token = ensure_install_token()
|
|
if token is not None:
|
|
log_install_token_banner(token)
|
|
else:
|
|
log.info("metamorph.bootstrap.skipped")
|
|
except Exception as e:
|
|
log.warning("metamorph.bootstrap.deferred", extra={"error": str(e)})
|
|
|
|
|
|
def create_app() -> Flask:
|
|
configure_logging(settings.LOG_LEVEL)
|
|
log = logging.getLogger("metamorph.boot")
|
|
|
|
app = Flask(__name__)
|
|
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 MB hard cap; per-file limit is 25 MB.
|
|
|
|
CORS(
|
|
app,
|
|
origins=settings.cors_origins,
|
|
supports_credentials=True,
|
|
max_age=600,
|
|
)
|
|
|
|
limiter.init_app(app)
|
|
app.register_blueprint(v1_bp)
|
|
|
|
log.info(
|
|
"metamorph.api.boot",
|
|
extra={
|
|
"cors_origins": settings.cors_origins,
|
|
"log_level": settings.LOG_LEVEL,
|
|
"evidence_dir": settings.EVIDENCE_DIR,
|
|
},
|
|
)
|
|
|
|
_try_bootstrap_at_boot(log)
|
|
return app
|
|
|
|
|
|
# WSGI entry point used by gunicorn (`gunicorn app.main:app`).
|
|
app = create_app()
|