Skip to content

Commit 3cee732

Browse files
authored
feat: parse kube config explicitly (#21)
1 parent 3ab9415 commit 3cee732

1 file changed

Lines changed: 99 additions & 58 deletions

File tree

src/kubectl.rs

Lines changed: 99 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
166215
fn 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

228277
pub 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

248281
pub 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

267288
pub 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

295296
pub 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

Comments
 (0)