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}