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
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:
61
openspec/config.yaml
Normal file
61
openspec/config.yaml
Normal 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
257
openspec/specs/main.md
Normal 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).
|
||||
Reference in New Issue
Block a user