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}