Ir al contenido

Blog ·

Un límite pertenece al núcleo

Revocar un agente alojado lo separaba de su pedido y dejaba una máquina Proxmox encendida que nadie podía alcanzar y que todos seguían pagando.

Por Provibr

Un Cloud Agent es la versión de nuestro agente en la que la máquina la operamos nosotros. El cliente lo pide, nosotros clonamos una plantilla de Proxmox, instalamos el agente en ella y lo facturamos cada mes. Desde el panel parece un agente cualquiera, y ese parecido es toda la historia de esta ronda.

Cualquier otro agente es del cliente. Revocarlo es una operación limpia y segura: la plataforma envía un aviso de revocación, el agente borra su identidad, su certificado y su almacén de credenciales, y después se puede eliminar la fila. No queda nada en marcha en ningún sitio, porque lo que estaba en marcha nunca fue nuestro.

En un Cloud Agent eso no es cierto, y nada lo impedía.

Lo que medimos antes de cambiar nada

Sobre la base de datos de desarrollo, con un pedido real detrás de un agente real, llamamos a la vía de eliminación exactamente igual que lo hace el botón del panel.

Informó de mode: "hard". La fila del agente había desaparecido. La fila del pedido seguía ahí, seguía active, seguía llevando el vmid 5998, y su agent_id se había convertido en NULL sin decir nada, porque la clave foránea del pedido hacia el agente es ON DELETE SET NULL.

Así que había una máquina virtual en un hipervisor, encendida, con un agente dentro que había borrado su propia identidad y que por tanto no podría volver a conectarse nunca, ligada a un pedido que seguía generando un cargo mensual, y ninguna fila en ningún sitio que apuntara de una cosa a la otra. Ni accesible, ni detenible desde el panel, ni gratis.

La restricción que produjo esto es correcta. Poner la referencia a NULL es el comportamiento adecuado para una columna que dice qué agente hay instalado aquí ahora mismo; una cascada habría borrado el pedido y con él el registro de facturación. El defecto es que nada hacía la pregunta que el esquema no podía hacer.

Dos guardas existentes, las dos mirando hacia el otro lado

Eliminar un agente ya tenía dos negativas, y merece la pena ser preciso sobre por qué no saltó ninguna de las dos.

La primera se niega a eliminar un agente que está inscrito en ese momento: hay que revocarlo antes, para que se limpie a sí mismo. Cuando la fila se estaba eliminando, el agente acababa de ser revocado en la misma llamada, así que no estaba inscrito y la comprobación quedaba satisfecha.

La segunda se niega a eliminar un agente del que todavía cuelgan integraciones o servidores, porque esas claves foráneas van en cascada y un solo clic se llevaría por delante el inventario entero. Un Cloud Agent no tiene ni lo uno ni lo otro: es una máquina recién creada que existe para ejecutar el agente, así que la comprobación pasó porque no había nada que contar.

Las dos guardas trataban de qué destruiría la eliminación. Lo que sobrevive a la eliminación, y cuesta dinero precisamente porque sobrevive, quedaba fuera de la forma de la pregunta.

El límite va en el núcleo, no en el botón

La reparación evidente es comprobarlo en la server action que hay detrás del botón. También está equivocada, y el motivo es un hueco en el que este código ya cayó una vez con un endpoint de autenticación de serie que rodeaba todos los controles del lado del servidor.

Contamos los puntos de llamada. Fuera de las dos funciones del núcleo hay siete llamadas a revocar, a eliminar o a la eliminación combinada, repartidas en tres ficheros: tres server actions en la página del agente, la acción masiva y dos rutas de la API v1. Una comprobación en la acción del panel habría dejado abiertas las rutas de la API, y una ruta de API es exactamente lo que usa el script de un cliente.

Así que el límite vive dentro de revokeAgentCore() y deleteAgentCore(), el punto de estrangulamiento por el que pasan las siete llamadas. Una sola función, consultada dos veces.

Está después de la rama idempotente y no antes. Un agente que ya está revocado no tiene nada que revocar, y decirle a quien llama que cancele antes su pedido sería una falsedad sobre una llamada que no va a hacer nada de todos modos. Un límite pertenece a la operación que cambia algo de verdad.

El predicado es ¿se puede cancelar todavía?, no ¿esto está vivo?

En el paquete compartido hay una lista llamada CLOUD_AGENT_LIVE_STATUSES: los estados en los que una máquina existe y cuesta dinero. Tiene cuatro miembros y es exactamente la lista equivocada para reutilizarla aquí.

El predicado que usamos es cloudAgentActionAllowed("cancel", status), que es cierto para siete de los ocho estados de pedido, todos salvo un pedido que ya está cancelado.

Los tres estados de la diferencia son los interesantes, y failed es el más afilado. Un pedido fallido no cuesta nada, así que no está vivo. Pero un pedido fallido es justo el que puede haber llegado lo bastante lejos como para crear una máquina y luego caerse, que es la razón por la que desde ahí se permite cancelar. Reutilizar la lista de facturación habría dejado sin proteger el único caso con una máquina huérfana, y en una revisión habría parecido de lo más razonable.

