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