feat(kubernetes): Add styling based on current context (#4550)

* feat(kubernetes): Add styling based on current context

Add an ability to customize the configuration of the kubernetes module style, based on the current context.

A new variable is added to the config section, called environments, which is a list of possible customizations. Each such customization is an object with a context_pattern regex, which matches context name, and an optional style and icon that will override the global configuration, if the currently used context matched the context_pattern.

Based on multiple attempts to add per-context styling and symbols to the kubernetes module.

- https://github.com/starship/starship/pull/1568 by @lht https://github.com/lht -> base
- https://github.com/starship/starship/pull/614 by @nomaed https://github.com/nomaed -> naming, symbol, some tests

Rebased and combined by @jankatins

Contains the following squasched commits

- Rename to contexts and move aliases into contexts
- Move deprecated functions to a submodule
- Cleanup: ignore None-valued KubeCtxComponents
- Add regex func + clean up matching-context search
- Placate paper clip

Closes: https://github.com/starship/starship/issues/570

Co-authored-by: =?UTF-8?q?Boris=20Aranovic=CC=8C?= <nomaed@gmail.com>
Co-authored-by: Jan Katins <jasc@gmx.net>
Co-authored-by: Kevin Song <chips@ksong.dev>

* refactor(kubernetes): Remove options and use clearer names

* test(kubernetes): Handle duplicated contexts right

* refactor(kubernetes): Cleaner user matching

* fix(kubernetes): Only show warning in case of problems

* feat(kubernetes): Add back alias replacements

* refactor(kubernetes): Cleanup rust usage

---------

Co-authored-by: Haitao Li <lihaitao@gmail.com>
Co-authored-by: =?UTF-8?q?Boris=20Aranovic=CC=8C?= <nomaed@gmail.com>
Co-authored-by: Kevin Song <chips@ksong.dev>
Co-authored-by: David Knaack <davidkna@users.noreply.github.com>
This commit is contained in:
Jan Katins 2023-09-02 09:19:33 +02:00 committed by GitHub
parent e867cda1eb
commit 6b444e05c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 744 additions and 128 deletions

View File

@ -906,6 +906,7 @@
"kubernetes": { "kubernetes": {
"default": { "default": {
"context_aliases": {}, "context_aliases": {},
"contexts": [],
"detect_extensions": [], "detect_extensions": [],
"detect_files": [], "detect_files": [],
"detect_folders": [], "detect_folders": [],
@ -4013,6 +4014,58 @@
"items": { "items": {
"type": "string" "type": "string"
} }
},
"contexts": {
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/KubernetesContextConfig"
}
}
},
"additionalProperties": false
},
"KubernetesContextConfig": {
"type": "object",
"properties": {
"context_pattern": {
"default": "",
"type": "string"
},
"user_pattern": {
"default": null,
"type": [
"string",
"null"
]
},
"symbol": {
"default": null,
"type": [
"string",
"null"
]
},
"style": {
"default": null,
"type": [
"string",
"null"
]
},
"context_alias": {
"default": null,
"type": [
"string",
"null"
]
},
"user_alias": {
"default": null,
"type": [
"string",
"null"
]
} }
}, },
"additionalProperties": false "additionalProperties": false

View File

