958 lines
38 KiB
TypeScript
Executable File
958 lines
38 KiB
TypeScript
Executable File
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { cn } from "@/lib/utils";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Cpu,
|
|
CheckCircle2,
|
|
XCircle,
|
|
AlertTriangle,
|
|
FileText,
|
|
Terminal,
|
|
Info,
|
|
Settings,
|
|
Download,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
ExternalLink,
|
|
RefreshCw,
|
|
} from "lucide-react";
|
|
import {
|
|
fetchQCrowsImages,
|
|
KATA_REQUIRED_KERNEL_OPTIONS,
|
|
KATA_RECOMMENDED_KERNEL_OPTIONS,
|
|
HYPERVISOR_KERNEL_FORMAT,
|
|
VMM_MODULE_MATRIX,
|
|
COMPRESS_PRIORITY,
|
|
INITRD_REGEN_DEFAULTS,
|
|
type QCrowsImage,
|
|
type QCrowsKernelConfig,
|
|
type QCrowsBootParams,
|
|
type InitrdRegenConfig,
|
|
type InitrdRegenResult,
|
|
} from "@/lib/kata-mock";
|
|
import { regenInitrd } from "@/lib/backend/adapter";
|
|
|
|
export function KernelDetail() {
|
|
const [images, setImages] = useState<QCrowsImage[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [selectedImage, setSelectedImage] = useState<QCrowsImage | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchQCrowsImages().then((data) => {
|
|
setImages(data);
|
|
if (data.length > 0) setSelectedImage(data[0]);
|
|
setLoading(false);
|
|
});
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-pulse text-slate-400">Loading kernel images...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-slate-100 flex items-center gap-2">
|
|
<Cpu className="h-5 w-5 text-emerald-400" />
|
|
Kernel Management
|
|
</h2>
|
|
<p className="text-sm text-slate-400 mt-1">
|
|
Inspect guest kernels bundled in QCrows images — config options, boot parameters, and hypervisor compatibility
|
|
</p>
|
|
</div>
|
|
<Badge variant="outline" className="border-emerald-700 text-emerald-400 text-[10px]">
|
|
QCrows v0.2 — Kernel REQUIRED
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-12 gap-6">
|
|
{/* Image selector sidebar */}
|
|
<div className="col-span-3">
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader className="py-3 px-4">
|
|
<CardTitle className="text-xs font-medium text-slate-400 uppercase tracking-wider">
|
|
QCrows Images
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-2 pb-3">
|
|
<div className="space-y-1">
|
|
{images.map((img) => (
|
|
<button
|
|
key={img.id}
|
|
onClick={() => setSelectedImage(img)}
|
|
className={cn(
|
|
"w-full text-left rounded-md px-3 py-2.5 text-sm transition-colors",
|
|
selectedImage?.id === img.id
|
|
? "bg-slate-700/60 text-white"
|
|
: "text-slate-400 hover:text-slate-200 hover:bg-slate-800/60"
|
|
)}
|
|
>
|
|
<div className="font-medium truncate">{img.menu.label}</div>
|
|
<div className="text-[11px] text-slate-500 mt-0.5 flex items-center gap-2">
|
|
<span>{img.metadata.kernelVersion}</span>
|
|
<span className="text-slate-600">|</span>
|
|
<span>{img.metadata.kernelFormat}</span>
|
|
<span className="text-slate-600">|</span>
|
|
<span>{img.metadata.kernelSizeMB}MB</span>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Main kernel detail area */}
|
|
<div className="col-span-9 space-y-5">
|
|
{selectedImage && (
|
|
<KernelDetailView image={selectedImage} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function KernelDetailView({ image }: { image: QCrowsImage }) {
|
|
const meta = image.metadata;
|
|
const kConfig = image.kernelConfig;
|
|
const bootParams = image.bootParams;
|
|
const [regenOpen, setRegenOpen] = useState(false);
|
|
const [regenConfig, setRegenConfig] = useState<Partial<InitrdRegenConfig>>(INITRD_REGEN_DEFAULTS);
|
|
const [regening, setRegening] = useState(false);
|
|
const [regenResult, setRegenResult] = useState<InitrdRegenResult | null>(null);
|
|
|
|
const executeRegen = async () => {
|
|
setRegening(true);
|
|
setRegenResult(null);
|
|
try {
|
|
const result = await regenInitrd(image, regenConfig);
|
|
setRegenResult(result);
|
|
} catch {
|
|
setRegenResult({
|
|
success: false, output: "", sizeMB: 0, vmm: regenConfig.vmm || "qemu",
|
|
format: regenConfig.format || "cpio-gzip", initStyle: regenConfig.initStyle || "busybox",
|
|
kernelVersion: meta.kernelVersion, moduleCount: 0,
|
|
messages: [], errors: ["Initrd regeneration failed unexpectedly"],
|
|
});
|
|
} finally {
|
|
setRegening(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Kernel Overview Cards */}
|
|
<div className="grid grid-cols-4 gap-4">
|
|
<KernelStatCard
|
|
label="Kernel Version"
|
|
value={meta.kernelVersion}
|
|
icon={<Cpu className="h-4 w-4" />}
|
|
color="emerald"
|
|
/>
|
|
<KernelStatCard
|
|
label="Format"
|
|
value={meta.kernelFormat === "vmlinuz" ? "vmlinuz (bzImage)" : "vmlinux (ELF)"}
|
|
icon={<FileText className="h-4 w-4" />}
|
|
color="blue"
|
|
/>
|
|
<KernelStatCard
|
|
label="Size"
|
|
value={`${meta.kernelSizeMB} MB`}
|
|
icon={<Download className="h-4 w-4" />}
|
|
color="amber"
|
|
/>
|
|
<KernelStatCard
|
|
label="Config Valid"
|
|
value={meta.kernelConfigValid ? "Pass" : "Fail"}
|
|
icon={meta.kernelConfigValid ? <CheckCircle2 className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
|
|
color={meta.kernelConfigValid ? "emerald" : "red"}
|
|
/>
|
|
</div>
|
|
|
|
{/* Hypervisor Compatibility */}
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader className="py-3 px-4">
|
|
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
|
|
<Settings className="h-4 w-4 text-blue-400" />
|
|
Hypervisor Kernel Format Compatibility
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 pb-4">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{meta.hypervisors.map((hyp) => {
|
|
const requirement = HYPERVISOR_KERNEL_FORMAT[hyp];
|
|
const matches = requirement && requirement.format === meta.kernelFormat;
|
|
const archMatch = requirement && requirement.arches.includes(meta.arch);
|
|
return (
|
|
<div
|
|
key={hyp}
|
|
className={cn(
|
|
"flex items-center justify-between rounded-md border px-3 py-2",
|
|
matches && archMatch
|
|
? "border-emerald-700/50 bg-emerald-900/10"
|
|
: "border-amber-700/50 bg-amber-900/10"
|
|
)}
|
|
>
|
|
<div>
|
|
<div className="text-sm font-medium text-slate-200">{hyp}</div>
|
|
<div className="text-[11px] text-slate-400">
|
|
Requires: {requirement?.format || "vmlinux"} ({requirement?.arches.join(", ") || "any"})
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
{matches && archMatch ? (
|
|
<>
|
|
<CheckCircle2 className="h-4 w-4 text-emerald-400" />
|
|
<span className="text-[11px] text-emerald-400 font-medium">Compatible</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<AlertTriangle className="h-4 w-4 text-amber-400" />
|
|
<span className="text-[11px] text-amber-400 font-medium">Format mismatch</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Initrd Regen Action */}
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader className="py-3 px-4">
|
|
<CardTitle className="text-sm font-medium text-slate-200 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<RefreshCw className="h-4 w-4 text-violet-400" />
|
|
Initrd Regeneration
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => { setRegenOpen(true); setRegenResult(null); }}
|
|
className="bg-violet-700 hover:bg-violet-600 text-white text-xs h-7"
|
|
>
|
|
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
|
|
Regen Initrd
|
|
</Button>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 pb-4">
|
|
<div className="text-xs text-slate-400 space-y-1.5">
|
|
<p>
|
|
Rebuild the initrd component with environment-aware module selection. Unlike <code className="text-slate-300 bg-slate-800 px-1 rounded">dracut</code> or{" "}
|
|
<code className="text-slate-300 bg-slate-800 px-1 rounded">mkinitramfs</code>, this produces an initrd where kata-agent IS the final process
|
|
(no pivot_root) and includes ONLY the modules required by the detected VMM.
|
|
</p>
|
|
<div className="flex items-center gap-4 mt-2 text-slate-500">
|
|
<span>Auto-detects: VMM, compression, init style, kata-agent</span>
|
|
<span>|</span>
|
|
<span>Module matrix: {Object.keys(VMM_MODULE_MATRIX).length} VMMs</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Regen Dialog */}
|
|
{regenOpen && !regenResult && (
|
|
<div className="mt-4 rounded-lg border border-slate-700/50 bg-[#0a0f1a] p-4 space-y-4">
|
|
<div className="text-sm font-medium text-slate-200">Configure Initrd Regeneration</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{/* VMM Selection */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs text-slate-400">Target VMM</label>
|
|
<select
|
|
value={regenConfig.vmm || ""}
|
|
onChange={(e) => setRegenConfig((c) => ({ ...c, vmm: e.target.value as InitrdRegenConfig["vmm"] || undefined }))}
|
|
className="w-full rounded-md border border-slate-700 bg-[#111827] text-sm text-slate-200 px-3 py-1.5"
|
|
>
|
|
<option value="">Auto-detect</option>
|
|
<option value="qemu">QEMU (virtio-pci)</option>
|
|
<option value="cloud-hypervisor">Cloud Hypervisor (virtio-mmio)</option>
|
|
<option value="firecracker">Firecracker (virtio-mmio minimal)</option>
|
|
<option value="dragonball">Dragonball (vsock-direct)</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Compression */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs text-slate-400">Compression</label>
|
|
<select
|
|
value={regenConfig.format || ""}
|
|
onChange={(e) => setRegenConfig((c) => ({ ...c, format: e.target.value as InitrdRegenConfig["format"] || undefined }))}
|
|
className="w-full rounded-md border border-slate-700 bg-[#111827] text-sm text-slate-200 px-3 py-1.5"
|
|
>
|
|
<option value="">Auto-detect from kernel config</option>
|
|
{COMPRESS_PRIORITY.map((c) => (
|
|
<option key={c.algo} value={c.algo}>
|
|
{c.algo.replace("cpio-", "").toUpperCase()} ({c.bootSpeed})
|
|
</option>
|
|
))}
|
|
<option value="cramfs">CRAMFS (read-only, minimal)</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Init Style */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs text-slate-400">Init Style</label>
|
|
<select
|
|
value={regenConfig.initStyle || ""}
|
|
onChange={(e) => setRegenConfig((c) => ({ ...c, initStyle: e.target.value as InitrdRegenConfig["initStyle"] || undefined }))}
|
|
className="w-full rounded-md border border-slate-700 bg-[#111827] text-sm text-slate-200 px-3 py-1.5"
|
|
>
|
|
<option value="">Auto-detect from rootfs</option>
|
|
<option value="busybox">Busybox (kata-agent as PID 1)</option>
|
|
<option value="systemd">systemd (via kata-containers.target)</option>
|
|
<option value="openrc">OpenRC (via init script)</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* In-place toggle */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs text-slate-400">Output Mode</label>
|
|
<div className="flex items-center gap-3 mt-1">
|
|
<button
|
|
onClick={() => setRegenConfig((c) => ({ ...c, inPlace: true }))}
|
|
className={cn(
|
|
"px-3 py-1.5 rounded-md text-xs font-medium border transition-colors",
|
|
regenConfig.inPlace
|
|
? "border-violet-600 bg-violet-900/20 text-violet-300"
|
|
: "border-slate-700 bg-[#111827] text-slate-400 hover:border-slate-600"
|
|
)}
|
|
>
|
|
In-place (update bundle)
|
|
</button>
|
|
<button
|
|
onClick={() => setRegenConfig((c) => ({ ...c, inPlace: false }))}
|
|
className={cn(
|
|
"px-3 py-1.5 rounded-md text-xs font-medium border transition-colors",
|
|
!regenConfig.inPlace
|
|
? "border-violet-600 bg-violet-900/20 text-violet-300"
|
|
: "border-slate-700 bg-[#111827] text-slate-400 hover:border-slate-600"
|
|
)}
|
|
>
|
|
Separate file
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Module preview for selected VMM */}
|
|
{regenConfig.vmm && VMM_MODULE_MATRIX[regenConfig.vmm] && (
|
|
<div className="text-xs text-slate-400 bg-[#111827] rounded-md p-3">
|
|
<div className="font-medium text-slate-300 mb-1.5">
|
|
Modules for {regenConfig.vmm}:
|
|
</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{VMM_MODULE_MATRIX[regenConfig.vmm].required.map((m) => (
|
|
<Badge key={m} variant="outline" className="text-[9px] border-emerald-700 text-emerald-400 px-1.5 py-0">
|
|
{m}
|
|
</Badge>
|
|
))}
|
|
{VMM_MODULE_MATRIX[regenConfig.vmm].optional.map((m) => (
|
|
<Badge key={m} variant="outline" className="text-[9px] border-slate-600 text-slate-500 px-1.5 py-0">
|
|
{m}?
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-3 pt-2">
|
|
<Button
|
|
size="sm"
|
|
onClick={executeRegen}
|
|
disabled={regening}
|
|
className="bg-violet-700 hover:bg-violet-600 text-white text-xs h-8"
|
|
>
|
|
{regening ? (
|
|
<>
|
|
<RefreshCw className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
|
Regenerating...
|
|
</>
|
|
) : (
|
|
<>
|
|
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
|
|
Regenerate Initrd
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => setRegenOpen(false)}
|
|
className="text-xs h-8 text-slate-400"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Regen Result */}
|
|
{regenResult && (
|
|
<div className={cn(
|
|
"mt-4 rounded-lg border p-4 space-y-3",
|
|
regenResult.success
|
|
? "border-emerald-700/50 bg-emerald-900/10"
|
|
: "border-red-700/50 bg-red-900/10"
|
|
)}>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{regenResult.success ? (
|
|
<CheckCircle2 className="h-5 w-5 text-emerald-400" />
|
|
) : (
|
|
<XCircle className="h-5 w-5 text-red-400" />
|
|
)}
|
|
<span className={cn("text-sm font-medium", regenResult.success ? "text-emerald-300" : "text-red-300")}>
|
|
{regenResult.success ? "Initrd regenerated successfully" : "Initrd regeneration failed"}
|
|
</span>
|
|
</div>
|
|
<Button size="sm" variant="ghost" onClick={() => { setRegenResult(null); setRegenOpen(false); }} className="text-xs text-slate-400 h-6">
|
|
Close
|
|
</Button>
|
|
</div>
|
|
|
|
{regenResult.success && (
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div className="bg-[#0a0f1a] rounded-md p-2.5">
|
|
<div className="text-[10px] text-slate-500 uppercase">Output</div>
|
|
<div className="text-sm text-slate-200 font-mono mt-0.5">{regenResult.output}</div>
|
|
</div>
|
|
<div className="bg-[#0a0f1a] rounded-md p-2.5">
|
|
<div className="text-[10px] text-slate-500 uppercase">Size</div>
|
|
<div className="text-sm text-slate-200 mt-0.5">{regenResult.sizeMB} MB</div>
|
|
</div>
|
|
<div className="bg-[#0a0f1a] rounded-md p-2.5">
|
|
<div className="text-[10px] text-slate-500 uppercase">Modules</div>
|
|
<div className="text-sm text-slate-200 mt-0.5">{regenResult.moduleCount} included</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{regenResult.messages.length > 0 && (
|
|
<div className="bg-[#0a0f1a] rounded-md p-3 max-h-40 overflow-y-auto">
|
|
<div className="text-[10px] text-slate-500 uppercase mb-1.5">Pipeline Log</div>
|
|
{regenResult.messages.map((msg, i) => (
|
|
<div key={i} className="text-xs text-slate-400 font-mono py-0.5">{msg}</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{regenResult.errors.length > 0 && (
|
|
<div className="bg-red-950/30 rounded-md p-3">
|
|
<div className="text-[10px] text-red-400 uppercase mb-1.5">Errors</div>
|
|
{regenResult.errors.map((err, i) => (
|
|
<div key={i} className="text-xs text-red-300 font-mono py-0.5">{err}</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Tabs: Config Options / Boot Params / Build Info */}
|
|
<Tabs defaultValue="config" className="space-y-4">
|
|
<TabsList className="bg-[#111827] border border-slate-700/50">
|
|
<TabsTrigger value="config" className="data-[state=active]:bg-slate-700/60">
|
|
Kernel Config
|
|
</TabsTrigger>
|
|
<TabsTrigger value="boot-params" className="data-[state=active]:bg-slate-700/60">
|
|
Boot Parameters
|
|
</TabsTrigger>
|
|
<TabsTrigger value="build" className="data-[state=active]:bg-slate-700/60">
|
|
Build Info
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="config">
|
|
<KernelConfigPanel config={kConfig} kernelVersion={meta.kernelVersion} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="boot-params">
|
|
<BootParamsPanel bootParams={bootParams} kernelVersion={meta.kernelVersion} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="build">
|
|
<BuildInfoPanel image={image} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function KernelStatCard({
|
|
label,
|
|
value,
|
|
icon,
|
|
color,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
icon: React.ReactNode;
|
|
color: "emerald" | "blue" | "amber" | "red";
|
|
}) {
|
|
const colorMap = {
|
|
emerald: "text-emerald-400",
|
|
blue: "text-blue-400",
|
|
amber: "text-amber-400",
|
|
red: "text-red-400",
|
|
};
|
|
|
|
return (
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className={colorMap[color]}>{icon}</span>
|
|
<span className="text-[11px] text-slate-400 uppercase tracking-wider font-medium">{label}</span>
|
|
</div>
|
|
<div className="text-lg font-semibold text-slate-100">{value}</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function KernelConfigPanel({
|
|
config,
|
|
kernelVersion,
|
|
}: {
|
|
config?: QCrowsKernelConfig;
|
|
kernelVersion: string;
|
|
}) {
|
|
const [showRecommended, setShowRecommended] = useState(false);
|
|
|
|
if (!config) {
|
|
return (
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardContent className="p-6 text-center text-slate-400">
|
|
No kernel config data available for this image.
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const requiredPass = Object.values(config.requiredOptions).filter(Boolean).length;
|
|
const requiredTotal = Object.keys(config.requiredOptions).length;
|
|
const optionalPass = Object.values(config.optionalOptions).filter(Boolean).length;
|
|
const optionalTotal = Object.keys(config.optionalOptions).length;
|
|
const allRequiredPass = requiredPass === requiredTotal;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Summary cards */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardContent className="p-4">
|
|
<div className="text-[11px] text-slate-400 uppercase tracking-wider mb-2">Required Options</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className={cn("text-xl font-bold", allRequiredPass ? "text-emerald-400" : "text-red-400")}>
|
|
{requiredPass}/{requiredTotal}
|
|
</span>
|
|
{allRequiredPass ? (
|
|
<CheckCircle2 className="h-5 w-5 text-emerald-400" />
|
|
) : (
|
|
<XCircle className="h-5 w-5 text-red-400" />
|
|
)}
|
|
</div>
|
|
<Progress
|
|
value={(requiredPass / requiredTotal) * 100}
|
|
className={cn("mt-2 h-1.5", allRequiredPass ? "[&>div]:bg-emerald-500" : "[&>div]:bg-red-500")}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardContent className="p-4">
|
|
<div className="text-[11px] text-slate-400 uppercase tracking-wider mb-2">Recommended Options</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xl font-bold text-blue-400">{optionalPass}/{optionalTotal}</span>
|
|
</div>
|
|
<Progress
|
|
value={(optionalPass / optionalTotal) * 100}
|
|
className="mt-2 h-1.5 [&>div]:bg-blue-500"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardContent className="p-4">
|
|
<div className="text-[11px] text-slate-400 uppercase tracking-wider mb-2">Config Statistics</div>
|
|
<div className="text-sm space-y-1">
|
|
<div className="flex justify-between text-slate-300">
|
|
<span>Total options</span>
|
|
<span className="font-mono">{config.totalOptions.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between text-slate-300">
|
|
<span>Built-in</span>
|
|
<span className="font-mono text-emerald-400">{config.builtInCount.toLocaleString()}</span>
|
|
</div>
|
|
<div className="flex justify-between text-slate-300">
|
|
<span>Modules</span>
|
|
<span className="font-mono text-blue-400">{config.moduleCount.toLocaleString()}</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Required options detail */}
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader className="py-3 px-4">
|
|
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
|
|
<AlertTriangle className="h-4 w-4 text-amber-400" />
|
|
Required Kata Kernel Options
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 pb-4 space-y-2">
|
|
{Object.entries(config.requiredOptions).map(([opt, enabled]) => {
|
|
const desc = KATA_REQUIRED_KERNEL_OPTIONS[opt] || "";
|
|
return (
|
|
<div
|
|
key={opt}
|
|
className={cn(
|
|
"flex items-center justify-between rounded-md border px-3 py-2",
|
|
enabled
|
|
? "border-emerald-700/30 bg-emerald-900/5"
|
|
: "border-red-700/30 bg-red-900/5"
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{enabled ? (
|
|
<CheckCircle2 className="h-4 w-4 text-emerald-400 shrink-0" />
|
|
) : (
|
|
<XCircle className="h-4 w-4 text-red-400 shrink-0" />
|
|
)}
|
|
<code className="text-sm font-mono text-slate-200">{opt}</code>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-[11px] text-slate-400">{desc}</span>
|
|
<Badge
|
|
variant="outline"
|
|
className={cn(
|
|
"text-[9px] px-1.5 py-0",
|
|
enabled ? "border-emerald-600 text-emerald-400" : "border-red-600 text-red-400"
|
|
)}
|
|
>
|
|
{enabled ? "y" : "MISSING"}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Recommended options (collapsible) */}
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader
|
|
className="py-3 px-4 cursor-pointer"
|
|
onClick={() => setShowRecommended(!showRecommended)}
|
|
>
|
|
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
|
|
<Info className="h-4 w-4 text-blue-400" />
|
|
Recommended Kata Kernel Options
|
|
<Badge variant="outline" className="border-blue-700 text-blue-400 text-[9px] px-1.5 py-0 ml-auto mr-2">
|
|
{optionalPass}/{optionalTotal}
|
|
</Badge>
|
|
{showRecommended ? (
|
|
<ChevronDown className="h-4 w-4 text-slate-400" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4 text-slate-400" />
|
|
)}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
{showRecommended && (
|
|
<CardContent className="px-4 pb-4 space-y-2">
|
|
{Object.entries(config.optionalOptions).map(([opt, enabled]) => {
|
|
const desc = KATA_RECOMMENDED_KERNEL_OPTIONS[opt] || "";
|
|
return (
|
|
<div
|
|
key={opt}
|
|
className={cn(
|
|
"flex items-center justify-between rounded-md border px-3 py-2",
|
|
enabled
|
|
? "border-blue-700/20 bg-blue-900/5"
|
|
: "border-slate-700/30 bg-slate-900/20"
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
{enabled ? (
|
|
<CheckCircle2 className="h-4 w-4 text-blue-400 shrink-0" />
|
|
) : (
|
|
<XCircle className="h-4 w-4 text-slate-500 shrink-0" />
|
|
)}
|
|
<code className="text-sm font-mono text-slate-200">{opt}</code>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-[11px] text-slate-400">{desc}</span>
|
|
<Badge
|
|
variant="outline"
|
|
className={cn(
|
|
"text-[9px] px-1.5 py-0",
|
|
enabled ? "border-blue-600 text-blue-400" : "border-slate-600 text-slate-500"
|
|
)}
|
|
>
|
|
{enabled ? "y" : "n/m"}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BootParamsPanel({
|
|
bootParams,
|
|
kernelVersion,
|
|
}: {
|
|
bootParams?: QCrowsBootParams;
|
|
kernelVersion: string;
|
|
}) {
|
|
if (!bootParams) {
|
|
return (
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardContent className="p-6 text-center text-slate-400">
|
|
No boot parameters data available for this image.
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
const dangerousParams = [
|
|
"module.sig_enforce=0",
|
|
"nokaslr",
|
|
"nopti",
|
|
"nosmap",
|
|
"nosmep",
|
|
];
|
|
|
|
const warnings = bootParams.params.filter((p) =>
|
|
dangerousParams.some((d) => p.includes(d.replace("=", "").replace("=0", "")))
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader className="py-3 px-4">
|
|
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
|
|
<Terminal className="h-4 w-4 text-emerald-400" />
|
|
Kernel Command Line
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 pb-4">
|
|
<div className="rounded-md bg-[#0a0f1a] border border-slate-700/50 p-4 font-mono text-sm text-slate-200 break-all">
|
|
<span className="text-slate-500">BOOT_IMAGE=/kernel/vmlinuz</span>{" "}
|
|
{bootParams.params.map((param, i) => (
|
|
<span key={i}>
|
|
{i > 0 && " "}
|
|
<span
|
|
className={cn(
|
|
dangerousParams.some((d) => param.includes(d.split("=")[0]))
|
|
? "text-amber-400 underline decoration-amber-600 decoration-wavy"
|
|
: "text-emerald-300"
|
|
)}
|
|
>
|
|
{param}
|
|
</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="mt-3 flex items-center gap-2">
|
|
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[10px]">
|
|
Source: {bootParams.source}
|
|
</Badge>
|
|
<Badge variant="outline" className="border-slate-600 text-slate-400 text-[10px]">
|
|
{bootParams.params.length} parameters
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Parameter breakdown */}
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader className="py-3 px-4">
|
|
<CardTitle className="text-sm font-medium text-slate-200">Parameter Breakdown</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 pb-4 space-y-2">
|
|
{bootParams.params.map((param, i) => {
|
|
const isDangerous = dangerousParams.some((d) => param.includes(d.split("=")[0]));
|
|
const [key, ...valParts] = param.split("=");
|
|
const val = valParts.join("=");
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={cn(
|
|
"flex items-center gap-3 rounded-md border px-3 py-2",
|
|
isDangerous
|
|
? "border-amber-700/30 bg-amber-900/5"
|
|
: "border-slate-700/30"
|
|
)}
|
|
>
|
|
{isDangerous ? (
|
|
<AlertTriangle className="h-4 w-4 text-amber-400 shrink-0" />
|
|
) : (
|
|
<CheckCircle2 className="h-4 w-4 text-emerald-400 shrink-0" />
|
|
)}
|
|
<code className="text-sm font-mono text-slate-200">{key}</code>
|
|
{val && (
|
|
<>
|
|
<span className="text-slate-500">=</span>
|
|
<code className="text-sm font-mono text-blue-300">{val}</code>
|
|
</>
|
|
)}
|
|
{isDangerous && (
|
|
<Badge variant="outline" className="border-amber-600 text-amber-400 text-[9px] px-1.5 py-0 ml-auto">
|
|
Security concern
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{warnings.length > 0 && (
|
|
<Card className="bg-[#111827] border-amber-700/30">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start gap-3">
|
|
<AlertTriangle className="h-5 w-5 text-amber-400 shrink-0 mt-0.5" />
|
|
<div>
|
|
<div className="text-sm font-medium text-amber-300">Security Warnings</div>
|
|
<div className="text-sm text-slate-400 mt-1">
|
|
The following parameters may reduce security isolation in the guest VM:
|
|
</div>
|
|
<ul className="mt-2 space-y-1">
|
|
{warnings.map((w, i) => (
|
|
<li key={i} className="text-sm text-amber-300 font-mono">{w}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BuildInfoPanel({ image }: { image: QCrowsImage }) {
|
|
const build = image.build;
|
|
const meta = image.metadata;
|
|
|
|
if (!build) {
|
|
return (
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardContent className="p-6 text-center text-slate-400">
|
|
No build information available for this image.
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader className="py-3 px-4">
|
|
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
|
|
<Info className="h-4 w-4 text-blue-400" />
|
|
Kernel Build Provenance
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 pb-4">
|
|
<div className="grid grid-cols-2 gap-x-8 gap-y-3">
|
|
<BuildInfoRow label="Build System" value={build.system} />
|
|
<BuildInfoRow label="Build ID" value={build.buildId || "N/A"} mono />
|
|
<BuildInfoRow label="Host Arch" value={build.hostArch} />
|
|
<BuildInfoRow label="Target Arch" value={build.targetArch} />
|
|
<BuildInfoRow label="Kernel Compiler" value={build.kernelCompiler || "N/A"} />
|
|
<BuildInfoRow label="Kernel Defconfig" value={build.kernelDefconfig || "N/A"} mono />
|
|
<BuildInfoRow label="Reproducible" value={build.reproducible ? "Yes" : "No"} />
|
|
<BuildInfoRow label="Built At" value={build.timestamp} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Kernel file paths after import */}
|
|
<Card className="bg-[#111827] border-slate-700/50">
|
|
<CardHeader className="py-3 px-4">
|
|
<CardTitle className="text-sm font-medium text-slate-200 flex items-center gap-2">
|
|
<ExternalLink className="h-4 w-4 text-slate-400" />
|
|
Import Paths (after registration)
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="px-4 pb-4 space-y-2">
|
|
<PathRow
|
|
label="Kernel binary"
|
|
path={`/usr/share/kata-containers/vmlinuz-${meta.kernelVersion}.container`}
|
|
exists={meta.kernelIncluded}
|
|
/>
|
|
<PathRow
|
|
label="Kernel config"
|
|
path={`/usr/share/kata-containers/config-${meta.kernelVersion}.container`}
|
|
exists={meta.kernelConfigValid}
|
|
/>
|
|
{meta.initrdIncluded && (
|
|
<PathRow
|
|
label="Initrd"
|
|
path={`/usr/share/kata-containers/kata-${meta.name}-initrd.img`}
|
|
exists={true}
|
|
/>
|
|
)}
|
|
<PathRow
|
|
label="Rootfs"
|
|
path={`/usr/share/kata-containers/kata-${meta.name}-rootfs${meta.rootfsType === "tar-gzip" ? ".tar.gz" : meta.rootfsType === "tar-xz" ? ".tar.xz" : ".tar.zst"}`}
|
|
exists={true}
|
|
/>
|
|
{meta.bootParamsIncluded && (
|
|
<PathRow
|
|
label="Boot params"
|
|
path="(appended to kernel_params in configuration.toml)"
|
|
exists={true}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BuildInfoRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
|
return (
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-slate-400">{label}</span>
|
|
<span className={cn("text-sm text-slate-200", mono && "font-mono")}>{value}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PathRow({ label, path, exists }: { label: string; path: string; exists: boolean }) {
|
|
return (
|
|
<div className="flex items-center gap-3 text-sm">
|
|
{exists ? (
|
|
<CheckCircle2 className="h-4 w-4 text-emerald-400 shrink-0" />
|
|
) : (
|
|
<XCircle className="h-4 w-4 text-red-400 shrink-0" />
|
|
)}
|
|
<span className="text-slate-400 w-28 shrink-0">{label}</span>
|
|
<code className="text-slate-200 font-mono text-xs break-all">{path}</code>
|
|
</div>
|
|
);
|
|
}
|