From cd9e261c1c20faf47efb73c928a14bae5f426750 Mon Sep 17 00:00:00 2001 From: Markus Mayer Date: Sun, 16 Jul 2023 16:17:31 +0200 Subject: [PATCH] Lookup context and cluster from their other values --- CHANGELOG.md | 8 ++++++ src/kubectl.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 38 +++++++++++++++++----------- 3 files changed, 99 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d38f4..ec47f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added + +- If only the context or the cluster is specified, the other part will be automatically + looked up from the current configuration. If a single match is found, its value will + be explicitly specified to `kubectl`. This should help when changing contexts while + having a port-forwarding session open as intermittent errors will consistently + produce the same forwarding rule regardless of the currently active context. + ### Fixed - Only default to current cluster when neither context nor cluster is specified. diff --git a/src/kubectl.rs b/src/kubectl.rs index 72f219d..31c2c9a 100644 --- a/src/kubectl.rs +++ b/src/kubectl.rs @@ -33,6 +33,7 @@ impl Kubectl { Ok(value.client_version.git_version) } + /// Gets the currently active contexts. pub fn current_context(&self) -> Result { let output = Command::new(&self.kubectl) .current_dir(&self.current_dir) @@ -50,6 +51,7 @@ impl Kubectl { Ok(value.into()) } + /// Gets the currently active contexts' cluster. pub fn current_cluster(&self) -> Result, ContextError> { let output = Command::new(&self.kubectl) .current_dir(&self.current_dir) @@ -71,6 +73,72 @@ impl Kubectl { } } + /// Given the name of the cluster, identifies a context. + pub fn context_from_cluster( + &self, + cluster: Option<&String>, + ) -> Result, ContextError> { + if cluster.is_none() { + return Ok(None); + } + + let context = cluster.expect("value exists"); + let jsonpath = + format!("jsonpath='{{$.contexts[?(@.context.cluster==\"{context}\")].name}}'"); + let output = Command::new(&self.kubectl) + .current_dir(&self.current_dir) + .args(["config", "view", "--merge=true", "-o", &jsonpath]) + .output()?; + + let value = String::from_utf8_lossy(&output.stdout); + let value = value.trim_matches('\''); + // Array values (in case multiple match) are separated by space. + let values: Vec<_> = value.split(' ').collect(); + if values.len() > 1 { + return Ok(None); + } + + let value = values[0]; + if !value.is_empty() { + Ok(Some(value.into())) + } else { + Ok(None) + } + } + + /// Given the name of the context, identifies its cluster. + pub fn cluster_from_context( + &self, + context: Option<&String>, + ) -> Result, ContextError> { + if context.is_none() { + return Ok(None); + } + + let context = context.expect("value exists"); + let jsonpath = + format!("jsonpath='{{$.contexts[?(@.name==\"{context}\")].context.cluster}}'"); + let output = Command::new(&self.kubectl) + .current_dir(&self.current_dir) + .args(["config", "view", "--merge=true", "-o", &jsonpath]) + .output()?; + + let value = String::from_utf8_lossy(&output.stdout); + let value = value.trim_matches('\''); + // Array values (in case multiple match) are separated by space. + let values: Vec<_> = value.split(' ').collect(); + if values.len() > 1 { + return Ok(None); + } + + let value = values[0]; + if !value.is_empty() { + Ok(Some(value.into())) + } else { + Ok(None) + } + } + pub fn port_forward( &self, id: ConfigId, diff --git a/src/main.rs b/src/main.rs index fd0f124..0d8910a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,7 +90,8 @@ fn main() -> Result { // Sanitize default values. let current_context = kubectl.current_context()?; let current_cluster = kubectl.current_cluster()?; - sanitize_config(&mut configs, current_context, current_cluster); + + sanitize_config(&mut configs, current_context, current_cluster, &kubectl); // Map out the config. println!("Forwarding to the following targets:"); @@ -126,26 +127,33 @@ fn sanitize_config( config: &mut PortForwardConfigs, current_context: String, current_cluster: Option, + kubectl: &Kubectl, ) { if config.config.retry_delay_sec < RetryDelay::NONE { config.config.retry_delay_sec = RetryDelay::NONE; } for config in config.targets.iter_mut() { - // Only bind to default cluster if none of the values is specified. - // This is important since otherwise we might end up in a situation where - // the user specified a context (implying _its_ default cluster) yet - // we are trying to specify the default cluster of the _current_ context. - // TODO: Get the "current cluster" from the specified context, if available. - if config.context.is_none() && config.cluster.is_none() { - config.cluster = current_cluster.clone(); - } - - // It appears we can always autofill the context value since the cluster values seem to - // take precedence when specified. This should work as long as the current context - // has a user that is allowed to access the specified cluster. - if config.context.is_none() { - config.context = Some(current_context.clone()); + match (&mut config.context, &mut config.cluster) { + (Some(_context), Some(_cluster)) => { /* nothing to do */ } + (Some(context), None) => match kubectl.cluster_from_context(Some(&context)) { + Ok(Some(cluster)) => { + config.cluster = Some(cluster); + } + Ok(None) => {} + Err(_) => {} + }, + (None, Some(cluster)) => match kubectl.context_from_cluster(Some(&cluster)) { + Ok(Some(context)) => { + config.context = Some(context); + } + Ok(None) => {} + Err(_) => {} + }, + (None, None) => { + config.context = Some(current_context.clone()); + config.cluster = current_cluster.clone(); + } } } }