1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3
4mod connector;
101mod iac;
102mod interface;
103mod operation;
104mod routing;
105
106pub use connector::DriverConnector;
107pub use iac::{IacBackend, InterfaceConfiguration, StaticRouteConfiguration};
108pub use interface::{
109 InterfaceName, InterfaceNameError, IpPrefix, IpPrefixError, IpPrefixParseError,
110};
111pub use operation::{
112 AdministrativeState, Driver, EnsureStaticRoute, Execute, OperationStep, RemoveStaticRoute, Run,
113 Sequence, SetInterfaceDescription, SetInterfaceState, operation,
114};
115pub use routing::{StaticRoute, StaticRouteError};
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use std::convert::Infallible;
121 use std::future::Future;
122 use std::net::{IpAddr, Ipv4Addr};
123 use std::pin::pin;
124 use std::sync::atomic::{AtomicBool, Ordering};
125 use std::sync::mpsc::{self, Receiver, Sender};
126 use std::task::{Context, Poll, Waker};
127
128 fn block_on<F: Future>(future: F) -> F::Output {
129 let mut future = pin!(future);
130 let waker = Waker::noop();
131 let mut context = Context::from_waker(waker);
132 match future.as_mut().poll(&mut context) {
133 Poll::Ready(output) => output,
134 Poll::Pending => panic!("test future unexpectedly yielded"),
135 }
136 }
137
138 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
139 enum TestError {
140 Failed,
141 }
142
143 impl std::fmt::Display for TestError {
144 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 formatter.write_str("test operation failed")
146 }
147 }
148
149 impl std::error::Error for TestError {}
150
151 struct TestDriver {
152 events: Sender<&'static str>,
153 fail_first: AtomicBool,
154 }
155
156 impl Driver for TestDriver {
157 type Error = TestError;
158 }
159
160 struct FirstOperation;
161 struct SecondOperation;
162
163 impl Execute<FirstOperation> for TestDriver {
164 type Output = u8;
165
166 #[allow(clippy::manual_async_fn)]
167 fn execute<'a>(
168 &'a self,
169 _operation: FirstOperation,
170 ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
171 async move {
172 self.events.send("first").expect("receiver is alive");
173 if self.fail_first.load(Ordering::Relaxed) {
174 Err(TestError::Failed)
175 } else {
176 Ok(7)
177 }
178 }
179 }
180 }
181
182 impl Execute<SecondOperation> for TestDriver {
183 type Output = String;
184
185 #[allow(clippy::manual_async_fn)]
186 fn execute<'a>(
187 &'a self,
188 _operation: SecondOperation,
189 ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
190 async move {
191 self.events.send("second").expect("receiver is alive");
192 Ok(String::from("completed"))
193 }
194 }
195 }
196
197 fn test_driver(fail_first: bool) -> (TestDriver, Receiver<&'static str>) {
198 let (events, receiver) = mpsc::channel();
199 (
200 TestDriver {
201 events,
202 fail_first: AtomicBool::new(fail_first),
203 },
204 receiver,
205 )
206 }
207
208 #[test]
209 fn operation_sequence_runs_in_order_and_returns_second_output() {
210 let (driver, events) = test_driver(false);
211 let output = block_on(
212 operation(FirstOperation)
213 .then(operation(SecondOperation))
214 .run(&driver),
215 )
216 .unwrap();
217
218 assert_eq!(output, "completed");
219 assert_eq!(events.try_iter().collect::<Vec<_>>(), ["first", "second"]);
220 }
221
222 #[test]
223 fn operation_sequence_stops_after_first_error() {
224 let (driver, events) = test_driver(true);
225 let result = block_on(
226 operation(FirstOperation)
227 .then(operation(SecondOperation))
228 .run(&driver),
229 );
230
231 assert_eq!(result, Err(TestError::Failed));
232 assert_eq!(events.try_iter().collect::<Vec<_>>(), ["first"]);
233 }
234
235 struct CustomOperation(&'static str);
236
237 impl Execute<CustomOperation> for TestDriver {
238 type Output = usize;
239
240 #[allow(clippy::manual_async_fn)]
241 fn execute<'a>(
242 &'a self,
243 operation: CustomOperation,
244 ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
245 async move {
246 self.events.send(operation.0).expect("receiver is alive");
247 Ok(operation.0.len())
248 }
249 }
250 }
251
252 #[test]
253 fn downstream_custom_operation_is_supported_without_core_changes() {
254 let (driver, events) = test_driver(false);
255 let result = block_on(operation(CustomOperation("extension")).run(&driver)).unwrap();
256
257 assert_eq!(result, "extension".len());
258 assert_eq!(events.try_iter().collect::<Vec<_>>(), ["extension"]);
259 }
260
261 struct TestIacBackend;
262
263 impl IacBackend<StaticRouteConfiguration> for TestIacBackend {
264 type Error = Infallible;
265 type Plan = bool;
266 type ApplyResult = usize;
267
268 #[allow(clippy::manual_async_fn)]
269 fn plan<'a>(
270 &'a self,
271 desired: &'a StaticRouteConfiguration,
272 ) -> impl Future<Output = Result<Self::Plan, Self::Error>> + Send + 'a {
273 async move { Ok(desired.route.destination().prefix_length() == 24) }
274 }
275
276 #[allow(clippy::manual_async_fn)]
277 fn apply<'a>(
278 &'a self,
279 desired: &'a StaticRouteConfiguration,
280 ) -> impl Future<Output = Result<Self::ApplyResult, Self::Error>> + Send + 'a {
281 async move { Ok(usize::from(desired.route.destination().prefix_length())) }
282 }
283 }
284
285 #[test]
286 fn iac_backend_can_plan_and_apply_a_standard_resource() {
287 let destination = "192.0.2.0/24".parse::<IpPrefix>().unwrap();
288 let route = StaticRoute::new(destination, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1))).unwrap();
289 let desired = StaticRouteConfiguration::new(route);
290 let backend = TestIacBackend;
291
292 assert!(block_on(backend.plan(&desired)).unwrap());
293 assert_eq!(block_on(backend.apply(&desired)).unwrap(), 24);
294 }
295}