Files
template-web-backend-python/openspec/specs/main.md
idp-template-mirror a7498db8f0
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
chore(mirror): sync desde templates/web-backend-python (ADR-095)
2026-06-16 22:06:54 -05:00

258 lines
13 KiB
Markdown
Raw 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.
# Spec — Golden Path `web-backend/python`
**Versión del shape:** 1.2.0
**Status:** Activo (PoC NexoBMS como primer consumidor)
**ADR de referencia:** ADR-053 (anatomía y decisiones), ADR-061 (workflow CI
self-contained — sin `uses:` cross-repo; tests sobre SQLite efímero),
ADR-066 (lifecycle del `AddonClaim` FUERA del chart Helm — aplicado por el
workflow del tenant antes de `helm upgrade --install`),
ADR-067/ADR-082 (FQDN subdominio DOT + wildcard TLS per-tenant)
> **1.2.0** (2026-06-10): nueva ruta raíz `/` = página de bienvenida por defecto
> (instrucciones de uso para el tenant; el primer `commit`+`push` la reemplaza con
> su desarrollo). Endpoint opcional → bump minor (§7). Además, §2.2 corrige el FQDN
> de la app a la forma subdominio DOT canónica (ADR-082).
**Visibilidad:** interna de Coralware — este spec NO se sincroniza al
repo Gitea `idp-templates/template-web-backend-python` ni al repo del
cliente.
---
## 1. Propósito
Definir el comportamiento operativo del shape `web-backend/python`
scaffold de app FastAPI con persistencia MariaDB y migraciones Alembic
que se materializa en un repo Gitea por tenant via `repo-provisioner`.
Este spec es la fuente de verdad del **flujo CI/CD del shape**. Cambios
al flowchart son cambios de contrato — exigen subir `shapeVersion` (semver)
y actualizar `repo-provisioner` y `tenant-manager` si los placeholders
o secrets esperados cambian.
---
## 2. Contrato del shape
### 2.1 Entradas
| Entrada | Origen | Descripción |
|---------|--------|-------------|
| `__APP_NAME__` | placeholder | Nombre del repo/app, sustituido server-side por `repo-provisioner` |
| `__TENANT_ID__` | placeholder | UUID del tenant (con guiones) |
| `__TENANT_ID_NODASHES__` | placeholder | UUID sin guiones, para construir DNS y namespaces |
| `__TIER__` | placeholder | Tier del tenant: `startup` | `business` | `enterprise` |
| Secret `<APP_NAME>-db-creds` | runbook MariaDB / AddonClaim (Fase 5.5+) | Claves `DATABASE_URL`, `MIGRATION_DATABASE_URL` |
| `KUBECONFIG_TENANT_B64` | secret de proyecto Gitea | Provisto por `tenant-manager` al crear el repo |
| `vars.TENANT_ID`, `vars.TENANT_ID_NODASHES`, `vars.TENANT_TIER` | vars de proyecto Gitea | Provisto por `tenant-manager` |
### 2.2 Salidas observables
| Salida | Forma |
|--------|-------|
| Imagen Docker | `registry.coralsafety.com/t-<tenant-id-short>/<APP_NAME>:<tag>` — proyecto Harbor dedicado del tenant (ADR-019 D-03) |
| Helm release | `<APP_NAME>` en namespace `apps` del cluster tenant (ADR-054) |
| App reachable | `https://<APP_NAME>.<tenant-id-con-guiones>.apps.coralware.cloud/health` retorna 200 (FQDN subdominio DOT, ADR-067/ADR-082) |
| Página de bienvenida | `/` sirve HTML de bienvenida por defecto; el primer `commit`+`push` del tenant la reemplaza con su desarrollo |
| Ingress controller | **Traefik Proxy + Gateway API** en `traefik-system` (bundle Fleet `traefik-bootstrap`) — ADR-060 Fase 1 (`GATEWAY-API-MIGRATION`); reemplaza `rke2-ingress-nginx` (EOL marzo 2026). El shape emite un `HTTPRoute` (gateway.networking.k8s.io/v1) adjunto al `Gateway` `tenant-gateway` (ns `apps`, creado por stack-deployer §3c) |
| TLS | Wildcard per-tenant `*.<tenant-id>.apps.coralware.cloud` vía Secret `tenant-wildcard-tls` (cert-manager local del cluster, ADR-067) |
| Endpoints obligatorios | `/`, `/health`, `/docs`, `/openapi.json`, `/demo`, `/metrics` |
| Métricas Prometheus | scrape en `/metrics`; etiquetas con `golden-path=web-backend` |
### 2.3 Invariantes
- La app **nunca** crashea por DB caída — `/health` retorna 503 con `db:down`.
- Migraciones Alembic corren **antes** del rollout del Deployment (Helm hook).
- Falla de Alembic ⇒ Helm aborta upgrade ⇒ Deployment anterior intacto.
- `helm upgrade --install` del chart funciona **sin `--no-hooks`**. El
`ServiceAccount` que referencia el pod del Job Alembic se materializa como
hook `pre-install,pre-upgrade` con `hook-weight: -5` (menor que el `0` del
Job) — así existe antes de que corra el Job. El SA-hook usa
`hook-delete-policy: before-hook-creation` (no `hook-succeeded`) porque el
Deployment de la fase principal lo necesita vivo en runtime. El
`imagePullSecret` y el Secret de BD los provee la plataforma, no el chart.
- SecurityContext: `runAsNonRoot=true`, `readOnlyRootFilesystem=true`,
`drop ALL caps`. Si el código rompe esto, el spec lo prohíbe.
- Sin IPs hardcoded; sin credenciales en código (env vars o Secret montado).
### 2.4 Lifecycle del `AddonClaim` (ADR-066)
El `AddonClaim` vive **FUERA del chart Helm** — en `manifests/addonclaim.yaml`
del scaffold del shape, con placeholders `${RELEASE_NAME}` / `${DATABASE_NAME}`
sustituidos por `envsubst` en el workflow del tenant. El workflow lo aplica con
`kubectl apply -f -` ANTES de `helm upgrade --install`. Helm nunca lo trackea.
Justificación y reglas operativas:
- **Por qué no en `templates/`:** un Helm hook `pre-install,pre-upgrade` cae en
el default `hook-delete-policy: before-hook-creation`, que borra el claim
previo antes de cada upgrade. El finalizer
`addon-provisioner.idp.coralware.cloud/cleanup-logical` ejecuta entonces
`REVOKE` + `DROP USER` + delete Secret en cada upgrade, rota credenciales
involuntariamente y atora el release v_n+1 en `pending-upgrade`. El intento
"sin delete-policy explícita" del chart 1.0.0 era incorrecto: el default
Helm sí borra.
- **Por qué no como manifest principal del chart (Opción B descartada):** los
hooks `pre-install/pre-upgrade` corren ANTES del manifest principal. El Job
Alembic (hook weight 0, con InitContainer `wait-for-secret`) intentaría
conectarse al Secret antes de que el AddonClaim del manifest principal haya
sido aplicado por Helm — chicken-and-egg fatal: InitContainer busy-wait
360s sobre un Secret que nunca aparece en la primera instalación.
- **Por qué FUERA del chart (Opción A elegida):** desacopla totalmente el
lifecycle de la app (chart Helm) del lifecycle de la BD (CRD del IDP). El
workflow del tenant aplica el AddonClaim primero — el controller arranca el
reconcile en paralelo — luego corre `helm upgrade --install` cuyos hooks
encuentran el AddonClaim ya en flight. El InitContainer `wait-for-secret`
absorbe la latencia del reconcile como hasta ahora.
- **`kubectl apply -f` es idempotente:** sin annotations Helm, el AddonClaim
no compite con ningún field manager. Helm en upgrade no lo trackea — no
intenta borrarlo en uninstall ni modificarlo en upgrade.
- **`helm uninstall` no toca el AddonClaim:** desacoplamiento garantizado.
Cleanup explícito post-uninstall: `kubectl delete addonclaim <release>-db
-n apps` dispara el finalizer; con `Retain` (default) preserva la BD;
cambiar a `Delete` previo si se quiere `DROP DATABASE`.
- **Migración v1 → v2** (apps desplegadas con chart 1.0.0, donde el
AddonClaim fue creado como Helm hook): el workflow aplica el script
`pre-helm-detach.sh` ANTES de `helm upgrade --install`. Idempotente: si el
AddonClaim NO tiene annotations Helm, no hace nada; si las tiene, ejecuta
`kubectl annotate ... meta.helm.sh/release-name- meta.helm.sh/release-namespace-`
y `kubectl label ... app.kubernetes.io/managed-by-` para que Helm deje de
reconocerlo como recurso del release. El siguiente `helm upgrade --install`
con chart 1.1.0 (sin AddonClaim en `templates/`) no lo verá en el diff y
no intentará borrarlo. Sunset del script tras una rotación de releases.
- **InitContainer `wait-for-secret`** absorbe la latencia del primer
reconcile del controller. Window dimensionado a `3 × RECONCILE_INTERVAL`
del addon-provisioner: con default 60s + initial_delay 60s, el chart
espera 72 × 5s = 360s. Subir el default del controller obliga a subir
este window en paralelo (ver `code/addon-provisioner/openspec/specs/main.md`
§3.1). Lección 2026-05-28: con RECONCILE_INTERVAL=300s y wait=240s el
init container moria antes de que el Secret se publicara — delay
observado 577s. Ticket ADDON-PROVISIONER-DB-SECRET-NOT-PUBLISHED.
---
## 3. Flujo principal
```mermaid
flowchart TD
dev_push([Dev hace git push\nal repo Gitea del tenant]) --> quality_gate[quality_gate\nruff + black + bandit + pytest --cov 80]
quality_gate -- fail --> pipeline_failed([pipeline_failed\nHelm no se invoca])
quality_gate -- ok --> image_build[image_build\nbuildx push a Harbor t-short]
image_build -- fail --> pipeline_failed
image_build -- ok --> pre_helm_detach[pre_helm_detach\nde-adoptar AddonClaim legacy\nidempotente migracion v1.0→1.1]
pre_helm_detach --> apply_addonclaim[apply_addonclaim\nkubectl apply manifests/addonclaim.yaml]
apply_addonclaim --> helm_deploy[helm_deploy\nhelm upgrade --install --wait]
helm_deploy --> alembic_hook[alembic_hook\nJob pre-upgrade alembic upgrade head]
alembic_hook -- fail --> app_intact_old_version([app_intact_old_version\nDeployment anterior corriendo\npipeline_failed])
alembic_hook -- ok --> rollout_apply[rollout_apply\nDeployment con nuevo image tag]
rollout_apply --> rollout_status_check[rollout_status_check\nkubectl rollout status timeout 3m]
rollout_status_check -- fail --> helm_rollback_manual([helm_rollback_manual\ndev decide rollback\npipeline_failed])
rollout_status_check -- ok --> smoke_health_check[smoke_health_check\nwget /health 200 + db:up]
smoke_health_check -- fail --> investigate([investigate\nlogs + events\npipeline_failed])
smoke_health_check -- ok --> app_running([app_running\ncriterios PoC 1-9 cumplidos])
```
**Node IDs como labels Prometheus.** Cada nodo es un valor del label
`phase` en métricas como `idp_shape_pipeline_phase_total{shape="web-backend-python", phase="alembic_hook", outcome="success|fail"}`.
---
## 4. Estados terminales
| Estado | Significado | Acción del dev |
|--------|-------------|----------------|
| `app_running` | Todo OK — app desplegada, /health 200, db:up | Ninguna |
| `pipeline_failed` | Cualquier etapa antes de rollout falló | Inspeccionar logs del CI, corregir, push otra vez |
| `app_intact_old_version` | Alembic falló — versión anterior sigue corriendo | Inspeccionar logs del Job alembic; corregir migración; push |
| `helm_rollback_manual` | Rollout no estabilizó en 3 min | Ejecutar `helm rollback` manual; investigar pod events |
| `investigate` | Smoke falla aunque rollout terminó | Logs del Deployment, eventos del cluster, /health interno |
---
## 5. Comportamiento fail-safe
- **DB caída en runtime:** `/health` 503 + `db:down`. App sigue sirviendo
endpoints que no requieren DB. Nunca exit 1, nunca CrashLoopBackOff
por DB intermitente. (Pod muere solo si DATABASE_URL falta al startup.)
- **Imagen no disponible en Harbor:** K8s reporta `ImagePullBackOff`
visible en Rancher; rollout no avanza; rollback automático no aplica
(dev decide).
- **Migration Alembic inválida:** transacción revertida; `alembic_version`
sin cambio; Helm aborta upgrade; Deployment con versión vieja sigue.
- **`/health` reporta `db:up` con DB caída:** prohibido — handler hace
`SELECT 1` por request, sin cache (validado en tests).
---
## 6. Pruebas negativas obligatorias (N1-N5)
Definidas en `docs/runbook-mariadb-poc.md` §6 y `poc-nexobms.md` §4.
Cualquier release del shape (cambio de `shapeVersion`) las re-ejecuta:
| ID | Prueba | Estado terminal esperado |
|----|--------|--------------------------|
| N1 | Test fallando | `pipeline_failed` en `quality_gate` |
| N2 | Migración Alembic inválida | `app_intact_old_version` |
| N3 | Credencial DB inválida | App corriendo, `/health` 503 con `db:down` |
| N4 | `DATABASE_URL` ausente al startup | `CrashLoopBackOff` visible en Rancher |
| N5 | Imagen tag inexistente | `ImagePullBackOff` visible en Rancher |
---
## 7. Versionado del shape
`coralware-shape.yaml` declara `shapeVersion` (semver):
| Cambio | Bump |
|--------|------|
| Añadir endpoint nuevo opcional | minor |
| Renombrar endpoint obligatorio | major |
| Cambiar nombre de Secret esperado | major |
| Cambiar threshold de coverage | minor |
| Añadir nueva variable de entorno opcional | minor |
| Añadir nodo nuevo al flowchart | minor |
| Cambiar terminal state | major |
Tenants existentes congelan su versión hasta migrar explícitamente.
`coralware.cloud/v1alpha1/Shape` permite marcar versiones deprecated;
`cove-api` puede surfacear avisos en el portal del tenant.
---
## 8. Sincronización a Gitea
`templates/web-backend-python/` (este repo IDP) → `idp-templates/template-web-backend-python` (Gitea).
Exclusiones del sync (no llegan al repo del cliente):
- `openspec/` — gobernanza interna del golden path.
- `.ruff_cache/`, `__pycache__/`, `.pytest_cache/`.
- `.venv/`, `venv/`.
Mecanismo: `rsync --exclude='openspec/' --exclude-from=.gitignore`
desde el scaffold local hacia el clone temporal del repo Gitea, commit,
push.
---
## 9. Implementación pendiente (referencia)
Ver ADR-053 §11. Resumen:
- [ ] Sync inicial del scaffold a Gitea (esta sesión).
- [ ] Render server-side de placeholders en `repo-provisioner`.
- [ ] Provisión de `KUBECONFIG_TENANT_B64` en Gitea Actions secrets por
`tenant-manager`.
- [ ] Smoke E2E del shape (extiende ADR-051) con stage
`app_deployed_and_healthy`.
- [ ] Migración del runbook manual MariaDB al `addon-provisioner`
(ADR-052) en Fase 5.5+.
- [ ] Sunset del step `pre_helm_detach` tras una rotación completa de releases
desplegadas con chart 1.0.0 (~2 semanas — la migración v1→v2 será de un
solo uso por release y luego idempotente skip).