ADR-033: Consola del demo con escritura acotada en el clúster
Aceptado el 15/11/2026 (decisión del carril `panel-de-control`, con aviso a planeación). Complementa la fase F7 de `docs/plan.md` y `docs/demo/02-subdominios-e-interfaz-de-sistemas.md` ("Los dos modos de exposición").
Contexto
El demo en vivo del 17/11/2026 necesita tres cosas sobre el ambiente: cambiar entre el modo AS-IS (la API de erp, wms, oms y crm responde desde internet) y el modo TO-BE (responde 403 y el único camino es api), lanzar los seis escenarios de carga de tests/load/ y ver sus resultados al lado del comparativo.
Hasta hoy las tres se hacen desde la terminal de quien despliega:
- El modo es un interruptor de despliegue,
LEGACY_EXPOSURE(platform/k8s/scripts/k8s.sh): el valorlegacyExposuredel chartistio-routingagrega o quita la ruta<sistema>-api-closed(403 en Problem Details) del VirtualService de cada sistema, y la malla aplica o retiraplatform/mesh/optional/legacy-as-is.yaml(las AuthorizationPolicy<sistema>-as-isdelegacy). Cambiarlo es unhelm upgradey unkubectl applycon credenciales de administrador del clúster. - Los escenarios corren con
tests/load/run-eks.sh: un pod de k6 enlegacy, scripts copiados conkubectl cp, secretos copiados del pod de Keycloak conkubectl exec. - Los resultados se leen en archivos JSON de
tests/load/results/.
El diseño ya preveía una consola (lab.andesretail.com, hoy control.andesretail.com) y decía que el cambio de modo "queda registrado en auditoría". Una consola así es una pieza nueva con poder de escritura sobre el ambiente: hay que acotarlo.
Opciones
| Opción | A favor | En contra |
|---|---|---|
A. Solo make desde la terminal, como hoy |
Ninguna pieza nueva con permisos; el despliegue sigue siendo la única fuente | Exige credenciales de administrador y el túnel a la API privada (ADR-027) en la máquina del demo; sin auditoría del cambio de modo; resultados en archivos; en vivo es lento y frágil |
| B. Consola en el clúster que cambia solo los recursos del modo y crea Jobs de k6, con una cuenta de servicio de permisos mínimos | Un clic en el demo; auditoría propia; no necesita credenciales en la máquina del demo; los permisos se pueden enumerar recurso por recurso | Una carga nueva con escritura; el estado del clúster puede diferir del último despliegue hasta el siguiente |
C. Consola que corre helm y k8s.sh por dentro |
Un solo camino para el modo (el despliegue) | Helm necesita permisos amplios sobre todo lo de la release (Secrets, Deployments): de hecho, administrador; la imagen cargaría Helm, los charts y el repositorio |
| D. GitOps (Argo CD o Flux): el modo es un valor en Git y un operador lo aplica | La fuente es Git y la auditoría es el historial | Un operador nuevo en el clúster a dos días del demo; cada cambio de modo pasa por un commit y una sincronización |
Decisión
Opción B. apps/demo-console, Node.js con Express desde platform/templates/node-adapter, en su propio espacio de nombres demo, con contrato en contracts/openapi/demo-console.v1.yaml.
Modo. La consola cambia exactamente lo que cambia
LEGACY_EXPOSURE, nada más:- El VirtualService de cada sistema en
istio-ingress(erp,wms,oms,crm): agrega o quita la ruta<sistema>-api-closed, con el mismo cuerpo que el chart, y lo anota (andesretail.com/legacy-exposure). - Con la malla aplicada, las AuthorizationPolicy
<sistema>-as-isdelegacy: las crea o las borra. Su contenido no lo arma el código: es el archivo generadooptional/legacy-as-is.yamldel render de la malla, montado en la consola. - El modo que muestra se lee de esos recursos, no de una variable propia; si no coinciden entre sí, muestra
mixed.
- El VirtualService de cada sistema en
Escenarios. Cada corrida es un Job en
democon la imagengrafana/k6:K6_VERSIONdeversions.env, la cuenta de serviciocarga(sin token de la API de Kubernetes), los scripts y la semilla desde ConfigMaps y, para el TO-BE, los secretos de cliente de los partners desde el Secretdemo/k6-partnersque crea el despliegue. Una corrida a la vez; tope conactiveDeadlineSeconds; detener borra el Job.Resultados. El resumen que imprime k6 (
guardardetests/load/lib/comun.js) se lee del log del pod y se guarda, con la corrida, en el ConfigMapdemo-console-state, junto al registro de auditoría. La imagen lleva las mediciones registradas detests/load/results/as-is-nubeyto-be-nubepara el comparativo; lo no medido sale "por medir".Permisos (Role, nunca ClusterRole):
demo: crear, leer, listar y borrar Jobs; leer pods y su log; leer y actualizar solo el ConfigMapdemo-console-state.istio-ingress: leer y modificar solo los VirtualServiceerp,wms,omsycrm.legacy: leer y borrar solo las AuthorizationPolicy<sistema>-as-is, leerallow-nothing(para saber si hay malla) y crear AuthorizationPolicy.- Nada sobre Secrets, Deployments ni ningún otro espacio. No llama al legado, a los servicios de dominio ni a Keycloak.
Acceso. Cloudflare Access en
control.<zona>(raízdns). En el clúster, la AuthorizationPolicydemo-console-accesssolo deja entrar a la consola desde el gateway de entrada. El operador que queda en la auditoría es el correo de la cabecera de Cloudflare Access, enmascarado.Eventos en vivo y excepción a la regla 4 de
CLAUDE.md. La consola consume eventosandes.*para mostrarlos, sin outbox de por medio y sin reintento, DLQ ni deduplicación persistente. Es una excepción a "todo consumidor deduplica" y a "todo grupo de hechos o comandos tiene reintento y DLQ" (docs/to-be/06-eventos.md), con este límite exacto:- Solo un consumidor sin efectos: no confirma trabajo de negocio, no escribe estado (ni base, ni tópico, ni llamada a otro servicio) y no cambia nada si un evento llega dos veces o no llega.
- Grupo propio,
demo-console, que empieza en el último offset; nadie más usa ese grupo. - Solo lectura de tópicos
andes.*de hechos, comandos y estado. Nuncaandes.audit.v1, ni reintento, ni DLQ, niraw.*. - En el contrato, cada recepción del grupo lleva
x-observer: true.scripts/topics-plan.jsacepta la marca solo para los grupos de su listaOBSERVER_GROUPS(hoy solodemo-console) y sobre esos tópicos, exige la marca en todas las recepciones de ese grupo y falla en cualquier otro caso;scripts/lib/topics-plan-negative.jslo prueba en negativo (enmake console-verify). - En pantalla deduplica por el identificador del evento (
ce_id) para no mostrar repetidos, y no muestra datos personales ni de pago: los eventos ya llegan sin ellos y la consola además reemplaza los campos sensibles por nombre (correo, teléfono, documento, dirección, nombres, tarjeta, tokens) y enmascara cada texto con el enmascarador de la plataforma, también en los comandos.
Apagar y degradar sistemas (ampliación del 15/11/2026, pedido de Andrés: "control debe tener las capacidades de apagar sistemas de acuerdo con los escenarios que vamos a probar"). La consola deja de solo enlazar a las perillas y las acciona:
- Por simulador (
erp,wms,oms,crm,payment-gateway): estado visible siempre (normal, lento, con errores, caído, apagado, y en la pasarela rechaza pagos, sin webhook, webhook doble) y una acción por estado. Lento, errores y caído son las perillas del contrato de control (latencia de 3 s, 50% de error,down: true), las mismas que usan los escenarios. Apagado es la caída real: el Deployment a 0 réplicas, como opción aparte. - Preajustes por escenario, un botón cada uno: ERP lento, ERP caído, WMS caído, OMS caído, pago rechazado, webhook duplicado y webhook perdido.
- Antes de lanzar un escenario de carga se restaura todo (cada script fija sus perillas) y al terminar o detenerlo se restaura de nuevo.
- Contención: botón "restaurar todo" siempre disponible; todo estado distinto de normal se restaura solo al vencer su tope (15 minutos por defecto, hasta 120, configurable); con una corrida en curso no se puede tumbar nada; cada acción queda en la auditoría con quién, cuándo, sistema, de qué estado a cuál y el preajuste. La restauración automática queda a nombre de
auto. - Límites: la consola habla solo con el puerto de control (9090) de cada simulador, nunca con su API de negocio ni con los servicios de dominio. Ese puerto no se publica fuera del clúster (istio-routing solo enruta al 8080). En la malla,
legacy/<sistema>-control-from-demodeja llegar al 9090 solo a la consola y, enerp,wmsyoms, a las corridas de k6. La caída real usa un permiso nombrado:deployments/scalede esas cinco cargas enlegacy,getypatch; nada sobre Deployments ni otras cargas.
- Por simulador (
El interruptor de despliegue sigue siendo la fuente del modo por defecto: un helm upgrade de istio-routing o un k8s.sh mesh devuelven el clúster a LEGACY_EXPOSURE (TO-BE si no se dice otra cosa). La consola cambia el modo entre despliegues, no lo redefine.
Qué se sacrifica
- Una carga con escritura sobre el borde y la malla. Si alguien toma la consola, puede abrir la API de los simuladores (lo mismo que el modo AS-IS) y lanzar carga. No puede tocar secretos, servicios de dominio ni otros espacios.
- Kubernetes no deja limitar
createpor nombre: la consola puede crear cualquier AuthorizationPolicy enlegacy. Es el permiso más amplio que tiene; se acepta porquelegacysolo tiene simuladores y porque modificar una política existente (permiso que de todos modos haría falta) ya permite lo mismo. - Entre dos despliegues, el estado del clúster puede no ser el de
LEGACY_EXPOSURE. El siguiente despliegue lo corrige hacia ese valor. La consola lo muestra siempre como está. - El correo del operador se toma de la cabecera de Cloudflare Access sin verificar su firma (verificarla exige descargar las llaves de Cloudflare desde el clúster). Se confía en que la única entrada es el gateway, detrás de Cloudflare. Queda como mejora.
- La regla 4 (deduplicar, reintento y DLQ) no aplica al grupo
demo-console. Si un evento no se puede leer, la consola lo salta y no se reintenta: la pantalla puede perder un evento, el negocio no. Contra Confluent, la consola necesita su propia llave de lectura (catálogo conkafka = truey la raízkafka), que nube agrega después de unir esta rama; hasta entonces los eventos en vivo solo existen conKAFKA_MODE=pod. - Una pantalla que tumba sistemas. Es aceptable en el demo porque lo que tumba son simuladores del legado, que existen para mostrar fallas; nunca toca servicios de dominio, bases ni Kafka. La contiene: Cloudflare Access y la AuthorizationPolicy de entrada (solo quien opera el demo llega), el puerto de control cerrado a todo salvo la consola y k6, el permiso de escala limitado por nombre, el tope de restauración automática y la auditoría. En una empresa real esta capacidad no existiría en producción: es del ambiente de demostración.
- El puerto de control no estaba cerrado solo a la consola antes de esta ampliación, y sigue sin estarlo del todo:
legacy/<sistema>-from-adapterdeja entrar al adaptador de cada sistema sin limitar el puerto, así que el adaptador también llega al 9090. No se cambia aquí (es la regla de la malla del legado y la usa el carril de la malla); queda reportado. En kind, sin malla, cualquier pod llega al 9090. - Una corrida a la vez: no se pueden medir dos escenarios en paralelo, igual que con
run-eks.sh. - Con la malla, la cuenta de servicio
cargadedemoentra al puerto de control (9090) deerp,wmsyomsmientras exista, no solo durante una corrida como enrun-eks.sh. Solo la consola crea pods con esa cuenta.