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:
@@ -233,11 +233,16 @@ Si decides migrar fuera de Cove, todo lo que necesitas vive en formato estándar
|
|||||||
|
|
||||||
| Componente | Cómo lo exportas |
|
| Componente | Cómo lo exportas |
|
||||||
|-----------|------------------|
|
|-----------|------------------|
|
||||||
| Código | `git remote add github && git push github main` |
|
| Código | `git remote add github <tu-repo-github> && git push github main` (espejo externo a GitHub) |
|
||||||
| Imagen | `docker pull` desde Harbor + push a tu registry |
|
| Imagen | `docker pull` desde Harbor + push a tu registry |
|
||||||
| Manifests | Ya están en `helm/` — `helm template` genera YAML aplicable |
|
| Manifests | Ya están en `helm/` — `helm template` genera YAML aplicable |
|
||||||
| Datos | Coralware ejecuta `mariadb-dump` y entrega `.sql.gz` |
|
| Datos | Coralware ejecuta `mariadb-dump` y entrega `.sql.gz` |
|
||||||
|
|
||||||
|
> **Nota:** el remote `github` de esta tabla es un **espejo externo de salida**
|
||||||
|
> (tu source of truth fuera de Cove), no el remote de despliegue. El primer push
|
||||||
|
> del dev hacia Cove usa el remote `cove` (`git push cove main`), que dispara el
|
||||||
|
> CI/CD — ver la guía de onboarding (welcome page del repo y email post-active).
|
||||||
|
|
||||||
Sin lock-in técnico.
|
Sin lock-in técnico.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,16 +6,22 @@ Servida en la ruta raíz `/`. Es lo primero que ve el tenant cuando su
|
|||||||
plataforma queda lista: explica cómo usar su entorno y deja claro que, en
|
plataforma queda lista: explica cómo usar su entorno y deja claro que, en
|
||||||
cuanto haga su primer commit + push, su desarrollo reemplazará esta página.
|
cuanto haga su primer commit + push, su desarrollo reemplazará esta página.
|
||||||
|
|
||||||
|
El copy visible va en inglés (UI de producto; el i18n se aborda más adelante).
|
||||||
Los placeholders `__APP_NAME__`, `__REPO_OWNER__` y `__REPO_NAME__` los
|
Los placeholders `__APP_NAME__`, `__REPO_OWNER__` y `__REPO_NAME__` los
|
||||||
sustituye `repo-provisioner` server-side al scaffoldear el repo del tenant.
|
sustituye `repo-provisioner` server-side al scaffoldear el repo del tenant.
|
||||||
|
|
||||||
|
PENDIENTE DE PROPAGACIÓN MANUAL (GOLDENPATH-TEMPLATE-DELIVERY): este archivo se
|
||||||
|
propaga a mano al repo Gitea `idp-templates`. Cualquier cambio aquí NO llega a
|
||||||
|
tenants nuevos hasta esa propagación manual. La secuencia git de onboarding debe
|
||||||
|
quedar idéntica a la fuente canónica (bloque `gitClone` de cove-api).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
WELCOME_HTML = """<!DOCTYPE html>
|
WELCOME_HTML = """<!DOCTYPE html>
|
||||||
<html lang="es">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>__APP_NAME__ — Tu plataforma Cove</title>
|
<title>__APP_NAME__ — Your Cove platform</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--copper: #b5651d;
|
--copper: #b5651d;
|
||||||
@@ -83,36 +89,42 @@ WELCOME_HTML = """<!DOCTYPE html>
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="brand"><span class="dot"></span> CORALWARE · COVE</div>
|
<div class="brand"><span class="dot"></span> CORALWARE · COVE</div>
|
||||||
|
|
||||||
<h1>Tu plataforma está lista 🎉</h1>
|
<h1>Your platform is ready 🎉</h1>
|
||||||
<p class="sub">Bienvenido. Este es tu entorno <strong>__APP_NAME__</strong>, corriendo en tu propio cluster.</p>
|
<p class="sub">Welcome. This is your <strong>__APP_NAME__</strong> environment, running on your own cluster.</p>
|
||||||
|
|
||||||
<span class="pill">__APP_NAME__</span>
|
<span class="pill">__APP_NAME__</span>
|
||||||
<span id="health" class="status"><span class="led"></span> comprobando…</span>
|
<span id="health" class="status"><span class="led"></span> checking…</span>
|
||||||
|
|
||||||
<div class="callout">
|
<div class="callout">
|
||||||
📦 <strong>En cuanto hagas tu primer <code>commit</code> + <code>push</code>, tu desarrollo
|
📦 <strong>As soon as you make your first <code>commit</code> + <code>push</code>, your code
|
||||||
reemplazará esta página.</strong> Esto es solo la bienvenida por defecto del Golden Path.
|
will replace this page.</strong> This is just the default Golden Path welcome.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Cómo empezar</h2>
|
<h2>Getting started</h2>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
Clona tu repositorio:
|
Clone your repository and set your identity:
|
||||||
<pre>git clone https://gitea.coralsafety.com/__REPO_OWNER__/__REPO_NAME__.git</pre>
|
<!-- Secuencia canónica de onboarding git (fuente única: bloque gitClone de cove-api).
|
||||||
|
Mantener idéntica a cove-api/cove_api.py y al email de tenant-manager. -->
|
||||||
|
<pre>git clone https://gitea.coralsafety.com/__REPO_OWNER__/__REPO_NAME__.git
|
||||||
|
cd __REPO_NAME__
|
||||||
|
git config user.name "Your Name"
|
||||||
|
git config user.email "you@example.com"
|
||||||
|
git remote rename origin cove</pre>
|
||||||
</li>
|
</li>
|
||||||
<li>Edita tu código en <code>code/</code> — la app es FastAPI (Python). Tu ruta <code>/</code> reemplaza esta página.</li>
|
<li>Edit your code in <code>code/</code> — the app is FastAPI (Python). Your <code>/</code> route replaces this page.</li>
|
||||||
<li>
|
<li>
|
||||||
Haz commit y push a <code>main</code>:
|
Commit and push to <code>main</code>:
|
||||||
<pre>git add .
|
<pre>git add .
|
||||||
git commit -m "Mi primer cambio"
|
git commit -m "My first change"
|
||||||
git push origin main</pre>
|
git push cove main</pre>
|
||||||
</li>
|
</li>
|
||||||
<li>El CI construye la imagen y la despliega automáticamente en tu cluster (~3–5 min).</li>
|
<li>CI builds the image and deploys it automatically to your cluster (~3–5 min).</li>
|
||||||
<li>Recarga esta misma URL — <strong>aquí verás tu desarrollo</strong>.</li>
|
<li>Reload this same URL — <strong>your app will show up here</strong>.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://gitea.coralsafety.com/__REPO_OWNER__/__REPO_NAME__" target="_blank">📂 Tu repositorio</a>
|
<a href="https://gitea.coralsafety.com/__REPO_OWNER__/__REPO_NAME__" target="_blank">📂 Your repository</a>
|
||||||
<a href="/docs">📘 API Docs (Swagger)</a>
|
<a href="/docs">📘 API Docs (Swagger)</a>
|
||||||
<a href="/health">💚 Health</a>
|
<a href="/health">💚 Health</a>
|
||||||
<a href="/demo">🧩 Demo</a>
|
<a href="/demo">🧩 Demo</a>
|
||||||
@@ -126,10 +138,10 @@ git push origin main</pre>
|
|||||||
var el = document.getElementById('health');
|
var el = document.getElementById('health');
|
||||||
var ok = d.status === 'ok';
|
var ok = d.status === 'ok';
|
||||||
el.className = 'status' + (ok ? ' ok' : '');
|
el.className = 'status' + (ok ? ' ok' : '');
|
||||||
el.innerHTML = '<span class="led"></span> ' + (ok ? 'En línea · DB: ' + d.db : 'Estado: ' + d.status);
|
el.innerHTML = '<span class="led"></span> ' + (ok ? 'Online · DB: ' + d.db : 'Status: ' + d.status);
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
var el = document.getElementById('health');
|
var el = document.getElementById('health');
|
||||||
el.innerHTML = '<span class="led"></span> /health no responde';
|
el.innerHTML = '<span class="led"></span> /health not responding';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ def test_root_serves_welcome_page(client: TestClient) -> None:
|
|||||||
body = response.text
|
body = response.text
|
||||||
# Mensaje clave: el desarrollo del tenant reemplaza esta página tras commit+push.
|
# Mensaje clave: el desarrollo del tenant reemplaza esta página tras commit+push.
|
||||||
assert "commit" in body and "push" in body
|
assert "commit" in body and "push" in body
|
||||||
assert "plataforma" in body.lower()
|
assert "platform" in body.lower()
|
||||||
# En el template (sin renderizar) el placeholder sigue presente.
|
# En el template (sin renderizar) el placeholder sigue presente.
|
||||||
assert "__APP_NAME__" in body
|
assert "__APP_NAME__" in body
|
||||||
|
|
||||||
|
|||||||
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