All checks were successful
__APP_NAME__ — build & deploy / ¿Credenciales de build listas? (push) Successful in 1s
__APP_NAME__ — build & deploy / Calidad + build + push (push) Has been skipped
__APP_NAME__ — build & deploy / ¿Cluster del tenant listo? (push) Has been skipped
__APP_NAME__ — build & deploy / Helm upgrade --install (push) Has been skipped
256 lines
11 KiB
Markdown
256 lines
11 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.
|
||
|
||
> **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
|