Files
template-web-backend-python/README.md
ilopez 5a215ed293
Some checks failed
__APP_NAME__ — build & deploy / Calidad + build + push (push) Failing after 1m19s
__APP_NAME__ — build & deploy / Helm upgrade --install (push) Has been skipped
feat: AddonClaim + InitContainer wait-for-secret en chart helm
Cierra GOLDEN-PATH-ADDONCLAIM-MISSING (P0 hands-off pipeline).

Cambios:
- helm/APP_NAME/templates/addonclaim.yaml (NEW): hook pre-install/pre-upgrade
  weight -10, SIN delete-policy (sobrevive upgrades, evita disparar finalizer
  cleanup-logical). Gated por .Values.database.enabled (default true).
- helm/APP_NAME/templates/role-db-secret-reader.yaml (NEW): Role + RoleBinding
  weight -7 que permite al SA leer el Secret <release>-db-creds. Lo consume
  el InitContainer wait-for-secret del Job Alembic.
- helm/APP_NAME/templates/job-alembic-upgrade.yaml: gated por
  database.enabled, anade InitContainer wait-for-secret (48x5s=240s) que
  espera a que addon-provisioner publique el Secret tras reconciliar el
  AddonClaim. Absorbe la latencia del @kopf.timer (RECONCILE_INTERVAL 300s
  + initial_delay 60s) sin tocar el operator.
- helm/APP_NAME/values.yaml: bloque database completo (enabled, addonType,
  databaseName, user.permissions, deletionPolicy Retain). Opt-out con
  database.enabled=false + alembic.enabled=false.
- .gitea/workflows/APP_NAME-build.yaml: helm timeout 5m -> 7m (margen para
  reconcile addon-provisioner + StatefulSet ready + DDL Job + Alembic).
- claim-mariadb.yaml (DELETED): obsoleto - estaba en la raiz del template
  y nunca se renderizaba por helm. Reemplazado por addonclaim.yaml dentro
  del chart helm.
- README.md: documenta nueva tabla de hook weights, opt-out database, y
  trade-off del orphan AddonClaim post-uninstall (cleanup explicito).

Decision arquitectural: Opcion D modificada (Opus 4.6 consult).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 07:56:42 -05:00

204 lines
8.0 KiB
Markdown

# __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 `<APP_NAME>-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 <release>-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 `<APP_NAME>-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