# __APP_NAME__ — Web Backend Python (FastAPI + Alembic) App generada desde el Golden Path **`web-backend/python`** de Coralware Cove IDP. Este repositorio contiene el scaffold completo: aplicación FastAPI, migraciones Alembic, frontend estático en `/demo`, Helm chart, workflow de CI y manifiesto de contrato con la plataforma. Cuando se materializa este template para tu tenant, los placeholders en mayúsculas se sustituyen automáticamente. > **Página de bienvenida:** al provisionarse, tu plataforma sirve una página de > bienvenida en `/` con instrucciones de uso. **En cuanto hagas tu primer > `commit` + `push`, tu desarrollo reemplaza esa página** — define tu propio > handler de `/` (en `code/app/main.py` o en `code/app/routers/`). > **Contrato del shape:** ver `coralware-shape.yaml` en la raíz. Define los > endpoints obligatorios, la BD esperada y la versión del shape. No elimines > ese archivo — la plataforma lo lee. --- ## Estructura ``` . ├── coralware-shape.yaml ← contrato con la plataforma ├── catalog-info.yaml ← entidad Backstage ├── code/ │ ├── app/ ← FastAPI app │ ├── alembic/ ← migraciones DB │ ├── tests/ ← pytest + cobertura │ ├── pyproject.toml │ ├── requirements.txt │ └── Dockerfile ├── helm/__APP_NAME__/ ← Helm chart (Deployment, Job Alembic, NP, RBAC) ├── manifests/ │ └── addonclaim.yaml ← AddonClaim fuera del chart (ADR-066) ├── scripts/ │ └── pre-helm-detach.sh ← migración v1.0.0 → v1.1.0 (idempotente) └── .gitea/workflows/ ← CI: tests + build + deploy ``` --- ## Ciclo de vida 1. **Tú escribes código** — endpoints en `code/app/routers/`, modelos en `code/app/models/`, migraciones con `alembic revision --autogenerate`. 2. **`git push`** a `main` dispara el workflow Gitea Actions. 3. El CI corre `black`, `ruff`, `bandit`, `pytest --cov`. Si algo falla, el pipeline aborta. 4. Si pasa, builda la imagen `linux/amd64` con BuildKit y la publica en el proyecto Harbor de tu tenant. 5. `helm upgrade --install` aplica el chart al cluster de tu tenant. Antes del rollout, un Job pre-upgrade corre `alembic upgrade head`. Si Alembic falla, Helm aborta y la app vieja queda intacta. 6. Tras rollout exitoso, smoke check valida `/health` 200. --- ## Orden de aplicación — AddonClaim + hooks Helm El workflow del tenant sigue este orden en cada `git push`: | Paso | Acción | Motivo | |------|--------|--------| | 1 | `bash scripts/pre-helm-detach.sh` | Migración chart 1.0.0 → 1.1.0: si el AddonClaim tiene labels/annotations de un release Helm previo, las quita. Idempotente: skip si no aplica. Sunset tras ~2 semanas. | | 2 | `kubectl apply -f manifests/addonclaim.yaml` (con `sed` para placeholders) | Crea o actualiza el `AddonClaim`. Idempotente. `addon-provisioner` empieza a reconciliar en paralelo. | | 3 | `helm upgrade --install ./helm/ --wait --timeout 10m` | Aplica el chart Helm. Los hooks `pre-install,pre-upgrade` corren en orden de `hook-weight` (ver tabla abajo). El Job Alembic espera al Secret. | > **Deploys serializados + auto-recuperación:** el workflow tiene un guard > `concurrency` por repo, así que dos `git push` seguidos se **encolan** (no corren > el `helm upgrade` en paralelo). Si un deploy quedó a medias (cancelado), el > siguiente run **detecta y recupera** el release atascado (`pending-install` → > reinstala; `pending-upgrade` → rollback) antes de reintentar. El chart usa hooks `pre-install,pre-upgrade` con `hook-weight` para garantizar el orden de creación de SUS recursos (sin AddonClaim — ese vive fuera). `helm upgrade --install` funciona **sin `--no-hooks`**: | Recurso | Template | `hook-weight` | Motivo | |---------|----------|---------------|--------| | `NetworkPolicy` | `networkpolicy.yaml` | `-8` | Bajo el `default-deny` del namespace `apps`, esta NP permite DNS + apiserver + addons:3306. Debe existir antes que el InitContainer del Job Alembic intente conectarse al apiserver. | | `Role` + `RoleBinding` | `role-db-secret-reader.yaml` | `-7` | Permite al SA leer el Secret `-db-creds` (lo consume el InitContainer `wait-for-secret` del Job Alembic) | | `ServiceAccount` | `serviceaccount.yaml` | `-5` | Lo referencia el pod del Job Alembic — debe existir antes | | `Job` Alembic | `job-alembic-upgrade.yaml` | `0` | InitContainer `wait-for-secret` espera al Secret publicado por `addon-provisioner` (hasta 360s — 3× RECONCILE_INTERVAL 60s), luego corre `alembic upgrade head` | ### Por qué el `AddonClaim` vive FUERA del chart (ADR-066) En chart 0.2.x el `AddonClaim` era hook `pre-install,pre-upgrade` weight `-10`. El default de Helm 3 (`hook-delete-policy: before-hook-creation`) borraba el claim previo antes de aplicar el nuevo en cada `helm upgrade --install`. El finalizer `addon-provisioner.idp.coralware.cloud/cleanup-logical` ejecutaba entonces `REVOKE` + `DROP USER` + delete del Secret en cada upgrade → rotación involuntaria de credenciales + release atorado en `pending-upgrade`. Convertir el `AddonClaim` a manifest principal del chart (no hook) también falla: los hooks `pre-install,pre-upgrade` corren ANTES del manifest principal, así que el Job Alembic intentaría leer el Secret antes de que el AddonClaim exista — chicken-and-egg fatal. La solución es desacoplar el lifecycle del CRD del lifecycle del release Helm: el `AddonClaim` lo aplica el workflow con `kubectl apply` (idempotente, sin ownership Helm), y Helm gobierna sólo la app + sus hooks Alembic/NP/RBAC. `helm uninstall` no toca el AddonClaim — desacoplamiento garantizado. Borrar la BD lógica es decisión explícita: ```bash kubectl delete addonclaim -db -n apps ``` (Con `deletionPolicy: Retain` — default — preserva la BD lógica; el operador emite Event `OrphanLogicalDatabase`. Cambiar a `Delete` previo para `DROP DATABASE`.) ### Por qué el ServiceAccount es hook El `ServiceAccount` se materializa como hook (no en la fase principal de Helm) porque el Job Alembic, que también es hook, declara `serviceAccountName`. Si el SA viviera en la fase principal se crearía **después** del Job → el pod del Job nunca arrancaría (`serviceaccount not found`) → `helm --wait` haría timeout. El SA-hook usa `hook-delete-policy: before-hook-creation` (no `hook-succeeded`): el Deployment lo necesita vivo en runtime, así que solo se borra/recrea al inicio del siguiente hook, manteniéndolo idempotente entre upgrades. El `imagePullSecret` lo provee la plataforma (stack-deployer), no el chart. El Secret `-db-creds` lo publica `addon-provisioner` tras reconciliar el `AddonClaim` (ver §Base de datos). --- ## Endpoints obligatorios | Path | Auth | Propósito | |------|------|-----------| | `/health` | none | Liveness + readiness; devuelve `{status: ok, db: up\|down}` | | `/docs` | configurable (`OPENAPI_REQUIRE_AUTH`) | Swagger UI | | `/openapi.json` | igual que `/docs` | Schema OpenAPI | | `/demo` | none | Frontend estático servido desde `code/app/static/demo/` | | `/metrics` | restringido vía NetworkPolicy | Métricas Prometheus | `/health`, `/docs`, `/demo` son contrato del shape — NO renombres. --- ## Base de datos Tu app espera un Secret `__APP_NAME__-db-creds` en su namespace con las claves `DATABASE_URL` / `DATABASE_HOST` / `DATABASE_PORT` / `DATABASE_NAME` / `DATABASE_USER` / `DATABASE_PASSWORD` (y opcionalmente `MIGRATION_DATABASE_URL`). El Secret lo materializa automáticamente la plataforma vía el flujo `AddonClaim` → `addon-provisioner` (ADR-052 + ADR-064 + ADR-066): 1. El workflow del tenant aplica `manifests/addonclaim.yaml` con `kubectl apply -f -` (placeholders sustituidos con `sed`). El claim declara `addonType: mariadb`, `databaseName` derivado del nombre del release y el `secretName` esperado. 2. `addon-provisioner` (corriendo en el plano de control IDP) reconcilia el `AddonClaim`: crea una BD lógica + user + grant en la instancia MariaDB compartida del tenant (ns `addons` del cluster tenant) vía Job DDL efímero. 3. Publica el Secret `__APP_NAME__-db-creds` en el ns `apps`. 4. El workflow corre `helm upgrade --install`. El InitContainer `wait-for-secret` del Job Alembic (hook `pre-install,pre-upgrade`) espera al Secret (hasta 360s), Alembic ejecuta `upgrade head`, y el Deployment rola. ### Opt-out — apps stateless Si tu app no necesita BD: 1. En `values.yaml`: ```yaml database: enabled: false # no se aplica Role/RoleBinding al SA alembic: enabled: false # no corre el Job Alembic ``` 2. Elimina el step "Aplicar AddonClaim" del workflow `.gitea/workflows/__APP_NAME__-build.yaml` (junto con `manifests/addonclaim.yaml` y el step `pre-helm-detach`). Con `database.enabled=false` la app se despliega sin BD ni migraciones. --- ## Variables de entorno | Variable | Origen | Default | Descripción | |----------|--------|---------|-------------| | `DATABASE_URL` | Secret | — | Connection string runtime | | `MIGRATION_DATABASE_URL` | Secret | = `DATABASE_URL` | Connection para Alembic | | `LOG_LEVEL` | ConfigMap | `INFO` | `DEBUG` para troubleshooting | | `CORRELATION_ID_HEADER` | ConfigMap | `X-Correlation-ID` | Header de trazabilidad | | `OPENAPI_REQUIRE_AUTH` | ConfigMap | `false` | Activar JWT en `/docs` | | `KEYCLOAK_ISSUER` | ConfigMap | — | URL del realm cuando auth activado | | `STATIC_DEMO_PATH` | ConfigMap | `/app/static/demo` | Path al frontend estático | --- ## Migraciones Alembic Migración base vacía en `code/alembic/versions/0001_initial.py`. Para crear una nueva tras agregar modelos: ```bash cd code alembic revision --autogenerate -m "agregar tabla users" ``` --- ## Desarrollo local ```bash cd code pip install -r requirements-dev.txt docker run --rm -d --name mariadb-dev \ -e MARIADB_ROOT_PASSWORD=dev -e MARIADB_DATABASE=app \ -p 3306:3306 mariadb:11 export DATABASE_URL="mysql+pymysql://root:dev@127.0.0.1:3306/app" export MIGRATION_DATABASE_URL="$DATABASE_URL" alembic upgrade head uvicorn app.main:app --reload --port 8000 ``` --- ## Plan de salida Si decides migrar fuera de Cove, todo lo que necesitas vive en formato estándar: | Componente | Cómo lo exportas | |-----------|------------------| | Código | `git remote add github && git push github main` (espejo externo a GitHub) | | Imagen | `docker pull` desde Harbor + push a tu registry | | Manifests | Ya están en `helm/` — `helm template` genera YAML aplicable | | 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. --- ## Referencias - `docs/adr-053-shape-web-backend-python.md` — anatomía del shape - `docs/adr-052-tenant-addon-claim-model.md` — modelo de add-ons (BD) - `docs/adr-064-free-trial-allows-mariadb.md` — free-trial permite MariaDB - `docs/adr-038-ci-estandar-reusable-workflow.md` — pipeline Python