Capa de interoperabilidad · Prueba técnica de arquitectura

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

  1. 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-is de legacy: las crea o las borra. Su contenido no lo arma el código: es el archivo generado optional/legacy-as-is.yaml del 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.
  2. Escenarios. Cada corrida es un Job en demo con la imagen grafana/k6:K6_VERSION de versions.env, la cuenta de servicio carga (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 Secret demo/k6-partners que crea el despliegue. Una corrida a la vez; tope con activeDeadlineSeconds; detener borra el Job.

  3. Resultados. El resumen que imprime k6 (guardar de tests/load/lib/comun.js) se lee del log del pod y se guarda, con la corrida, en el ConfigMap demo-console-state, junto al registro de auditoría. La imagen lleva las mediciones registradas de tests/load/results/as-is-nube y to-be-nube para el comparativo; lo no medido sale "por medir".

  4. Permisos (Role, nunca ClusterRole):

    • demo: crear, leer, listar y borrar Jobs; leer pods y su log; leer y actualizar solo el ConfigMap demo-console-state.
    • istio-ingress: leer y modificar solo los VirtualService erp, wms, oms y crm.
    • legacy: leer y borrar solo las AuthorizationPolicy <sistema>-as-is, leer allow-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.
  5. Acceso. Cloudflare Access en control.<zona> (raíz dns). En el clúster, la AuthorizationPolicy demo-console-access solo 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.

  6. Eventos en vivo y excepción a la regla 4 de CLAUDE.md. La consola consume eventos andes.* 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. Nunca andes.audit.v1, ni reintento, ni DLQ, ni raw.*.
    • En el contrato, cada recepción del grupo lleva x-observer: true. scripts/topics-plan.js acepta la marca solo para los grupos de su lista OBSERVER_GROUPS (hoy solo demo-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.js lo prueba en negativo (en make 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.
  7. 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-demo deja llegar al 9090 solo a la consola y, en erp, wms y oms, a las corridas de k6. La caída real usa un permiso nombrado: deployments/scale de esas cinco cargas en legacy, get y patch; nada sobre Deployments ni otras cargas.

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