From 48292e959e96b4b1e98703ea921029f61de4b5fe Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Sun, 7 Jun 2026 20:31:50 -0500 Subject: [PATCH] fix(kube): switch to --kubeconfig flag; add Test Connection diagnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src-tauri/src/commands/kube.rs | 175 +++++++++++++++++------ src-tauri/src/lib.rs | 1 + src/lib/tauriCommands.ts | 4 + src/pages/Settings/KubeconfigManager.tsx | 40 +++++- 4 files changed, 179 insertions(+), 41 deletions(-) diff --git a/src-tauri/src/commands/kube.rs b/src-tauri/src/commands/kube.rs index 6dca23d4..aa57a57f 100644 --- a/src-tauri/src/commands/kube.rs +++ b/src-tauri/src/commands/kube.rs @@ -199,6 +199,61 @@ pub async fn connect_cluster_from_kubeconfig( Ok(()) } +/// Diagnostic: test a kubeconfig's ability to reach the cluster. +/// +/// Returns a human-readable summary including the context name, kubectl binary +/// path, exit code, and the full stdout/stderr from `kubectl cluster-info`. +/// This command is safe to call at any time — it writes a temp file, tests the +/// connection, then deletes the file regardless of the outcome. +#[tauri::command] +pub async fn test_kubectl_connection( + cluster_id: String, + state: State<'_, AppState>, +) -> Result { + let (kubeconfig_content, context) = { + let clusters = state.clusters.lock().await; + let cluster = clusters.get(&cluster_id).ok_or_else(|| { + format!( + "Cluster {} not found in session — try re-selecting the cluster", + cluster_id + ) + })?; + (cluster.kubeconfig_content.clone(), cluster.context.clone()) + }; + + let temp_dir = std::env::temp_dir(); + let temp_path = temp_dir.join(format!("kubeconfig-{}-diag.yaml", cluster_id)); + let _cleanup = TempFileCleanup(temp_path.clone()); + + std::fs::write(&temp_path, kubeconfig_content.as_ref()) + .map_err(|e| format!("Failed to write kubeconfig temp file: {e}"))?; + + let kubectl_path = locate_kubectl()?; + + let output = Command::new(&kubectl_path) + .arg("cluster-info") + .arg("--context") + .arg(context.as_str()) + .arg("--kubeconfig") + .arg(&temp_path) + .output() + .await + .map_err(|e| format!("Failed to execute kubectl: {e}"))?; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let exit_code = output.status.code().unwrap_or(-1); + + Ok(format!( + "Context: {context}\nKubectl: {kubectl}\nExit: {exit}\n\n--- stdout ---\n{stdout}\n--- stderr ---\n{stderr}", + context = context, + kubectl = kubectl_path.display(), + exit = exit_code, + stdout = if stdout.is_empty() { "(none)" } else { &stdout }, + stderr = if stderr.is_empty() { "(none)" } else { &stderr }, + )) +} + #[tauri::command] pub async fn remove_cluster(id: String, state: State<'_, AppState>) -> Result<(), String> { // Check existence in memory BEFORE touching the DB @@ -280,7 +335,8 @@ pub async fn test_cluster_connection( let output = Command::new(kubectl_path) .arg("cluster-info") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -334,7 +390,8 @@ pub async fn discover_pods( .arg(&namespace) .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -473,7 +530,8 @@ pub async fn start_port_forward( .args(&args) .arg("--context") .arg(cluster.context.as_str()) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .spawn() .map_err(|e| format!("Failed to spawn kubectl: {e}"))?; @@ -781,7 +839,8 @@ pub async fn list_namespaces( .arg("namespaces") .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -868,7 +927,8 @@ pub async fn list_pods( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -991,7 +1051,8 @@ pub async fn list_services( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -1149,7 +1210,8 @@ pub async fn list_deployments( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -1283,7 +1345,8 @@ pub async fn list_statefulsets( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -1401,7 +1464,8 @@ pub async fn list_daemonsets( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -1566,7 +1630,8 @@ pub async fn get_pod_logs( .arg(namespace) .arg("-c") .arg(container_name) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -1616,7 +1681,8 @@ pub async fn scale_deployment( .arg(replicas.to_string()) .arg("-n") .arg(namespace) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -1662,7 +1728,8 @@ pub async fn restart_deployment( .arg(deployment_name) .arg("-n") .arg(namespace) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -1708,7 +1775,8 @@ pub async fn delete_resource( .arg(resource_name) .arg("-n") .arg(namespace) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -1777,7 +1845,8 @@ pub async fn exec_pod( cmd.arg("--").arg(shell_cmd).arg("-c").arg(&command); - cmd.env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + cmd.arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()); @@ -2038,7 +2107,8 @@ pub async fn list_replicasets( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -2156,7 +2226,8 @@ pub async fn list_jobs( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -2312,7 +2383,8 @@ pub async fn list_cronjobs( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -2439,7 +2511,8 @@ pub async fn list_configmaps( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -2537,7 +2610,8 @@ pub async fn list_secrets( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -2636,7 +2710,8 @@ pub async fn list_nodes( .arg("nodes") .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -2829,7 +2904,8 @@ pub async fn list_events( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -2958,7 +3034,8 @@ pub async fn list_ingresses( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3084,7 +3161,8 @@ pub async fn list_persistentvolumeclaims( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3209,7 +3287,8 @@ pub async fn list_persistentvolumes( .arg("persistentvolumes") .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3340,7 +3419,8 @@ pub async fn list_serviceaccounts( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3438,7 +3518,8 @@ pub async fn list_roles( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3523,7 +3604,8 @@ pub async fn list_clusterroles( .arg("clusterroles") .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3603,7 +3685,8 @@ pub async fn list_rolebindings( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3699,7 +3782,8 @@ pub async fn list_clusterrolebindings( .arg("clusterrolebindings") .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3790,7 +3874,8 @@ pub async fn list_horizontalpodautoscalers( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -3903,7 +3988,8 @@ pub async fn list_storageclasses( .arg("storageclasses") .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -4013,7 +4099,8 @@ pub async fn list_networkpolicies( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -4124,7 +4211,8 @@ pub async fn list_resourcequotas( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -4245,7 +4333,8 @@ pub async fn list_limitranges( let output = kubectl_cmd .arg("-o") .arg("json") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -4337,7 +4426,8 @@ pub async fn cordon_node( let output = Command::new(kubectl_path) .arg("cordon") .arg(node_name) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -4378,7 +4468,8 @@ pub async fn uncordon_node( let output = Command::new(kubectl_path) .arg("uncordon") .arg(node_name) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -4422,7 +4513,8 @@ pub async fn drain_node( .arg("--ignore-daemonsets") .arg("--delete-emptydir-data") .arg("--force") - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -4468,7 +4560,8 @@ pub async fn rollback_deployment( .arg(deployment_name) .arg("-n") .arg(namespace) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .output() @@ -4514,7 +4607,8 @@ pub async fn create_resource( .arg("-") .arg("-n") .arg(namespace) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .stdin(Stdio::piped()) @@ -4577,7 +4671,8 @@ pub async fn edit_resource( .arg("-") .arg("-n") .arg(namespace) - .env("KUBECONFIG", temp_path.to_string_lossy().to_string()) + .arg("--kubeconfig") + .arg(&temp_path) .arg("--context") .arg(context.as_str()) .stdin(Stdio::piped()) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ecbebac0..128d4e11 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -181,6 +181,7 @@ pub fn run() { // Kubernetes Management commands::kube::add_cluster, commands::kube::connect_cluster_from_kubeconfig, + commands::kube::test_kubectl_connection, commands::kube::remove_cluster, commands::kube::list_clusters, commands::kube::start_port_forward, diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts index 4c13c276..ba8c89d3 100644 --- a/src/lib/tauriCommands.ts +++ b/src/lib/tauriCommands.ts @@ -917,6 +917,10 @@ export const removeClusterCmd = (id: string) => export const connectClusterFromKubeconfigCmd = (id: string) => invoke("connect_cluster_from_kubeconfig", { id }); +/** Diagnostic: runs kubectl cluster-info and returns a human-readable summary. */ +export const testKubectlConnectionCmd = (clusterId: string) => + invoke("test_kubectl_connection", { clusterId }); + export const listClustersCmd = () => invoke("list_clusters"); diff --git a/src/pages/Settings/KubeconfigManager.tsx b/src/pages/Settings/KubeconfigManager.tsx index e406ddea..027f9755 100644 --- a/src/pages/Settings/KubeconfigManager.tsx +++ b/src/pages/Settings/KubeconfigManager.tsx @@ -1,11 +1,13 @@ import { useState, useEffect } from 'react'; -import { Upload, Check, Trash2, FileCode } from 'lucide-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'; @@ -15,6 +17,8 @@ export default function KubeconfigManager() { 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(null); const loadConfigs = async () => { try { @@ -90,6 +94,22 @@ export default function KubeconfigManager() { } }; + 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 (
@@ -200,6 +220,16 @@ export default function KubeconfigManager() {
+ {!config.is_active && (
+ + {/* Test result for this config */} + {testResult?.id === config.id && ( +
+
{testResult.output}
+
+ )} ))} @@ -243,6 +280,7 @@ export default function KubeconfigManager() {
  • Multiple clusters can be configured and switched between
  • The active configuration is used for kubectl commands
  • All kubeconfig files are encrypted using AES-256-GCM
  • +
  • Use the Test button to diagnose connection issues