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
This commit is contained in:
zensayyy 2022-02-16 23:20:29 +01:00 committed by GitHub
parent e70454956f
commit d09f71720e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 244 additions and 18 deletions

View File

@ -1865,9 +1865,10 @@ kotlin_binary = "kotlinc"
## Kubernetes ## 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 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`. If the `$KUBECONFIG` env var is set the module will use that if not it will use the `~/.kube/config`.
::: tip ::: tip
@ -1891,8 +1892,10 @@ To enable it, set `disabled` to `false` in your configuration file.
| Variable | Example | Description | | 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 | | 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` | | symbol | | Mirrors the value of option `symbol` |
| style\* | | Mirrors the value of option `style` | | 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 # ~/.config/starship.toml
[kubernetes] [kubernetes]
format = 'on [$context \($namespace\)](dimmed green) ' format = 'on [($user on )($cluster in )$context \($namespace\)](dimmed green) '
disabled = false disabled = false
[kubernetes.context_aliases] [kubernetes.context_aliases]
"dev.local.cluster.k8s" = "dev" "dev.local.cluster.k8s" = "dev"
".*/openshift-cluster/.*" = "openshift" ".*/openshift-cluster/.*" = "openshift"
"gke_.*_(?P<cluster>[\\w-]+)" = "gke-$cluster" "gke_.*_(?P<var_cluster>[\\w-]+)" = "gke-$var_cluster"
``` ```
#### Regex Matching #### 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 contexts carry the namespace and user in the kube context: `namespace/name/user`:
".*/openshift-cluster/.*" = "openshift" ".*/openshift-cluster/.*" = "openshift"
# Or better, to rename every OpenShift cluster at once: # Or better, to rename every OpenShift cluster at once:
".*/(?P<cluster>[\\w-]+)/.*" = "$cluster" ".*/(?P<var_cluster>[\\w-]+)/.*" = "$var_cluster"
# Contexts from GKE, AWS and other cloud providers usually carry additional information, like the region/zone. # 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`) # 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`): # and renames every matching kube context into a more readable format (`gke-cluster-name`):
"gke_.*_(?P<cluster>[\\w-]+)" = "gke-$cluster" "gke_.*_(?P<var_cluster>[\\w-]+)" = "gke-$var_cluster"
``` ```
## Line Break ## Line Break

View File

@ -10,6 +10,12 @@ use crate::configs::kubernetes::KubernetesConfig;
use crate::formatter::StringFormatter; use crate::formatter::StringFormatter;
use crate::utils; use crate::utils;
struct KubeCtxComponents {
user: Option<String>,
namespace: Option<String>,
cluster: Option<String>,
}
fn get_kube_context(filename: path::PathBuf) -> Option<String> { fn get_kube_context(filename: path::PathBuf) -> Option<String> {
let contents = utils::read_file(filename).ok()?; let contents = utils::read_file(filename).ok()?;
@ -27,7 +33,10 @@ fn get_kube_context(filename: path::PathBuf) -> Option<String> {
Some(current_ctx.to_string()) Some(current_ctx.to_string())
} }
fn get_kube_ns(filename: path::PathBuf, current_ctx: String) -> Option<String> { fn get_kube_ctx_component(
filename: path::PathBuf,
current_ctx: String,
) -> Option<KubeCtxComponents> {
let contents = utils::read_file(filename).ok()?; let contents = utils::read_file(filename).ok()?;
let yaml_docs = YamlLoader::load_from_str(&contents).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<String> {
} }
let conf = &yaml_docs[0]; 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 contexts
.iter() .iter()
.filter_map(|ctx| Some((ctx, ctx["name"].as_str()?))) .filter_map(|ctx| Some((ctx, ctx["name"].as_str()?)))
.find(|(_, name)| *name == current_ctx) .find(|(_, name)| *name == current_ctx)
.and_then(|(ctx, _)| ctx["context"]["namespace"].as_str()) });
})?;
if ns.is_empty() { let ctx_components = KubeCtxComponents {
user: ctx_yaml
.and_then(|(ctx, _)| ctx["context"]["user"].as_str())
.and_then(|s| {
if s.is_empty() {
return None; return None;
} }
Some(ns.to_owned()) 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> { 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<Module<'a>> {
let kube_ctx = env::split_paths(&kube_cfg).find_map(get_kube_context)?; let kube_ctx = env::split_paths(&kube_cfg).find_map(get_kube_context)?;
let kube_ns = let ctx_components: Vec<Option<KubeCtxComponents>> = env::split_paths(&kube_cfg)
env::split_paths(&kube_cfg).find_map(|filename| get_kube_ns(filename, kube_ctx.clone())); .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| { let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter formatter
@ -101,7 +147,26 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
}) })
.map(|variable| match variable { .map(|variable| match variable {
"context" => Some(Ok(get_kube_context_name(&config, &kube_ctx))), "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, _ => None,
}) })
.parse(None, Some(context)) .parse(None, Some(context))
@ -463,4 +528,162 @@ users: []
dir.close() 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()
}
} }