Files
2026-06-19 02:57:05 +00:00

261 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# __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.
> **Página de bienvenida:** al provisionarse, tu plataforma sirve una página de
> bienvenida en `/` con instrucciones de uso. **En cuanto hagas tu primer
> `commit` + `push`, tu desarrollo reemplaza esa página** — define tu propio
> handler de `/` (en `code/app/main.py` o en `code/app/routers/`).
> **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 (Deployment, Job Alembic, NP, RBAC)
├── manifests/
│ └── addonclaim.yaml ← AddonClaim fuera del chart (ADR-066)
├── scripts/
│ └── pre-helm-detach.sh ← migración v1.0.0 → v1.1.0 (idempotente)
└── .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 aplicación — AddonClaim + hooks Helm
El workflow del tenant sigue este orden en cada `git push`:
| Paso | Acción | Motivo |
|------|--------|--------|
| 1 | `bash scripts/pre-helm-detach.sh` | Migración chart 1.0.0 → 1.1.0: si el AddonClaim tiene labels/annotations de un release Helm previo, las quita. Idempotente: skip si no aplica. Sunset tras ~2 semanas. |
| 2 | `kubectl apply -f manifests/addonclaim.yaml` (con `sed` para placeholders) | Crea o actualiza el `AddonClaim`. Idempotente. `addon-provisioner` empieza a reconciliar en paralelo. |
| 3 | `helm upgrade --install <release> ./helm/<APP_NAME> --wait --timeout 10m` | Aplica el chart Helm. Los hooks `pre-install,pre-upgrade` corren en orden de `hook-weight` (ver tabla abajo). El Job Alembic espera al Secret. |
> **Deploys serializados + auto-recuperación:** el workflow tiene un guard
> `concurrency` por repo, así que dos `git push` seguidos se **encolan** (no corren
> el `helm upgrade` en paralelo). Si un deploy quedó a medias (cancelado), el
> siguiente run **detecta y recupera** el release atascado (`pending-install` →
> reinstala; `pending-upgrade` → rollback) antes de reintentar.
El chart usa hooks `pre-install,pre-upgrade` con `hook-weight` para garantizar
el orden de creación de SUS recursos (sin AddonClaim — ese vive fuera).
`helm upgrade --install` funciona **sin `--no-hooks`**:
| Recurso | Template | `hook-weight` | Motivo |
|---------|----------|---------------|--------|
| `NetworkPolicy` | `networkpolicy.yaml` | `-8` | Bajo el `default-deny` del namespace `apps`, esta NP permite DNS + apiserver + addons:3306. Debe existir antes que el InitContainer del Job Alembic intente conectarse al apiserver. |
| `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 360s — 3× RECONCILE_INTERVAL 60s), luego corre `alembic upgrade head` |
### Por qué el `AddonClaim` vive FUERA del chart (ADR-066)
En chart 0.2.x el `AddonClaim` era hook `pre-install,pre-upgrade` weight `-10`.
El default de Helm 3 (`hook-delete-policy: before-hook-creation`) borraba el
claim previo antes de aplicar el nuevo en cada `helm upgrade --install`. El
finalizer `addon-provisioner.idp.coralware.cloud/cleanup-logical` ejecutaba
entonces `REVOKE` + `DROP USER` + delete del Secret en cada upgrade → rotación
involuntaria de credenciales + release atorado en `pending-upgrade`.
Convertir el `AddonClaim` a manifest principal del chart (no hook) también
falla: los hooks `pre-install,pre-upgrade` corren ANTES del manifest principal,
así que el Job Alembic intentaría leer el Secret antes de que el AddonClaim
exista — chicken-and-egg fatal.
La solución es desacoplar el lifecycle del CRD del lifecycle del release Helm:
el `AddonClaim` lo aplica el workflow con `kubectl apply` (idempotente, sin
ownership Helm), y Helm gobierna sólo la app + sus hooks Alembic/NP/RBAC.
`helm uninstall` no toca el AddonClaim — desacoplamiento garantizado.
Borrar la BD lógica es decisión explícita:
```bash
kubectl delete addonclaim <release>-db -n apps
```
(Con `deletionPolicy: Retain` — default — preserva la BD lógica; el operador
emite Event `OrphanLogicalDatabase`. Cambiar a `Delete` previo para `DROP DATABASE`.)
### Por qué el ServiceAccount es hook
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 + ADR-066):
1. El workflow del tenant aplica `manifests/addonclaim.yaml` con
`kubectl apply -f -` (placeholders sustituidos con `sed`). El claim declara
`addonType: mariadb`, `databaseName` derivado del nombre del release 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 workflow corre `helm upgrade --install`. El InitContainer
`wait-for-secret` del Job Alembic (hook `pre-install,pre-upgrade`) espera al
Secret (hasta 360s), Alembic ejecuta `upgrade head`, y el Deployment rola.
### Opt-out — apps stateless
Si tu app no necesita BD:
1. En `values.yaml`:
```yaml
database:
enabled: false # no se aplica Role/RoleBinding al SA
alembic:
enabled: false # no corre el Job Alembic
```
2. Elimina el step "Aplicar AddonClaim" del workflow `.gitea/workflows/__APP_NAME__-build.yaml`
(junto con `manifests/addonclaim.yaml` y el step `pre-helm-detach`).
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 <tu-repo-github> && git push github main` (espejo externo a GitHub) |
| 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` |
> **Nota:** el remote `github` de esta tabla es un **espejo externo de salida**
> (tu source of truth fuera de Cove), no el remote de despliegue. El primer push
> del dev hacia Cove usa el remote `cove` (`git push cove main`), que dispara el
> CI/CD — ver la guía de onboarding (welcome page del repo y email post-active).
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
> Nota (ADR-095, Fase B1): este template es la FUENTE ÚNICA autorada. La org Gitea
> `idp-templates/template-web-backend-python` es un artefacto DERIVADO — no editar a
> mano; el workflow `templates-mirror.yaml` (gate Fase 1 + espejo solo-en-verde) lo
> sincroniza desde aquí.