cni_plugin/
cni.rs

1use std::{
2	env,
3	io::{stdin, Read},
4	path::PathBuf,
5	str::FromStr,
6};
7
8use log::{debug, error};
9use regex::Regex;
10use semver::Version;
11
12use crate::{
13	command::Command,
14	config::NetworkConfig,
15	error::{CniError, EmptyValueError, RegexValueError},
16	path::CniPath,
17	reply::reply,
18	version::VersionPayload,
19};
20
21/// The main entrypoint to this plugin and the enum which contains plugin input.
22///
23/// See the field definitions on [`Inputs`][crate::Inputs] for more details on
24/// the command subfields.
25#[derive(Clone, Debug)]
26pub enum Cni {
27	/// The ADD command: add namespace to network, or apply modifications.
28	///
29	/// A CNI plugin, upon receiving an ADD command, should either:
30	/// - create the interface defined by `ifname` inside the namespace at the
31	///   `netns` path, or
32	/// - adjust the configuration of the interface defined by `ifname` inside
33	///   the namespace at the `netns` path.
34	///
35	/// More details in the [spec](https://github.com/containernetworking/cni/blob/master/SPEC.md#add-add-container-to-network-or-apply-modifications).
36	Add {
37		/// The container ID, as provided by the runtime.
38		container_id: String,
39
40		/// The name of the interface inside the container.
41		ifname: String,
42
43		/// The container’s “isolation domain” or namespace path.
44		netns: PathBuf,
45
46		/// List of paths to search for CNI plugin executables.
47		path: Vec<PathBuf>,
48
49		/// The input network configuration.
50		config: NetworkConfig,
51	},
52
53	/// The DEL command: remove namespace from network, or un-apply modifications.
54	///
55	/// A CNI plugin, upon receiving a DEL command, should either:
56	/// - delete the interface defined by `ifname` inside the namespace at the
57	///   `netns` path, or
58	/// - undo any modifications applied in the plugin's ADD functionality.
59	///
60	/// More details in the [spec](https://github.com/containernetworking/cni/blob/master/SPEC.md#del-remove-container-from-network-or-un-apply-modifications).
61	Del {
62		/// The container ID, as provided by the runtime.
63		container_id: String,
64
65		/// The name of the interface inside the container.
66		ifname: String,
67
68		/// The container’s “isolation domain” or namespace path.
69		///
70		/// May not be provided for DEL commands.
71		netns: Option<PathBuf>,
72
73		/// List of paths to search for CNI plugin executables.
74		path: Vec<PathBuf>,
75
76		/// The input network configuration.
77		config: NetworkConfig,
78	},
79
80	/// The CHECK command: check that a namespace's networking is as expected.
81	///
82	/// This was introduced in CNI spec v1.0.0.
83	///
84	/// More details in the [spec](https://github.com/containernetworking/cni/blob/master/SPEC.md#check-check-containers-networking-is-as-expected).
85	Check {
86		/// The container ID, as provided by the runtime.
87		container_id: String,
88
89		/// The name of the interface inside the container.
90		ifname: String,
91
92		/// The container’s “isolation domain” or namespace path.
93		netns: PathBuf,
94
95		/// List of paths to search for CNI plugin executables.
96		path: Vec<PathBuf>,
97
98		/// The input network configuration.
99		config: NetworkConfig,
100	},
101
102	/// The VERSION command: used to probe plugin version support.
103	///
104	/// The plugin should reply with a [`VersionReply`][crate::reply::VersionReply].
105	///
106	/// Note that when using [`Cni::load()`], this command is already handled,
107	/// and you should mark this [`unreachable!()`].
108	Version(Version),
109}
110
111impl Cni {
112	/// Reads the plugin inputs from the environment and STDIN.
113	///
114	/// This reads _and validates_ the required `CNI_*` environment variables,
115	/// and the STDIN for a JSON-encoded input object, but it does not output
116	/// anything to STDOUT nor exits the process, nor does it panic.
117	///
118	/// Note that [as per convention][args-deprecation], the `CNI_ARGS` variable
119	/// is deprecated, and this library deliberately chooses to ignore it. You
120	/// may of course read and parse it yourself.
121	///
122	/// A number of things are logged in here. If you have used
123	/// [`install_logger`][crate::install_logger], this may result in output
124	/// being sent to STDERR (and/or to file).
125	///
126	/// In general you should prefer [`Cni::load()`].
127	///
128	/// [args-deprecation]: https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md#cni_args
129	pub fn from_env() -> Result<Self, CniError> {
130		fn require_env<T>(var: &'static str) -> Result<T, CniError>
131		where
132			T: FromStr,
133			T::Err: std::error::Error + 'static,
134		{
135			env::var(var)
136				.map_err(|err| CniError::MissingEnv { var, err })
137				.and_then(|val| {
138					debug!("read env var {} = {:?}", var, val);
139					val.parse().map_err(|err| CniError::InvalidEnv {
140						var,
141						err: Box::new(err),
142					})
143				})
144		}
145		fn load_env<T>(var: &'static str) -> Result<Option<T>, CniError>
146		where
147			T: FromStr,
148			T::Err: std::error::Error + 'static,
149		{
150			require_env(var).map(Some).or_else(|err| {
151				if let CniError::MissingEnv { .. } = err {
152					Ok(None)
153				} else {
154					Err(err)
155				}
156			})
157		}
158
159		let path: CniPath = load_env("CNI_PATH")?.unwrap_or_default();
160		let path = path.0;
161
162		let mut payload = Vec::with_capacity(1024);
163		debug!("reading stdin til EOF...");
164		stdin().read_to_end(&mut payload)?;
165		debug!("read payload bytes={}", payload.len());
166		if payload.is_empty() {
167			return Err(CniError::MissingInput);
168		}
169
170		fn check_container_id(id: &str) -> Result<(), CniError> {
171			if id.is_empty() {
172				return Err(CniError::InvalidEnv {
173					var: "CNI_CONTAINERID",
174					err: Box::new(EmptyValueError),
175				});
176			}
177
178			let re = Regex::new(r"^[a-z0-9][a-z0-9_.\-]*$").unwrap();
179			if !re.is_match(id) {
180				return Err(CniError::InvalidEnv {
181					var: "CNI_CONTAINERID",
182					err: Box::new(RegexValueError(re)),
183				});
184			}
185
186			Ok(())
187		}
188
189		match require_env("CNI_COMMAND")? {
190			Command::Add => {
191				let container_id: String = require_env("CNI_CONTAINERID")?;
192				check_container_id(&container_id)?;
193
194				let config: NetworkConfig = serde_json::from_slice(&payload)?;
195				Self::check_version(&config.cni_version)?;
196
197				Ok(Self::Add {
198					container_id,
199					ifname: require_env("CNI_IFNAME")?,
200					netns: require_env("CNI_NETNS")?,
201					path,
202					config,
203				})
204			}
205			Command::Del => {
206				let container_id: String = require_env("CNI_CONTAINERID")?;
207				check_container_id(&container_id)?;
208
209				let config: NetworkConfig = serde_json::from_slice(&payload)?;
210				Self::check_version(&config.cni_version)?;
211
212				Ok(Self::Del {
213					container_id,
214					ifname: require_env("CNI_IFNAME")?,
215					netns: load_env("CNI_NETNS")?,
216					path,
217					config,
218				})
219			}
220			Command::Check => {
221				let container_id: String = require_env("CNI_CONTAINERID")?;
222				check_container_id(&container_id)?;
223
224				let config: NetworkConfig = serde_json::from_slice(&payload)?;
225				Self::check_version(&config.cni_version)?;
226
227				Ok(Self::Check {
228					container_id,
229					ifname: require_env("CNI_IFNAME")?,
230					netns: require_env("CNI_NETNS")?,
231					path,
232					config,
233				})
234			}
235			Command::Version => {
236				let config: VersionPayload = serde_json::from_slice(&payload)?;
237				Ok(Self::Version(config.cni_version))
238			}
239		}
240	}
241
242	/// Reads the plugin inputs from the environment and STDIN and reacts to errors and the VERSION command.
243	///
244	/// This does the same thing as [`Cni::from_env()`] but it also immediately
245	/// replies to the `VERSION` command, and also immediately replies if errors
246	/// result from reading the inputs, both of which write to STDOUT and exit.
247	///
248	/// This version also logs a debug message with the name and version of this
249	/// library crate.
250	pub fn load() -> Self {
251		debug!(
252			"CNI plugin built with {} crate version {}",
253			env!("CARGO_PKG_NAME"),
254			env!("CARGO_PKG_VERSION")
255		);
256
257		let cni_version = Version::parse("1.0.0").unwrap();
258
259		match Self::from_env() {
260			Err(e) => {
261				error!("{}", e);
262				reply(e.into_reply(cni_version))
263			}
264			Ok(Cni::Version(v)) => Self::handle_version(v),
265			Ok(c) => c,
266		}
267	}
268
269	// TODO: parse network config (administrator) files
270	// maybe also with something that searches in common locations
271}