Sync del fix ADDON-PROVISIONER-DB-SECRET-NOT-PUBLISHED (IDP P0). Ver IDP commit 673c16d para contexto completo. El init container wait-for-secret del Job Alembic ahora espera 72×5s=360s (antes 48×5s=240s), dimensionado a 3× el RECONCILE_INTERVAL del addon-provisioner. El controller bajó su default 300s → 60s en el mismo parche. Defense-in-depth: con el controller corriendo a 60s, el delay típico del Secret publish es ~60-180s. Los 360s del init container absorben hasta 3 ticks fallidos del controller (cluster recién Ready, kubeconfig no propagado, etc.). helm timeout 7m → 10m para que el wait extendido del init container + Alembic + StatefulSet ready quepan dentro del comando helm. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
125 lines
5.3 KiB
YAML
125 lines
5.3 KiB
YAML
{{- if and .Values.alembic.enabled .Values.database.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 exito; en fallo queda
|
|
# visible para `kubectl logs job/...` y diagnostico.
|
|
#
|
|
# InitContainer `wait-for-secret`:
|
|
# - El AddonClaim ya fue aplicado por el workflow del tenant ANTES del
|
|
# `helm upgrade --install` (ADR-066, vive FUERA del chart en
|
|
# manifests/addonclaim.yaml). addon-provisioner es timer-based
|
|
# (RECONCILE_INTERVAL 60s, initial_delay 60s) y materializa el Secret
|
|
# `{{ .Values.database.secretName }}` de forma asincrona.
|
|
# - El InitContainer hace busy-wait hasta que el Secret existe — absorbe la
|
|
# latencia del reconcile dentro del pod, sin tocar el operator.
|
|
# - Loop: 72 iteraciones x 5s = 360s — margen ~3x el peor caso esperado
|
|
# (initial_delay 60s + 2x ticks de RECONCILE_INTERVAL 60s = 180s). Cabe
|
|
# en helm timeout 7m. Lección 2026-05-28: con RECONCILE_INTERVAL=300s el
|
|
# init container moria a 240s antes de que el Secret se publicara
|
|
# (delay observado 577s, ticket ADDON-PROVISIONER-DB-SECRET-NOT-PUBLISHED).
|
|
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
|
|
initContainers:
|
|
- name: wait-for-secret
|
|
# curl + REST API K8s con SA token. Evita dependencia de un binario
|
|
# kubectl con tags que cambian (bitnami/kubectl retiro los tags
|
|
# semanticos minor 2026-05). curlimages/curl es Docker Official.
|
|
image: curlimages/curl:8.20.0
|
|
command:
|
|
- sh
|
|
- -c
|
|
- |
|
|
# set -u: undefined vars matan el script.
|
|
# set -e NO: queremos que curl fail (DNS transitorio, API server
|
|
# caido por instante) NO mate el loop — el retry lo absorbe.
|
|
set -u
|
|
# Espera hasta que addon-provisioner publique el Secret tras
|
|
# reconciliar el AddonClaim (hook weight -10). 72 x 5s = 360s.
|
|
SECRET="{{ .Values.database.secretName }}"
|
|
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
|
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
|
i=1
|
|
while [ "$i" -le 72 ]; do
|
|
# || echo "000": si curl falla (exit 6 DNS, etc.) capturamos
|
|
# un codigo HTTP "000" para que el retry siga corriendo.
|
|
HTTP=$(curl -s -o /dev/null -w '%{http_code}' \
|
|
--cacert "$CACERT" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
"https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/secrets/$SECRET" \
|
|
|| echo "000")
|
|
if [ "$HTTP" = "200" ]; then
|
|
echo "Secret $SECRET encontrado en intento $i"
|
|
exit 0
|
|
fi
|
|
echo "[$i/72] Secret $SECRET aun no publicado (HTTP=$HTTP), esperando 5s..."
|
|
sleep 5
|
|
i=$((i + 1))
|
|
done
|
|
echo "ERROR: Secret $SECRET no aparecio tras 360s — addon-provisioner stuck?"
|
|
exit 1
|
|
env:
|
|
- name: NAMESPACE
|
|
valueFrom:
|
|
fieldRef:
|
|
fieldPath: metadata.namespace
|
|
resources:
|
|
requests: {cpu: 10m, memory: 32Mi}
|
|
limits: {cpu: 100m, memory: 64Mi}
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
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 automaticamente (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 }}
|