diff --git a/Cargo.lock b/Cargo.lock index 607c8d976..c42e9c014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,6 +402,11 @@ dependencies = [ "vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "linked-hash-map" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "log" version = "0.4.8" @@ -772,6 +777,7 @@ dependencies = [ "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1002,6 +1008,14 @@ dependencies = [ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "yaml-rust" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" @@ -1051,6 +1065,7 @@ dependencies = [ "checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" "checksum libgit2-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a30f8637eb59616ee3b8a00f6adff781ee4ddd8343a615b8238de756060cc1b3" "checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" +"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum mach 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" @@ -1126,3 +1141,4 @@ dependencies = [ "checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum wincolor 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "96f5016b18804d24db43cebf3c77269e7569b8954a8464501c216cc5e070eaa9" +"checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" diff --git a/README.md b/README.md index 4943206ba..79e7dfc31 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ The prompt shows information you need while you're working, while staying sleek - `✘` — deleted files - Execution time of the last command if it exceeds the set threshold - Indicator for jobs in the background (`✦`) +- Current Kubernetes Cluster and Namespace (`☸`) ## 🚀 Installation diff --git a/docs/config/README.md b/docs/config/README.md index be29f2bc4..5e2cf8de6 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -85,6 +85,7 @@ The default `prompt_order` is used to define the order in which modules are show prompt_order = [ "username", "hostname", + "kubernetes", "directory", "git_branch", "git_state", @@ -528,6 +529,35 @@ symbol = "+ " threshold = 4 ``` + +## Kubernetes + +Displays the current Kubernetes context name and, if set, the namespace from +the kubeconfig file. The namespace needs to be set in the kubeconfig file, this +can be done via `kubectl config set-context starship-cluster --namespace +astronaut`. If the `$KUBECONFIG` env var is set the module will use that if +not it will use the `~/.kube/config`. + +### Options + +| Variable | Default | Description | +| ---------- | ------------- | --------------------------------------------------- | +| `symbol` | `"☸ "` | The symbol used before displaying the Cluster info. | +| `style` | `"bold blue"` | The style for the module. | +| `disabled` | `false` | Disables the `kubernetes` module | + +### Example + +```toml +# ~/.config/starship.toml + +[kubernetes] +symbol = "⛵ " +style = "dim green" +disabled = true +``` + + ## Line Break The `line_break` module separates the prompt into two lines. diff --git a/starship/Cargo.toml b/starship/Cargo.toml index 4c4c27215..66fbacc2e 100644 --- a/starship/Cargo.toml +++ b/starship/Cargo.toml @@ -45,6 +45,7 @@ chrono = "0.4" sysinfo = "0.9.5" byte-unit = "3.0.3" starship_module_config_derive = { version = "0.20", path = "../starship_module_config_derive" } +yaml-rust = "0.4" [dev-dependencies] tempfile = "3.1.0" diff --git a/starship/src/module.rs b/starship/src/module.rs index e1bfb97ff..8ebb89fd3 100644 --- a/starship/src/module.rs +++ b/starship/src/module.rs @@ -20,6 +20,7 @@ pub const ALL_MODULES: &[&str] = &[ "hostname", "java", "jobs", + "kubernetes", "line_break", "memory_usage", "nix_shell", diff --git a/starship/src/modules/kubernetes.rs b/starship/src/modules/kubernetes.rs new file mode 100644 index 000000000..5142c7dd6 --- /dev/null +++ b/starship/src/modules/kubernetes.rs @@ -0,0 +1,182 @@ +use ansi_term::Color; +use dirs; +use yaml_rust::YamlLoader; + +use std::env; +use std::path; + +use super::{Context, Module}; +use crate::utils; + +const KUBE_CHAR: &str = "☸ "; + +fn get_kube_context(contents: &str) -> Option<(String, String)> { + let yaml_docs = YamlLoader::load_from_str(&contents).ok()?; + if yaml_docs.is_empty() { + return None; + } + let conf = &yaml_docs[0]; + + let current_ctx = conf["current-context"].as_str()?; + + if current_ctx.is_empty() { + return None; + } + + let ns = conf["contexts"] + .as_vec() + .and_then(|contexts| { + contexts + .iter() + .filter_map(|ctx| Some((ctx, ctx["name"].as_str()?))) + .find(|(_, name)| *name == current_ctx) + .and_then(|(ctx, _)| ctx["context"]["namespace"].as_str()) + }) + .unwrap_or(""); + + Some((current_ctx.to_string(), ns.to_string())) +} + +pub fn module<'a>(context: &'a Context) -> Option> { + let filename = match env::var("KUBECONFIG") { + Ok(path) => path::PathBuf::from(path), + Err(_) => dirs::home_dir()?.join(".kube").join("config"), + }; + + let contents = utils::read_file(filename).ok()?; + + match get_kube_context(&contents) { + Some(kube_cfg) => { + let (kube_ctx, kube_ns) = kube_cfg; + + let mut module = context.new_module("kubernetes"); + + let module_style = module + .config_value_style("style") + .unwrap_or_else(|| Color::Cyan.bold()); + module.set_style(module_style); + module.get_prefix().set_value("on "); + + module.new_segment("symbol", KUBE_CHAR); + module.new_segment("context", &kube_ctx); + if kube_ns != "" { + module.new_segment("namespace", &format!(" ({})", kube_ns)); + } + Some(module) + } + None => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_empty_config() { + let input = ""; + let result = get_kube_context(&input); + let expected = None; + + assert_eq!(result, expected); + } + + #[test] + fn parse_no_config() { + let input = r#" +apiVersion: v1 +clusters: [] +contexts: [] +current-context: "" +kind: Config +preferences: {} +users: [] +"#; + let result = get_kube_context(&input); + let expected = None; + + assert_eq!(result, expected); + } + + #[test] + fn parse_only_context() { + let input = r#" +apiVersion: v1 +clusters: [] +contexts: +- context: + cluster: test_cluster + user: test_user + name: test_context +current-context: test_context +kind: Config +preferences: {} +users: [] +"#; + let result = get_kube_context(&input); + let expected = Some(("test_context".to_string(), "".to_string())); + + assert_eq!(result, expected); + } + + #[test] + fn parse_context_and_ns() { + let input = r#" +apiVersion: v1 +clusters: [] +contexts: +- context: + cluster: test_cluster + user: test_user + namespace: test_namespace + name: test_context +current-context: test_context +kind: Config +preferences: {} +users: [] +"#; + let result = get_kube_context(&input); + let expected = Some(("test_context".to_string(), "test_namespace".to_string())); + + assert_eq!(result, expected); + } + + #[test] + fn parse_multiple_contexts() { + let input = r#" +apiVersion: v1 +clusters: [] +contexts: +- context: + cluster: another_cluster + user: another_user + namespace: another_namespace + name: another_context +- context: + cluster: test_cluster + user: test_user + namespace: test_namespace + name: test_context +current-context: test_context +kind: Config +preferences: {} +users: [] +"#; + let result = get_kube_context(&input); + let expected = Some(("test_context".to_string(), "test_namespace".to_string())); + + assert_eq!(result, expected); + } + + #[test] + fn parse_broken_config() { + let input = r#" +--- +dummy_string +"#; + let result = get_kube_context(&input); + let expected = None; + + assert_eq!(result, expected); + } +} diff --git a/starship/src/modules/mod.rs b/starship/src/modules/mod.rs index f073c5a6c..d5174ada2 100644 --- a/starship/src/modules/mod.rs +++ b/starship/src/modules/mod.rs @@ -11,6 +11,7 @@ mod golang; mod hostname; mod java; mod jobs; +mod kubernetes; mod line_break; mod memory_usage; mod nix_shell; @@ -44,6 +45,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "git_branch" => git_branch::module(context), "git_state" => git_state::module(context), "git_status" => git_status::module(context), + "kubernetes" => kubernetes::module(context), "username" => username::module(context), #[cfg(feature = "battery")] "battery" => battery::module(context),