From fcec92ea1d63c422e74789f3ce57ba10304d8b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irving=20L=C3=B3pez?= Date: Wed, 6 May 2026 17:48:13 -0500 Subject: [PATCH] feat: sincronizar scaffold completo del shape web-backend/python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sincroniza el scaffold canónico desde templates/web-backend-python/ del repo IDP. Reemplaza el placeholder mínimo previo (.gitea/workflows/ci.yaml, README.md, .gitignore, catalog-info.yaml) por el scaffold completo documentado en ADR-053 §3: - code/ — FastAPI app con /health (db check), /metrics, /demo estático, middleware correlation_id, settings Pydantic, db.py con SQLAlchemy - code/alembic/ — migración inicial vacía + env.py usando MIGRATION_DATABASE_URL - code/tests/ — pytest + httpx TestClient (test_health, test_demo) - helm/APP_NAME/ — chart con Deployment, Service, IngressRoute Traefik, NetworkPolicy, ServiceMonitor, Job alembic-upgrade como hook pre-upgrade,pre-install (backoffLimit: 0) - coralware-shape.yaml — manifiesto v1alpha1 del contrato del shape - claim-mariadb.yaml — AddonClaim declarativo (uso futuro ADR-052) - catalog-info.yaml — entidad Backstage actualizada - .gitea/workflows/APP_NAME-build.yaml — invoca reusable workflow ADR-038 + job helm-deploy con KUBECONFIG_TENANT_B64 Placeholders __APP_NAME__, __TIER__, __TENANT_ID__, __TENANT_ID_NODASHES__ serán sustituidos server-side por repo-provisioner cuando materialice el template para un tenant (pendiente — ADR-053 §11.5). El openspec/ del golden path NO se sincroniza al repo Gitea (gobernanza interna de Coralware). Refs: ADR-052, ADR-053, poc-nexobms.md (PoC NexoBMS Demetrio). --- .gitea/workflows/APP_NAME-build.yaml | 83 ++++++++++ .gitea/workflows/ci.yaml | 15 -- .gitignore | 24 ++- README.md | 144 +++++++++++++++++- catalog-info.yaml | 2 + claim-mariadb.yaml | 25 +++ code/Dockerfile | 28 ++++ code/alembic.ini | 44 ++++++ code/alembic/env.py | 57 +++++++ code/alembic/versions/0001_initial.py | 25 +++ code/app/__init__.py | 0 code/app/db.py | 40 +++++ code/app/main.py | 115 ++++++++++++++ code/app/models/__init__.py | 8 + code/app/routers/__init__.py | 2 + code/app/settings.py | 32 ++++ code/app/static/demo/index.html | 41 +++++ code/pyproject.toml | 37 +++++ code/requirements-dev.txt | 6 + code/requirements.txt | 10 ++ code/tests/__init__.py | 0 code/tests/conftest.py | 34 +++++ code/tests/test_demo.py | 22 +++ code/tests/test_health.py | 30 ++++ coralware-shape.yaml | 38 +++++ helm/APP_NAME/.helmignore | 11 ++ helm/APP_NAME/Chart.yaml | 12 ++ helm/APP_NAME/templates/_helpers.tpl | 32 ++++ helm/APP_NAME/templates/deployment.yaml | 87 +++++++++++ helm/APP_NAME/templates/ingressroute.yaml | 19 +++ .../templates/job-alembic-upgrade.yaml | 59 +++++++ helm/APP_NAME/templates/networkpolicy.yaml | 66 ++++++++ helm/APP_NAME/templates/service.yaml | 15 ++ helm/APP_NAME/templates/servicemonitor.yaml | 16 ++ helm/APP_NAME/values.yaml | 69 +++++++++ 35 files changed, 1216 insertions(+), 32 deletions(-) create mode 100644 .gitea/workflows/APP_NAME-build.yaml delete mode 100644 .gitea/workflows/ci.yaml create mode 100644 claim-mariadb.yaml create mode 100644 code/Dockerfile create mode 100644 code/alembic.ini create mode 100644 code/alembic/env.py create mode 100644 code/alembic/versions/0001_initial.py create mode 100644 code/app/__init__.py create mode 100644 code/app/db.py create mode 100644 code/app/main.py create mode 100644 code/app/models/__init__.py create mode 100644 code/app/routers/__init__.py create mode 100644 code/app/settings.py create mode 100644 code/app/static/demo/index.html create mode 100644 code/pyproject.toml create mode 100644 code/requirements-dev.txt create mode 100644 code/requirements.txt create mode 100644 code/tests/__init__.py create mode 100644 code/tests/conftest.py create mode 100644 code/tests/test_demo.py create mode 100644 code/tests/test_health.py create mode 100644 coralware-shape.yaml create mode 100644 helm/APP_NAME/.helmignore create mode 100644 helm/APP_NAME/Chart.yaml create mode 100644 helm/APP_NAME/templates/_helpers.tpl create mode 100644 helm/APP_NAME/templates/deployment.yaml create mode 100644 helm/APP_NAME/templates/ingressroute.yaml create mode 100644 helm/APP_NAME/templates/job-alembic-upgrade.yaml create mode 100644 helm/APP_NAME/templates/networkpolicy.yaml create mode 100644 helm/APP_NAME/templates/service.yaml create mode 100644 helm/APP_NAME/templates/servicemonitor.yaml create mode 100644 helm/APP_NAME/values.yaml diff --git a/.gitea/workflows/APP_NAME-build.yaml b/.gitea/workflows/APP_NAME-build.yaml new file mode 100644 index 0000000..0dc4bf7 --- /dev/null +++ b/.gitea/workflows/APP_NAME-build.yaml @@ -0,0 +1,83 @@ +name: __APP_NAME__ — build & deploy + +on: + push: + branches: [main] + paths: + - 'code/**' + - 'helm/**' + - '.gitea/workflows/__APP_NAME__-build.yaml' + +jobs: + # Tests + build + push a Harbor — patrón ADR-038 reusable workflow. + build: + uses: coralware/IDP/.gitea/workflows/_python-microservice.yaml@main + with: + service_name: __APP_NAME__ + service_dir: code + uses_metrics: false + coverage_threshold: 80 # menor que el IDP interno (90); el dev sube cuando madure + trivy_exit_code: 0 # informacional en MVP; subir a 1 antes de producción + enforce_rollback: false # rollout lo gestiona el job helm-deploy abajo + + # Deploy al cluster del tenant — Helm upgrade --install con hook Alembic. + helm-deploy: + needs: build + runs-on: [self-hosted, linux/amd64] + container: alpine/helm:3.14.4 + steps: + - uses: actions/checkout@v4 + + - name: Install kubectl + run: apk add --no-cache kubectl + + - name: Decodificar kubeconfig del tenant + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_TENANT_B64 }} + run: | + if [ -z "$KUBECONFIG_B64" ]; then + echo "ERROR: secret KUBECONFIG_TENANT_B64 no configurado en el proyecto Gitea." + echo "tenant-manager debe provisionarlo al crear el repo." + exit 1 + fi + mkdir -p /root/.kube + echo "$KUBECONFIG_B64" | base64 -d > /root/.kube/config + chmod 600 /root/.kube/config + + - name: Helm upgrade --install + env: + TENANT_ID: ${{ vars.TENANT_ID }} + TENANT_ID_NODASHES: ${{ vars.TENANT_ID_NODASHES }} + TENANT_TIER: ${{ vars.TENANT_TIER }} + IMAGE_TAG: ${{ needs.build.outputs.image_tag }} + run: | + helm upgrade --install __APP_NAME__ ./helm/__APP_NAME__ \ + --namespace tenant-${TENANT_ID_NODASHES}-apps \ + --create-namespace \ + --set image.tag=${IMAGE_TAG} \ + --set tenantId=${TENANT_ID} \ + --set tenantIdNodashes=${TENANT_ID_NODASHES} \ + --set tier=${TENANT_TIER} \ + --wait --timeout 5m + + - name: Verificar rollout + env: + TENANT_ID_NODASHES: ${{ vars.TENANT_ID_NODASHES }} + run: | + kubectl -n tenant-${TENANT_ID_NODASHES}-apps \ + rollout status deploy/__APP_NAME__ --timeout=3m + + - name: Smoke check post-deploy + env: + INGRESS_HOST: __APP_NAME__-${{ vars.TENANT_ID_NODASHES }}.apps.coralware.cloud + run: | + for i in $(seq 1 12); do + if wget -q -O - --timeout=10 "https://${INGRESS_HOST}/health" | grep -q '"status":"ok"'; then + echo "Smoke OK: /health responde 200 con db:up" + exit 0 + fi + echo "Intento ${i}/12 — /health aún no responde, esperando 5s…" + sleep 5 + done + echo "ERROR: /health no respondió 200 dentro del timeout" + exit 1 diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml deleted file mode 100644 index ed5e22b..0000000 --- a/.gitea/workflows/ci.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: ci - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: [self-hosted] - steps: - - uses: actions/checkout@v3 - - name: Build - run: echo "Configurar pasos de build para tu proyecto" diff --git a/.gitignore b/.gitignore index cb048bc..8fa8c20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,14 @@ -# Python __pycache__/ *.py[cod] -.env -venv/ +*.egg-info/ +.pytest_cache/ +.ruff_cache/ +.coverage +htmlcov/ .venv/ - -# Node.js -node_modules/ -dist/ +venv/ .env - -# Go -*.exe -*.out -vendor/ - -# General +.idea/ +.vscode/ +*.swp .DS_Store -*.log diff --git a/README.md b/README.md index 0055071..1adcf3d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,143 @@ -# template-web-backend-python +# __APP_NAME__ — Web Backend Python (FastAPI + Alembic) -Template Golden Path — Web Backend con Python \ No newline at end of file +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. + +> **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 con hook Alembic pre-upgrade +├── claim-mariadb.yaml ← AddonClaim de la BD (ver §Base de datos) +└── .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 imagen multi-arch con BuildKit y la publica en Harbor. +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. + +--- + +## 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` y `MIGRATION_DATABASE_URL`. El Secret lo provisiona la +plataforma: + +- **PoC / MVP** — Coralware lo crea manualmente siguiendo el runbook + `docs/runbook-mariadb-poc.md` en el repo IDP. Tú no haces nada. +- **Producción (Fase 5.5+)** — `claim-mariadb.yaml` lo materializa + automáticamente cuando esté disponible el controller `addon-provisioner` + (ver ADR-052). + +--- + +## 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` | +| 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` | + +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-038-ci-estandar-reusable-workflow.md` — pipeline Python diff --git a/catalog-info.yaml b/catalog-info.yaml index 6637a17..179dc2c 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -9,3 +9,5 @@ spec: type: __TYPE__ lifecycle: experimental owner: __OWNER__ + dependsOn: + - resource:__APP_NAME__-db diff --git a/claim-mariadb.yaml b/claim-mariadb.yaml new file mode 100644 index 0000000..9125f15 --- /dev/null +++ b/claim-mariadb.yaml @@ -0,0 +1,25 @@ +# AddonClaim — referencia futura para cuando ADR-052 esté implementado. +# +# DURANTE LA POC NEXOBMS Y HASTA QUE addon-provisioner ESTÉ DISPONIBLE: +# este archivo NO se aplica. Coralware materializa el Secret +# __APP_NAME__-db-creds manualmente siguiendo docs/runbook-mariadb-poc.md. +# +# CUANDO ADR-052 ESTÉ EN FASE 5.5+: +# - el dev/CI hace `kubectl apply -f claim-mariadb.yaml` al ns de la app +# - addon-provisioner crea BD lógica + user + Secret automáticamente +# - el campo .spec.secretName debe coincidir con .Values.database.secretName +# del Helm chart (por convención: "-db-creds") +apiVersion: idp.coralware.cloud/v1alpha1 +kind: AddonClaim +metadata: + name: __APP_NAME__-db + labels: + idp.coralware.cloud/tenant-id: __TENANT_ID__ + idp.coralware.cloud/app: __APP_NAME__ +spec: + addonType: mariadb + databaseName: __APP_NAME___db # nombre de la BD lógica + user: + permissions: rw # rw | ro — default rw + secretName: __APP_NAME__-db-creds + deletionPolicy: Retain # Retain (default) | Delete diff --git a/code/Dockerfile b/code/Dockerfile new file mode 100644 index 0000000..f9f3b6b --- /dev/null +++ b/code/Dockerfile @@ -0,0 +1,28 @@ +# Multi-stage para imagen final lean. +# Stage 1: deps — instala dependencias Python en una venv aislada. +# Stage 2: runtime — copia código y venv, corre como user no-root con FS read-only. + +FROM python:3.12-slim AS deps +WORKDIR /build +COPY requirements.txt . +RUN python -m venv /venv \ + && /venv/bin/pip install --no-cache-dir --upgrade pip \ + && /venv/bin/pip install --no-cache-dir -r requirements.txt + +FROM python:3.12-slim AS runtime +WORKDIR /app + +COPY --from=deps /venv /venv +ENV PATH="/venv/bin:$PATH" + +COPY app /app/app +COPY alembic /app/alembic +COPY alembic.ini /app/alembic.ini + +RUN useradd -u 1001 -r appuser \ + && chown -R appuser:appuser /app +USER appuser + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log"] diff --git a/code/alembic.ini b/code/alembic.ini new file mode 100644 index 0000000..4ee7fc7 --- /dev/null +++ b/code/alembic.ini @@ -0,0 +1,44 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# La URL real la lee env.py de la variable MIGRATION_DATABASE_URL (o DATABASE_URL). +# NO hardcodear aquí. +sqlalchemy.url = + +[post_write_hooks] + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/code/alembic/env.py b/code/alembic/env.py new file mode 100644 index 0000000..7d32691 --- /dev/null +++ b/code/alembic/env.py @@ -0,0 +1,57 @@ +""" +Alembic env.py — punto de entrada de las migraciones. + +Lee la URL de conexión de MIGRATION_DATABASE_URL (recomendado: user con DDL +separado), o de DATABASE_URL como fallback. +""" + +from logging.config import fileConfig + +from sqlalchemy import engine_from_config, pool + +from alembic import context +from app.settings import settings + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +config.set_main_option("sqlalchemy.url", settings.migration_url) + +# Importar Base y modelos para que --autogenerate los detecte: +# from app.models import Base +# target_metadata = Base.metadata +target_metadata = None + + +def run_migrations_offline() -> None: + """Genera SQL sin conectar — útil para revisar antes de aplicar.""" + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Aplica migraciones contra la BD real.""" + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/code/alembic/versions/0001_initial.py b/code/alembic/versions/0001_initial.py new file mode 100644 index 0000000..2dd8065 --- /dev/null +++ b/code/alembic/versions/0001_initial.py @@ -0,0 +1,25 @@ +"""migración inicial — esquema vacío + +Revision ID: 0001_initial +Revises: +Create Date: 2026-05-04 +""" + +import sqlalchemy as sa # noqa: F401 + +from alembic import op # noqa: F401 + +revision = "0001_initial" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Esquema base vacío. Cuando agregues modelos en app/models/, regenera con: + # alembic revision --autogenerate -m "agregar tabla X" + pass + + +def downgrade() -> None: + pass diff --git a/code/app/__init__.py b/code/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/app/db.py b/code/app/db.py new file mode 100644 index 0000000..21e01f7 --- /dev/null +++ b/code/app/db.py @@ -0,0 +1,40 @@ +""" +Capa de conexión SQLAlchemy. +- engine único por proceso, configurado al startup +- sesiones por request (dependency injection FastAPI) +- ping ligero para /health +""" + +from collections.abc import Iterator + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import Session, sessionmaker + +from app.settings import settings + +engine = create_engine( + settings.DATABASE_URL, + pool_pre_ping=True, + pool_recycle=3600, +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_session() -> Iterator[Session]: + """Dependency FastAPI: una sesión por request, cierre garantizado.""" + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def db_alive() -> bool: + """Ping de conexión usado por /health. Retorna False ante cualquier excepción.""" + try: + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + return True + except Exception: + return False diff --git a/code/app/main.py b/code/app/main.py new file mode 100644 index 0000000..556fa42 --- /dev/null +++ b/code/app/main.py @@ -0,0 +1,115 @@ +""" +Aplicación FastAPI — entry point. + +Endpoints obligatorios del shape web-backend/python (ver coralware-shape.yaml): +- /health → liveness + readiness +- /docs → Swagger UI (auth opcional) +- /demo → frontend estático servido desde STATIC_DEMO_PATH +- /metrics → Prometheus + +Endpoints de dominio: agregar en app/routers/ y registrar en _register_routers(). +""" + +import logging +import os + +from fastapi import Depends, FastAPI, HTTPException, Request, status +from fastapi.responses import JSONResponse, Response +from fastapi.staticfiles import StaticFiles +from prometheus_client import CONTENT_TYPE_LATEST, generate_latest + +from app.db import db_alive +from app.settings import settings + +logging.basicConfig( + level=getattr(logging, settings.LOG_LEVEL, logging.INFO), + format="%(asctime)s [%(levelname)s] [cid=%(correlation_id)s] %(message)s", + datefmt="%Y-%m-%dT%H:%M:%SZ", +) + + +class CorrelationIdFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + if not hasattr(record, "correlation_id"): + record.correlation_id = "-" + return True + + +logging.getLogger().addFilter(CorrelationIdFilter()) +log = logging.getLogger("__APP_NAME__") + + +def _require_auth_dep() -> None: + """ + Dependency placeholder para /docs cuando OPENAPI_REQUIRE_AUTH=true. + Implementación real: validar JWT contra KEYCLOAK_ISSUER (ADR-029 R-004). + """ + if not settings.OPENAPI_REQUIRE_AUTH: + return + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Auth requerido — implementar validación JWT contra Keycloak", + ) + + +def create_app() -> FastAPI: + app = FastAPI( + title="__APP_NAME__", + version="0.1.0", + dependencies=[Depends(_require_auth_dep)] if settings.OPENAPI_REQUIRE_AUTH else [], + ) + + @app.middleware("http") + async def correlation_id_middleware(request: Request, call_next): + cid = request.headers.get(settings.CORRELATION_ID_HEADER) or _generate_cid() + request.state.correlation_id = cid + response = await call_next(request) + response.headers[settings.CORRELATION_ID_HEADER] = cid + return response + + @app.get("/health", tags=["platform"]) + async def health() -> JSONResponse: + db_status = "up" if db_alive() else "down" + code = 200 if db_status == "up" else 503 + return JSONResponse( + status_code=code, + content={"status": "ok" if code == 200 else "degraded", "db": db_status}, + ) + + @app.get("/metrics", tags=["platform"]) + async def metrics() -> Response: + return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST) + + if os.path.isdir(settings.STATIC_DEMO_PATH): + app.mount( + "/demo", + StaticFiles(directory=settings.STATIC_DEMO_PATH, html=True), + name="demo", + ) + else: + log.warning( + "STATIC_DEMO_PATH=%s no existe — endpoint /demo no será servido", + settings.STATIC_DEMO_PATH, + ) + + _register_routers(app) + return app + + +def _generate_cid() -> str: + """Genera correlation_id si el cliente no envió uno.""" + import time + import uuid + + return f"__APP_NAME__-{int(time.time() * 1000)}-{uuid.uuid4().hex[:8]}" + + +def _register_routers(app: FastAPI) -> None: + """ + Registra los routers de dominio. + Agrega aquí los include_router(...) conforme tu app crezca. + """ + pass + + +app = create_app() diff --git a/code/app/models/__init__.py b/code/app/models/__init__.py new file mode 100644 index 0000000..baee6e3 --- /dev/null +++ b/code/app/models/__init__.py @@ -0,0 +1,8 @@ +# Modelos SQLAlchemy. Define tu Base aquí y heredala en cada modelo: +# +# from sqlalchemy.orm import DeclarativeBase +# class Base(DeclarativeBase): +# pass +# +# Tras crear modelos, genera la migración con: +# alembic revision --autogenerate -m "descripción del cambio" diff --git a/code/app/routers/__init__.py b/code/app/routers/__init__.py new file mode 100644 index 0000000..a2e9693 --- /dev/null +++ b/code/app/routers/__init__.py @@ -0,0 +1,2 @@ +# Routers de dominio. Crea un módulo por agregado (users.py, orders.py, etc.) +# y regístralos en app/main.py:_register_routers(). diff --git a/code/app/settings.py b/code/app/settings.py new file mode 100644 index 0000000..e738f4d --- /dev/null +++ b/code/app/settings.py @@ -0,0 +1,32 @@ +""" +Configuración runtime via variables de entorno. +Toda la configuración pasa por aquí — no leas env vars dispersos en otros módulos. +""" + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=None, case_sensitive=True) + + DATABASE_URL: str = Field(..., description="Connection string runtime de la app") + MIGRATION_DATABASE_URL: str = Field( + default="", + description="Connection string para Alembic; si vacío, usa DATABASE_URL", + ) + + LOG_LEVEL: str = "INFO" + CORRELATION_ID_HEADER: str = "X-Correlation-ID" + + OPENAPI_REQUIRE_AUTH: bool = False + KEYCLOAK_ISSUER: str = "" + + STATIC_DEMO_PATH: str = "/app/static/demo" + + @property + def migration_url(self) -> str: + return self.MIGRATION_DATABASE_URL or self.DATABASE_URL + + +settings = Settings() diff --git a/code/app/static/demo/index.html b/code/app/static/demo/index.html new file mode 100644 index 0000000..b402a60 --- /dev/null +++ b/code/app/static/demo/index.html @@ -0,0 +1,41 @@ + + + + + + __APP_NAME__ — demo + + + +

