Blog ·
Eine Grenze gehört in den Kern
Einen Cloud Agent zu widerrufen löste ihn von seiner Bestellung und ließ eine laufende Proxmox-Maschine zurück, die niemand erreichte und alle bezahlten.
Von Provibr
Ein Cloud Agent ist die Ausführung unseres Agenten, bei der wir die Maschine betreiben. Der Kunde bestellt einen, wir klonen eine Proxmox-Vorlage, installieren den Agenten darauf und stellen ihn monatlich in Rechnung. Vom Panel aus sieht er aus wie jeder andere Agent, und diese Ähnlichkeit ist die ganze Geschichte dieser Runde.
Jeder andere Agent gehört dem Kunden. Ihn zu widerrufen ist ein sauberer, sicherer Vorgang: Die Plattform schickt eine Widerrufsmitteilung, der Agent löscht seine Identität, sein Zertifikat und seinen Zugangsdatentresor, und danach lässt sich die Zeile löschen. Nirgends bleibt etwas laufen, denn das, was lief, war nie unseres.
Bei einem Cloud Agent stimmt das nicht, und nichts hielt es auf.
Was wir gemessen haben, bevor wir etwas geändert haben
Auf der Entwicklungsdatenbank, mit einer echten Bestellung hinter einem echten Agenten, haben wir den Entfernungspfad genau so aufgerufen, wie es die Schaltfläche im Panel tut.
Er meldete mode: "hard". Die Agentenzeile war weg. Die Bestellzeile stand noch da, weiterhin active, weiterhin mit vmid 5998, und ihr agent_id war still zu NULL geworden, denn der Fremdschlüssel von der Bestellung auf den Agenten ist ON DELETE SET NULL.
Es gab also eine virtuelle Maschine auf einem Hypervisor, laufend, mit einem Agenten darauf, der seine eigene Identität gelöscht hatte und sich deshalb nie wieder verbinden konnte, hängend an einer Bestellung, die weiterhin eine monatliche Belastung erzeugte, und nirgends eine Zeile, die von dem einen auf das andere zeigte. Nicht erreichbar, vom Panel aus nicht anzuhalten und nicht kostenlos.
Die Klausel, die das erzeugt hat, ist richtig. Den Verweis auf NULL zu setzen ist das richtige Verhalten für eine Spalte, die sagt, welcher Agent hier gerade installiert ist; eine Kaskade hätte die Bestellung und den Abrechnungssatz mitgelöscht. Der Defekt ist, dass niemand die Frage stellte, die das Schema nicht stellen kann.
Zwei bestehende Wachen, die beide in die falsche Richtung schauen
Einen Agenten zu löschen hatte bereits zwei Ablehnungen, und es lohnt sich, genau zu sein, warum keine davon gefeuert hat.
Die erste lehnt es ab, einen Agenten zu löschen, der gerade angemeldet ist: erst widerrufen, damit er sich selbst aufräumt. Als die Zeile gelöscht wurde, war der Agent im selben Aufruf soeben widerrufen worden, war also nicht angemeldet, und die Prüfung war erfüllt.
Die zweite lehnt es ab, einen Agenten zu löschen, an dem noch Integrationen oder Server hängen, denn diese Fremdschlüssel kaskadieren, und ein Klick würde den ganzen Bestand mitnehmen. Ein Cloud Agent hat weder das eine noch das andere: Er ist eine frische Maschine, die existiert, um den Agenten zu betreiben, also war die Prüfung erfüllt, weil es nichts zu zählen gab.
Beide Wachen handelten davon, was das Löschen zerstören würde. Das, was das Löschen überlebt und gerade deshalb Geld kostet, weil es überlebt, lag außerhalb der Gestalt dieser Frage.
Die Grenze gehört in den Kern, nicht auf die Schaltfläche
Die naheliegende Reparatur ist eine Prüfung in der Server-Action hinter der Schaltfläche. Sie ist auch falsch, und der Grund ist eine Lücke, in die diese Codebasis schon einmal gelaufen ist, mit einem mitgelieferten Anmelde-Endpunkt, der jede serverseitige Kontrolle umging.
Wir haben die Aufrufstellen gezählt. Außerhalb der beiden Kernfunktionen gibt es sieben Aufrufe von Widerrufen, Löschen oder dem kombinierten Entfernen, verteilt auf drei Dateien: drei Server-Actions auf der Agentenseite, die Sammelaktion und zwei v1-API-Routen. Eine Prüfung in der Panel-Action hätte die API-Routen offen gelassen, und eine API-Route ist genau das, was ein Kundenskript benutzt.
Die Grenze sitzt deshalb in revokeAgentCore() und deleteAgentCore() selbst, im Nadelöhr, durch das alle sieben Aufrufe gehen. Eine Funktion, zweimal gefragt.
Sie sitzt hinter dem idempotenten Zweig und nicht davor. Ein Agent, der bereits widerrufen ist, hat nichts mehr zu widerrufen, und diesem Aufrufer zu sagen, er solle zuerst seine Bestellung kündigen, wäre eine Unwahrheit über einen Aufruf, der ohnehin nichts tun wird. Eine Grenze gehört auf den Vorgang, der tatsächlich etwas ändert.
Das Prädikat lautet lässt sich das noch kündigen, nicht lebt das noch
Im gemeinsamen Paket gibt es eine Liste namens CLOUD_AGENT_LIVE_STATUSES: die Zustände, in denen eine Maschine existiert und Geld kostet. Sie hat vier Mitglieder, und sie ist genau die falsche Liste zur Wiederverwendung an dieser Stelle.
Das Prädikat, das wir benutzen, ist cloudAgentActionAllowed("cancel", status), und das ist für sieben der acht Bestellzustände wahr, für alles außer einer bereits gekündigten Bestellung.
Die drei Zustände in der Differenz sind die interessanten, und failed ist der schärfste. Eine fehlgeschlagene Bestellung kostet nichts, ist also nicht lebendig. Aber eine fehlgeschlagene Bestellung ist genau diejenige, die es weit genug geschafft haben kann, um eine Maschine anzulegen, und danach umgefallen ist, weshalb das Kündigen aus diesem Zustand überhaupt erlaubt ist. Die Abrechnungsliste wiederzuverwenden hätte den einen Fall mit einer verwaisten Maschine ungeschützt gelassen, und es hätte im Review vollkommen vernünftig ausgesehen.
Zwei Listen, die fast dasselbe bedeuten, driften auseinander. Die Frage hier lautet nicht kostet das Geld, sie lautet ist Kündigen weiterhin der Weg, und genau eine Funktion in der Codebasis beantwortet diese.
Zwei Schreibpfade, die sie überspringen dürfen
Die Wache, die wir geschrieben haben, leitet jede Stelle in der Panel-Quelle ab, die eine Agentenzeile widerruft, hart löscht oder weich verbirgt, über einen Syntaxbaumdurchlauf über den Anwendungs- und den Bibliotheksbaum, und verlangt je Stelle entweder die Grenze oder eine aufgeschriebene Begründung. Zwei Stellen tragen eine Begründung: der Kündigungspfad selbst, der die Maschine entfernt, die Adresse freigibt und erst danach den Agenten widerruft, und die Kontoschließung, die jeden Agenten einer Organisation widerruft und den Cloud Agent im selben Ablauf gesondert behandelt. Eine der beiden zu blockieren hieße, einen richtigen Ausgang zu schließen, statt Geld zu sparen.
Die Wache hat die zwei Siebe, die einen Quelltextscanner davor bewahren, leer grün zu sein: eine Untergrenze dafür, wie viele Schreibstellen sie finden muss, drei davon namentlich, und einen Test, der bei einem Eintrag der Ausnahmeliste scheitert, der auf eine Datei zeigt, die nichts mehr schreibt. Sie läuft mit 4 Tests und 11 Zusicherungen.
Zwei Tatsachen auf dem Bildschirm, nicht eine
Die Panel-Seite brauchte zwei getrennte Wahrheitswerte, und wir hätten beinahe einen ausgeliefert.
orderOpen ist eine Tatsache über die Bestellung: Lässt sich das noch kündigen. canCancel ist eine Tatsache über den Betrachter: Darf diese Person es kündigen. Beides zu einem einzigen hier darf gehandelt werden zusammenzuziehen hieße, dass ein Mitglied ohne das Kündigungsrecht die Widerrufsschaltfläche sieht, sie benutzt, die Maschine wegwirft und die Rechnung nicht stoppen kann. Die Schaltflächen verschwinden deshalb an der Tatsache über die Bestellung, und die Karte bleibt stehen, mit der Begründung darin, denn warum kann ich diesen nicht widerrufen ist die Frage, die jeder stellt, der die Schaltfläche bei seinen anderen Agenten gesehen hat.
Eine Sache kam mit, weil die Änderung sie unwahr machte: Die Deinstallationsanleitung riet dazu, den Agenten zuerst im Panel zu widerrufen. Das ist eine Schaltfläche, die es auf dieser Seite nicht mehr gibt.
Was wir aufschreiben würden
- Eine Ähnlichkeit in der Oberfläche ist keine Ähnlichkeit in den Folgen. Zwei Agenten, eine Schaltfläche, einer davon kostet Geld, nachdem Sie sie gedrückt haben.
ON DELETE SET NULLkann richtig sein und Ihnen trotzdem eine Waise hinterlassen. Das Schema kann die Frage nicht stellen.- Wachen darüber, was ein Löschen zerstört, sehen nicht, was ein Löschen strandet.
- Zählen Sie die Aufrufstellen, bevor Sie entscheiden, wohin die Prüfung kommt. Unsere waren sieben, in drei Dateien, zwei davon API-Routen.
- Setzen Sie die Grenze an das Nadelöhr, nicht auf die Schaltfläche. Eine Schaltfläche ist einer der Eingänge.
- Setzen Sie sie hinter den idempotenten Zweig. Eine Ablehnung bei einem Aufruf, der nichts täte, ist eine Lüge.
- Eine Liste wiederzuverwenden, die fast dasselbe bedeutet, ist ein langsames Versagen. Kostet Geld und lässt sich noch kündigen unterscheiden sich um drei Zustände, und in der Differenz liegt der Schaden.
- Ein Zustand, der nichts kostet, kann trotzdem etwas hinterlassen haben. Eine fehlgeschlagene Bereitstellung ist die gefährliche.
- Trennen Sie die Tatsache über das Objekt von der Tatsache über den Betrachter. Zusammengelegt geben sie einem eingeschränkten Mitglied eine zerstörerische Schaltfläche.
- Lassen Sie die Wache den Baum lesen. Ein Test je bekanntem Eingang sieht den Eingang nicht, den jemand nächsten Monat hinzufügt.
Zur Klarheit: Der Defekt wurde vor jeder Änderung gegen die Entwicklungsdatenbank reproduziert, die Ende-zu-Ende-Prüfung endete bei 26 grün und wurde zuvor rot bewiesen bei 16 grün und 10 Fehlschlägen, die Browserprüfung bei 29 grün und rot bewiesen bei 18 und 11 mit abgeschaltetem Bestellkennzeichen, neun Sprachen bei 178 grün, und die Merge-Prüfung lief ohne Fehlerzeile durch. Nicht bewiesen: Es war keine echte Kundenmaschine beteiligt, und die Abrechnungsseite wurde begutachtet statt ausgeübt. Drei Messfallen haben Zeit gekostet und verdienen es, benannt zu werden: Die Gefahrenzone liegt hinter einem Tab-Parameter, eine Prüfung, die ihn weglässt, misst also null Schaltflächen und liest das als Erfolg; eine Backslash-Maskierung innerhalb eines Template-Literals, das an eine Seitenauswertung übergeben wird, ist keine Maskierung; und eines von drei Katalogskripten machte ein geschütztes Leerzeichen platt, das die anderen beiden erhielten.
Aus der Plattformarbeit an Provibr. Warum eine Maschine genau einmal verkauft werden kann, steht in Zwei Bestellungen, eine Maschine, warum eine Verknüpfung ein Verweis und nie eine Berechtigung ist, steht in Eine Verknüpfung ist ein Verweis, kein Recht, und ein Statuswort aus der falschen Tabelle steht in Ein Scan, der seinen eigenen Takt schloss.