chore(mirror): sync desde templates/rest-api-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:
@@ -13,6 +13,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'code/**'
|
- 'code/**'
|
||||||
- 'helm/**'
|
- 'helm/**'
|
||||||
|
- 'scripts/**'
|
||||||
- '.gitea/workflows/__APP_NAME__-build.yaml'
|
- '.gitea/workflows/__APP_NAME__-build.yaml'
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
@@ -204,99 +205,19 @@ jobs:
|
|||||||
echo "$KUBECONFIG_B64" | base64 -d > "${HOME}/.kube/config"
|
echo "$KUBECONFIG_B64" | base64 -d > "${HOME}/.kube/config"
|
||||||
chmod 600 "${HOME}/.kube/config"
|
chmod 600 "${HOME}/.kube/config"
|
||||||
|
|
||||||
- name: Esperar rancher-webhook ready (RANCHER-WEBHOOK-RACE-RECOVERY)
|
# ── Deploy del tenant — lógica factorizada a script reusable (ADR-095 §B2-D2) ─
|
||||||
# Race condition durante el bootstrap del cluster tenant: el
|
# La secuencia completa del deploy (wait-webhook → recovery-release → helm
|
||||||
# `rancher-webhook` Deployment en cattle-system aún no tiene endpoints
|
# upgrade --install → rollout → smoke) vive en scripts/tenant-first-deploy.sh,
|
||||||
# cuando el primer `helm upgrade --install` intenta crear el namespace
|
# IDÉNTICO byte-a-byte al de web-backend (fuente única, CS-7). Este template no
|
||||||
# `apps`, lo que dispara el webhook
|
# tiene manifests/addonclaim.yaml → el script salta el bloque AddonClaim por
|
||||||
# `rancher.cattle.io.namespaces.create-non-kubesystem` y aborta el
|
# presencia de archivo. HELM_TIMEOUT=5m preserva el timeout previo de rest-api.
|
||||||
# release con `no endpoints available for service rancher-webhook`.
|
# helm y el kubeconfig ya quedaron listos en los steps previos (setup de entorno).
|
||||||
# Paridad con el template web-backend (este paso faltaba en rest-api).
|
- name: Deploy del tenant (tenant-first-deploy.sh)
|
||||||
run: |
|
|
||||||
# `kubectl wait` falla con NotFound si el Deployment aún no existe:
|
|
||||||
# durante el bootstrap del cluster tenant, `rancher-webhook` puede no
|
|
||||||
# haberse creado todavía cuando corre este step. Primero esperamos a
|
|
||||||
# que el objeto APAREZCA (poll), luego a que esté Available.
|
|
||||||
until kubectl get deployment rancher-webhook -n cattle-system >/dev/null 2>&1; do
|
|
||||||
echo "Esperando a que el Deployment rancher-webhook exista en cattle-system..."
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
kubectl wait deployment rancher-webhook \
|
|
||||||
-n cattle-system \
|
|
||||||
--for=condition=Available \
|
|
||||||
--timeout=600s
|
|
||||||
|
|
||||||
- name: Recuperar release Helm atascado (TENANT-BUILD-NO-CONCURRENCY-GUARD)
|
|
||||||
# Red de seguridad ADEMÁS del guard `concurrency`: si un run previo quedó a
|
|
||||||
# mitad de una operación Helm (cancelado, runner reiniciado, o carrera
|
|
||||||
# histórica anterior a este fix), el release queda en un estado `pending-*`
|
|
||||||
# y el siguiente `helm upgrade --install` aborta. Lo detectamos y recuperamos
|
|
||||||
# antes de desplegar.
|
|
||||||
# Fail-safe: SOLO actúa sobre estados `pending-*`/`uninstalling`; un release
|
|
||||||
# `deployed`, `failed` o inexistente NO se toca. Si el parseo no encuentra
|
|
||||||
# STATUS (formato inesperado) cae al branch no-op → comportamiento idéntico
|
|
||||||
# al actual, nunca peor.
|
|
||||||
run: |
|
|
||||||
# `helm status` de un release inexistente (first-deploy frío) sale con
|
|
||||||
# exit≠0; bajo el shell del runner (`bash -e -o pipefail`) eso abortaría
|
|
||||||
# el step ANTES del case. `|| true` lo neutraliza → STATUS="" → branch no-op.
|
|
||||||
STATUS="$(helm status __APP_NAME__ -n apps 2>/dev/null | sed -n 's/^STATUS: //p')" || true
|
|
||||||
echo "Estado actual del release __APP_NAME__: ${STATUS:-<no existe>}"
|
|
||||||
case "$STATUS" in
|
|
||||||
pending-install|uninstalling)
|
|
||||||
# Nunca hubo una revisión desplegada con éxito → no se puede rollback.
|
|
||||||
# `helm uninstall` limpia el secret del release trabado (equivale al
|
|
||||||
# destrabado manual del 2026-06-10) sin riesgo de datos: el primer deploy
|
|
||||||
# aún no levantó la app.
|
|
||||||
echo "Release en '${STATUS}' (sin revisión estable) → helm uninstall para limpiar."
|
|
||||||
helm uninstall __APP_NAME__ -n apps --wait --timeout 2m || true
|
|
||||||
;;
|
|
||||||
pending-upgrade|pending-rollback)
|
|
||||||
# Hubo una revisión estable previa → rollback a la última desplegada
|
|
||||||
# (preserva la app viva; el upgrade siguiente reintenta la nueva imagen).
|
|
||||||
echo "Release en '${STATUS}' → helm rollback a la última revisión estable."
|
|
||||||
helm rollback __APP_NAME__ 0 -n apps --wait --timeout 2m || true
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Release en estado estable o inexistente → sin recuperación necesaria."
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
- name: Helm upgrade --install
|
|
||||||
env:
|
env:
|
||||||
|
APP_NAME: __APP_NAME__
|
||||||
|
IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
|
||||||
TENANT_ID: ${{ vars.TENANT_ID }}
|
TENANT_ID: ${{ vars.TENANT_ID }}
|
||||||
TENANT_ID_NODASHES: ${{ vars.TENANT_ID_NODASHES }}
|
TENANT_ID_NODASHES: ${{ vars.TENANT_ID_NODASHES }}
|
||||||
TENANT_TIER: ${{ vars.TENANT_TIER }}
|
TENANT_TIER: ${{ vars.TENANT_TIER }}
|
||||||
IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
|
HELM_TIMEOUT: 5m
|
||||||
# ADR-054: namespace funcional fijo en cluster tenant — el cluster ya es el sandbox.
|
run: bash scripts/tenant-first-deploy.sh
|
||||||
run: |
|
|
||||||
helm upgrade --install __APP_NAME__ ./helm/__APP_NAME__ \
|
|
||||||
--namespace 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
|
|
||||||
run: |
|
|
||||||
kubectl -n apps rollout status deploy/__APP_NAME__ --timeout=3m
|
|
||||||
|
|
||||||
- name: Smoke check post-deploy
|
|
||||||
# FQDN en formato subdominio (ADR-067 / ADR-082): <app>.<tenant-uuid>.apps...
|
|
||||||
# Coincide con el host del Ingress del cluster tenant (helm/APP_NAME/values.yaml)
|
|
||||||
# y con el wildcard A record *.{tenant-uuid}.apps.coralware.cloud que crea tenant-manager.
|
|
||||||
env:
|
|
||||||
INGRESS_HOST: __APP_NAME__.${{ vars.TENANT_ID }}.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"
|
|
||||||
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
|
|
||||||
|
|||||||
179
scripts/tenant-first-deploy.sh
Executable file
179
scripts/tenant-first-deploy.sh
Executable file
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# tenant-first-deploy.sh — Deploy del tenant al cluster (ADR-095 Fase B2, CS-7).
|
||||||
|
#
|
||||||
|
# Factoriza la LÓGICA DE DEPLOY del job `helm-deploy` del workflow del template
|
||||||
|
# (antes inline en `.gitea/workflows/__APP_NAME__-build.yaml`) a un script reusable.
|
||||||
|
# Lo invocan DOS consumidores con el MISMO shell byte-idéntico:
|
||||||
|
# 1. El workflow del tenant, contra el cluster real (producción).
|
||||||
|
# 2. El gate efímero B2 (`infrastructure/scripts/templates-deploy-gate.sh`),
|
||||||
|
# contra un cluster k3s descartable, con GATE_MODE=1.
|
||||||
|
#
|
||||||
|
# Por qué un script y no lógica duplicada en el gate (ADR-095 §B2-D2): el bug
|
||||||
|
# canónico de la "enfermedad B" (el `helm status` de un release inexistente que
|
||||||
|
# bajo `set -e -o pipefail` abortaba el first-deploy frío, 06-14) es SEMÁNTICA DE
|
||||||
|
# SHELL — solo ejecutar ese mismo shell lo caza. Reimplementarlo en el gate
|
||||||
|
# reintroduce la divergencia productor↔consumidor (clase ADR-083).
|
||||||
|
#
|
||||||
|
# El script es AGNÓSTICO DEL CLUSTER: asume que `helm`/`kubectl` ya apuntan al
|
||||||
|
# cluster destino (el caller decodifica el kubeconfig y asegura helm en el PATH —
|
||||||
|
# eso es setup de entorno, específico del secret del tenant / de la VM efímera, no
|
||||||
|
# lógica de deploy). El script es IDÉNTICO en ambos templates (web-backend +
|
||||||
|
# rest-api): la divergencia se resuelve por DATOS, no por código —
|
||||||
|
# - AddonClaim: se aplica solo si existe `manifests/addonclaim.yaml`
|
||||||
|
# (web-backend lo tiene; rest-api no) → un único script sirve a ambos.
|
||||||
|
# - Timeout de helm: env var HELM_TIMEOUT (web-backend 10m por defecto; el
|
||||||
|
# workflow rest-api pasa 5m para preservar su comportamiento previo).
|
||||||
|
#
|
||||||
|
# Variables de entorno:
|
||||||
|
# APP_NAME (OBLIGATORIO) nombre de la app == nombre del release Helm.
|
||||||
|
# IMAGE_TAG (OBLIGATORIO) tag de la imagen ya publicada en Harbor.
|
||||||
|
# TENANT_ID (opcional) uuid del tenant — usado en --set y en el smoke host.
|
||||||
|
# TENANT_ID_NODASHES (opcional) uuid sin guiones — usado en --set.
|
||||||
|
# TENANT_TIER (opcional) tier del tenant — usado en --set.
|
||||||
|
# NAMESPACE (default `apps`) namespace funcional del cluster tenant (ADR-054).
|
||||||
|
# HELM_TIMEOUT (default `10m`) timeout de `helm upgrade --install --wait`.
|
||||||
|
# GATE_MODE (default `0`) si `1`, salta el smoke HTTPS final (DNS+cert+
|
||||||
|
# Gateway reales) — esa cobertura es del canary, no del efímero;
|
||||||
|
# el script termina en `rollout status` (ADR-095 §B2-D2).
|
||||||
|
#
|
||||||
|
# Uso (desde el workflow del tenant, cwd = raíz del repo del tenant):
|
||||||
|
# APP_NAME=__APP_NAME__ IMAGE_TAG=$SHA TENANT_ID=... \
|
||||||
|
# bash scripts/tenant-first-deploy.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${APP_NAME:?ERROR: APP_NAME no está seteado}"
|
||||||
|
: "${IMAGE_TAG:?ERROR: IMAGE_TAG no está seteado}"
|
||||||
|
NAMESPACE="${NAMESPACE:-apps}"
|
||||||
|
HELM_TIMEOUT="${HELM_TIMEOUT:-10m}"
|
||||||
|
GATE_MODE="${GATE_MODE:-0}"
|
||||||
|
TENANT_ID="${TENANT_ID:-}"
|
||||||
|
TENANT_ID_NODASHES="${TENANT_ID_NODASHES:-}"
|
||||||
|
TENANT_TIER="${TENANT_TIER:-}"
|
||||||
|
|
||||||
|
log() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) [tenant-first-deploy] $*"; }
|
||||||
|
|
||||||
|
log "Iniciando deploy app=${APP_NAME} ns=${NAMESPACE} tag=${IMAGE_TAG} gate_mode=${GATE_MODE}"
|
||||||
|
|
||||||
|
# ── ADR-066: AddonClaim vive FUERA del chart Helm ───────────────────────────
|
||||||
|
# El claim lo gestiona addon-provisioner via finalizer cleanup-logical; no debe
|
||||||
|
# acoplarse al lifecycle del release Helm para no rotar credenciales en cada
|
||||||
|
# upgrade. Solo aplica a templates con base de datos (web-backend); rest-api no
|
||||||
|
# tiene `manifests/addonclaim.yaml` → se salta el bloque entero.
|
||||||
|
if [ -f manifests/addonclaim.yaml ]; then
|
||||||
|
# De-adoptar AddonClaim legacy (migración chart 1.0.0 -> 1.1.0).
|
||||||
|
# Idempotente: skip silencioso si AddonClaim no existe (primera instalación)
|
||||||
|
# o si ya está de-adoptado (segundo run post-migración). Sunset del helper tras
|
||||||
|
# una rotación completa (~2 semanas).
|
||||||
|
if [ -f scripts/pre-helm-detach.sh ]; then
|
||||||
|
log "De-adoptando AddonClaim legacy (pre-helm-detach.sh)…"
|
||||||
|
RELEASE_NAME="${APP_NAME}" NAMESPACE="${NAMESPACE}" bash scripts/pre-helm-detach.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Aplicar AddonClaim. Sustitución de placeholders con sed (evita dependencia de
|
||||||
|
# envsubst, no garantizado en la imagen del runner). kubectl apply es
|
||||||
|
# idempotente: primer push lo crea, push subsiguientes son no-op si el spec no
|
||||||
|
# cambia. helm upgrade --install nunca lo trackea ni lo borra.
|
||||||
|
# databaseName = release name con guiones a underscores + sufijo _db (coincide
|
||||||
|
# con el default del chart 1.0.0: .Release.Name | replace "-" "_" | printf "%s_db").
|
||||||
|
log "Aplicando AddonClaim…"
|
||||||
|
DATABASE_NAME="$(echo "${APP_NAME}" | tr '-' '_')_db"
|
||||||
|
sed -e "s|\${RELEASE_NAME}|${APP_NAME}|g" \
|
||||||
|
-e "s|\${DATABASE_NAME}|${DATABASE_NAME}|g" \
|
||||||
|
manifests/addonclaim.yaml | kubectl apply -f -
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Esperar rancher-webhook ready (RANCHER-WEBHOOK-RACE-RECOVERY) ────────────
|
||||||
|
# Race durante el bootstrap del cluster tenant: el `rancher-webhook` Deployment en
|
||||||
|
# cattle-system aún no tiene endpoints cuando el primer `helm upgrade --install`
|
||||||
|
# crea el namespace `apps`, lo que dispara el webhook
|
||||||
|
# `rancher.cattle.io.namespaces.create-non-kubesystem` y aborta el release con
|
||||||
|
# `no endpoints available for service rancher-webhook`. Espera explícita evita el
|
||||||
|
# flake. `kubectl wait` falla con NotFound si el Deployment aún no existe → primero
|
||||||
|
# esperamos a que APAREZCA (poll), luego a que esté Available.
|
||||||
|
log "Esperando rancher-webhook en cattle-system…"
|
||||||
|
until kubectl get deployment rancher-webhook -n cattle-system >/dev/null 2>&1; do
|
||||||
|
echo "Esperando a que el Deployment rancher-webhook exista en cattle-system..."
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
kubectl wait deployment rancher-webhook \
|
||||||
|
-n cattle-system \
|
||||||
|
--for=condition=Available \
|
||||||
|
--timeout=600s
|
||||||
|
|
||||||
|
# ── Recuperar release Helm atascado (TENANT-BUILD-NO-CONCURRENCY-GUARD) ──────
|
||||||
|
# Red de seguridad ADEMÁS del guard `concurrency`: si un run previo quedó a mitad
|
||||||
|
# de una operación Helm (cancelado, runner reiniciado, o carrera histórica), el
|
||||||
|
# release queda en `pending-*` y el siguiente `helm upgrade --install` aborta. Lo
|
||||||
|
# detectamos y recuperamos antes de desplegar.
|
||||||
|
# Fail-safe: SOLO actúa sobre `pending-*`/`uninstalling`; un release `deployed`,
|
||||||
|
# `failed` o inexistente NO se toca.
|
||||||
|
# `helm status` de un release inexistente (first-deploy frío) sale con exit≠0; bajo
|
||||||
|
# `set -e -o pipefail` eso abortaría ANTES del case. `|| true` lo neutraliza →
|
||||||
|
# STATUS="" → branch no-op. (ESTE es el bug canónico de la enfermedad B; ADR-095.)
|
||||||
|
STATUS="$(helm status "${APP_NAME}" -n "${NAMESPACE}" 2>/dev/null | sed -n 's/^STATUS: //p')" || true
|
||||||
|
log "Estado actual del release ${APP_NAME}: ${STATUS:-<no existe>}"
|
||||||
|
case "$STATUS" in
|
||||||
|
pending-install|uninstalling)
|
||||||
|
# Nunca hubo una revisión desplegada con éxito → no se puede rollback.
|
||||||
|
# `helm uninstall` limpia el secret del release trabado sin riesgo de datos:
|
||||||
|
# el primer deploy aún no levantó la app.
|
||||||
|
log "Release en '${STATUS}' (sin revisión estable) → helm uninstall para limpiar."
|
||||||
|
helm uninstall "${APP_NAME}" -n "${NAMESPACE}" --wait --timeout 2m || true
|
||||||
|
;;
|
||||||
|
pending-upgrade|pending-rollback)
|
||||||
|
# Hubo una revisión estable previa → rollback a la última desplegada (preserva
|
||||||
|
# la app viva; el upgrade siguiente reintenta la nueva imagen).
|
||||||
|
log "Release en '${STATUS}' → helm rollback a la última revisión estable."
|
||||||
|
helm rollback "${APP_NAME}" 0 -n "${NAMESPACE}" --wait --timeout 2m || true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log "Release en estado estable o inexistente → sin recuperación necesaria."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# ── Helm upgrade --install ──────────────────────────────────────────────────
|
||||||
|
# ADR-054: en cluster tenant los namespaces son funcionales (apps/addons), sin
|
||||||
|
# tenant-id en el nombre — el cluster ya es el sandbox del tenant.
|
||||||
|
# HELM_TIMEOUT (default 10m) absorbe el peor caso del reconcile de addon-provisioner
|
||||||
|
# (RECONCILE_INTERVAL 60s + initial_delay 60s + 3 ticks ≈ 240s) + StatefulSet
|
||||||
|
# MariaDB ready (~60s) + Job DDL (~10s) + InitContainer wait-for-secret 360s +
|
||||||
|
# Alembic. Lección 2026-05-28 (ADDON-PROVISIONER-DB-SECRET-NOT-PUBLISHED).
|
||||||
|
log "helm upgrade --install ${APP_NAME} (timeout ${HELM_TIMEOUT})…"
|
||||||
|
helm upgrade --install "${APP_NAME}" "./helm/${APP_NAME}" \
|
||||||
|
--namespace "${NAMESPACE}" \
|
||||||
|
--create-namespace \
|
||||||
|
--set image.tag="${IMAGE_TAG}" \
|
||||||
|
--set tenantId="${TENANT_ID}" \
|
||||||
|
--set tenantIdNodashes="${TENANT_ID_NODASHES}" \
|
||||||
|
--set tier="${TENANT_TIER}" \
|
||||||
|
--wait --timeout "${HELM_TIMEOUT}"
|
||||||
|
|
||||||
|
# ── Verificar rollout ───────────────────────────────────────────────────────
|
||||||
|
log "Verificando rollout deploy/${APP_NAME}…"
|
||||||
|
kubectl -n "${NAMESPACE}" rollout status "deploy/${APP_NAME}" --timeout=3m
|
||||||
|
|
||||||
|
# ── Smoke check post-deploy ─────────────────────────────────────────────────
|
||||||
|
# En GATE_MODE (gate efímero B2) se salta: el smoke exige DNS+cert+Gateway reales
|
||||||
|
# (`<app>.<uuid>.apps.coralware.cloud`), cobertura del canary, no del efímero. El
|
||||||
|
# script termina exitosamente en el rollout (ADR-095 §B2-D2).
|
||||||
|
if [ "${GATE_MODE}" = "1" ]; then
|
||||||
|
log "GATE_MODE=1 → se salta el smoke HTTPS (cobertura del canary). Deploy OK hasta rollout."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# FQDN en formato subdominio (ADR-067 / ADR-082): <app>.<tenant-uuid>.apps...
|
||||||
|
# Coincide con el host del Ingress del cluster tenant (helm/APP_NAME/values.yaml) y
|
||||||
|
# con el wildcard A record *.{tenant-uuid}.apps.coralware.cloud que crea tenant-manager.
|
||||||
|
INGRESS_HOST="${APP_NAME}.${TENANT_ID}.apps.coralware.cloud"
|
||||||
|
log "Smoke check https://${INGRESS_HOST}/health …"
|
||||||
|
for i in $(seq 1 12); do
|
||||||
|
if wget -q -O - --timeout=10 "https://${INGRESS_HOST}/health" | grep -q '"status":"ok"'; then
|
||||||
|
log "Smoke OK: /health responde 200"
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user