From eb42f5ac7003da1f9543f5258dd674cec96a7320 Mon Sep 17 00:00:00 2001 From: Max Niederman Date: Sun, 9 Mar 2025 08:45:58 -0700 Subject: [PATCH] feat: add network namespace module (#6449) --- .github/config-schema.json | 35 +++++++++++ docs/config/README.md | 33 ++++++++++ src/configs/mod.rs | 3 + src/configs/netns.rs | 26 ++++++++ src/configs/starship_root.rs | 1 + src/module.rs | 1 + src/modules/mod.rs | 3 + src/modules/netns.rs | 116 +++++++++++++++++++++++++++++++++++ 8 files changed, 218 insertions(+) create mode 100644 src/configs/netns.rs create mode 100644 src/modules/netns.rs diff --git a/.github/config-schema.json b/.github/config-schema.json index 6ec2c4b0e..111fb0650 100644 --- a/.github/config-schema.json +++ b/.github/config-schema.json @@ -1082,6 +1082,19 @@ } ] }, + "netns": { + "default": { + "disabled": false, + "format": "[$symbol \\[$name\\]]($style) ", + "style": "blue bold dimmed", + "symbol": "🛜" + }, + "allOf": [ + { + "$ref": "#/definitions/NetnsConfig" + } + ] + }, "nim": { "default": { "detect_extensions": [ @@ -4601,6 +4614,28 @@ }, "additionalProperties": false }, + "NetnsConfig": { + "type": "object", + "properties": { + "format": { + "default": "[$symbol \\[$name\\]]($style) ", + "type": "string" + }, + "symbol": { + "default": "🛜", + "type": "string" + }, + "style": { + "default": "blue bold dimmed", + "type": "string" + }, + "disabled": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, "NimConfig": { "type": "object", "properties": { diff --git a/docs/config/README.md b/docs/config/README.md index a0aed4b0f..d9df8d32f 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -353,6 +353,7 @@ $time\ $status\ $os\ $container\ +$netns\ $shell\ $character""" ``` @@ -3011,6 +3012,38 @@ format = '[$symbol]($style)' style = 'bold purple' ``` +## Network Namespace + +The `netns` module shows the current network namespace. +This uses `ip netns identify` to get the network namespace, so only network namespaces mounted at `/var/run/netns` will be detected. + +### Options + +| Option | Default | Description | +| ---------- | ------------------------------- | ----------------------------------------------------------------- | +| `format` | `'[$symbol \[$name\]]($style)'` | The format for the module. | +| `symbol` | `'🛜 '` | The symbol used before the network namespace (defaults to empty). | +| `style` | `'blue bold dimmed'` | The style for the module. | +| `disabled` | `false` | Disables the `netns` module. | + +### Variables + +| Variable | Example | Description | +| -------- | ---------- | ----------------------------------------- | +| name | `my-netns` | The name of the current network namespace | +| symbol | | Mirrors the value of option `symbol` | +| style\* | | Mirrors the value of option `style` | + +### Example + +```toml +# ~/.config/starship.toml + +[netns] +style = 'bold yellow' +symbol = '🌐 ' +``` + ## Nim The `nim` module shows the currently installed version of [Nim](https://nim-lang.org/). diff --git a/src/configs/mod.rs b/src/configs/mod.rs index 820d7f5b0..294e80a31 100644 --- a/src/configs/mod.rs +++ b/src/configs/mod.rs @@ -57,6 +57,7 @@ pub mod memory_usage; pub mod meson; pub mod mojo; pub mod nats; +pub mod netns; pub mod nim; pub mod nix_shell; pub mod nodejs; @@ -224,6 +225,8 @@ pub struct FullConfig<'a> { #[serde(borrow)] nats: nats::NatsConfig<'a>, #[serde(borrow)] + netns: netns::NetnsConfig<'a>, + #[serde(borrow)] nim: nim::NimConfig<'a>, #[serde(borrow)] nix_shell: nix_shell::NixShellConfig<'a>, diff --git a/src/configs/netns.rs b/src/configs/netns.rs new file mode 100644 index 000000000..1276888c9 --- /dev/null +++ b/src/configs/netns.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, Serialize)] +#[cfg_attr( + feature = "config-schema", + derive(schemars::JsonSchema), + schemars(deny_unknown_fields) +)] +#[serde(default)] +pub struct NetnsConfig<'a> { + pub format: &'a str, + pub symbol: &'a str, + pub style: &'a str, + pub disabled: bool, +} + +impl Default for NetnsConfig<'_> { + fn default() -> Self { + NetnsConfig { + format: "[$symbol \\[$name\\]]($style) ", + symbol: "🛜", + style: "blue bold dimmed", + disabled: false, + } + } +} diff --git a/src/configs/starship_root.rs b/src/configs/starship_root.rs index 8bbe0948b..93733f54b 100644 --- a/src/configs/starship_root.rs +++ b/src/configs/starship_root.rs @@ -125,6 +125,7 @@ pub const PROMPT_ORDER: &[&str] = &[ "time", "status", "container", + "netns", "os", "shell", "character", diff --git a/src/module.rs b/src/module.rs index 9c51daa8c..f88137e7e 100644 --- a/src/module.rs +++ b/src/module.rs @@ -62,6 +62,7 @@ pub const ALL_MODULES: &[&str] = &[ "meson", "mojo", "nats", + "netns", "nim", "nix_shell", "nodejs", diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 1301ce8f2..0648f349e 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -54,6 +54,7 @@ mod memory_usage; mod meson; mod mojo; mod nats; +mod netns; mod nim; mod nix_shell; mod nodejs; @@ -166,6 +167,7 @@ pub fn handle<'a>(module: &str, context: &'a Context) -> Option> { "meson" => meson::module(context), "mojo" => mojo::module(context), "nats" => nats::module(context), + "netns" => netns::module(context), "nim" => nim::module(context), "nix_shell" => nix_shell::module(context), "nodejs" => nodejs::module(context), @@ -291,6 +293,7 @@ pub fn description(module: &str) -> &'static str { } "mojo" => "The currently installed version of Mojo", "nats" => "The current NATS context", + "netns" => "The current network namespace", "nim" => "The currently installed version of Nim", "nix_shell" => "The nix-shell environment", "nodejs" => "The currently installed version of NodeJS", diff --git a/src/modules/netns.rs b/src/modules/netns.rs new file mode 100644 index 000000000..31b179b86 --- /dev/null +++ b/src/modules/netns.rs @@ -0,0 +1,116 @@ +use super::{Context, Module}; + +#[cfg(not(target_os = "linux"))] +pub fn module<'a>(_context: &'a Context) -> Option> { + None +} + +#[cfg(target_os = "linux")] +pub fn module<'a>(context: &'a Context) -> Option> { + use crate::{config::ModuleConfig, configs::netns::NetnsConfig, formatter::StringFormatter}; + + fn netns_name(context: &Context) -> Option { + context + .exec_cmd("ip", &["netns", "identify"]) + .map(|output| output.stdout.trim().to_string()) + .filter(|name| !name.is_empty()) + } + + let mut module = context.new_module("netns"); + let config = NetnsConfig::try_load(module.config); + + if config.disabled { + return None; + } + + let netns_name = netns_name(context)?; + + let parsed = StringFormatter::new(config.format).and_then(|formatter| { + formatter + .map_meta(|variable, _| match variable { + "symbol" => Some(config.symbol), + _ => None, + }) + .map_style(|variable| match variable { + "style" => Some(Ok(config.style)), + _ => None, + }) + .map(|variable| match variable { + "name" => Some(Ok(&netns_name)), + _ => None, + }) + .parse(None, Some(context)) + }); + + module.set_segments(match parsed { + Ok(segments) => segments, + Err(error) => { + log::warn!("Error in module `netns`: \n{}", error); + return None; + } + }); + + Some(module) +} + +#[cfg(test)] +mod tests { + use crate::test::ModuleRenderer; + use crate::utils::CommandOutput; + use nu_ansi_term::Color; + + fn mock_ip_netns_identify(netns_name: &str) -> Option { + Some(CommandOutput { + stdout: format!("{}\n", netns_name), + stderr: String::new(), + }) + } + + #[test] + fn test_none_if_disabled() { + let expected = None; + let actual = ModuleRenderer::new("netns") + .config(toml::toml! { + [netns] + disabled = true + }) + .collect(); + + assert_eq!(expected, actual); + } + + #[test] + #[cfg(target_os = "linux")] + fn test_netns_identify() { + let actual = ModuleRenderer::new("netns") + .config(toml::toml! { + [netns] + disabled = false + }) + .cmd("ip netns identify", mock_ip_netns_identify("test_netns")) + .collect(); + + let expected = Some(format!( + "{} ", + Color::Blue.bold().dimmed().paint("🛜 [test_netns]") + )); + + assert_eq!(actual, expected); + } + + #[test] + #[cfg(target_os = "linux")] + fn test_netns_identify_empty() { + let actual = ModuleRenderer::new("netns") + .config(toml::toml! { + [netns] + disabled = false + }) + .cmd("ip netns identify", mock_ip_netns_identify("")) + .collect(); + + let expected = None; + + assert_eq!(actual, expected); + } +}