__APP_NAME__

+

App generada desde el Golden Path web-backend/python.

+

+ Este es el frontend estático servido en /demo. Modifica + code/app/static/demo/ para reemplazarlo con tu UI. +

+ +
cargando…
+ + + + diff --git a/code/pyproject.toml b/code/pyproject.toml new file mode 100644 index 0000000..b172b70 --- /dev/null +++ b/code/pyproject.toml @@ -0,0 +1,37 @@ +[project] +name = "__APP_NAME__" +version = "0.1.0" +description = "Web backend Python (FastAPI + Alembic) — Golden Path web-backend/python" +requires-python = ">=3.12" + +[tool.black] +line-length = 100 +target-version = ["py312"] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "B", "UP", "S"] +ignore = ["S101"] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["S105", "S106"] + +[tool.bandit] +exclude_dirs = ["tests", ".venv"] + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] +addopts = "-v --strict-markers" +asyncio_mode = "auto" + +[tool.coverage.run] +source = ["app"] +omit = ["app/__init__.py", "tests/*"] + +[tool.coverage.report] +fail_under = 80 +show_missing = true diff --git a/code/requirements-dev.txt b/code/requirements-dev.txt new file mode 100644 index 0000000..6f03657 --- /dev/null +++ b/code/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +-r ../../code/requirements-dev-base.txt + +httpx==0.27.2 +pytest-asyncio==0.24.0 +testcontainers[mariadb]==4.8.2 diff --git a/code/requirements.txt b/code/requirements.txt new file mode 100644 index 0000000..61699be --- /dev/null +++ b/code/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 +pydantic==2.9.2 +pydantic-settings==2.6.0 +sqlalchemy==2.0.36 +alembic==1.13.3 +pymysql==1.1.1 +cryptography==43.0.3 +prometheus-client==0.21.0 +python-jose[cryptography]==3.3.0 diff --git a/code/tests/__init__.py b/code/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/code/tests/conftest.py b/code/tests/conftest.py new file mode 100644 index 0000000..4551ada --- /dev/null +++ b/code/tests/conftest.py @@ -0,0 +1,34 @@ +""" +Fixtures para los tests del shape. + +Levanta una MariaDB efímera con testcontainers para que los tests corran +contra una BD real (no mocks). El reusable workflow ADR-038 garantiza que +Docker está disponible en el runner. +""" + +import os +from collections.abc import Iterator + +import pytest +from fastapi.testclient import TestClient +from testcontainers.mariadb import MariaDbContainer + + +@pytest.fixture(scope="session") +def mariadb_url() -> Iterator[str]: + """MariaDB efímera por sesión de tests; URL exportada como DATABASE_URL.""" + with MariaDbContainer("mariadb:11") as mdb: + url = mdb.get_connection_url().replace("mysql://", "mysql+pymysql://", 1) + os.environ["DATABASE_URL"] = url + os.environ["MIGRATION_DATABASE_URL"] = url + yield url + + +@pytest.fixture +def client(mariadb_url: str) -> Iterator[TestClient]: + """TestClient con la app cargada contra la BD efímera.""" + from app.main import create_app + + app = create_app() + with TestClient(app) as c: + yield c diff --git a/code/tests/test_demo.py b/code/tests/test_demo.py new file mode 100644 index 0000000..554787a --- /dev/null +++ b/code/tests/test_demo.py @@ -0,0 +1,22 @@ +"""Cubre el endpoint /demo: sirve el index estático.""" +from fastapi.testclient import TestClient + + +def test_demo_serves_index_html(client: TestClient) -> None: + response = client.get("/demo/") + assert response.status_code == 200 + assert "text/html" in response.headers.get("content-type", "") + assert "__APP_NAME__" in response.text or "demo" in response.text.lower() + + +def test_metrics_endpoint_responds(client: TestClient) -> None: + response = client.get("/metrics") + assert response.status_code == 200 + assert "text/plain" in response.headers.get("content-type", "") + + +def test_openapi_accessible_when_auth_disabled(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200 + spec = response.json() + assert spec["info"]["title"] == "__APP_NAME__" diff --git a/code/tests/test_health.py b/code/tests/test_health.py new file mode 100644 index 0000000..d092d98 --- /dev/null +++ b/code/tests/test_health.py @@ -0,0 +1,30 @@ +"""Cubre el endpoint /health: respuesta 200 con la BD viva, 503 cuando cae.""" +from unittest.mock import patch + +from fastapi.testclient import TestClient + + +def test_health_db_up_returns_200(client: TestClient) -> None: + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "ok", "db": "up"} + + +def test_health_db_down_returns_503(client: TestClient) -> None: + with patch("app.main.db_alive", return_value=False): + response = client.get("/health") + assert response.status_code == 503 + body = response.json() + assert body["status"] == "degraded" + assert body["db"] == "down" + + +def test_health_propagates_correlation_id(client: TestClient) -> None: + cid = "test-cid-12345" + response = client.get("/health", headers={"X-Correlation-ID": cid}) + assert response.headers.get("X-Correlation-ID") == cid + + +def test_health_generates_correlation_id_when_absent(client: TestClient) -> None: + response = client.get("/health") + assert response.headers.get("X-Correlation-ID") diff --git a/coralware-shape.yaml b/coralware-shape.yaml new file mode 100644 index 0000000..f065f96 --- /dev/null +++ b/coralware-shape.yaml @@ -0,0 +1,38 @@ +# Manifiesto de contrato del shape — leído por la plataforma Coralware +# para entender qué endpoints expone tu app y qué dependencias necesita. +# NO eliminar este archivo. Sí puedes ajustar valores conforme tu app evolucione. +apiVersion: coralware.cloud/v1alpha1 +kind: Shape +metadata: + name: web-backend-python +spec: + goldenPath: web-backend + goldenPathLang: python + shapeVersion: "1.0.0" + + app: + name: __APP_NAME__ + framework: fastapi + pythonVersion: "3.12" + + endpoints: + health: /health + docs: /docs + demo: /demo + metrics: /metrics + + database: + required: true + type: mariadb + secretName: __APP_NAME__-db-creds + keys: + - DATABASE_URL + - MIGRATION_DATABASE_URL + + migrations: + tool: alembic + hook: pre-upgrade + + observability: + metricsPort: 9090 + prometheusScrape: true diff --git a/helm/APP_NAME/.helmignore b/helm/APP_NAME/.helmignore new file mode 100644 index 0000000..15c644d --- /dev/null +++ b/helm/APP_NAME/.helmignore @@ -0,0 +1,11 @@ +.DS_Store +.git/ +.gitignore +.idea/ +.vscode/ +*.tmproj +.project +*.tgz +.helmignore +README.md +LICENSE diff --git a/helm/APP_NAME/Chart.yaml b/helm/APP_NAME/Chart.yaml new file mode 100644 index 0000000..0156f0d --- /dev/null +++ b/helm/APP_NAME/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +name: __APP_NAME__ +description: Helm chart del Golden Path web-backend/python +type: application +version: 0.1.0 +appVersion: "0.1.0" +keywords: + - fastapi + - web-backend + - coralware-cove +maintainers: + - name: __OWNER__ diff --git a/helm/APP_NAME/templates/_helpers.tpl b/helm/APP_NAME/templates/_helpers.tpl new file mode 100644 index 0000000..b90b3ea --- /dev/null +++ b/helm/APP_NAME/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* +Labels comunes a todos los recursos del chart. +Sigue convenciones CLAUDE.md global + IDP (idp.coralware.cloud/*). +*/}} +{{- define "app.labels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: {{ .Chart.Name }} +idp.coralware.cloud/tier: {{ .Values.tier | quote }} +idp.coralware.cloud/tenant-id: {{ .Values.tenantId | quote }} +idp.coralware.cloud/golden-path: {{ .Values.goldenPath }} +idp.coralware.cloud/golden-path-lang: {{ .Values.goldenPathLang }} +{{- end }} + +{{/* +Selector labels — subset estable de app.labels usado en Deployment.spec.selector +y Service.spec.selector. NO debe incluir labels que cambien (version, tier). +*/}} +{{- define "app.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Checksum del Secret de la BD — fuerza rollout cuando rota el secret. +*/}} +{{- define "app.dbSecretChecksum" -}} +{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.database.secretName }} +{{- if $secret }}{{ toYaml $secret.data | sha256sum }}{{ else }}absent{{ end }} +{{- end }} diff --git a/helm/APP_NAME/templates/deployment.yaml b/helm/APP_NAME/templates/deployment.yaml new file mode 100644 index 0000000..526a88e --- /dev/null +++ b/helm/APP_NAME/templates/deployment.yaml @@ -0,0 +1,87 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "app.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "app.labels" . | nindent 8 }} + annotations: + checksum/db-secret: {{ include "app.dbSecretChecksum" . }} + spec: + serviceAccountName: {{ .Release.Name }} + imagePullSecrets: + - name: {{ .Values.image.pullSecret }} + securityContext: + runAsNonRoot: true + runAsUser: 1001 + seccompProfile: + type: RuntimeDefault + containers: + - name: app + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8000 + protocol: TCP + envFrom: + - secretRef: + name: {{ .Values.database.secretName }} + - configMapRef: + name: {{ .Release.Name }}-config + env: + - name: STATIC_DEMO_PATH + value: {{ .Values.config.staticDemoPath | quote }} + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.probes.readiness.failureThreshold }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Release.Name }} + labels: + {{- include "app.labels" . | nindent 4 }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-config + labels: + {{- include "app.labels" . | nindent 4 }} +data: + LOG_LEVEL: {{ .Values.config.logLevel | quote }} + CORRELATION_ID_HEADER: {{ .Values.config.correlationIdHeader | quote }} + OPENAPI_REQUIRE_AUTH: {{ .Values.config.openapiRequireAuth | quote }} + KEYCLOAK_ISSUER: {{ .Values.config.keycloakIssuer | quote }} diff --git a/helm/APP_NAME/templates/ingressroute.yaml b/helm/APP_NAME/templates/ingressroute.yaml new file mode 100644 index 0000000..e85a4dc --- /dev/null +++ b/helm/APP_NAME/templates/ingressroute.yaml @@ -0,0 +1,19 @@ +{{- if .Values.ingress.enabled }} +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: {{ .Release.Name }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + entryPoints: + - websecure + routes: + - match: Host(`{{ .Values.ingress.host }}`) + kind: Rule + services: + - name: {{ .Release.Name }} + port: {{ .Values.service.port }} + tls: + secretName: {{ .Values.ingress.tlsSecretName }} +{{- end }} diff --git a/helm/APP_NAME/templates/job-alembic-upgrade.yaml b/helm/APP_NAME/templates/job-alembic-upgrade.yaml new file mode 100644 index 0000000..351789c --- /dev/null +++ b/helm/APP_NAME/templates/job-alembic-upgrade.yaml @@ -0,0 +1,59 @@ +{{- if .Values.alembic.enabled }} +# Job pre-upgrade que corre Alembic antes de rolar el Deployment. +# - backoffLimit: 0 → si falla, NO reintenta; Helm aborta el upgrade. +# - hook-delete-policy hook-succeeded → solo se borra al éxito; en fallo queda +# visible para `kubectl logs job/...` y diagnóstico. +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-alembic-{{ .Release.Revision }} + labels: + {{- include "app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-upgrade,pre-install + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + backoffLimit: 0 + ttlSecondsAfterFinished: 600 + template: + metadata: + labels: + {{- include "app.labels" . | nindent 8 }} + spec: + restartPolicy: Never + serviceAccountName: {{ .Release.Name }} + imagePullSecrets: + - name: {{ .Values.image.pullSecret }} + securityContext: + runAsNonRoot: true + runAsUser: 1001 + containers: + - name: alembic + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + command: {{ .Values.alembic.command | toJson }} + envFrom: + - secretRef: + name: {{ .Values.database.secretName }} + env: + # Si el Secret expone MIGRATION_DATABASE_URL, lo usa; si no, env.py + # cae a DATABASE_URL automáticamente (ver settings.migration_url). + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ .Values.database.secretName }} + key: DATABASE_URL + - name: MIGRATION_DATABASE_URL + valueFrom: + secretKeyRef: + name: {{ .Values.database.secretName }} + key: MIGRATION_DATABASE_URL + optional: true + resources: + {{- toYaml .Values.alembic.resources | nindent 12 }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] +{{- end }} diff --git a/helm/APP_NAME/templates/networkpolicy.yaml b/helm/APP_NAME/templates/networkpolicy.yaml new file mode 100644 index 0000000..2a70261 --- /dev/null +++ b/helm/APP_NAME/templates/networkpolicy.yaml @@ -0,0 +1,66 @@ +{{- if .Values.networkPolicy.enabled }} +# NetworkPolicy mínima del shape: bajo deny-all-default del namespace, permite: +# - Ingress: Traefik (web-backend público) + Prometheus (scrape de /metrics). +# - Egress: DNS, Harbor (pull en startup), ns-addons del tenant (BD). +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ .Release.Name }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "app.selectorLabels" . | nindent 6 }} + policyTypes: [Ingress, Egress] + + ingress: + # Tráfico HTTP desde Traefik (IngressRoute) + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik-system + ports: + - protocol: TCP + port: 8000 + # Scrape de métricas Prometheus + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: cattle-monitoring-system + podSelector: + matchLabels: + app.kubernetes.io/name: prometheus + ports: + - protocol: TCP + port: 8000 + + egress: + # DNS + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + # ns-addons del tenant: MariaDB primary + pool + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.networkPolicy.addonsNamespace }} + ports: + - protocol: TCP + port: 3306 + - protocol: TCP + port: 6033 + # Harbor pull (registro de imágenes) + - to: + - ipBlock: + cidr: 0.0.0.0/0 + ports: + - protocol: TCP + port: 443 +{{- end }} diff --git a/helm/APP_NAME/templates/service.yaml b/helm/APP_NAME/templates/service.yaml new file mode 100644 index 0000000..e4d4ff6 --- /dev/null +++ b/helm/APP_NAME/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "app.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP diff --git a/helm/APP_NAME/templates/servicemonitor.yaml b/helm/APP_NAME/templates/servicemonitor.yaml new file mode 100644 index 0000000..6dd256f --- /dev/null +++ b/helm/APP_NAME/templates/servicemonitor.yaml @@ -0,0 +1,16 @@ +{{- if .Values.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ .Release.Name }} + labels: + {{- include "app.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "app.selectorLabels" . | nindent 6 }} + endpoints: + - port: {{ .Values.serviceMonitor.port }} + interval: {{ .Values.serviceMonitor.interval }} + path: /metrics +{{- end }} diff --git a/helm/APP_NAME/values.yaml b/helm/APP_NAME/values.yaml new file mode 100644 index 0000000..16dc5ef --- /dev/null +++ b/helm/APP_NAME/values.yaml @@ -0,0 +1,69 @@ +# Defaults sensatos para tier startup. El CI inyecta tier/tenantId via --set +# leyendo valores del TenantProfile correspondiente. + +replicaCount: 1 + +image: + repository: registry.coralsafety.com/__TENANT_ID_NODASHES__/__APP_NAME__ + tag: latest + pullPolicy: Always + pullSecret: harbor-tenant-pull + +# Identidad del tenant — populated por --set desde el CI +tenantId: "" +tenantIdNodashes: "" +tier: startup +goldenPath: web-backend +goldenPathLang: python + +database: + secretName: __APP_NAME__-db-creds + +config: + logLevel: INFO + correlationIdHeader: X-Correlation-ID + openapiRequireAuth: false + keycloakIssuer: "" + staticDemoPath: /app/static/demo + +resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +probes: + liveness: + initialDelaySeconds: 20 + periodSeconds: 10 + readiness: + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 3 + +service: + type: ClusterIP + port: 8000 + +ingress: + enabled: true + host: __APP_NAME__-__TENANT_ID_NODASHES__.apps.coralware.cloud + tlsSecretName: __APP_NAME__-tls + +alembic: + enabled: true + command: ["alembic", "upgrade", "head"] + resources: + requests: {cpu: 50m, memory: 128Mi} + limits: {cpu: 200m, memory: 256Mi} + +serviceMonitor: + enabled: true + interval: 30s + port: http + +networkPolicy: + enabled: true + addonsNamespace: __TENANT_ID_NODASHES__-addons