@ -2470,7 +2470,8 @@ kotlin_binary = 'kotlinc'
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. 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-context --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`. 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
@ -2486,18 +2487,45 @@ case the module will only be active in directories that match those conditions.
### Options ### Options
::: warning
The `context_aliases` and `user_aliases` options are deprecated. Use `contexts` and the corresponding `context_alias`
and `user_alias` options instead.
:::
| Option | Default | Description | | Option | Default | Description |
| ------------------- | -------------------------------------------------- | --------------------------------------------------------------------- | | ------------------- | -------------------------------------------------- | --------------------------------------------------------------------- |
| `symbol` | `'☸ '` | A format string representing the symbol displayed before the Cluster. | | `symbol` | `'☸ '` | A format string representing the symbol displayed before the Cluster. |
| `format` | `'[$symbol$context( \($namespace\))]($style) in '` | The format for the module. | | `format` | `'[$symbol$context( \($namespace\))]($style) in '` | The format for the module. |
| `style` | `'cyan bold'` | The style for the module. | | `style` | `'cyan bold'` | The style for the module. |
| `context_aliases` | `{}` | Table of context aliases to display. | | `context_aliases`* | `{}` | Table of context aliases to display. |
| `user_aliases` | `{}` | Table of user aliases to display. | | `user_aliases`* | `{}` | Table of user aliases to display. |
| `detect_extensions` | `[]` | Which extensions should trigger this module. | | `detect_extensions` | `[]` | Which extensions should trigger this module. |
| `detect_files` | `[]` | Which filenames should trigger this module. | | `detect_files` | `[]` | Which filenames should trigger this module. |
| `detect_folders` | `[]` | Which folders should trigger this modules. | | `detect_folders` | `[]` | Which folders should trigger this modules. |
| `contexts` | `[]` | Customized styles and symbols for specific contexts. |
| `disabled` | `true` | Disables the `kubernetes` module. | | `disabled` | `true` | Disables the `kubernetes` module. |
*: This option is deprecated, please add `contexts` with the corresponding `context_alias` and `user_alias` options instead.
To customize the style of the module for specific environments, use the following configuration as
part of the `contexts` list:
| Variable | Description |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `context_pattern` | **Required** Regular expression to match current Kubernetes context name. |
| `user_pattern` | Regular expression to match current Kubernetes user name. |
| `context_alias` | Context alias to display instead of the full context name. |
| `user_alias` | User alias to display instead of the full user name. |
| `style` | The style for the module when using this context. If not set, will use module's style. |
| `symbol` | The symbol for the module when using this context. If not set, will use module's symbol. |
Note that all regular expression are anchored with `^<pattern>$` and so must match the whole string. The `*_pattern`
regular expressions may contain capture groups, which can be referenced in the corresponding alias via `$name` and `$N`
(see example below and the
[rust Regex::replace() documentation](https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace)).
### Variables ### Variables
| Variable | Example | Description | | Variable | Example | Description |
@ -2519,13 +2547,9 @@ case the module will only be active in directories that match those conditions.
[kubernetes] [kubernetes]
format = 'on [⛵ ($user on )($cluster in )$context \($namespace\)](dimmed green) ' format = 'on [⛵ ($user on )($cluster in )$context \($namespace\)](dimmed green) '
disabled = false disabled = false
[kubernetes.context_aliases] contexts = [
'dev.local.cluster.k8s' = 'dev' { context_pattern = "dev.local.cluster.k8s", style = "green", symbol = "💔 " },
'.*/openshift-cluster/.*' = 'openshift' ]
'gke_.*_(?P<var_cluster>[\w-]+)' = 'gke-$var_cluster'
[kubernetes.user_aliases]
'dev.local.cluster.k8s' = 'dev'
'root/.*' = 'root'
``` ```
Only show the module in directories that contain a `k8s` file. Only show the module in directories that contain a `k8s` file.
@ -2538,29 +2562,37 @@ disabled = false
detect_files = ['k8s'] detect_files = ['k8s']
``` ```
#### Regex Matching #### Kubernetes Context specific config
Additional to simple aliasing, `context_aliases` and `user_aliases` also supports The `contexts` configuration option is used to customise what the current Kubernetes context name looks
extended matching and renaming using regular expressions. like (style and symbol) if the name matches the defined regular expression.
The regular expression must match on the entire kube context,
capture groups can be referenced using `$name` and `$N` in the replacement.
This is more explained in the [regex crate](https://docs.rs/regex/1.5.4/regex/struct.Regex.html#method.replace) documentation.
Long and automatically generated cluster names can be identified
and shortened using regular expressions:
```toml ```toml
[kubernetes.context_aliases] # ~/.config/starship.toml
# 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<var_cluster>[\w-]+)/.*' = '$var_cluster'
[[kubernetes.contexts]]
# "bold red" style + default symbol when Kubernetes current context name equals "production" *and* the current user
# equals "admin_user"
context_pattern = "production"
user_pattern = "admin_user"
style = "bold red"
context_alias = "prod"
user_alias = "admin"
[[kubernetes.contexts]]
# "green" style + a different symbol when Kubernetes current context name contains openshift
context_pattern = ".*openshift.*"
style = "green"
symbol = "💔 "
context_alias = "openshift"
[[kubernetes.contexts]]
# Using capture groups
# 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<var_cluster>[\w-]+)' = 'gke-$var_cluster' context_pattern = "gke_.*_(?P<cluster>[\\w-]+)"
context_alias = "gke-$cluster"
``` ```
## Line Break ## Line Break

View File

@ -18,6 +18,7 @@ pub struct KubernetesConfig<'a> {
pub detect_extensions: Vec<&'a str>, pub detect_extensions: Vec<&'a str>,
pub detect_files: Vec<&'a str>, pub detect_files: Vec<&'a str>,
pub detect_folders: Vec<&'a str>, pub detect_folders: Vec<&'a str>,
pub contexts: Vec<KubernetesContextConfig<'a>>,
} }
impl<'a> Default for KubernetesConfig<'a> { impl<'a> Default for KubernetesConfig<'a> {
@ -32,6 +33,23 @@ impl<'a> Default for KubernetesConfig<'a> {
detect_extensions: vec![], detect_extensions: vec![],
detect_files: vec![], detect_files: vec![],
detect_folders: vec![], detect_folders: vec![],
contexts: vec![],
} }
} }
} }
#[derive(Clone, Deserialize, Serialize, Default)]
#[cfg_attr(
feature = "config-schema",
derive(schemars::JsonSchema),
schemars(deny_unknown_fields)
)]
#[serde(default)]
pub struct KubernetesContextConfig<'a> {
pub context_pattern: &'a str,
pub user_pattern: Option<&'a str>,
pub symbol: Option<&'a str>,
pub style: Option<&'a str>,
pub context_alias: Option<&'a str>,
pub user_alias: Option<&'a str>,
}

View File

@ -1,7 +1,6 @@
use yaml_rust::YamlLoader; use yaml_rust::YamlLoader;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap;
use std::env; use std::env;
use std::path; use std::path;
@ -11,99 +10,91 @@ use crate::configs::kubernetes::KubernetesConfig;
use crate::formatter::StringFormatter; use crate::formatter::StringFormatter;
use crate::utils; use crate::utils;
#[derive(Default)]
struct KubeCtxComponents { struct KubeCtxComponents {
user: Option<String>, user: Option<String>,
namespace: Option<String>, namespace: Option<String>,
cluster: Option<String>, cluster: Option<String>,
} }
fn get_kube_context(filename: path::PathBuf) -> Option<String> { fn get_current_kube_context_name(filename: path::PathBuf) -> Option<String> {
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()?;
if yaml_docs.is_empty() { let conf = yaml_docs.get(0)?;
return None; conf["current-context"]
} .as_str()
let conf = &yaml_docs[0]; .filter(|s| !s.is_empty())
.map(String::from)
let current_ctx = conf["current-context"].as_str()?;
if current_ctx.is_empty() {
return None;
}
Some(current_ctx.to_string())
} }
fn get_kube_ctx_component(filename: path::PathBuf, current_ctx: &str) -> Option<KubeCtxComponents> { fn get_kube_ctx_components(
filename: path::PathBuf,
current_ctx_name: &str,
) -> 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()?;
if yaml_docs.is_empty() { let conf = yaml_docs.get(0)?;
return None; let contexts = conf["contexts"].as_vec()?;
}
let conf = &yaml_docs[0];
let ctx_yaml = conf["contexts"].as_vec().and_then(|contexts| { // Find the context with the name we're looking for
contexts // or return None if we can't find it
.iter() let (ctx_yaml, _) = contexts
.filter_map(|ctx| Some((ctx, ctx["name"].as_str()?))) .iter()
.find(|(_, name)| *name == current_ctx) .filter_map(|ctx| Some((ctx, ctx["name"].as_str()?)))
}); .find(|(_, name)| name == &current_ctx_name)?;
let ctx_components = KubeCtxComponents { let ctx_components = KubeCtxComponents {
user: ctx_yaml user: ctx_yaml["context"]["user"]
.and_then(|(ctx, _)| ctx["context"]["user"].as_str()) .as_str()
.and_then(|s| { .filter(|s| !s.is_empty())
if s.is_empty() { .map(String::from),
return None; namespace: ctx_yaml["context"]["namespace"]
} .as_str()
Some(s.to_owned()) .filter(|s| !s.is_empty())
}), .map(String::from),
namespace: ctx_yaml cluster: ctx_yaml["context"]["cluster"]
.and_then(|(ctx, _)| ctx["context"]["namespace"].as_str()) .as_str()
.and_then(|s| { .filter(|s| !s.is_empty())
if s.is_empty() { .map(String::from),
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) Some(ctx_components)
} }
fn get_kube_user<'a>(config: &'a KubernetesConfig, kube_user: &'a str) -> Cow<'a, str> { fn get_aliased_name<'a>(
return get_alias(&config.user_aliases, kube_user).unwrap_or(Cow::Borrowed(kube_user)); pattern: Option<&'a str>,
} current_value: Option<&str>,
alias: Option<&'a str>,
fn get_kube_context_name<'a>(config: &'a KubernetesConfig, kube_ctx: &'a str) -> Cow<'a, str> { ) -> Option<String> {
return get_alias(&config.context_aliases, kube_ctx).unwrap_or(Cow::Borrowed(kube_ctx)); let replacement = alias.or(current_value)?.to_string();
} let Some(pattern) = pattern else {
// If user pattern not set, treat it as a match-all pattern
fn get_alias<'a>( return Some(replacement);
aliases: &'a HashMap<String, &'a str>, };
alias_candidate: &'a str, // If a pattern is set, but we have no value, there is no match
) -> Option<Cow<'a, str>> { let value = current_value?;
if let Some(val) = aliases.get(alias_candidate) { if value == pattern {
return Some(Cow::Borrowed(val)); return Some(replacement);
} }
let re = match regex::Regex::new(&format!("^{pattern}$")) {
return aliases.iter().find_map(|(k, v)| { Ok(re) => re,
let re = regex::Regex::new(&format!("^{k}$")).ok()?; Err(error) => {
let replaced = re.replace(alias_candidate, *v); log::warn!(
match replaced { "Could not compile regular expression `{}`:\n{}",
Cow::Owned(replaced) => Some(Cow::Owned(replaced)), &format!("^{pattern}$"),
_ => None, error
);
return None;
} }
}); };
let replaced = re.replace(value, replacement.as_str());
match replaced {
Cow::Owned(replaced) => Some(replaced),
// It didn't match...
_ => None,
}
} }
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> { pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
@ -118,18 +109,28 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// If we have some config for doing the directory scan then we use it but if we don't then we // If we have some config for doing the directory scan then we use it but if we don't then we
// assume we should treat it like the module is enabled to preserve backward compatibility. // assume we should treat it like the module is enabled to preserve backward compatibility.
let have_scan_config = !(config.detect_files.is_empty() let have_scan_config = [
&& config.detect_folders.is_empty() &config.detect_files,
&& config.detect_extensions.is_empty()); &config.detect_folders,
&config.detect_extensions,
]
.into_iter()
.any(|v| !v.is_empty());
let is_kube_project = context let is_kube_project = have_scan_config.then(|| {
.try_begin_scan()? context
.set_files(&config.detect_files) .try_begin_scan()
.set_folders(&config.detect_folders) .map(|scanner| {
.set_extensions(&config.detect_extensions) scanner
.is_match(); .set_files(&config.detect_files)
.set_folders(&config.detect_folders)
.set_extensions(&config.detect_extensions)
.is_match()
})
.unwrap_or(false)
});
if have_scan_config && !is_kube_project { if !is_kube_project.unwrap_or(true) {
return None; return None;
} }
@ -139,39 +140,89 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
.get_env("KUBECONFIG") .get_env("KUBECONFIG")
.unwrap_or(default_config_file.to_str()?.to_string()); .unwrap_or(default_config_file.to_str()?.to_string());
let kube_ctx = env::split_paths(&kube_cfg).find_map(get_kube_context)?; let current_kube_ctx_name =
env::split_paths(&kube_cfg).find_map(get_current_kube_context_name)?;
let ctx_components: Vec<KubeCtxComponents> = env::split_paths(&kube_cfg) // Even if we have multiple config files, the first key wins
.filter_map(|filename| get_kube_ctx_component(filename, &kube_ctx)) // https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/
.collect(); // > Never change the value or map key. ... Example: If two files specify a red-user,
// > use only values from the first file's red-user. Even if the second file has
// > non-conflicting entries under red-user, discard them.
// for that reason, we can pick the first context with that name
let ctx_components: KubeCtxComponents = env::split_paths(&kube_cfg)
.find_map(|filename| get_kube_ctx_components(filename, &current_kube_ctx_name))
.unwrap_or_else(|| {
// TODO: figure out if returning is more sensible. But currently we have tests depending on this
log::warn!(
"Invalid KUBECONFIG: identified current-context `{}`, but couldn't find the context in any config file(s): `{}`.\n",
&current_kube_ctx_name,
&kube_cfg
);
KubeCtxComponents::default()
});
// Select the first style that matches the context_pattern and,
// if it is defined, the user_pattern
let (matched_context_config, display_context, display_user) = config
.contexts
.iter()
.find_map(|context_config| {
let context_alias = get_aliased_name(
Some(context_config.context_pattern),
Some(&current_kube_ctx_name),
context_config.context_alias,
)?;
let user_alias = get_aliased_name(
context_config.user_pattern,
ctx_components.user.as_deref(),
context_config.user_alias,
);
if matches!((context_config.user_pattern, &user_alias), (Some(_), None)) {
// defined pattern, but it didn't match
return None;
}
Some((Some(context_config), context_alias, user_alias))
})
.unwrap_or_else(|| (None, current_kube_ctx_name.clone(), ctx_components.user));
// TODO: remove deprecated aliases after starship 2.0
let display_context =
deprecated::get_alias(display_context, &config.context_aliases, "context").unwrap();
let display_user =
display_user.and_then(|user| deprecated::get_alias(user, &config.user_aliases, "user"));
let display_style = matched_context_config
.and_then(|ctx_cfg| ctx_cfg.style)
.unwrap_or(config.style);
let display_symbol = matched_context_config
.and_then(|ctx_cfg| ctx_cfg.symbol)
.unwrap_or(config.symbol);
let parsed = StringFormatter::new(config.format).and_then(|formatter| { let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter formatter
.map_meta(|variable, _| match variable { .map_meta(|variable, _| match variable {
"symbol" => Some(config.symbol), "symbol" => Some(display_symbol),
_ => None, _ => None,
}) })
.map_style(|variable| match variable { .map_style(|variable| match variable {
"style" => Some(Ok(config.style)), "style" => Some(Ok(display_style)),
_ => None, _ => None,
}) })
.map(|variable| match variable { .map(|variable| match variable {
"context" => Some(Ok(get_kube_context_name(&config, &kube_ctx))), "context" => Some(Ok(Cow::Borrowed(display_context.as_str()))),
"namespace" => ctx_components "namespace" => ctx_components
.iter() .namespace
.find_map(|kube| kube.namespace.as_deref()) .as_ref()
.map(|namespace| Ok(Cow::Borrowed(namespace))), .map(|kube_ns| Ok(Cow::Borrowed(kube_ns.as_str()))),
"user" => ctx_components
.iter()
.find_map(|kube| kube.user.as_deref())
.map(|user| Ok(get_kube_user(&config, user))),
"cluster" => ctx_components "cluster" => ctx_components
.iter() .cluster
.find_map(|kube| kube.cluster.as_deref()) .as_ref()
.map(|cluster| Ok(Cow::Borrowed(cluster))), .map(|kube_cluster| Ok(Cow::Borrowed(kube_cluster.as_str()))),
"user" => display_user
.as_ref()
.map(|kube_user| Ok(Cow::Borrowed(kube_user.as_str()))),
_ => None, _ => None,
}) })
.parse(None, Some(context)) .parse(None, Some(context))
@ -188,6 +239,47 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
Some(module) Some(module)
} }
mod deprecated {
use std::borrow::Cow;
use std::collections::HashMap;
pub fn get_alias<'a>(
current_value: String,
aliases: &'a HashMap<String, &'a str>,
name: &'a str,
) -> Option<String> {
let alias = if let Some(val) = aliases.get(current_value.as_str()) {
// simple match without regex
Some((*val).to_string())
} else {
// regex match
aliases.iter().find_map(|(k, v)| {
let re = regex::Regex::new(&format!("^{k}$")).ok()?;
let replaced = re.replace(current_value.as_str(), *v);
match replaced {
// We have a match if the replaced string is different from the original
Cow::Owned(replaced) => Some(replaced),
_ => None,
}
})
};
match alias {
Some(alias) => {
log::warn!(
"Usage of '{}_aliases' is deprecated and will be removed in 2.0; Use 'contexts' with '{}_alias' instead. (`{}` -> `{}`)",
&name,
&name,
&current_value,
&alias
);
Some(alias)
}
None => Some(current_value),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::test::ModuleRenderer; use crate::test::ModuleRenderer;
@ -429,6 +521,21 @@ users: []
) )
} }
#[test]
fn test_config_context_ctx_alias_regex_replace() -> io::Result<()> {
base_test_ctx_alias(
"gke_infra-cluster-28cccff6_europe-west4_cluster-1",
toml::toml! {
[kubernetes]
disabled = false
[[kubernetes.contexts]]
context_pattern = "gke_.*_(?P<cluster>[\\w-]+)"
context_alias = "example: $cluster"
},
"☸ example: cluster-1",
)
}
#[test] #[test]
fn test_ctx_alias_broken_regex() -> io::Result<()> { fn test_ctx_alias_broken_regex() -> io::Result<()> {
base_test_ctx_alias( base_test_ctx_alias(
@ -577,7 +684,9 @@ users: []
} }
#[test] #[test]
fn test_multiple_config_files_with_ns() -> io::Result<()> { fn test_multiple_config_files_with_context_defined_once() -> io::Result<()> {
// test that we get the current context from the first config file in the KUBECONFIG,
// no matter if it is only defined in the latter
let dir = tempfile::tempdir()?; let dir = tempfile::tempdir()?;
let filename_cc = dir.path().join("config_cc"); let filename_cc = dir.path().join("config_cc");
@ -630,7 +739,7 @@ users: []
}) })
.collect(); .collect();
// And tes with context and namespace first // And test with context and namespace first
let actual_ctx_first = ModuleRenderer::new("kubernetes") let actual_ctx_first = ModuleRenderer::new("kubernetes")
.path(dir.path()) .path(dir.path())
.env( .env(
@ -655,6 +764,87 @@ users: []
dir.close() dir.close()
} }
#[test]
fn test_multiple_config_files_with_context_defined_twice() -> io::Result<()> {
// tests that, if two files contain the same context,
// only the context config from the first is used.
let dir = tempfile::tempdir()?;
let config1 = dir.path().join("config1");
let mut file1 = File::create(&config1)?;
file1.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
cluster: test_cluster1
namespace: test_namespace1
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file1.sync_all()?;
let config2 = dir.path().join("config2");
let mut file2 = File::create(&config2)?;
file2.write_all(
b"
apiVersion: v1
clusters: []
contexts:
- context:
cluster: test_cluster2
user: test_user2
name: test_context
current-context: test_context
kind: Config
preferences: {}
users: []
",
)?;
file2.sync_all()?;
let paths1 = [config1.clone(), config2.clone()];
let kubeconfig_content1 = env::join_paths(paths1.iter()).unwrap();
let actual1 = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", kubeconfig_content1.to_string_lossy())
.config(toml::toml! {
[kubernetes]
format = "($user )($cluster )($namespace )"
disabled = false
})
.collect();
let expected1 = Some("test_cluster1 test_namespace1 ".to_string());
assert_eq!(expected1, actual1);
let paths2 = [config2, config1];
let kubeconfig_content2 = env::join_paths(paths2.iter()).unwrap();
let actual2 = ModuleRenderer::new("kubernetes")
.path(dir.path())
.env("KUBECONFIG", kubeconfig_content2.to_string_lossy())
.config(toml::toml! {
[kubernetes]
format = "($user )($cluster )($namespace )"
disabled = false
})
.collect();
let expected2 = Some("test_user2 test_cluster2 ".to_string());
assert_eq!(expected2, actual2);
dir.close()
}
fn base_test_user_alias( fn base_test_user_alias(
user_name: &str, user_name: &str,
config: toml::Table, config: toml::Table,
@ -744,6 +934,23 @@ users: []
) )
} }
#[test]
fn test_config_context_user_alias_regex_replace() -> io::Result<()> {
base_test_user_alias(
"gke_infra-user-28cccff6_europe-west4_cluster-1",
toml::toml! {
[kubernetes]
disabled = false
format = "[$symbol$context( \\($user\\))]($style) in "
[[kubernetes.contexts]]
context_pattern = ".*"
user_pattern = "gke_.*_(?P<cluster>[\\w-]+)"
user_alias = "example: $cluster"
},
"☸ test_context (example: cluster-1)",
)
}
#[test] #[test]
fn test_user_alias_broken_regex() -> io::Result<()> { fn test_user_alias_broken_regex() -> io::Result<()> {
base_test_user_alias( base_test_user_alias(
@ -932,4 +1139,310 @@ users: []
assert_eq!(expected, actual); assert_eq!(expected, actual);
dir.close() dir.close()
} }
#[test]
fn test_config_context_overwrites_defaults() -> std::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]
disabled = false
style = "bold red"
[[kubernetes.contexts]]
context_pattern = "test.*"
style = "bold green"
symbol = "§ "
})
.collect();
let expected = Some(format!(
"{} in ",
Color::Green.bold().paint("§ test_context (test_namespace)")
));
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_both_pattern_must_match() -> 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
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]
disabled = false
format = "$symbol$context ($user )"
[[kubernetes.contexts]]
context_pattern = "test.*"
user_pattern = "test.*"
context_alias = "yy"
user_alias = "xx"
symbol = "§ "
})
.collect();
let expected = Some("§ yy xx ".to_string());
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_only_one_pattern_matches() -> 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
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]
disabled = false
format = "$symbol$context ($user )"
[[kubernetes.contexts]]
context_pattern = "test.*"
user_pattern = "test_BAD.*"
context_alias = "yy"
user_alias = "xx"
symbol = "§ "
})
.collect();
let expected = Some("☸ test_context test_user ".to_string());
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_uses_aliases() -> std::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]
disabled = false
style = "bold red"
format = "$symbol($user )($context )($cluster )($namespace)"
[[kubernetes.contexts]]
context_pattern = "test.*"
context_alias = "xyz"
user_alias = "abc"
symbol = "§ "
})
.collect();
let expected = Some("§ abc xyz test_namespace".to_string());
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_user_pattern_does_not_match() -> std::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]
disabled = false
style = "bold red"
format = "$symbol($user )($context )($cluster )($namespace)"
[[kubernetes.contexts]]
context_pattern = "test"
user_pattern = "not_matching"
context_alias = "xyz"
user_alias = "abc"
symbol = "§ "
})
.collect();
let expected = Some("☸ test_user test_context test_namespace".to_string());
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_contexts_does_not_match() -> std::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]
disabled = false
style = "bold red"
contexts = [
{context_pattern = "tests_.*", style = "bold green", symbol = "§ "},
]
})
.collect();
let expected = Some(format!(
"{} in ",
Color::Red.bold().paint("☸ test_context (test_namespace)")
));
assert_eq!(expected, actual);
dir.close()
}
#[test]
fn test_config_context_bad_regex_should_not_panic() -> std::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]
disabled = false
style = "bold red"
contexts = [
{context_pattern = "tests_(.*", style = "bold green", symbol = "§ "},
]
})
.collect();
let expected = Some(format!(
"{} in ",
Color::Red.bold().paint("☸ test_context (test_namespace)")
));
assert_eq!(expected, actual);
dir.close()
}
} }