From a2cff014e90f10c91d1344cbe9bed7a3744df4cf Mon Sep 17 00:00:00 2001 From: Shaun Arman Date: Sun, 7 Jun 2026 19:40:53 -0500 Subject: [PATCH] fix(kube): use current-context for kubectl auth; fix SelectValue label display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## kubectl credentials still failing after --context fix Root cause: both extract_context() (kube.rs) and upload_kubeconfig() (shell.rs) ignored the kubeconfig's current-context field and always picked contexts[0] from the contexts array. If a kubeconfig has multiple contexts and current-context points to entry N>0, we silently used the wrong context — one that may have empty or expired credentials — causing the 401 "the server has asked for the client to provide credentials" error on every kubectl call. Fixes: - extract_context(): read current-context field first; fall back to contexts[0] only when current-context is absent or empty. - extract_current_context_name(): new helper in kubeconfig.rs using the same line-scanner approach as parse_kubeconfig_contexts (no extra dependencies). - upload_kubeconfig(): use current-context to select the matching context entry when storing context name in kubeconfig_files; falls back to first entry. NOTE: existing kubeconfig rows in the database have the old (wrong) context stored. Re-uploading kubeconfig files after deploying this build will fix them. ## Cluster dropdown still showing UUID Root cause: SelectValue rendered ctx.value (the raw UUID passed to SelectItem's value prop) instead of the display label (SelectItem's children). The custom Select component had no mechanism to mirror a selected item's children into the trigger area. Fix: Select now builds a value→label Map by walking the children tree at render time (collectLabels). The map is memoised on children. SelectValue reads the display label from the map; if found, shows the label; otherwise falls back to the raw value so existing behaviour is preserved for callers that don't need it. --- src-tauri/src/commands/kube.rs | 15 ++++++++++++-- src-tauri/src/commands/shell.rs | 21 ++++++++++++++++---- src-tauri/src/shell/kubeconfig.rs | 20 +++++++++++++++++++ src/components/ui/index.tsx | 33 +++++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/commands/kube.rs b/src-tauri/src/commands/kube.rs index f783582d..6dca23d4 100644 --- a/src-tauri/src/commands/kube.rs +++ b/src-tauri/src/commands/kube.rs @@ -117,6 +117,16 @@ fn extract_context(content: &str) -> Result { let value: serde_yaml::Value = serde_yaml::from_str(content).map_err(|e| format!("Invalid kubeconfig YAML: {}", e))?; + // Prefer current-context — this is what kubectl uses by default and what the + // user intends when they upload their kubeconfig. Falling back to contexts[0] + // picks the wrong entry when the file has multiple contexts. + if let Some(current) = value.get("current-context").and_then(|c| c.as_str()) { + if !current.is_empty() { + return Ok(current.to_string()); + } + } + + // No current-context set — fall back to the first context in the list let contexts = value .get("contexts") .and_then(|c| c.as_sequence()) @@ -126,8 +136,9 @@ fn extract_context(content: &str) -> Result { return Err("No contexts found in kubeconfig".to_string()); } - let first_context = contexts[0].get("name").and_then(|n| n.as_str()); - first_context + contexts[0] + .get("name") + .and_then(|n| n.as_str()) .map(|s| s.to_string()) .ok_or_else(|| "Context name not found".to_string()) } diff --git a/src-tauri/src/commands/shell.rs b/src-tauri/src/commands/shell.rs index 85252bd9..31a53cc6 100644 --- a/src-tauri/src/commands/shell.rs +++ b/src-tauri/src/commands/shell.rs @@ -41,11 +41,24 @@ pub async fn upload_kubeconfig( // Generate ID let id = uuid::Uuid::now_v7().to_string(); - // Parse kubeconfig to extract context + // Parse kubeconfig to extract context. + // Use current-context to select the right entry — the user's kubeconfig may + // have multiple contexts and current-context is the one kubectl defaults to. let contexts = crate::shell::kubeconfig::parse_kubeconfig_contexts(&content)?; - let context = contexts - .first() - .ok_or_else(|| "No contexts found in kubeconfig".to_string())?; + + let current_context_name = crate::shell::kubeconfig::extract_current_context_name(&content); + + let context = if let Some(ref current) = current_context_name { + contexts + .iter() + .find(|c| &c.name == current) + .or_else(|| contexts.first()) + .ok_or_else(|| "No contexts found in kubeconfig".to_string())? + } else { + contexts + .first() + .ok_or_else(|| "No contexts found in kubeconfig".to_string())? + }; // Encrypt content let encrypted_content = crate::integrations::auth::encrypt_token(&content)?; diff --git a/src-tauri/src/shell/kubeconfig.rs b/src-tauri/src/shell/kubeconfig.rs index 126fa545..8d2f0606 100644 --- a/src-tauri/src/shell/kubeconfig.rs +++ b/src-tauri/src/shell/kubeconfig.rs @@ -30,6 +30,26 @@ pub async fn auto_detect_kubeconfig(_state: &AppState) -> Result<(), String> { Err("Kubeconfig auto-detection not yet implemented".to_string()) } +/// Return the `current-context` value from a kubeconfig YAML, if set. +/// Uses simple line scanning so it stays consistent with `parse_kubeconfig_contexts`. +pub fn extract_current_context_name(content: &str) -> Option { + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.starts_with("current-context:") { + let val = trimmed + .trim_start_matches("current-context:") + .trim() + .trim_matches('"') + .trim_matches('\'') + .to_string(); + if !val.is_empty() { + return Some(val); + } + } + } + None +} + pub fn parse_kubeconfig_contexts(content: &str) -> Result, String> { // Parse YAML kubeconfig file // Simple string parsing to extract contexts and cluster URLs diff --git a/src/components/ui/index.tsx b/src/components/ui/index.tsx index 485c7900..aa9684c8 100644 --- a/src/components/ui/index.tsx +++ b/src/components/ui/index.tsx @@ -188,10 +188,29 @@ interface SelectContextValue { onChange: (value: string) => void; open: boolean; setOpen: (open: boolean) => void; + labelMap: Map; } const SelectContext = React.createContext(null); +/** Walk a React node tree collecting SelectItem value→children mappings. */ +function collectLabels( + nodes: React.ReactNode, + map: Map +): void { + React.Children.forEach(nodes, (child) => { + if (!React.isValidElement(child)) return; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const props = child.props as any; + if (child.type === SelectItem && typeof props.value === "string") { + map.set(props.value, props.children); + } + if (props.children) { + collectLabels(props.children as React.ReactNode, map); + } + }); +} + interface SelectProps { value?: string; onValueChange?: (value: string) => void; @@ -207,8 +226,13 @@ export function Select({ value = "", onValueChange, children }: SelectProps) { }, [onValueChange] ); + const labelMap = React.useMemo(() => { + const map = new Map(); + collectLabels(children, map); + return map; + }, [children]); return ( - +
{children}
); @@ -242,7 +266,12 @@ export function SelectTrigger({ export function SelectValue({ placeholder }: { placeholder?: string }) { const ctx = React.useContext(SelectContext)!; - return {ctx.value || placeholder}; + const label = ctx.value ? ctx.labelMap.get(ctx.value) : undefined; + return ( + + {label ?? (ctx.value ? ctx.value : placeholder)} + + ); } export function SelectContent({