Skip to content

Commit

Permalink
Lookup context and cluster from their other values
Browse files Browse the repository at this point in the history
  • Loading branch information
sunsided committed Jul 16, 2023
1 parent f29a96b commit cd9e261
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 15 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
68 changes: 68 additions & 0 deletions src/kubectl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ impl Kubectl {
Ok(value.client_version.git_version)
}

/// Gets the currently active contexts.
pub fn current_context(&self) -> Result<String, ContextError> {
let output = Command::new(&self.kubectl)
.current_dir(&self.current_dir)
Expand All @@ -50,6 +51,7 @@ impl Kubectl {
Ok(value.into())
}

/// Gets the currently active contexts' cluster.
pub fn current_cluster(&self) -> Result<Option<String>, ContextError> {
let output = Command::new(&self.kubectl)
.current_dir(&self.current_dir)
Expand All @@ -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<Option<String>, 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<Option<String>, 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,
Expand Down
38 changes: 23 additions & 15 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ fn main() -> Result<ExitCode> {
// 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:");
Expand Down Expand Up @@ -126,26 +127,33 @@ fn sanitize_config(
config: &mut PortForwardConfigs,
current_context: String,
current_cluster: Option<String>,
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();
}
}
}
}
Expand Down

0 comments on commit cd9e261

Please sign in to comment.