@@ -163,6 +163,55 @@ struct JobStatus {
163163 ready : Option < u32 > ,
164164}
165165
166+ #[ derive( Deserialize ) ]
167+ struct KubeConfig {
168+ #[ serde( default , rename = "current-context" ) ]
169+ current_context : Option < String > ,
170+ #[ serde( default ) ]
171+ contexts : Vec < KubeContextEntry > ,
172+ }
173+
174+ #[ derive( Deserialize ) ]
175+ struct KubeContextEntry {
176+ name : String ,
177+ #[ serde( default ) ]
178+ context : KubeContext ,
179+ }
180+
181+ #[ derive( Default , Deserialize ) ]
182+ struct KubeContext {
183+ #[ serde( default ) ]
184+ namespace : Option < String > ,
185+ }
186+
187+ impl KubeConfig {
188+ fn context_names ( & self ) -> Vec < String > {
189+ self . contexts
190+ . iter ( )
191+ . map ( |c| c. name . trim ( ) . to_string ( ) )
192+ . filter ( |name| !name. is_empty ( ) )
193+ . collect ( )
194+ }
195+
196+ fn current_context ( & self ) -> Option < String > {
197+ self . current_context
198+ . as_deref ( )
199+ . map ( str:: trim)
200+ . filter ( |name| !name. is_empty ( ) )
201+ . map ( str:: to_string)
202+ }
203+
204+ fn namespace_for ( & self , context : & str ) -> Option < String > {
205+ self . contexts
206+ . iter ( )
207+ . find ( |c| c. name == context)
208+ . and_then ( |c| c. context . namespace . as_deref ( ) )
209+ . map ( str:: trim)
210+ . filter ( |namespace| !namespace. is_empty ( ) )
211+ . map ( str:: to_string)
212+ }
213+ }
214+
166215fn kubectl_base ( context : Option < & str > ) -> Command {
167216 let mut cmd = Command :: new ( "kubectl" ) ;
168217 if let Some ( ctx) = context {
@@ -226,70 +275,22 @@ pub async fn ensure_context_exists(context: &str) -> Result<()> {
226275}
227276
228277pub async fn list_contexts ( ) -> Result < Vec < String > > {
229- let output = Command :: new ( "kubectl" )
230- . args ( [ "config" , "get-contexts" , "-o" , "name" ] )
231- . output ( )
232- . await
233- . context ( "failed to run kubectl config get-contexts" ) ?;
234- if !output. status . success ( ) {
235- bail ! (
236- "kubectl config get-contexts failed: {}" ,
237- String :: from_utf8_lossy( & output. stderr) . trim( )
238- ) ;
239- }
240- let list = String :: from_utf8_lossy ( & output. stdout )
241- . lines ( )
242- . map ( |s| s. trim ( ) . to_string ( ) )
243- . filter ( |s| !s. is_empty ( ) )
244- . collect ( ) ;
245- Ok ( list)
278+ Ok ( kube_config ( ) . await ?. context_names ( ) )
246279}
247280
248281pub async fn current_context ( ) -> Result < String > {
249- let output = Command :: new ( "kubectl" )
250- . args ( [ "config" , "current-context" ] )
251- . output ( )
252- . await
253- . context ( "failed to run kubectl config current-context" ) ?;
254- if !output. status . success ( ) {
255- bail ! (
256- "kubectl config current-context failed: {}" ,
257- String :: from_utf8_lossy( & output. stderr) . trim( )
258- ) ;
259- }
260- let context = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
261- if context. is_empty ( ) {
262- bail ! ( "kubectl config current-context returned an empty context" ) ;
263- }
264- Ok ( context)
282+ kube_config ( )
283+ . await ?
284+ . current_context ( )
285+ . context ( "kubectl config view returned an empty current-context" )
265286}
266287
267288pub async fn get_context_namespace ( context : & str ) -> Result < Option < String > > {
268- let output = Command :: new ( "kubectl" )
269- . args ( [
270- "config" ,
271- "view" ,
272- "-o" ,
273- & format ! (
274- "jsonpath={{.contexts[?(@.name==\" {}\" )].context.namespace}}" ,
275- context
276- ) ,
277- ] )
278- . output ( )
279- . await
280- . context ( "failed to run kubectl config view" ) ?;
281- if !output. status . success ( ) {
282- bail ! (
283- "kubectl config view failed: {}" ,
284- String :: from_utf8_lossy( & output. stderr) . trim( )
285- ) ;
286- }
287- let ns = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
288- if ns. is_empty ( ) {
289- Ok ( None )
290- } else {
291- Ok ( Some ( ns) )
292- }
289+ Ok ( kube_config ( ) . await ?. namespace_for ( context) )
290+ }
291+
292+ async fn kube_config ( ) -> Result < KubeConfig > {
293+ run_kubectl_json ( None , & [ "config" , "view" , "-o" , "json" ] , "config view" ) . await
293294}
294295
295296pub async fn get_pod_info ( context : Option < & str > , namespace : & str , pod : & str ) -> Result < PodInfo > {
@@ -730,4 +731,44 @@ mod tests {
730731 } ;
731732 assert ! ( !is_ready( & pod) ) ;
732733 }
734+
735+ #[ test]
736+ fn kube_config_finds_namespace_for_quoted_context_name ( ) {
737+ let config: KubeConfig = serde_json:: from_str (
738+ r#"{
739+ "current-context": "dev\"\\ctx",
740+ "contexts": [
741+ {
742+ "name": "dev\"\\ctx",
743+ "context": {"namespace": "team-a"}
744+ }
745+ ]
746+ }"# ,
747+ )
748+ . unwrap ( ) ;
749+
750+ assert_eq ! ( config. current_context( ) . as_deref( ) , Some ( r#"dev"\ctx"# ) ) ;
751+ assert_eq ! (
752+ config. namespace_for( r#"dev"\ctx"# ) . as_deref( ) ,
753+ Some ( "team-a" )
754+ ) ;
755+ assert_eq ! ( config. context_names( ) , vec![ r#"dev"\ctx"# . to_string( ) ] ) ;
756+ }
757+
758+ #[ test]
759+ fn kube_config_treats_blank_namespace_as_missing ( ) {
760+ let config: KubeConfig = serde_json:: from_str (
761+ r#"{
762+ "contexts": [
763+ {
764+ "name": "dev",
765+ "context": {"namespace": " "}
766+ }
767+ ]
768+ }"# ,
769+ )
770+ . unwrap ( ) ;
771+
772+ assert_eq ! ( config. namespace_for( "dev" ) , None ) ;
773+ }
733774}
0 commit comments