Naar de inhoud

Blog ·

Een grens hoort in de kern

Een Cloud Agent intrekken maakte hem los van de bestelling en liet een draaiende Proxmox-machine achter die niemand bereikte en waarvoor iedereen bleef betalen.

Door Provibr

Een Cloud Agent is de versie van onze agent waarbij wij de machine draaien. De klant bestelt er een, wij klonen een Proxmox-sjabloon, installeren de agent erop en brengen hem maandelijks in rekening. Vanuit het paneel ziet hij eruit als elke andere agent, en die gelijkenis is het hele verhaal van deze ronde.

Elke andere agent is van de klant. Hem intrekken is een schone, veilige operatie: het platform stuurt een intrekbericht, de agent wist zijn identiteit, zijn certificaat en zijn kluis met inloggegevens, en daarna kan de rij weg. Er blijft nergens iets draaien, want wat er draaide was nooit van ons.

Voor een Cloud Agent geldt dat niet, en niets hield dat tegen.

Wat we gemeten hebben voordat we iets veranderden

Op de ontwikkeldatabase, met een echte bestelling achter een echte agent, riepen we het verwijderpad precies zo aan als de knop in het paneel dat doet.

Hij meldde mode: "hard". De agentrij was weg. De bestelrij stond er nog, nog steeds active, nog steeds met vmid 5998, en zijn agent_id was stilletjes NULL geworden, want de foreign key van de bestelling naar de agent is ON DELETE SET NULL.

Er stond dus een virtuele machine op een hypervisor te draaien, met daarop een agent die zijn eigen identiteit gewist had en dus nooit meer kon verbinden, vastgeknoopt aan een bestelling die nog elke maand geld kostte, en nergens een rij die van de een naar de ander wees. Niet bereikbaar, niet te stoppen vanuit het paneel, en niet gratis.

De constraint die dit opleverde is correct. De verwijzing op NULL zetten is het juiste gedrag voor een kolom die zegt welke agent hier op dit moment geïnstalleerd staat; een cascade zou de bestelling verwijderd hebben en de facturatieregistratie ermee. Het defect is dat niets de vraag stelde die het schema niet kón stellen.

Twee bestaande bewakingen, allebei de verkeerde kant op

Een agent verwijderen had al twee weigeringen, en het is de moeite waard precies te zijn over waarom geen van beide afging.

De eerste weigert een agent te verwijderen die op dit moment ingeschreven staat: trek hem eerst in, dan ruimt hij zichzelf op. Tegen de tijd dat de rij verwijderd werd was de agent net in dezelfde aanroep ingetrokken, dus stond hij niet ingeschreven en was de controle tevreden.

De tweede weigert een agent te verwijderen waar nog integraties of servers aan hangen, want die foreign keys cascaderen en één klik zou de hele inventaris meenemen. Een Cloud Agent heeft geen van beide: het is een verse machine die bestaat om de agent te draaien, dus de controle slaagde omdat er niets te tellen viel.

Allebei die bewakingen gingen over wat het verwijderen zou vernietigen. Wat het verwijderen overleeft, en juist geld kost omdát het het overleeft, viel buiten de vorm van de vraag.

De grens gaat in de kern, niet in de knop

De voor de hand liggende reparatie is een controle in de server-action achter de knop. Die is ook fout, en de reden is een gat waar deze codebase eerder in gelopen is met een standaard authenticatie-endpoint dat elke controle aan de serverkant omzeilde.

We hebben de aanroeppunten geteld. Buiten de twee kernfuncties zijn er zeven aanroepen naar intrekken, verwijderen of het gecombineerde verwijderpad, verspreid over drie bestanden: drie server-actions op de agentpagina, de bulkactie, en twee v1-API-routes. Een controle in de paneelactie had de API-routes open gelaten, en een API-route is precies wat een klantscript gebruikt.

De grens zit dus binnen revokeAgentCore() en deleteAgentCore() zelf, het keelpunt waar alle zeven aanroepen langskomen. Eén functie, twee keer gevraagd.

Hij zit achter de idempotente tak en niet ervoor. Een agent die al ingetrokken is heeft niets meer om in te trekken, en die aanroeper vertellen dat hij eerst zijn bestelling moet annuleren zou een onwaarheid zijn over een aanroep die toch niets gaat doen. Een grens hoort op de operatie die werkelijk iets verandert.

Het predicaat is kan dit nog geannuleerd worden, niet leeft dit

Er staat een lijst in het gedeelde pakket die CLOUD_AGENT_LIVE_STATUSES heet: de standen waarin een machine bestaat en geld kost. Hij heeft vier leden en het is precies de verkeerde lijst om hier te hergebruiken.

Het predicaat dat we gebruiken is cloudAgentActionAllowed("cancel", status), dat waar is voor zeven van de acht bestelstanden, alles behalve een bestelling die al geannuleerd is.

De drie standen in het verschil zijn de interessante, en failed is de scherpste. Een mislukte bestelling kost niets, dus hij leeft niet. Maar een mislukte bestelling is juist de bestelling die ver genoeg gekomen kan zijn om een machine aan te maken en daarna om te vallen, en dat is precies de reden dat annuleren vanuit die stand überhaupt mag. De facturatielijst hergebruiken had het ene geval met een verweesde machine onbewaakt gelaten, en het had er in een review volkomen redelijk uitgezien.

