From d09f71720e1fada98ac02431685e200dd0847b96 Mon Sep 17 00:00:00 2001 From: zensayyy <99101223+zensayyy@users.noreply.github.com> Date: Wed, 16 Feb 2022 23:20:29 +0100 Subject: [PATCH] feat(kubernetes): add context user and cluster variables (#3569) * added kubernetes context user, cluster + basic test * updated docs * docs format * changed get_kube_ctx_component to return struct --- docs/config/README.md | 17 +-- src/modules/kubernetes.rs | 245 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 244 insertions(+), 18 deletions(-) diff --git a/docs/config/README.md b/docs/config/README.md index 35544677..99ee1472 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -1865,9 +1865,10 @@ kotlin_binary = "kotlinc" ## Kubernetes -Displays the current [Kubernetes context](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context) name and, if set, the namespace from the kubeconfig file. +Displays the current [Kubernetes context](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context) name and, if set, the namespace, user and cluster 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`. +`kubectl config set-context starship-context --namespace astronaut`. +Similarly the user and cluster can be set with `kubectl config set-context starship-context --user starship-user` and `kubectl config set-context starship-context --cluster starship-cluster`. If the `$KUBECONFIG` env var is set the module will use that if not it will use the `~/.kube/config`. ::: tip @@ -1891,8 +1892,10 @@ To enable it, set `disabled` to `false` in your configuration file. | Variable | Example | Description | | --------- | -------------------- | ---------------------------------------- | -| context | `starship-cluster` | The current kubernetes context | +| context | `starship-context` | The current kubernetes context name | | namespace | `starship-namespace` | If set, the current kubernetes namespace | +| user | `starship-user` | If set, the current kubernetes user | +| cluster | `starship-cluster` | If set, the current kubernetes cluster | | symbol | | Mirrors the value of option `symbol` | | style\* | | Mirrors the value of option `style` | @@ -1904,12 +1907,12 @@ To enable it, set `disabled` to `false` in your configuration file. # ~/.config/starship.toml [kubernetes] -format = 'on [⛵ $context \($namespace\)](dimmed green) ' +format = 'on [⛵ ($user on )($cluster in )$context \($namespace\)](dimmed green) ' disabled = false [kubernetes.context_aliases] "dev.local.cluster.k8s" = "dev" ".*/openshift-cluster/.*" = "openshift" -"gke_.*_(?P[\\w-]+)" = "gke-$cluster" +"gke_.*_(?P[\\w-]+)" = "gke-$var_cluster" ``` #### Regex Matching @@ -1929,12 +1932,12 @@ and shortened using regular expressions: # OpenShift contexts carry the namespace and user in the kube context: `namespace/name/user`: ".*/openshift-cluster/.*" = "openshift" # Or better, to rename every OpenShift cluster at once: -".*/(?P[\\w-]+)/.*" = "$cluster" +".*/(?P[\\w-]+)/.*" = "$var_cluster" # Contexts from GKE, AWS and other cloud providers usually carry additional information, like the region/zone. # The following entry matches on the GKE format (`gke_projectname_zone_cluster-name`) # and renames every matching kube context into a more readable format (`gke-cluster-name`): -"gke_.*_(?P[\\w-]+)" = "gke-$cluster" +"gke_.*_(?P[\\w-]+)" = "gke-$var_cluster" ``` ## Line Break diff --git a/src/modules/kubernetes.rs b/src/modules/kubernetes.rs index eacd6e3d..e8aab32c 100644 --- a/src/modules/kubernetes.rs +++ b/src/modules/kubernetes.rs @@ -10,6 +10,12 @@ use crate::configs::kubernetes::KubernetesConfig; use crate::formatter::StringFormatter; use crate::utils; +struct KubeCtxComponents { + user: Option, + namespace: Option, + cluster: Option, +} + fn get_kube_context(filename: path::PathBuf) -> Option { let contents = utils::read_file(filename).ok()?; @@ -27,7 +33,10 @@ fn get_kube_context(filename: path::PathBuf) -> Option { Some(current_ctx.to_string()) } -fn get_kube_ns(filename: path::PathBuf, current_ctx: String) -> Option { +fn get_kube_ctx_component( + filename: path::PathBuf, + current_ctx: String, +) -> Option { let contents = utils::read_file(filename).ok()?; let yaml_docs = YamlLoader::load_from_str(&contents).ok()?; @@ -36,18 +45,41 @@ fn get_kube_ns(filename: path::PathBuf, current_ctx: String) -> Option { } let conf = &yaml_docs[0]; - let ns = conf["contexts"].as_vec().and_then(|contexts| { + let ctx_yaml = 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()) - })?; + }); - if ns.is_empty() { - return None; - } - Some(ns.to_owned()) + let ctx_components = KubeCtxComponents { + user: ctx_yaml + .and_then(|(ctx, _)| ctx["context"]["user"].as_str()) + .and_then(|s| { + if s.is_empty() { + return None; + } + Some(s.to_owned()) + }), + namespace: ctx_yaml + .and_then(|(ctx, _)| ctx["context"]["namespace"].as_str()) + .and_then(|s| { + if s.is_empty() { + return None; + } + Some(s.to_owned()) + }), + cluster: ctx_yaml + .and_then(|(ctx, _)| ctx["context"]["cluster"].as_str()) + .and_then(|s| { + if s.is_empty() { + return None; + } + Some(s.to_owned()) + }), + }; + + Some(ctx_components) } fn get_kube_context_name<'a>(config: &'a KubernetesConfig, kube_ctx: &'a str) -> Cow<'a, str> { @@ -86,8 +118,22 @@ pub fn module<'a>(context: &'a Context) -> Option> { let kube_ctx = env::split_paths(&kube_cfg).find_map(get_kube_context)?; - let kube_ns = - env::split_paths(&kube_cfg).find_map(|filename| get_kube_ns(filename, kube_ctx.clone())); + let ctx_components: Vec> = env::split_paths(&kube_cfg) + .map(|filename| get_kube_ctx_component(filename, kube_ctx.clone())) + .collect(); + + let kube_user = ctx_components.iter().find(|&ctx| match ctx { + Some(kube) => kube.user.is_some(), + None => false, + }); + let kube_ns = ctx_components.iter().find(|&ctx| match ctx { + Some(kube) => kube.namespace.is_some(), + None => false, + }); + let kube_cluster = ctx_components.iter().find(|&ctx| match ctx { + Some(kube) => kube.cluster.is_some(), + None => false, + }); let parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter @@ -101,7 +147,26 @@ pub fn module<'a>(context: &'a Context) -> Option> { }) .map(|variable| match variable { "context" => Some(Ok(get_kube_context_name(&config, &kube_ctx))), - "namespace" => kube_ns.as_ref().map(|s| Ok(Cow::Borrowed(s.as_str()))), + + "namespace" => kube_ns.and_then(|ctx| { + ctx.as_ref().map(|kube| { + // unwrap is safe as kube_ns only holds kube.namespace.is_some() + Ok(Cow::Borrowed(kube.namespace.as_ref().unwrap().as_str())) + }) + }), + "user" => kube_user.and_then(|ctx| { + ctx.as_ref().map(|kube| { + // unwrap is safe as kube_user only holds kube.user.is_some() + Ok(Cow::Borrowed(kube.user.as_ref().unwrap().as_str())) + }) + }), + "cluster" => kube_cluster.and_then(|ctx| { + ctx.as_ref().map(|kube| { + // unwrap is safe as kube_cluster only holds kube.cluster.is_some() + Ok(Cow::Borrowed(kube.cluster.as_ref().unwrap().as_str())) + }) + }), + _ => None, }) .parse(None, Some(context)) @@ -463,4 +528,162 @@ users: [] dir.close() } + + #[test] + fn test_kube_user() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let filename = dir.path().join("config"); + + let mut file = File::create(&filename)?; + file.write_all( + b" +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: [] +", + )?; + file.sync_all()?; + + let actual = ModuleRenderer::new("kubernetes") + .path(dir.path()) + .env("KUBECONFIG", filename.to_string_lossy().as_ref()) + .config(toml::toml! { + [kubernetes] + format = "($user)" + disabled = false + }) + .collect(); + + let expected = Some("test_user".to_string()); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn test_kube_cluster() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let filename = dir.path().join("config"); + + let mut file = File::create(&filename)?; + file.write_all( + b" +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: [] +", + )?; + file.sync_all()?; + + let actual = ModuleRenderer::new("kubernetes") + .path(dir.path()) + .env("KUBECONFIG", filename.to_string_lossy().as_ref()) + .config(toml::toml! { + [kubernetes] + format = "($cluster)" + disabled = false + }) + .collect(); + + let expected = Some("test_cluster".to_string()); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn test_kube_user_missing() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let filename = dir.path().join("config"); + + let mut file = File::create(&filename)?; + file.write_all( + b" +apiVersion: v1 +clusters: [] +contexts: + - context: + cluster: test_cluster + namespace: test_namespace + name: test_context +current-context: test_context +kind: Config +preferences: {} +users: [] +", + )?; + file.sync_all()?; + + let actual = ModuleRenderer::new("kubernetes") + .path(dir.path()) + .env("KUBECONFIG", filename.to_string_lossy().as_ref()) + .config(toml::toml! { + [kubernetes] + format = "$symbol($user )($cluster )($namespace)" + disabled = false + }) + .collect(); + + let expected = Some("☸ test_cluster test_namespace".to_string()); + assert_eq!(expected, actual); + dir.close() + } + + #[test] + fn test_kube_cluster_missing() -> io::Result<()> { + let dir = tempfile::tempdir()?; + + let filename = dir.path().join("config"); + + let mut file = File::create(&filename)?; + file.write_all( + b" +apiVersion: v1 +clusters: [] +contexts: + - context: + user: test_user + namespace: test_namespace + name: test_context +current-context: test_context +kind: Config +preferences: {} +users: [] +", + )?; + file.sync_all()?; + + let actual = ModuleRenderer::new("kubernetes") + .path(dir.path()) + .env("KUBECONFIG", filename.to_string_lossy().as_ref()) + .config(toml::toml! { + [kubernetes] + format = "$symbol($user )($cluster )($namespace)" + disabled = false + }) + .collect(); + + let expected = Some("☸ test_user test_namespace".to_string()); + assert_eq!(expected, actual); + dir.close() + } }