cni_plugin/
reply.rs

1//! Reply types and helpers.
2
3use std::{collections::HashMap, io::stdout, net::IpAddr, path::PathBuf, process::exit};
4
5use ipnetwork::IpNetwork;
6use log::debug;
7use semver::Version;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11pub use crate::dns::Dns;
12use crate::macaddr::MacAddr;
13pub use crate::version::VersionReply;
14
15/// Trait for a reply type to be handled by the [`reply()`] function.
16///
17/// This is mostly internal, but may be used if you want to output your own
18/// reply types for some reason.
19pub trait ReplyPayload<'de>: std::fmt::Debug + Serialize + Deserialize<'de> {
20	/// The [`exit`] code to be set when replying with this type.
21	///
22	/// Defaults to 0 (success).
23	fn code(&self) -> i32 {
24		0
25	}
26}
27
28/// The reply structure used when returning an error.
29#[derive(Clone, Debug, Deserialize, Serialize)]
30#[serde(rename_all = "camelCase")]
31pub struct ErrorReply<'msg> {
32	/// The CNI version of the plugin input config.
33	#[serde(deserialize_with = "crate::version::deserialize_version")]
34	#[serde(serialize_with = "crate::version::serialize_version")]
35	pub cni_version: Version,
36
37	/// A code for the error.
38	///
39	/// Must match the exit code.
40	///
41	/// Codes 1-99 are reserved by the spec, codes 100+ may be used for plugins'
42	/// own error codes. Code 0 is not to be used, as it is for successful exit.
43	pub code: i32,
44
45	/// A short message characterising the error.
46	///
47	/// This is generally a static non-interpolated string.
48	pub msg: &'msg str,
49
50	/// A longer message describing the error.
51	pub details: String,
52}
53
54impl<'de> ReplyPayload<'de> for ErrorReply<'de> {
55	/// Sets the exit status of the process to the code of the error reply.
56	fn code(&self) -> i32 {
57		self.code
58	}
59}
60
61/// The reply structure used when returning a success.
62#[derive(Clone, Debug, Deserialize, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct SuccessReply {
65	/// The CNI version of the plugin input config.
66	#[serde(deserialize_with = "crate::version::deserialize_version")]
67	#[serde(serialize_with = "crate::version::serialize_version")]
68	pub cni_version: Version,
69
70	/// The list of all interfaces created by this plugin.
71	///
72	/// If `prev_result` was included in the input config and had interfaces,
73	/// they need to be carried on through into this list.
74	#[serde(default)]
75	pub interfaces: Vec<Interface>,
76
77	/// The list of all IPs assigned by this plugin.
78	///
79	/// If `prev_result` was included in the input config and had IPs,
80	/// they need to be carried on through into this list.
81	#[serde(default)]
82	pub ips: Vec<Ip>,
83
84	/// The list of all routes created by this plugin.
85	///
86	/// If `prev_result` was included in the input config and had routes,
87	/// they need to be carried on through into this list.
88	#[serde(default)]
89	pub routes: Vec<Route>,
90
91	/// Final DNS configuration for the namespace.
92	pub dns: Dns,
93
94	/// Custom reply fields.
95	///
96	/// Note that these are off-spec and may be discarded by libcni.
97	#[serde(flatten)]
98	pub specific: HashMap<String, Value>,
99}
100
101impl<'de> ReplyPayload<'de> for SuccessReply {}
102
103impl SuccessReply {
104	/// Cast into an abbreviated success reply if the interface list is empty.
105	pub fn into_ipam(self) -> Option<IpamSuccessReply> {
106		if self.interfaces.is_empty() {
107			Some(IpamSuccessReply {
108				cni_version: self.cni_version,
109				ips: self.ips,
110				routes: self.routes,
111				dns: self.dns,
112				specific: self.specific,
113			})
114		} else {
115			None
116		}
117	}
118}
119
120/// The reply structure used when returning an abbreviated IPAM success.
121///
122/// It is identical to [`SuccessReply`] except for the lack of the `interfaces`
123/// field.
124#[derive(Clone, Debug, Deserialize, Serialize)]
125#[serde(rename_all = "camelCase")]
126pub struct IpamSuccessReply {
127	/// The CNI version of the plugin input config.
128	#[serde(deserialize_with = "crate::version::deserialize_version")]
129	#[serde(serialize_with = "crate::version::serialize_version")]
130	pub cni_version: Version,
131
132	/// The list of all IPs assigned by this plugin.
133	///
134	/// If `prev_result` was included in the input config and had IPs,
135	/// they need to be carried on through into this list.
136	#[serde(default)]
137	pub ips: Vec<Ip>,
138
139	/// The list of all routes created by this plugin.
140	///
141	/// If `prev_result` was included in the input config and had routes,
142	/// they need to be carried on through into this list.
143	#[serde(default)]
144	pub routes: Vec<Route>,
145
146	/// Final DNS configuration for the namespace.
147	#[serde(default)]
148	pub dns: Dns,
149
150	/// Custom reply fields.
151	///
152	/// Note that these are off-spec and may be discarded by libcni.
153	#[serde(flatten)]
154	pub specific: HashMap<String, Value>,
155}
156
157impl<'de> ReplyPayload<'de> for IpamSuccessReply {}
158
159/// Interface structure for success reply types.
160#[derive(Clone, Debug, Deserialize, Serialize)]
161#[serde(rename_all = "camelCase")]
162pub struct Interface {
163	/// The name of the interface.
164	pub name: String,
165
166	/// The hardware address of the interface (if applicable).
167	#[serde(default, skip_serializing_if = "Option::is_none")]
168	pub mac: Option<MacAddr>,
169
170	/// The path to the namespace the interface is in.
171	///
172	/// This should be the value passed by `CNI_NETNS`.
173	///
174	/// If the interface is on the host, this should be set to an empty string.
175	pub sandbox: PathBuf,
176}
177
178/// IP structure for success reply types.
179#[derive(Clone, Debug, Deserialize, Serialize)]
180#[serde(rename_all = "camelCase")]
181pub struct Ip {
182	/// The IP address.
183	pub address: IpNetwork,
184
185	/// The default gateway for this subnet, if one exists.
186	#[serde(default, skip_serializing_if = "Option::is_none")]
187	pub gateway: Option<IpAddr>,
188
189	/// The interface this IP is for.
190	///
191	/// This must be the index into the `interfaces` list on the parent success
192	/// reply structure. It should be `None` for IPAM success replies.
193	#[serde(default, skip_serializing_if = "Option::is_none")]
194	pub interface: Option<usize>, // None for ipam
195}
196
197/// Route structure for success reply types.
198#[derive(Clone, Debug, Deserialize, Serialize)]
199#[serde(rename_all = "camelCase")]
200pub struct Route {
201	/// The destination of the route.
202	pub dst: IpNetwork,
203
204	/// The next hop address.
205	///
206	/// If unset, a value in `gateway` in the `ips` array may be used by the
207	/// runtime, but this is not mandated and is left to its discretion.
208	#[serde(skip_serializing_if = "Option::is_none")]
209	pub gw: Option<IpAddr>,
210}
211
212/// Output the reply as JSON on STDOUT and exit.
213pub fn reply<'de, T>(result: T) -> !
214where
215	T: ReplyPayload<'de>,
216{
217	debug!("replying with {:#?}", result);
218	serde_json::to_writer(stdout(), &result)
219		.expect("Error writing result to stdout... chances are you won't get this either");
220
221	exit(result.code());
222}