feat: sincronizar scaffold completo del shape web-backend/python
Some checks failed
__APP_NAME__ — build & deploy / build (push) Failing after 0s
__APP_NAME__ — build & deploy / helm-deploy (push) Has been skipped

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).
This commit is contained in:
2026-05-06 17:48:13 -05:00
parent 00a801f525
commit fcec92ea1d
35 changed files with 1216 additions and 32 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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