13 KiB
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 primercommit+pushla 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 Giteaidp-templates/template-web-backend-pythonni 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 |
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 —
/healthretorna 503 condb:down. - Migraciones Alembic corren antes del rollout del Deployment (Helm hook).
- Falla de Alembic ⇒ Helm aborta upgrade ⇒ Deployment anterior intacto.
helm upgrade --installdel chart funciona sin--no-hooks. ElServiceAccountque referencia el pod del Job Alembic se materializa como hookpre-install,pre-upgradeconhook-weight: -5(menor que el0del Job) — así existe antes de que corra el Job. El SA-hook usahook-delete-policy: before-hook-creation(nohook-succeeded) porque el Deployment de la fase principal lo necesita vivo en runtime. ElimagePullSecrety 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 hookpre-install,pre-upgradecae en el defaulthook-delete-policy: before-hook-creation, que borra el claim previo antes de cada upgrade. El finalizeraddon-provisioner.idp.coralware.cloud/cleanup-logicalejecuta entoncesREVOKE+DROP USER+ delete Secret en cada upgrade, rota credenciales involuntariamente y atora el release v_n+1 enpending-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-upgradecorren ANTES del manifest principal. El Job Alembic (hook weight 0, con InitContainerwait-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 --installcuyos hooks encuentran el AddonClaim ya en flight. El InitContainerwait-for-secretabsorbe la latencia del reconcile como hasta ahora. -
kubectl apply -fes 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 uninstallno toca el AddonClaim: desacoplamiento garantizado. Cleanup explícito post-uninstall:kubectl delete addonclaim <release>-db -n appsdispara el finalizer; conRetain(default) preserva la BD; cambiar aDeleteprevio si se quiereDROP 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.shANTES dehelm upgrade --install. Idempotente: si el AddonClaim NO tiene annotations Helm, no hace nada; si las tiene, ejecutakubectl annotate ... meta.helm.sh/release-name- meta.helm.sh/release-namespace-ykubectl label ... app.kubernetes.io/managed-by-para que Helm deje de reconocerlo como recurso del release. El siguientehelm upgrade --installcon chart 1.1.0 (sin AddonClaim entemplates/) no lo verá en el diff y no intentará borrarlo. Sunset del script tras una rotación de releases. -
InitContainer
wait-for-secretabsorbe la latencia del primer reconcile del controller. Window dimensionado a3 × RECONCILE_INTERVALdel 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 (vercode/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
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:
/health503 +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
ImagePullBackOffvisible en Rancher; rollout no avanza; rollback automático no aplica (dev decide). - Migration Alembic inválida: transacción revertida;
alembic_versionsin cambio; Helm aborta upgrade; Deployment con versión vieja sigue. /healthreportadb:upcon DB caída: prohibido — handler haceSELECT 1por 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_B64en Gitea Actions secrets portenant-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_detachtras 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).