Compare commits

..
Author SHA1 Message Date
devops 8c52229e71 feat(commerce-backend): revalidar el storefront al crear/borrar productos
El subscriber revalidate-storefront.ts (PR #9) solo escuchaba
product.updated y los eventos de variante, asi que un producto nuevo
(product.created) o eliminado (product.deleted) no disparaba
revalidateTag: el storefront tardaba hasta el ciclo normal de cache
(~60s por pod) en reflejar altas/bajas de catalogo, a diferencia de
precio/descripcion/imagenes en productos existentes, que ya viajan por
product.updated.
2026-08-13 16:55:16 -05:00
devops 2d09867f70 Merge pull request 'chore(gitops): fijar Medusa a v1.0.90 tras carrera de promocion en CI' (#10) from fix/medusa-manifest-lagged-race into main 2026-08-13 21:41:45 +00:00
devops 6a0e9965a9 chore(gitops): fijar Medusa a v1.0.90 tras carrera de promocion en CI
El pipeline build-medusa.yaml (run #90) construyo y publico
ecommerce-medusa:v1.0.90 con exito, pero su paso "Actualizar
manifiesto Medusa" quedo skipped: el chequeo de promocion segura
comparo github.sha contra origin/main y encontro que el commit
"chore: release v1.0.91" del frontend ya habia cambiado el HEAD, asi
que se abstuvo de sobrescribirlo. medusa.yaml se quedo en v1.0.88, sin
el subscriber revalidate-storefront agregado en el PR #9.

Bump manual del tag en ambos containers (migrations y medusa) para
que Argo CD despliegue la imagen que ya esta en el registry.
2026-08-13 16:39:28 -05:00
devops 095912d02d chore: release v1.0.91 [skip ci] 2026-08-13 21:19:17 +00:00
devops a1d2f9db67 Merge pull request 'feat(ecommerce): revalidar el storefront al instante tras editar precios' (#9) from fix/medusa-price-instant-revalidate into main
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 7m39s
Build and Push Medusa / Construir y publicar Medusa (push) Successful in 15m24s
2026-08-13 21:06:13 +00:00
devops d1a4a84d24 feat(ecommerce): revalidar el storefront al instante tras editar precios
Antes el precio actualizado en Medusa tardaba hasta ~60s (o más, con
varias réplicas del frontend sin cache handler compartido) en verse en
el front, por el revalidate:60 del fetch a /store/products.

Ahora:
- Nueva ruta app/api/revalidate en el storefront que llama a
  revalidateTag("medusa-products") al recibir un POST autenticado con
  el header x-revalidate-secret.
- Nuevo subscriber en el backend Medusa (product.updated,
  product-variant.updated/created/deleted) que llama a esa ruta usando
  STOREFRONT_URL (ya existe en commerce-config) y un REVALIDATE_SECRET
  compartido entre ambos servicios.
- secrets.template.yaml documenta la nueva clave REVALIDATE_SECRET en
  commerce-secrets y commerce-storefront (mismo valor en ambos).

Requiere reaplicar commerce-secrets y commerce-storefront con el script
actualizado en el repo scripts (fix/revalidate-secret-scripts) para que
el REVALIDATE_SECRET exista en el cluster.
2026-08-13 16:02:49 -05:00
devops a9c57d4985 chore: release v1.0.89 [skip ci] 2026-08-13 07:07:55 +00:00
devops 584a42ea72 Merge pull request 'fix(ecommerce): detalle de producto en dynamic render, no SSG+ISR stale' (#8) from fix/product-detail-static-mock-price into main
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 5m55s
2026-08-13 07:02:11 +00:00
devopsandClaude Sonnet 5 a6a92f75ea fix(ecommerce): render product detail dynamically instead of stale SSG+ISR
Product detail pages were generateStaticParams'd at Docker build time,
where MEDUSA_* env vars aren't set, so the static snapshot baked in the
mock catalog (price: null for all products). With revalidate=60 and no
shared ISR cache handler across the 2 frontend replicas, each pod healed
its own cache independently on next visit past staleness — so a product
could show "Consultar precio" on one pod and the real price on the other,
while the fully-dynamic catalog listing always hit Medusa fresh. Verified
against the live Medusa API that pricing/region data itself was correct
for the reported failing products.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DXqBoNfYQNwFg65mbx2FWM
2026-08-13 01:59:44 -05:00
devops 0eea6234d6 chore: release v1.0.87 [skip ci] 2026-08-13 00:24:20 -05:00
gitea-actions 77bf6089fb chore(gitops): deploy Medusa v1.0.88 [skip ci] 2026-08-13 05:01:13 +00:00
devops 0134a4dd0b Merge pull request 'fix: desactivar cookie Secure en sesion admin de Medusa' (#7) from fix/medusa-admin-session-cookie into main
Build and Push Medusa / Construir y publicar Medusa (push) Successful in 18m10s
2026-08-13 04:08:03 +00:00
devops 3fe63591ec Merge pull request 'fix: forzar ISR en /product/[slug] para que no quede atascado en mock' (#6) from fix/product-page-static-mock-price into main
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 7m12s
2026-08-13 04:04:40 +00:00
devops c6cf394c26 fix: desactivar cookie Secure en sesion admin de Medusa
Todos los Ingress del lab terminan en Traefik por HTTP plano (entrypoint
web, sin TLS); Cloudflare es quien atiende HTTPS de cara al navegador.
Con secure:true (default en produccion), express-session nunca emite
Set-Cookie porque no detecta la conexion como HTTPS, dejando el
dashboard admin con 401 en /admin/users/me pese a loguear bien.
2026-08-12 23:01:35 -05:00
devops 7a09202c27 fix: forzar ISR en /product/[slug] para que no quede atascado en datos mock del build
generateStaticParams usa el catálogo mock en build time porque
HEADLESS_PROVIDER y MEDUSA_* no existen como build args del Dockerfile.
Sin un revalidate explícito, esa ruta queda estática para siempre con
price:null. El listado no sufre esto porque su ruta es dinámica por
searchParams.
2026-08-12 22:56:03 -05:00
devops a902dc0a26 chore: release v1.0.86 [skip ci] 2026-08-12 07:16:46 +00:00
devops 04dc6bd8bc Merge pull request 'fix: usar region_id en vez de currency_code para precios de Medusa' (#5) from fix/medusa-region-id-pricing into main
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 9m9s
2026-08-12 07:03:28 +00:00
devops 60072cfcbe fix: usar region_id en vez de currency_code para precios de Medusa
El validador de /store/products (StoreGetProductsParams, .strict()) solo
acepta region_id/country_code/province/cart_id para resolver contexto de
precio -- currency_code como campo plano no existe y rompía con 400
"Unrecognized fields: 'currency_code'" en cada carga de /catalog.

country_code tampoco alcanza: probado en vivo, devuelve "Missing required
pricing context to calculate prices - region_id". Se necesita region_id
explícito.

Se agrega MEDUSA_REGION_ID (env var) apuntando a la región "Colombia"
(COP) creada vía Admin API, ya que no existía ninguna región configurada
en el backend.
2026-08-12 02:01:49 -05:00
devops c9b376c4e6 Merge pull request 'fix: conectar el frontend al catálogo real de Medusa' (#4) from fix/frontend-wire-medusa-catalog into main 2026-08-12 05:16:18 +00:00
devops cea41ce428 fix: conectar el frontend al catálogo real de Medusa
El Deployment frontend-deploy no tenía ninguna variable de entorno, así
que HEADLESS_PROVIDER quedaba undefined y el provider caía siempre en
mockCatalogProvider (data/products.json estático), aunque
medusaCatalogProvider ya estaba implementado y listo en
lib/headless/providers/medusa.ts.

Se agrega HEADLESS_PROVIDER=medusa, MEDUSA_BACKEND_URL apuntando al
Service interno medusa-svc:9000 (evita depender de Cloudflare/NPM en
cada render SSR), y envFrom hacia el secret commerce-storefront
(MEDUSA_PUBLISHABLE_KEY) ya creado en el namespace ecommerce.

No se bumpea el tag de imagen: build.yaml excluye explícitamente los
cambios de frontend.yaml del trigger de build, y la versión la gestiona
la propia CI vía sed sobre este archivo.
2026-08-12 00:12:10 -05:00
devops 381e6ab6ab docs(playbooks): documentar incidente medusa-deploy atascado en Init (2026-08)
Postmortem del incidente resuelto en e9c93bf: sintoma reportado como
CrashLoopBackOff que en realidad era Init:0/2 indefinido (0 restarts).
Documenta las hipotesis descartadas en orden (secret/DATABASE_URL,
NetworkPolicy, salud de Postgres, ResourceQuota, DNS, MTU/PMTUD
cross-node), la causa raiz (pg_isready no parseaba ?sslmode=disable en
la URI, exit 3 "no attempt"), el fix con flags explicitos, y la deuda
tecnica pendiente en la branch fix/medusa-wait-for-postgres.
2026-08-10 00:47:23 -05:00
devops e9c93bfb82 fix(medusa): usar flags explicitos en pg_isready en vez de DATABASE_URL crudo
El init container wait-for-postgres pasaba $DATABASE_URL completo a
pg_isready. Al agregarle ?sslmode=disable (via create-commerce-secrets.sh),
pg_isready fallaba con "no attempt" (exit 3, conninfo invalida para su
parser de URI) y el pod quedaba atascado en Init indefinidamente, sin
llegar nunca al CrashLoopBackOff visible. Postgres, DNS, Endpoints y
NetworkPolicy estaban sanos; el fallo era puramente del parseo de la URI.

Se reemplaza por -h/-p/-U/-d explicitos (host y puerto fijos del Service,
usuario y db ya disponibles via envFrom), asi el init container queda
inmune a query params futuros en DATABASE_URL.
2026-08-10 00:27:10 -05:00
devops f25b10a2f9 Merge pull request 'fix(medusa): forzar podAffinity con commerce-postgres para evitar blackhole MTU cross-node' (#3) from fix/medusa-postgres-node-affinity into main 2026-08-09 07:36:33 +00:00
devops 7a83ed61b9 fix(medusa): forzar podAffinity con commerce-postgres para evitar blackhole MTU cross-node
Diagnostico: pg_isready y el cliente pg de Medusa fallan de forma
consistente (100%, 186/186 intentos) cuando el pod queda agendado en un
nodo distinto al de commerce-postgres-0. TCP handshake (nc/ping) funciona
cruzando nodos, pero el primer paquete de datos real de la sesion nunca
llega - blackhole de PMTU Discovery entre los nodos del cluster k3d
(flannel VXLAN / MTU del bridge Docker subyacente).

Se agrega podAffinity requiredDuringSchedulingIgnoredDuringExecution
(no nodeSelector fijo, mantiene portabilidad) para que medusa-deploy
siempre comparta nodo con commerce-postgres-0 y evite cruzar el
blackhole. Es un workaround, no el fix de raiz: documentado en
docs/known-issues.md junto con el TODO de ajustar el MTU del cluster
a 1400.
2026-08-09 02:34:15 -05:00
devops 987ef04b0d Merge pull request 'fix(medusa): agregar initContainer wait-for-postgres para evitar CrashLoopBackOff por fallo transitorio de red en arranque' (#2) from fix/medusa-wait-for-postgres into main 2026-08-09 06:42:39 +00:00
devops f4ad14028e fix(medusa): agregar initContainer wait-for-postgres para evitar CrashLoopBackOff por fallo transitorio de red en arranque 2026-08-09 01:29:43 -05:00
devops 025f16a17b Merge pull request 'fix(gitops): unificar repoURL a https en ecommerce-app' (#1) from fix/repo-url-consistency into main 2026-08-09 05:52:14 +00:00
devops a90c0a75a2 fix(gitops): usar https en repoURL de ecommerce-app para consistencia con application.yaml 2026-08-09 00:10:14 -05:00
11 changed files with 404 additions and 10 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ spec:
project: default project: default
source: source:
repoURL: http://gitea.cruzcloud.net/devops/apps-registry.git repoURL: https://gitea.cruzcloud.net/devops/apps-registry.git
targetRevision: main targetRevision: main
path: workloads/ecommerce path: workloads/ecommerce
kustomize: {} kustomize: {}
+44
View File
@@ -0,0 +1,44 @@
# Known issues
## Blackhole de red cross-node en el cluster k3d (MTU/PMTUD, flannel VXLAN)
**Estado:** workaround aplicado, fix definitivo pendiente.
**Síntoma:** cualquier pod que necesite hablar con `commerce-postgres-0`
(StatefulSet, sin réplicas, fijo a un nodo) falla de forma consistente y
reproducible al 100% cuando queda agendado en un nodo **distinto** al de
Postgres. El handshake TCP (SYN/ACK, `nc`, `ping`) funciona perfecto entre
nodos, pero el primer paquete de datos real de la sesión (protocolo Postgres,
`pg_isready` incluido) nunca llega — se cuelga hasta hacer timeout.
**Diagnóstico:**
- Confirmado aislando el nodo con `nodeName` en pods de prueba: 100% de
fallos (186/186 intentos) agendando cross-node (`agent-0` → Postgres en
`server-0`); 100% de éxito agendando en el mismo nodo.
- MTU dentro de los pods (interfaz `flannel.1`/VXLAN) es 1450 en ambos nodos
— consistente, no es un mismatch obvio a nivel flannel.
- `ping` sin `-M do` (permite fragmentación) no muestra pérdida de paquetes
hasta payloads de 2000 bytes entre los contenedores Docker de los nodos.
- Encaja con un blackhole de PMTU Discovery: el bridge Docker externo que
conecta los contenedores de los nodos k3d probablemente tiene un MTU real
menor a 1500, y los paquetes TCP con el bit DF puesto (como los de la
sesión de Postgres) se pierden en vez de fragmentarse o generar el ICMP
"fragmentation needed" que permitiría el ajuste automático.
**Workaround temporal (aplicado en `workloads/ecommerce/commerce/medusa.yaml`):**
`podAffinity` con `requiredDuringSchedulingIgnoredDuringExecution` sobre
`medusa-deploy`, forzando que sus pods se agenden siempre en el mismo nodo
que `commerce-postgres-0` (`matchLabels: app: commerce-postgres`,
`topologyKey: kubernetes.io/hostname`). Esto evita el tráfico cross-node
entre Medusa y Postgres, pero no resuelve el problema de fondo — cualquier
otro workload que necesite cruzar nodos para hablar con Postgres (u otro
servicio) puede pisar el mismo blackhole.
**TODO — fix definitivo pendiente:**
Ajustar el MTU del cluster k3d a un valor seguro (`1400`) de forma
consistente en la red Docker del cluster y en flannel, para eliminar el
blackhole de raíz y poder quitar el `podAffinity` (o dejarlo como
optimización, no como requisito de disponibilidad).
@@ -0,0 +1,212 @@
# Incidente: medusa-deploy atascado sin levantar (2026-08)
**Ventana del incidente:** ~2026-08-08 (onset) → 2026-08-10 05:31 UTC (resuelto)
**Servicio afectado:** `medusa-deploy` en namespace `ecommerce`
**Impacto:** el rollout nuevo nunca llegaba a estar listo; el pod anterior
(`medusa-deploy-59878b956f-hf45b`) siguió sirviendo tráfico sin interrupción
durante todo el incidente (`maxUnavailable: 0`), por lo que no hubo downtime
de cara al usuario — el impacto fue "no se puede desplegar", no "el servicio
está caído".
## Síntoma inicial reportado vs. estado real
El incidente se reportó como **CrashLoopBackOff** ("sigue igual desde ayer,
más de 20-24 horas"). Al correr `kubectl get pods -n ecommerce -o wide` esa
descripción resultó **inexacta**: no había ningún pod en CrashLoopBackOff.
El estado real era:
```
commerce-postgres-0 1/1 Running 0 4d9h
medusa-deploy-59878b956f-hf45b 1/1 Running 0 21h (revisión vieja, sana)
medusa-deploy-9d8784d7b-g9jql 0/1 Init:0/2 0 3h56m (revisión nueva, atascada)
```
0 *restarts* en el pod nuevo — no estaba crasheando y reiniciando, estaba
**colgado indefinidamente en el primer init container** (`wait-for-postgres`),
sin timeout ni backoff que lo sacara de ese estado. Esta distinción importa:
un pod en `Init:0/2` con 0 restarts no aparece en las alertas típicas de
CrashLoopBackOff, lo que probablemente explica por qué pasó desapercibido
tanto tiempo.
**Lección:** verificar siempre el estado real con `kubectl get pods` antes de
asumir el tipo de falla que describe quien reporta el incidente. Un
`Init:X/Y` con 0 restarts y un `CrashLoopBackOff` requieren líneas de
investigación distintas.
## Hipótesis descartadas (en orden)
### 1. Credenciales / secret inválido o vacío
- `kubectl get secret -n ecommerce commerce-secrets` → la clave
`DATABASE_URL` existe.
- Se verificó longitud del valor (`~126` caracteres, no vacío ni truncado)
sin volcar el contenido en texto plano.
- Se probó el `DATABASE_URL` real contra Postgres con `pg_isready` ejecutado
**dentro del propio pod `commerce-postgres-0`**: `accepting connections`,
exit 0.
- **Descartada:** el secret existe, tiene el formato correcto y las
credenciales son válidas.
### 2. NetworkPolicy bloqueando tráfico
- `kubectl describe networkpolicy -n ecommerce ecommerce-network-security`
`PodSelector: <none>` (aplica a todos los pods), `Allowing ingress
traffic: To Port: <any>, From: NamespaceSelector: <none>` (permite todo el
ingress desde cualquier namespace), `Not affecting egress traffic`.
- **Descartada:** la policy es efectivamente allow-all para ingress y no
restringe egress. No podía estar bloqueando la conexión de `medusa` hacia
`postgres-svc`.
### 3. Salud de Postgres
- `kubectl exec -n ecommerce commerce-postgres-0 -- psql -U medusa -c
"SELECT count(*) FROM pg_stat_activity;"` → `8` conexiones activas,
respuesta inmediata.
- `kubectl get endpoints -n ecommerce postgres-svc` → apunta correctamente a
`10.42.0.98:5432`, la IP real del pod.
- **Descartada:** Postgres estaba sano, aceptando conexiones y con el
Endpoint del Service correctamente resuelto.
### 4. ResourceQuota / LimitRange del namespace
- `kubectl describe resourcequota -n ecommerce` → `pods: 7/12`,
`requests.cpu: 625m/3`, `requests.memory: 1504Mi/4Gi`, `limits.cpu:
4250m/8`, `limits.memory: 4480Mi/8Gi` — todo muy por debajo de los topes.
- `kubectl describe limitrange -n ecommerce` → rangos por-contenedor
(16Mi1Gi mem, 10m2 cpu) compatibles con los `resources` definidos en
`medusa.yaml`.
- Sin eventos de `exceeded quota` en el namespace.
- **Descartada:** no había presión de cuota ni un contenedor rechazado por
LimitRange.
### 5. DNS
- `kubectl exec` en el pod viejo (sano) → `getent hosts postgres-svc`
resolvía a `10.42.0.98` correctamente.
- CoreDNS (`kube-system`) → `Running`, sano.
- Repetido el mismo chequeo **dentro del propio init container atascado**
(`wait-for-postgres` del pod nuevo) → misma resolución correcta, y
`/etc/resolv.conf` con `nameserver 10.43.0.10` normal.
- **Descartada:** la resolución DNS funcionaba de forma idéntica en el pod
sano y en el pod atascado.
### 6. MTU/PMTUD cross-node (blackhole conocido, ver `docs/known-issues.md`)
- Existe un blackhole de red confirmado (186/186 fallos reproducidos) cuando
un pod que necesita hablar con `commerce-postgres-0` queda agendado en un
nodo distinto al de Postgres, mitigado hoy con `podAffinity` en
`medusa.yaml`.
- `kubectl get pods -o wide` mostró que **los tres pods relevantes
(`commerce-postgres-0`, el pod viejo y el pod nuevo) estaban en el mismo
nodo** (`k3d-lab-cluster-server-0`).
- **Descartada para este incidente puntual:** al no haber tráfico
cross-node involucrado, el blackhole de MTU no podía ser la causa. Sigue
siendo una condición latente real del cluster (ver sección de deuda
técnica).
## Causa raíz real
El init container `wait-for-postgres` ejecutaba:
```sh
until pg_isready -d "$DATABASE_URL" -t 5; do ...
```
pasando la URI completa de `DATABASE_URL` a `pg_isready`. En algún momento
se agregó el query param `?sslmode=disable` al `DATABASE_URL` generado por
`create-commerce-secrets.sh` (repo `scripts`, branch
`fix/medusa-database-url-sslmode`, ya ejecutada contra el secret vivo del
cluster). Con ese query param, el parser de conninfo de `pg_isready` en la
imagen `postgres:17-alpine` no lograba interpretar la URI y devolvía:
```
postgres-svc:5432 - no attempt
```
`exit 3` — **`no attempt`** es un status propio de `pg_isready` que indica
que el cliente **ni siquiera intentó conectar** por un problema en los
parámetros de conexión (a diferencia de `no response`, que sí implica un
intento de conexión fallido). Esto se reprodujo de forma determinística
ejecutando el mismo comando **dentro del propio init container atascado**
(sin crear pods efímeros nuevos), y se aisló la causa probando la misma URI:
- con `pg_isready -h postgres-svc -p 5432 -U medusa -d medusa` (sin URI) →
`accepting connections`, exit 0.
- con la URI completa pero sin el `?sslmode=disable` → también exitosa.
- con la URI completa incluyendo `?sslmode=disable` → reproducía el fallo.
Como el `until` no tiene timeout ni backoff máximo, el init container
quedaba reintentando indefinidamente sin nunca fallar de forma visible
(de ahí que no apareciera como CrashLoopBackOff).
## Fix aplicado
`workloads/ecommerce/commerce/medusa.yaml`, init container
`wait-for-postgres`:
```diff
- until pg_isready -d "$DATABASE_URL" -t 5; do
+ until pg_isready -h postgres-svc -p 5432 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t 5; do
```
`postgres-svc` y `5432` son fijos (el Service de Postgres); `POSTGRES_USER`
y `POSTGRES_DB` ya llegan al contenedor vía el mismo `envFrom:
secretRef: commerce-secrets`. El chequeo de disponibilidad deja de depender
por completo del formato de `DATABASE_URL`, incluyendo cualquier query
param futuro.
Commit `e9c93bf` en `main` de `apps-registry`, pusheado y sincronizado por
Argo CD (`ecommerce-app`, revisión `e9c93bfb82ec09b8ea2cc3468040ef9d72416542`,
sync a las `2026-08-10T05:29:32Z`). El pod atascado
`medusa-deploy-9d8784d7b-g9jql` fue reemplazado automáticamente por
`medusa-deploy-56c7c878b6-xgnkb`, que completó `wait-for-postgres` en
segundos y quedó `1/1 Running` sin intervención manual adicional.
## Lecciones aprendidas
**Separar el chequeo de disponibilidad (`wait-for-postgres`) de las
migraciones (`migrations`) en init containers distintos fue lo correcto** y
ya estaba así desde que se introdujo este init container
(commit `f4ad140`). Eso permitió aislar el fallo al primer init container
sin ambigüedad — la falla nunca llegó a tocar `migrations`. La lección no es
cambiar esa separación, sino mantenerla: cualquier chequeo de
disponibilidad debe usar el mínimo de parámetros necesarios (host/puerto/
usuario/db) en vez de reusar una URI de conexión pensada para la app,
que puede cambiar de formato por razones ajenas al chequeo.
**No fue necesario crear pods de diagnóstico efímeros, y no crearlos evitó
ruido adicional en el cluster.** Toda la reproducción y aislamiento de la
causa raíz (DNS, `pg_isready` con distintos argumentos, lectura de
`/etc/resolv.conf`) se hizo con `kubectl exec` sobre pods que ya existían
(el pod viejo sano, el pod nuevo atascado, y `commerce-postgres-0`). Crear
pods de prueba repetidos consume cuota (`ecommerce-quota` está en `pods:
7/12`, con margen pero no ilimitado) y puede introducir su propio ruido de
scheduling/red — como muestra `docs/known-issues.md`, el diagnóstico previo
del blackhole de MTU sí necesitó pods de prueba dedicados (186 intentos)
porque el fenómeno dependía del nodo de scheduling, algo que no se puede
observar desde un pod ya corriendo. La regla práctica: usar pods
existentes cuando el fallo es reproducible ahí, y reservar pods efímeros
para cuando la variable a probar (ej. nodo, imagen, versión) no se puede
cambiar en un pod ya desplegado — midiendo el impacto (cuota, ruido) de
cada corrida.
## Deuda técnica pendiente (no resuelta por este incidente)
El blackhole de red cross-node por MTU/PMTUD documentado en
`docs/known-issues.md` sigue sin arreglo de fondo (ajustar MTU a `1400` en
la red Docker/flannel del cluster k3d). El workaround actual es el
`podAffinity` en `medusa.yaml` que fija `medusa-deploy` al mismo nodo que
`commerce-postgres-0`.
Existe una branch abierta, `fix/medusa-wait-for-postgres`, que:
- Elimina ese `podAffinity`.
- Borra `docs/known-issues.md` por completo.
- **No incluye el fix de MTU real** (no toca configuración de flannel ni de
la red Docker del cluster).
Mergear esa branch tal como está day-1 reintroduciría el blackhole cross-node
sin red de contención y borraría la única documentación del problema. No se
tocó como parte de este incidente. Queda pendiente decidir si se cierra sin
mergear, o si se retoma agregando primero el fix real de MTU antes de poder
quitar el `podAffinity` con seguridad.
@@ -25,6 +25,16 @@ module.exports = defineConfig({
jwtSecret: required("JWT_SECRET"), jwtSecret: required("JWT_SECRET"),
cookieSecret: required("COOKIE_SECRET"), cookieSecret: required("COOKIE_SECRET"),
}, },
// Todos los Ingress del lab terminan en Traefik por HTTP plano
// (entrypoint "web", sin TLS); Cloudflare es quien atiende HTTPS de
// cara al navegador. Con `secure: true` (el default en producción),
// express-session nunca emite Set-Cookie porque no detecta la
// conexión como HTTPS, y el dashboard admin queda con 401 en
// /admin/users/me pese a loguear bien. La API con Bearer token no se
// ve afectada por esto.
cookieOptions: {
secure: false,
},
}, },
admin: { admin: {
@@ -0,0 +1,46 @@
import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework";
import { ContainerRegistrationKeys } from "@medusajs/framework/utils";
export default async function revalidateStorefrontHandler({
container,
}: SubscriberArgs) {
const logger = container.resolve(ContainerRegistrationKeys.LOGGER);
const storefrontUrl = process.env.STOREFRONT_URL;
const secret = process.env.REVALIDATE_SECRET;
if (!storefrontUrl || !secret) {
logger.warn(
"STOREFRONT_URL o REVALIDATE_SECRET no configurados; se omite la revalidación del storefront.",
);
return;
}
try {
const response = await fetch(`${storefrontUrl}/api/revalidate`, {
method: "POST",
headers: { "x-revalidate-secret": secret },
});
if (!response.ok) {
logger.warn(
`Revalidación del storefront respondió HTTP ${response.status}.`,
);
}
} catch (error) {
logger.warn(
`No se pudo revalidar el storefront: ${(error as Error).message}`,
);
}
}
export const config: SubscriberConfig = {
event: [
"product.created",
"product.updated",
"product.deleted",
"product-variant.updated",
"product-variant.created",
"product-variant.deleted",
],
};
@@ -0,0 +1,13 @@
import { revalidateTag } from "next/cache";
export async function POST(request: Request) {
const secret = request.headers.get("x-revalidate-secret");
if (!secret || secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ message: "Invalid secret" }, { status: 401 });
}
revalidateTag("medusa-products");
return Response.json({ revalidated: true, tag: "medusa-products" }, { status: 200 });
}
@@ -9,10 +9,17 @@ import { getProductBySlug, getProducts } from "@/lib/headless/client";
interface ProductPageProps { params: Promise<{ slug: string }> } interface ProductPageProps { params: Promise<{ slug: string }> }
export async function generateStaticParams() { // generateStaticParams + revalidate quedaban aquí para pre-renderizar en
const products = await getProducts(); // build time, pero HEADLESS_PROVIDER/MEDUSA_* no existen en el contexto del
return products.map((product) => ({ slug: product.slug })); // Docker build: el snapshot estático quedaba congelado con el catálogo mock
} // (todos los productos con price: null) hasta que ISR lo revalidara. Con
// frontend-deploy corriendo 2 réplicas y sin cacheHandler compartido, cada
// pod revalida su propia caché de forma independiente, así que un producto
// podía mostrar "Consultar precio" en un pod y el precio real en el otro
// según cuál hubiera sido "calentado". force-dynamic elimina esa caché por
// pod y deja esta ruta en vivo contra Medusa, igual que el listado
// (dinámico por searchParams).
export const dynamic = "force-dynamic";
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> { export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
const { slug } = await params; const { slug } = await params;
+32 -2
View File
@@ -20,9 +20,39 @@ spec:
imagePullSecrets: imagePullSecrets:
- name: gitea-registry-secret - name: gitea-registry-secret
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: commerce-postgres
topologyKey: kubernetes.io/hostname
initContainers: initContainers:
- name: wait-for-postgres
image: postgres:17-alpine
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- |
until pg_isready -h postgres-svc -p 5432 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t 5; do
echo "postgres no disponible aun, reintentando..."
sleep 2
done
envFrom:
- secretRef:
name: commerce-secrets
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi
- name: migrations - name: migrations
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.85 image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.90
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- npx - npx
@@ -43,7 +73,7 @@ spec:
containers: containers:
- name: medusa - name: medusa
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.85 image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.90
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
envFrom: envFrom:
- configMapRef: - configMapRef:
@@ -22,6 +22,11 @@ stringData:
JWT_SECRET: CAMBIAR JWT_SECRET: CAMBIAR
COOKIE_SECRET: CAMBIAR COOKIE_SECRET: CAMBIAR
# Debe ser el MISMO valor que REVALIDATE_SECRET en el Secret
# commerce-storefront: el subscriber de Medusa lo envía como header
# x-revalidate-secret al invocar POST /api/revalidate en el storefront.
REVALIDATE_SECRET: CAMBIAR
--- ---
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
@@ -30,3 +35,6 @@ metadata:
type: Opaque type: Opaque
stringData: stringData:
MEDUSA_PUBLISHABLE_KEY: pk_CAMBIAR_DESPUES_DE_CREARLA_EN_MEDUSA MEDUSA_PUBLISHABLE_KEY: pk_CAMBIAR_DESPUES_DE_CREARLA_EN_MEDUSA
# Mismo valor que REVALIDATE_SECRET en commerce-secrets.
REVALIDATE_SECRET: CAMBIAR
+11 -1
View File
@@ -16,9 +16,19 @@ spec:
- name: gitea-registry-secret - name: gitea-registry-secret
containers: containers:
- name: web - name: web
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.77 image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.91
ports: ports:
- containerPort: 80 - containerPort: 80
env:
- name: HEADLESS_PROVIDER
value: medusa
- name: MEDUSA_BACKEND_URL
value: http://medusa-svc:9000
- name: MEDUSA_REGION_ID
value: reg_01KZT86WPAH7A7ZZRDSX3350V2
envFrom:
- secretRef:
name: commerce-storefront
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
@@ -258,6 +258,14 @@ function backendUrl(): string {
return value.replace(/\/$/, ""); return value.replace(/\/$/, "");
} }
function regionId(): string {
const value = process.env.MEDUSA_REGION_ID;
if (!value) {
throw new Error("MEDUSA_REGION_ID no está configurado.");
}
return value;
}
async function medusaFetch<T>( async function medusaFetch<T>(
path: string, path: string,
searchParams?: URLSearchParams, searchParams?: URLSearchParams,
@@ -515,7 +523,10 @@ async function fetchProducts(
): Promise<Product[]> { ): Promise<Product[]> {
const params = new URLSearchParams({ const params = new URLSearchParams({
limit: String(limit), limit: String(limit),
currency_code: "cop", // El validador de /store/products no acepta currency_code (400
// "Unrecognized fields"); el precio calculado solo se resuelve con
// region_id.
region_id: regionId(),
fields: fields:
"+metadata,+images,+tags,+categories,+collection,+variants.inventory_quantity,+variants.calculated_price,+variants.options", "+metadata,+images,+tags,+categories,+collection,+variants.inventory_quantity,+variants.calculated_price,+variants.options",
}); });
@@ -543,7 +554,10 @@ export const medusaCatalogProvider: CatalogProvider = {
const params = new URLSearchParams({ const params = new URLSearchParams({
handle: slug, handle: slug,
limit: "1", limit: "1",
currency_code: "cop", // El validador de /store/products no acepta currency_code (400
// "Unrecognized fields"); el precio calculado solo se resuelve con
// region_id.
region_id: regionId(),
fields: fields:
"+metadata,+images,+tags,+categories,+collection,+variants.inventory_quantity,+variants.calculated_price,+variants.options", "+metadata,+images,+tags,+categories,+collection,+variants.inventory_quantity,+variants.calculated_price,+variants.options",
}); });