Twee lijsten die bijna hetzelfde betekenen lopen uit elkaar. De vraag is hier niet kost dit geld, maar is annuleren nog de route, en precies één functie in de codebase beantwoordt die.

Twee schrijfpaden die hem mogen overslaan

De bewaking die we schreven leidt elke plek in de paneelbron af die een agentrij intrekt, hard verwijdert of zacht verbergt, met een AST-wandeling over de applicatie- en bibliotheekboom, en eist per plek ofwel de grens ofwel een opgeschreven reden. Twee plekken dragen een reden: het annuleerpad zelf, dat de machine verwijdert, het adres vrijgeeft en pas dán de agent intrekt, en het sluiten van een account, dat elke agent van een organisatie intrekt en de Cloud Agent in dezelfde stroom apart afhandelt. Een van beide blokkeren zou een juiste uitgang dichtzetten in plaats van geld besparen.

De bewaking heeft de twee vangnetten die een bronscanner behoeden voor vacuüm groen: een ondergrens op het aantal schrijfplekken dat hij moet vinden, waarvan er drie bij naam genoemd zijn, en een test die faalt op een regel in de allowlist die wijst naar een bestand dat niets meer schrijft. Hij draait 4 tests en 11 beweringen.

Twee feiten op het scherm, niet één

De paneelkant had twee losse booleans nodig en we hadden er bijna één opgeleverd.

orderOpen is een feit over de bestelling: kan deze nog geannuleerd worden. canCancel is een feit over de kijker: mag deze persoon hem annuleren. Ze samenvouwen tot één mag ik hier iets doen zou betekenen dat een lid zonder annuleerrecht de intrekknop ziet, hem gebruikt, de machine weggooit en de rekening niet kan stoppen. De knoppen verdwijnen daarom op het bestelfeit, en de kaart blijft staan met de reden erin, want waarom kan ik deze niet intrekken is de vraag die iedereen stelt die de knop op zijn andere agents heeft zien staan.

Eén ding kwam mee omdat de wijziging het onwaar maakte: de deïnstallatie-instructies raadden aan de agent eerst in het paneel in te trekken. Dat is een knop die op deze pagina niet meer bestaat.

Wat we zouden opschrijven

  1. Een gelijkenis in de interface is geen gelijkenis in de gevolgen. Twee agents, één knop, en een van de twee kost geld nadat je hem indrukt.
  2. ON DELETE SET NULL kan juist zijn en je toch met een wees achterlaten. Het schema kan de vraag niet stellen.
  3. Bewakingen over wat een verwijdering vernietigt zien niet wat een verwijdering laat stranden.
  4. Tel de aanroeppunten voordat je kiest waar de controle komt. De onze waren er zeven, in drie bestanden, waarvan twee API-routes.
  5. Zet de grens op het keelpunt, niet op de knop. Een knop is één van de ingangen.
  6. Zet hem achter de idempotente tak. Een weigering op een aanroep die niets zou doen is een leugen.
  7. Een lijst hergebruiken die bijna hetzelfde betekent is een langzame storing. Kost geld en kan nog geannuleerd worden verschillen drie standen, en in dat verschil zit de schade.
  8. Een stand die niets kost kan toch iets achtergelaten hebben. Een mislukte inrichting is de gevaarlijke.
  9. Scheid het feit over het object van het feit over de kijker. Samengevoegd geven ze een beperkt lid een destructieve knop.
  10. Laat de bewaking de boom lezen. Een test per bekende ingang ziet de ingang niet die iemand volgende maand toevoegt.

Voor de duidelijkheid over wat dit is: het defect is tegen de ontwikkeldatabase gereproduceerd voordat er iets veranderde, de end-to-end-controle eindigde op 26 groen en is vooraf rood bewezen op 16 groen en 10 falend, de browsercontrole op 29 groen en rood bewezen op 18 en 11 met de bestelvlag geforceerd uit, negen talen op 178 groen, en de mergeverificatie liep zonder een faalregel. Onbewezen: er was geen echte klantmachine bij betrokken, en de facturatiekant is bekeken en niet uitgeoefend. Drie meetvallen kostten tijd en zijn het noemen waard: de gevarenzone woont achter een tab-parameter, dus een controle die hem weglaat meet nul knoppen en leest dat als succes; een backslash-escape binnen een template-literal die aan een pagina-evaluatie meegegeven wordt is geen escape; en een van de drie catalogusscripts maakte een onbreekbare spatie plat die de andere twee bewaarden.

Uit het platformwerk aan Provibr. Waarom een machine precies één keer verkocht kan worden staat in twee bestellingen, één machine, waarom een snelkoppeling een verwijzing is en nooit een toestemming staat in een snelkoppeling is een verwijzing, geen recht, en een statuswoord dat uit de verkeerde tabel geleend is staat in een scan die zijn eigen tick sloot.

Begin met één host en één agent

Maak een account aan, installeer de agent met één commando en koppel je eerste Proxmox-host. De proefperiode loopt 14 dagen en vraagt geen betaalgegevens.