Some checks failed
PR Review Automation / review (pull_request) Has been cancelled
Test / frontend-typecheck (pull_request) Successful in 1m37s
Test / frontend-tests (pull_request) Successful in 1m39s
Test / rust-fmt-check (pull_request) Successful in 10m45s
Test / rust-clippy (pull_request) Successful in 12m27s
Test / rust-tests (pull_request) Successful in 14m43s
Credential error persists: switch all 40 kubectl invocations from using KUBECONFIG env var to the explicit --kubeconfig CLI flag. The flag has higher precedence in kubectl's lookup order and is unambiguous regardless of any inherited KUBECONFIG env var in the parent process environment. Also adds test_kubectl_connection Tauri command (runs kubectl cluster-info with the stored kubeconfig) and a Test button in Settings → Kubeconfig so the exact kubectl output — context name, exit code, full stdout/stderr — is visible without needing to inspect tracing logs. This output will reveal whether the issue is expired certs, a missing exec-auth plugin, wrong context, or something else entirely.
290 lines
9.8 KiB
TypeScript
290 lines
9.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Upload, Check, Trash2, FileCode, FlaskConical } from 'lucide-react';
|
|
import { Button, Card, CardHeader, CardTitle, CardContent, Badge } from '@/components/ui';
|
|
import {
|
|
uploadKubeconfigCmd,
|
|
listKubeconfigsCmd,
|
|
activateKubeconfigCmd,
|
|
deleteKubeconfigCmd,
|
|
connectClusterFromKubeconfigCmd,
|
|
testKubectlConnectionCmd,
|
|
type KubeconfigInfo,
|
|
} from '@/lib/tauriCommands';
|
|
|
|
export default function KubeconfigManager() {
|
|
const [configs, setConfigs] = useState<KubeconfigInfo[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [uploadContent, setUploadContent] = useState('');
|
|
const [uploadName, setUploadName] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [testResult, setTestResult] = useState<{ id: string; output: string } | null>(null);
|
|
const [testingId, setTestingId] = useState<string | null>(null);
|
|
|
|
const loadConfigs = async () => {
|
|
try {
|
|
const data = await listKubeconfigsCmd();
|
|
setConfigs(data);
|
|
} catch (err) {
|
|
setError(String(err));
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadConfigs();
|
|
}, []);
|
|
|
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = async (event) => {
|
|
const content = event.target?.result as string;
|
|
setUploadContent(content);
|
|
setUploadName(file.name.replace(/\.(yaml|yml)$/, ''));
|
|
};
|
|
reader.readAsText(file);
|
|
};
|
|
|
|
const handleUpload = async () => {
|
|
if (!uploadContent || !uploadName) {
|
|
setError('Please select a file and provide a name');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setError('');
|
|
try {
|
|
await uploadKubeconfigCmd(uploadName, uploadContent);
|
|
setUploadContent('');
|
|
setUploadName('');
|
|
await loadConfigs();
|
|
} catch (err) {
|
|
setError(String(err));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleActivate = async (id: string) => {
|
|
setIsLoading(true);
|
|
setError('');
|
|
try {
|
|
await activateKubeconfigCmd(id);
|
|
await loadConfigs();
|
|
} catch (err) {
|
|
setError(String(err));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
if (!confirm('Are you sure you want to delete this kubeconfig?')) return;
|
|
|
|
setIsLoading(true);
|
|
setError('');
|
|
try {
|
|
await deleteKubeconfigCmd(id);
|
|
await loadConfigs();
|
|
} catch (err) {
|
|
setError(String(err));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleTestConnection = async (id: string) => {
|
|
setTestingId(id);
|
|
setTestResult(null);
|
|
setError('');
|
|
try {
|
|
// Ensure the cluster is loaded into the session first
|
|
await connectClusterFromKubeconfigCmd(id).catch(() => {});
|
|
const output = await testKubectlConnectionCmd(id);
|
|
setTestResult({ id, output });
|
|
} catch (err) {
|
|
setTestResult({ id, output: String(err) });
|
|
} finally {
|
|
setTestingId(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold mb-2">Kubeconfig Manager</h1>
|
|
<p className="text-muted-foreground">
|
|
Upload and manage multiple Kubernetes cluster configurations for kubectl commands
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-800">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Upload Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Upload className="h-5 w-5" />
|
|
Upload Kubeconfig
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">Select File</label>
|
|
<input
|
|
type="file"
|
|
accept=".yaml,.yml"
|
|
onChange={handleFileUpload}
|
|
className="block w-full text-sm text-foreground file:mr-4 file:py-2 file:px-4 file:rounded file:border file:border-input file:text-sm file:font-semibold file:bg-secondary file:text-secondary-foreground hover:file:bg-secondary/80 cursor-pointer"
|
|
/>
|
|
</div>
|
|
|
|
{uploadContent && (
|
|
<>
|
|
<div>
|
|
<label htmlFor="config-name" className="block text-sm font-medium mb-2">
|
|
Configuration Name
|
|
</label>
|
|
<input
|
|
id="config-name"
|
|
type="text"
|
|
value={uploadName}
|
|
onChange={(e) => setUploadName(e.target.value)}
|
|
placeholder="e.g., production-cluster"
|
|
className="w-full px-3 py-2 border rounded-md"
|
|
/>
|
|
</div>
|
|
|
|
<div className="rounded-lg bg-slate-950 p-4 font-mono text-xs text-slate-400 max-h-60 overflow-y-auto">
|
|
<pre>{uploadContent.substring(0, 500)}...</pre>
|
|
</div>
|
|
|
|
<Button onClick={handleUpload} disabled={isLoading} className="w-full">
|
|
Upload Kubeconfig
|
|
</Button>
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Configs List */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<FileCode className="h-5 w-5" />
|
|
Configured Clusters ({configs.length})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{configs.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground text-center py-8">
|
|
No kubeconfig files uploaded yet
|
|
</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{configs.map((config) => (
|
|
<div
|
|
key={config.id}
|
|
className={`p-4 rounded-lg border ${
|
|
config.is_active
|
|
? 'border-primary bg-primary/5'
|
|
: 'border-border'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="space-y-1 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold">{config.name}</h3>
|
|
{config.is_active && (
|
|
<Badge variant="default" className="bg-green-600">
|
|
<Check className="h-3 w-3 mr-1" />
|
|
Active
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="text-sm text-muted-foreground space-y-1">
|
|
<div>
|
|
<span className="font-medium">Context:</span> {config.context}
|
|
</div>
|
|
{config.cluster_url && (
|
|
<div>
|
|
<span className="font-medium">Cluster:</span> {config.cluster_url}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleTestConnection(config.id)}
|
|
disabled={testingId === config.id}
|
|
title="Test kubectl connection"
|
|
>
|
|
<FlaskConical className="h-4 w-4 mr-1" />
|
|
{testingId === config.id ? 'Testing…' : 'Test'}
|
|
</Button>
|
|
{!config.is_active && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleActivate(config.id)}
|
|
disabled={isLoading}
|
|
>
|
|
Activate
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDelete(config.id)}
|
|
disabled={isLoading}
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Test result for this config */}
|
|
{testResult?.id === config.id && (
|
|
<div className="mt-3 rounded-md bg-slate-950 p-3 font-mono text-xs text-slate-300 overflow-x-auto max-h-64 overflow-y-auto">
|
|
<pre>{testResult.output}</pre>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Info Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>About Kubeconfig Files</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
|
<p>
|
|
Kubeconfig files contain authentication credentials and cluster connection details for
|
|
kubectl commands.
|
|
</p>
|
|
<ul className="list-disc list-inside space-y-1 ml-2">
|
|
<li>Upload your cluster's kubeconfig file (usually ~/.kube/config)</li>
|
|
<li>Multiple clusters can be configured and switched between</li>
|
|
<li>The active configuration is used for kubectl commands</li>
|
|
<li>All kubeconfig files are encrypted using AES-256-GCM</li>
|
|
<li>Use the <strong>Test</strong> button to diagnose connection issues</li>
|
|
</ul>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|