feat(deploymentkey): geführte Rotation des Deployment-Keys (#031)
Bisher ließ sich der Instanz-Key nur von Hand auf jedem Host tauschen. Die Rotation läuft jetzt in fünf Schritten: Nachfolger erzeugen, per Dry-Run neben den alten verteilen, Anmeldung je Host prüfen, umstellen, alten Key aus allen Blöcken entfernen und seinen Private Key verwerfen. Der Nachfolger steht während der Rotation in Block und Pflichtliste, damit kein Host ohne funktionierenden Key dasteht. Umgestellt wird nur nach erfolgreicher Prüfung oder bewusstem Übergehen eines Hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { Repeat } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { umstellenAktion } from "@/server/actions/deploymentkey";
|
||||
|
||||
/**
|
||||
* Schritt 4 der Rotation (#031). Hosts, auf denen die Pruefung scheiterte, lassen sich bewusst
|
||||
* uebergehen – sshkv kommt danach nicht mehr auf sie und sie brauchen einen neuen Bootstrap.
|
||||
*/
|
||||
export function Umstellen({
|
||||
fehlgeschlagen,
|
||||
}: {
|
||||
fehlgeschlagen: readonly { hostId: string; hostName: string }[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [uebergehen, setUebergehen] = useState<string[]>([]);
|
||||
const [fehler, setFehler] = useState<string>();
|
||||
const [laeuft, starten] = useTransition();
|
||||
const bereit = fehlgeschlagen.every((h) => uebergehen.includes(h.hostId));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{fehlgeschlagen.length > 0 && (
|
||||
<fieldset className="flex flex-col gap-2">
|
||||
<legend className="text-danger mb-2 text-[13.5px]">
|
||||
Auf diesen Hosts hat der neue Key nicht funktioniert. Übergangene Hosts erreicht sshkv nach dem
|
||||
Umstellen nicht mehr – sie brauchen einen neuen Bootstrap.
|
||||
</legend>
|
||||
{fehlgeschlagen.map((h) => (
|
||||
<label key={h.hostId} className="flex min-h-11 items-center gap-3 text-[14px]">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-accent size-4"
|
||||
checked={uebergehen.includes(h.hostId)}
|
||||
onChange={(e) =>
|
||||
setUebergehen((alt) =>
|
||||
e.target.checked ? [...alt, h.hostId] : alt.filter((id) => id !== h.hostId),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
<span className="font-mono">{h.hostName}</span> übergehen
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
variante="primaer"
|
||||
icon={<Repeat className="size-4" />}
|
||||
disabled={!bereit || laeuft}
|
||||
onClick={() =>
|
||||
starten(async () => {
|
||||
setFehler(undefined);
|
||||
const antwort = await umstellenAktion(uebergehen);
|
||||
if (antwort.fehler) setFehler(antwort.fehler);
|
||||
else router.refresh();
|
||||
})
|
||||
}
|
||||
>
|
||||
Auf neuen Key umstellen
|
||||
</Button>
|
||||
{fehler && (
|
||||
<p role="alert" className="text-danger text-[13.5px]">
|
||||
{fehler}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user