Dos listas que significan casi lo mismo acaban divergiendo. La pregunta aquí no es ¿esto cuesta dinero?, sino ¿sigue siendo la cancelación el camino?, y exactamente una función del código responde a esa segunda.

Dos vías de escritura a las que se les permite saltárselo

La guarda que escribimos deriva todos los puntos del código del panel que revocan, eliminan de forma dura u ocultan de forma blanda la fila de un agente, mediante un recorrido del AST sobre los árboles de aplicación y de librería, y exige en cada punto o bien el límite o bien un motivo por escrito. Dos puntos llevan motivo: la propia vía de cancelación, que retira la máquina, libera la dirección y solo entonces revoca el agente, y el cierre de cuenta, que revoca todos los agentes de una organización y trata el Cloud Agent aparte dentro del mismo flujo. Bloquear cualquiera de los dos cerraría una salida correcta en lugar de ahorrar dinero.

La guarda tiene los dos filtros que impiden que un analizador de código esté en verde por vacío: una cota inferior sobre cuántos puntos de escritura tiene que encontrar, tres de ellos nombrados, y una prueba que falla ante una entrada de la lista de permitidos que apunta a un fichero que ya no escribe nada. Ejecuta 4 pruebas y 11 aserciones.

Dos hechos en pantalla, no uno

El lado del panel necesitaba dos booleanos separados y estuvimos a punto de entregar uno solo.

orderOpen es un hecho sobre el pedido: si todavía se puede cancelar. canCancel es un hecho sobre quien mira: si esa persona puede cancelarlo. Fundirlos en un único se puede actuar aquí significaría que un miembro sin el permiso de cancelación ve el botón de revocar, lo usa, tira la máquina y no puede parar la factura. Por eso los botones desaparecen según el hecho del pedido, y la tarjeta se queda con el motivo dentro, porque ¿por qué no puedo revocar este? es la pregunta que hace todo el que ha visto el botón en sus otros agentes.

Una cosa vino de acompañante porque el cambio la volvió falsa: las instrucciones de desinstalación aconsejaban revocar antes el agente en el panel. Ese es un botón que ya no existe en esta página.

Lo que dejaríamos por escrito

  1. Un parecido en la interfaz no es un parecido en las consecuencias. Dos agentes, un botón, y uno de ellos cuesta dinero después de pulsarlo.
  2. ON DELETE SET NULL puede estar bien y aun así dejar un huérfano. El esquema no puede hacer la pregunta.
  3. Las guardas sobre lo que una eliminación destruye no verán lo que una eliminación abandona.
  4. Cuente los puntos de llamada antes de elegir dónde va la comprobación. Los nuestros eran siete, en tres ficheros, dos de ellos rutas de API.
  5. Ponga el límite en el punto de estrangulamiento, no en el botón. Un botón es una de las entradas.
  6. Póngalo después de la rama idempotente. Una negativa sobre una llamada que no haría nada es una mentira.
  7. Reutilizar una lista que significa casi lo mismo es un fallo lento. Cuesta dinero y todavía se puede cancelar difieren en tres estados, y la diferencia es justo donde está el daño.
  8. Un estado que no cuesta nada puede haber dejado algo detrás. Un aprovisionamiento fallido es el peligroso.
  9. Separe el hecho sobre el objeto del hecho sobre quien mira. Fundidos, ponen un botón destructivo en manos de un miembro limitado.
  10. Deje que la guarda lea el árbol. Una prueba por cada entrada conocida no puede ver la entrada que alguien añada el mes que viene.

Para que quede claro qué es esto: el defecto se reprodujo contra la base de datos de desarrollo antes de cualquier cambio, la comprobación de extremo a extremo terminó en 26 en verde y se demostró antes en rojo con 16 en verde y 10 fallando, la comprobación en navegador en 29 en verde y demostrada en rojo con 18 y 11 al forzar la desactivación del indicador de pedido, nueve idiomas en 178 en verde, y la verificación de la fusión se ejecutó sin ninguna línea de fallo. Sin demostrar: no hubo ninguna máquina de un cliente real de por medio, y el lado de la facturación se inspeccionó en lugar de ejercitarse. Tres trampas de medición costaron tiempo y merece la pena nombrarlas: la zona de peligro vive detrás de un parámetro de pestaña, así que una comprobación que lo omite mide cero botones y lee eso como un éxito; un escape con barra invertida dentro de una plantilla literal entregada a la evaluación de una página no es un escape; y uno de los tres scripts de catálogo aplanó un espacio de no separación que los otros dos conservaban.

Del trabajo de plataforma en Provibr. Por qué una máquina se puede vender exactamente una vez está en dos pedidos, una máquina, por qué un acceso directo es una referencia y nunca un permiso está en un acceso directo es una referencia, no un derecho, y una palabra de estado tomada prestada de la tabla equivocada está en un escaneo que cerró su propia franja.

Empiece con un host y un agente

Cree una cuenta, instale el agente con un solo comando y conecte su primer host de Proxmox. El periodo de prueba dura 14 días y no pide ningún dato de pago.