chore(mirror): sync desde templates/web-backend-python (ADR-095)
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

This commit is contained in:
idp-template-mirror
2026-06-16 22:06:54 -05:00
parent 30c2239dcd
commit a7498db8f0
5 changed files with 356 additions and 21 deletions

61
openspec/config.yaml Normal file
View File

@@ -0,0 +1,61 @@
schema: spec-driven
context: |
Componente: Golden Path "web-backend/python" (Coralware Cove IDP)
Tipo: Shape de aplicación tenant — FastAPI + Alembic + MariaDB
Repo Gitea generado: idp-templates/template-web-backend-python
Audiencia consumidora: tenants externos (clientes, p.ej. NexoBMS)
Gobernanza: este openspec es interno de Coralware — NO se sincroniza al
repo Gitea del template ni llega al repo del cliente.
Stack del shape:
- Python 3.12 + FastAPI + Uvicorn
- SQLAlchemy + Alembic (migraciones como Helm hook pre-upgrade)
- MariaDB 11 (provisión manual via runbook hasta ADR-052 / Fase 5.5+)
- Helm chart en helm/<APP_NAME>/ versionado en repo del tenant
- CI: Gitea Actions invocando reusable workflow ADR-038 + job helm-deploy
- Runtime: cluster Rancher RKE2 del tenant
Convenciones obligatorias del shape:
- Endpoints fijos: /health, /docs, /openapi.json, /demo, /metrics
- /health retorna {status, db} y 503 si DB caída (fail-safe, no crash)
- Labels: app.kubernetes.io/name=<APP_NAME>, idp.coralware.cloud/tier,
idp.coralware.cloud/tenant-id, idp.coralware.cloud/golden-path=web-backend,
idp.coralware.cloud/golden-path-lang=python
- SecurityContext: runAsNonRoot=true, runAsUser=1001, readOnlyRootFilesystem=true,
drop ALL caps, allowPrivilegeEscalation=false
- Secret esperado: <APP_NAME>-db-creds con DATABASE_URL + MIGRATION_DATABASE_URL
- Imagen: registry.coralsafety.com/tenant-<id>/<APP_NAME>:<tag>
- Pipeline ADR-038: black → ruff → bandit → pytest --cov 80 → buildkit → trivy
- Coverage threshold del shape: 80% (vs 90% en MSs internos del IDP)
ADRs relacionados:
- ADR-027 — Golden Paths Fase 5
- ADR-029 — API standards (R-004 JWT, R-007 correlation_id)
- ADR-032 — Backstage catalog model
- ADR-038 — CI estándar reusable workflow Python
- ADR-052 — TenantAddon / AddonClaim (modelo BD reutilizable)
- ADR-053 — Anatomía y contrato del shape web-backend/python
PoC inicial:
- NexoBMS (Demetrio) — primer caso de uso real
- Tier startup gratuito durante 30 días
- 11 criterios de éxito + 5 pruebas negativas (N1-N5) en runbook
rules:
proposal:
- Incluir sección "Non-goals" explícita (qué NO entrega el shape)
- Referenciar ADRs y poc-nexobms.md cuando aplique
- Indicar impacto sobre repo-provisioner si cambia el contrato de
placeholders (__APP_NAME__, __TIER__, __TENANT_ID__, __TENANT_ID_NODASHES__)
design:
- Especificar variables de entorno consumidas por la app y por Helm values
- Documentar interacción con AddonClaim (ADR-052) y runbook MariaDB (modo manual)
- Incluir comportamiento fail-safe (errores de DB no crashean la app)
- Si el cambio toca el flujo CI/CD, actualizar el flowchart Mermaid de specs/main.md
ANTES de tocar el workflow YAML
tasks:
- Versionar el cambio del shape: shapeVersion en coralware-shape.yaml sigue semver
- Mantener exclusión de openspec/ en el sync a Gitea (no llega al cliente)
- Validar render server-side de placeholders por repo-provisioner antes de cada release
- Tests del scaffold: pytest --cov ≥ 80 sobre code/ del template antes de publicar

257
openspec/specs/main.md Normal file
View File

@@ -0,0 +1,257 @@
# 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).