# __APP_NAME__ — Web Backend Python (FastAPI + Alembic) App generada desde el Golden Path **`web-backend/python`** de Coralware Cove IDP. Este repositorio contiene el scaffold completo: aplicación FastAPI, migraciones Alembic, frontend estático en `/demo`, Helm chart, workflow de CI y manifiesto de contrato con la plataforma. Cuando se materializa este template para tu tenant, los placeholders en mayúsculas se sustituyen automáticamente. > **Contrato del shape:** ver `coralware-shape.yaml` en la raíz. Define los > endpoints obligatorios, la BD esperada y la versión del shape. No elimines > ese archivo — la plataforma lo lee. --- ## Estructura ``` . ├── coralware-shape.yaml ← contrato con la plataforma ├── catalog-info.yaml ← entidad Backstage ├── code/ │ ├── app/ ← FastAPI app │ ├── alembic/ ← migraciones DB │ ├── tests/ ← pytest + cobertura │ ├── pyproject.toml │ ├── requirements.txt │ └── Dockerfile ├── helm/__APP_NAME__/ ← Helm chart con AddonClaim + hook Alembic └── .gitea/workflows/ ← CI: tests + build + deploy ``` --- ## Ciclo de vida 1. **Tú escribes código** — endpoints en `code/app/routers/`, modelos en `code/app/models/`, migraciones con `alembic revision --autogenerate`. 2. **`git push`** a `main` dispara el workflow Gitea Actions. 3. El CI corre `black`, `ruff`, `bandit`, `pytest --cov`. Si algo falla, el pipeline aborta. 4. Si pasa, builda la imagen `linux/amd64` con BuildKit y la publica en el proyecto Harbor de tu tenant. 5. `helm upgrade --install` aplica el chart al cluster de tu tenant. Antes del rollout, un Job pre-upgrade corre `alembic upgrade head`. Si Alembic falla, Helm aborta y la app vieja queda intacta. 6. Tras rollout exitoso, smoke check valida `/health` 200. --- ## Orden de hooks Helm El chart usa hooks `pre-install,pre-upgrade` con `hook-weight` para garantizar el orden de creación. El `helm upgrade --install` funciona **sin `--no-hooks`**: | Recurso | Template | `hook-weight` | Motivo | |---------|----------|---------------|--------| | `AddonClaim` | `addonclaim.yaml` | `-10` | Declara la intención de la app de tener una BD — el primero en aplicarse para que `addon-provisioner` empiece a reconciliar lo antes posible | | `Role` + `RoleBinding` | `role-db-secret-reader.yaml` | `-7` | Permite al SA leer el Secret `-db-creds` (lo consume el InitContainer `wait-for-secret` del Job Alembic) | | `ServiceAccount` | `serviceaccount.yaml` | `-5` | Lo referencia el pod del Job Alembic — debe existir antes | | `Job` Alembic | `job-alembic-upgrade.yaml` | `0` | InitContainer `wait-for-secret` espera al Secret publicado por `addon-provisioner` (hasta 240s), luego corre `alembic upgrade head` | El `AddonClaim` (hook weight `-10`) NO declara `hook-delete-policy` intencionalmente: así sobrevive entre `helm upgrade` (re-apply idempotente) y se evita disparar el finalizer `cleanup-logical` del controller en cada upgrade (que dropearía el user de la BD y regeneraría el password, causando ventana de downtime). Trade-off: `helm uninstall` deja el AddonClaim huérfano y MariaDB sigue aprovisionada. Es coherente con `deletionPolicy: Retain` — borrar BD productiva debe ser decisión explícita, no side-effect del uninstall. Cleanup explícito: ```bash kubectl delete addonclaim -db -n apps ``` El `ServiceAccount` se materializa como hook (no en la fase principal de Helm) porque el Job Alembic, que también es hook, declara `serviceAccountName`. Si el SA viviera en la fase principal se crearía **después** del Job → el pod del Job nunca arrancaría (`serviceaccount not found`) → `helm --wait` haría timeout. El SA-hook usa `hook-delete-policy: before-hook-creation` (no `hook-succeeded`): el Deployment lo necesita vivo en runtime, así que solo se borra/recrea al inicio del siguiente hook, manteniéndolo idempotente entre upgrades. El `imagePullSecret` lo provee la plataforma (stack-deployer), no el chart. El Secret `-db-creds` lo publica `addon-provisioner` tras reconciliar el `AddonClaim` (ver §Base de datos). --- ## Endpoints obligatorios | Path | Auth | Propósito | |------|------|-----------| | `/health` | none | Liveness + readiness; devuelve `{status: ok, db: up\|down}` | | `/docs` | configurable (`OPENAPI_REQUIRE_AUTH`) | Swagger UI | | `/openapi.json` | igual que `/docs` | Schema OpenAPI | | `/demo` | none | Frontend estático servido desde `code/app/static/demo/` | | `/metrics` | restringido vía NetworkPolicy | Métricas Prometheus | `/health`, `/docs`, `/demo` son contrato del shape — NO renombres. --- ## Base de datos Tu app espera un Secret `__APP_NAME__-db-creds` en su namespace con las claves `DATABASE_URL` / `DATABASE_HOST` / `DATABASE_PORT` / `DATABASE_NAME` / `DATABASE_USER` / `DATABASE_PASSWORD` (y opcionalmente `MIGRATION_DATABASE_URL`). El Secret lo materializa automáticamente la plataforma vía el flujo `AddonClaim` → `addon-provisioner` (ADR-052 + ADR-064): 1. El chart aplica `helm/__APP_NAME__/templates/addonclaim.yaml` como hook `pre-install,pre-upgrade` weight `-10` — declara `addonType: mariadb`, `databaseName` derivado del Release name y el `secretName` esperado. 2. `addon-provisioner` (corriendo en el plano de control IDP) reconcilia el `AddonClaim`: crea una BD lógica + user + grant en la instancia MariaDB compartida del tenant (ns `addons` del cluster tenant) vía Job DDL efímero. 3. Publica el Secret `__APP_NAME__-db-creds` en el ns `apps`. 4. El InitContainer `wait-for-secret` del Job Alembic espera al Secret (hasta 240s), Alembic ejecuta `upgrade head`, y el Deployment rola. ### Opt-out — apps stateless Si tu app no necesita BD, en `values.yaml`: ```yaml database: enabled: false # no se aplica AddonClaim ni Role/RoleBinding alembic: enabled: false # no corre el Job Alembic ``` Con `database.enabled=false` la app se despliega sin BD ni migraciones. --- ## Variables de entorno | Variable | Origen | Default | Descripción | |----------|--------|---------|-------------| | `DATABASE_URL` | Secret | — | Connection string runtime | | `MIGRATION_DATABASE_URL` | Secret | = `DATABASE_URL` | Connection para Alembic | | `LOG_LEVEL` | ConfigMap | `INFO` | `DEBUG` para troubleshooting | | `CORRELATION_ID_HEADER` | ConfigMap | `X-Correlation-ID` | Header de trazabilidad | | `OPENAPI_REQUIRE_AUTH` | ConfigMap | `false` | Activar JWT en `/docs` | | `KEYCLOAK_ISSUER` | ConfigMap | — | URL del realm cuando auth activado | | `STATIC_DEMO_PATH` | ConfigMap | `/app/static/demo` | Path al frontend estático | --- ## Migraciones Alembic Migración base vacía en `code/alembic/versions/0001_initial.py`. Para crear una nueva tras agregar modelos: ```bash cd code alembic revision --autogenerate -m "agregar tabla users" ``` --- ## Desarrollo local ```bash cd code pip install -r requirements-dev.txt docker run --rm -d --name mariadb-dev \ -e MARIADB_ROOT_PASSWORD=dev -e MARIADB_DATABASE=app \ -p 3306:3306 mariadb:11 export DATABASE_URL="mysql+pymysql://root:dev@127.0.0.1:3306/app" export MIGRATION_DATABASE_URL="$DATABASE_URL" alembic upgrade head uvicorn app.main:app --reload --port 8000 ``` --- ## Plan de salida Si decides migrar fuera de Cove, todo lo que necesitas vive en formato estándar: | Componente | Cómo lo exportas | |-----------|------------------| | Código | `git remote add github && git push github main` | | Imagen | `docker pull` desde Harbor + push a tu registry | | Manifests | Ya están en `helm/` — `helm template` genera YAML aplicable | | Datos | Coralware ejecuta `mariadb-dump` y entrega `.sql.gz` | Sin lock-in técnico. --- ## Referencias - `docs/adr-053-shape-web-backend-python.md` — anatomía del shape - `docs/adr-052-tenant-addon-claim-model.md` — modelo de add-ons (BD) - `docs/adr-064-free-trial-allows-mariadb.md` — free-trial permite MariaDB - `docs/adr-038-ci-estandar-reusable-workflow.md` — pipeline Python