1use seaplane::{
2 api::compute::v1::{Architecture as ArchitectureModel, Flight as FlightModel},
3 rexports::container_image_ref::ImageReference,
4};
5
6use crate::{
7 cli::{
8 cmds::flight::{
9 common::Architecture, str_to_image_ref, SeaplaneFlightCommonArgMatches,
10 FLIGHT_MINIMUM_DEFAULT,
11 },
12 validator::validate_flight_name,
13 },
14 context::Ctx,
15 error::{CliErrorKind, Result},
16 ops::generate_flight_name,
17};
18
19#[derive(Debug, Clone)]
24pub struct FlightCtx {
25 pub image: Option<ImageReference>,
26 pub name_id: String,
27 pub minimum: u64,
28 pub maximum: Option<u64>,
29 pub architecture: Vec<ArchitectureModel>,
30 pub api_permission: bool,
31 pub reset_maximum: bool,
32 pub generated_name: bool,
34}
35
36impl Default for FlightCtx {
37 fn default() -> Self {
38 Self {
39 name_id: generate_flight_name(),
40 image: None,
41 minimum: FLIGHT_MINIMUM_DEFAULT,
42 maximum: None,
43 architecture: Vec::new(),
44 api_permission: false,
45 reset_maximum: false,
46 generated_name: true,
47 }
48 }
49}
50
51impl FlightCtx {
52 pub fn from_inline_flight(inline_flight: &str, registry: &str) -> Result<FlightCtx> {
58 if inline_flight.contains(' ') {
59 return Err(CliErrorKind::InlineFlightHasSpace.into_err());
60 }
61
62 let mut fctx = FlightCtx::default();
63
64 let parts = inline_flight.split(',');
65
66 macro_rules! parse_item {
67 ($item:expr, $f:expr) => {{
68 let mut item = $item.split('=');
69 item.next();
70 if let Some(value) = item.next() {
71 if value.is_empty() {
72 return Err(
73 CliErrorKind::InlineFlightMissingValue($item.to_string()).into_err()
74 );
75 }
76 $f(value)
77 } else {
78 Err(CliErrorKind::InlineFlightMissingValue($item.to_string()).into_err())
79 }
80 }};
81 ($item:expr) => {{
82 parse_item!($item, |n| { Ok(n) })
83 }};
84 }
85
86 for part in parts {
87 match part.trim() {
88 name if part.starts_with("name") => {
90 fctx.name_id = parse_item!(name, |n: &str| {
91 if validate_flight_name(n).is_err() {
92 Err(CliErrorKind::InlineFlightInvalidName(n.to_string()).into_err())
93 } else {
94 Ok(n.to_string())
95 }
96 })?;
97 fctx.generated_name = false;
98 }
99 img if part.starts_with("image") => {
101 fctx.image = Some(str_to_image_ref(registry, parse_item!(img)?)?);
102 }
103 max if part.starts_with("max") => {
105 fctx.maximum = Some(parse_item!(max)?.parse()?);
106 }
107 min if part.starts_with("min") => {
109 fctx.minimum = parse_item!(min)?.parse()?;
110 }
111 arch if part.starts_with("arch") => {
113 fctx.architecture.push(parse_item!(arch)?.parse()?);
114 }
115 "api-permission" | "api-permissions" => {
116 fctx.api_permission = true;
117 }
118 perm if part.starts_with("api-permission") => {
120 let _ = parse_item!(perm, |perm: &str| {
121 fctx.api_permission = match perm {
122 t if t.eq_ignore_ascii_case("true") => true,
123 f if f.eq_ignore_ascii_case("false") => true,
124 _ => {
125 return Err(CliErrorKind::InlineFlightUnknownItem(
126 perm.to_string(),
127 )
128 .into_err());
129 }
130 };
131 Ok(())
132 });
133 }
134 _ => {
135 return Err(CliErrorKind::InlineFlightUnknownItem(part.to_string()).into_err());
136 }
137 }
138 }
139
140 if fctx.image.is_none() {
141 return Err(CliErrorKind::InlineFlightMissingImage.into_err());
142 }
143
144 Ok(fctx)
145 }
146
147 pub fn from_flight_common(
149 matches: &SeaplaneFlightCommonArgMatches,
150 ctx: &Ctx,
151 ) -> Result<FlightCtx> {
152 let matches = matches.0;
153 let mut generated_name = false;
154 let name = matches
156 .get_one::<String>("name")
157 .map(ToOwned::to_owned)
158 .unwrap_or_else(|| {
159 generated_name = true;
160 generate_flight_name()
161 });
162
163 let image = if let Some(s) = matches.get_one::<String>("image") {
165 Some(str_to_image_ref(&ctx.registry, s)?)
166 } else {
167 None
168 };
169
170 Ok(FlightCtx {
171 image,
172 name_id: name,
173 minimum: *matches
174 .get_one::<u64>("minimum")
175 .unwrap_or(&FLIGHT_MINIMUM_DEFAULT),
177 maximum: matches.get_one::<u64>("maximum").copied(),
178 architecture: matches
181 .get_many::<Architecture>("architecture")
182 .unwrap_or_default()
183 .map(|a| a.into())
184 .collect(),
185 api_permission: matches.get_flag("api-permission"),
187 reset_maximum: matches.get_flag("no-maximum"),
188 generated_name,
189 })
190 }
191
192 pub fn model(&self) -> FlightModel {
194 let mut flight_model = FlightModel::builder()
196 .name(self.name_id.clone())
197 .minimum(self.minimum);
198
199 #[cfg(feature = "unstable")]
200 {
201 flight_model = flight_model.api_permission(self.api_permission);
202 }
203
204 if let Some(image) = self.image.clone() {
205 flight_model = flight_model.image_reference(image);
206 }
207
208 if let Some(n) = self.maximum {
211 flight_model = flight_model.maximum(n);
212 }
213
214 for arch in &self.architecture {
217 flight_model = flight_model.add_architecture(*arch);
218 }
219
220 flight_model
222 .build()
223 .expect("Failed to build Flight from inputs")
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::context::DEFAULT_IMAGE_REGISTRY_URL as IR;
231
232 #[test]
233 fn from_inline_flight_valid() {
234 assert!(FlightCtx::from_inline_flight(
235 "image=demos/nginx:latest,name=foo,maximum=2,minimum=2,api-permission,architecture=amd64", IR
236 )
237 .is_ok());
238 assert!(FlightCtx::from_inline_flight(
239 "image=demos/nginx:latest,name=foo,maximum=2,minimum=2,api-permission",
240 IR
241 )
242 .is_ok());
243 assert!(FlightCtx::from_inline_flight(
244 "image=demos/nginx:latest,name=foo,maximum=2,minimum=2",
245 IR
246 )
247 .is_ok());
248 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,name=foo", IR).is_ok());
249 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest", IR).is_ok());
250 assert!(FlightCtx::from_inline_flight(
251 "image=demos/nginx:latest,name=foo,max=2,minimum=2,api-permission,architecture=amd64",
252 IR
253 )
254 .is_ok());
255 assert!(FlightCtx::from_inline_flight(
256 "image=demos/nginx:latest,name=foo,maximum=2,min=2,api-permission",
257 IR
258 )
259 .is_ok());
260 assert!(
261 FlightCtx::from_inline_flight("image=demos/nginx:latest,api-permissions", IR).is_ok()
262 );
263 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,arch=amd64", IR).is_ok());
264 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,arch=arm64", IR).is_ok());
265 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,api-permission=true", IR)
266 .is_ok(),);
267 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,api-permission=false", IR)
268 .is_ok());
269 }
270
271 #[test]
272 fn from_inline_flight_invalid() {
273 assert_eq!(FlightCtx::from_inline_flight(
274 "image= demos/nginx:latest,name=foo,maximum=2,minimum=2,api-permission,architecture=amd64", IR
275 )
276 .unwrap_err().kind(), &CliErrorKind::InlineFlightHasSpace);
277 assert_eq!(
278 FlightCtx::from_inline_flight(
279 "image=demos/nginx:latest, name=foo,maximum=2,minimum=2,api-permission",
280 IR
281 )
282 .unwrap_err()
283 .kind(),
284 &CliErrorKind::InlineFlightHasSpace
285 );
286 assert_eq!(
287 FlightCtx::from_inline_flight("name=foo,maximum=2,minimum=2", IR)
288 .unwrap_err()
289 .kind(),
290 &CliErrorKind::InlineFlightMissingImage
291 );
292 assert_eq!(
293 FlightCtx::from_inline_flight(",image=demos/nginx:latest,name=foo", IR)
294 .unwrap_err()
295 .kind(),
296 &CliErrorKind::InlineFlightUnknownItem("".into())
297 );
298 assert_eq!(
299 FlightCtx::from_inline_flight("image=demos/nginx:latest,", IR)
300 .unwrap_err()
301 .kind(),
302 &CliErrorKind::InlineFlightUnknownItem("".into())
303 );
304 assert_eq!(
305 FlightCtx::from_inline_flight("image=demos/nginx:latest,foo", IR)
306 .unwrap_err()
307 .kind(),
308 &CliErrorKind::InlineFlightUnknownItem("foo".into())
309 );
310 assert_eq!(
311 FlightCtx::from_inline_flight("image=demos/nginx:latest,name=invalid_name", IR)
312 .unwrap_err()
313 .kind(),
314 &CliErrorKind::InlineFlightInvalidName("invalid_name".into())
315 );
316 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,max=2.3", IR)
317 .unwrap_err()
318 .kind()
319 .is_parse_int(),);
320 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,max=foo", IR)
321 .unwrap_err()
322 .kind()
323 .is_parse_int());
324 assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,arch=foo", IR)
325 .unwrap_err()
326 .kind()
327 .is_strum_parse(),);
328 assert_eq!(
329 FlightCtx::from_inline_flight("image=demos/nginx:latest,name", IR)
330 .unwrap_err()
331 .kind(),
332 &CliErrorKind::InlineFlightMissingValue("name".into())
333 );
334 assert_eq!(
335 FlightCtx::from_inline_flight("image=demos/nginx:latest,name=foo,arch", IR)
336 .unwrap_err()
337 .kind(),
338 &CliErrorKind::InlineFlightMissingValue("arch".into())
339 );
340 assert_eq!(
341 FlightCtx::from_inline_flight("image,name=foo", IR)
342 .unwrap_err()
343 .kind(),
344 &CliErrorKind::InlineFlightMissingValue("image".into())
345 );
346 assert_eq!(
347 FlightCtx::from_inline_flight("image=demos/nginx:latest,name=foo,min=", IR)
348 .unwrap_err()
349 .kind(),
350 &CliErrorKind::InlineFlightMissingValue("min".into())
351 );
352 assert_eq!(
353 FlightCtx::from_inline_flight("image=demos/nginx:latest,name=foo,max=", IR)
354 .unwrap_err()
355 .kind(),
356 &CliErrorKind::InlineFlightMissingValue("max".into())
357 );
358 }
359}