Files
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

13 KiB
Raw Permalink Blame History

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
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

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).