use seaplane::{
api::compute::v1::{Architecture as ArchitectureModel, Flight as FlightModel},
rexports::container_image_ref::ImageReference,
};
use crate::{
cli::{
cmds::flight::{
common::Architecture, str_to_image_ref, SeaplaneFlightCommonArgMatches,
FLIGHT_MINIMUM_DEFAULT,
},
validator::validate_flight_name,
},
context::Ctx,
error::{CliErrorKind, Result},
ops::generate_flight_name,
};
#[derive(Debug, Clone)]
pub struct FlightCtx {
pub image: Option<ImageReference>,
pub name_id: String,
pub minimum: u64,
pub maximum: Option<u64>,
pub architecture: Vec<ArchitectureModel>,
pub api_permission: bool,
pub reset_maximum: bool,
pub generated_name: bool,
}
impl Default for FlightCtx {
fn default() -> Self {
Self {
name_id: generate_flight_name(),
image: None,
minimum: FLIGHT_MINIMUM_DEFAULT,
maximum: None,
architecture: Vec::new(),
api_permission: false,
reset_maximum: false,
generated_name: true,
}
}
}
impl FlightCtx {
pub fn from_inline_flight(inline_flight: &str, registry: &str) -> Result<FlightCtx> {
if inline_flight.contains(' ') {
return Err(CliErrorKind::InlineFlightHasSpace.into_err());
}
let mut fctx = FlightCtx::default();
let parts = inline_flight.split(',');
macro_rules! parse_item {
($item:expr, $f:expr) => {{
let mut item = $item.split('=');
item.next();
if let Some(value) = item.next() {
if value.is_empty() {
return Err(
CliErrorKind::InlineFlightMissingValue($item.to_string()).into_err()
);
}
$f(value)
} else {
Err(CliErrorKind::InlineFlightMissingValue($item.to_string()).into_err())
}
}};
($item:expr) => {{
parse_item!($item, |n| { Ok(n) })
}};
}
for part in parts {
match part.trim() {
name if part.starts_with("name") => {
fctx.name_id = parse_item!(name, |n: &str| {
if validate_flight_name(n).is_err() {
Err(CliErrorKind::InlineFlightInvalidName(n.to_string()).into_err())
} else {
Ok(n.to_string())
}
})?;
fctx.generated_name = false;
}
img if part.starts_with("image") => {
fctx.image = Some(str_to_image_ref(registry, parse_item!(img)?)?);
}
max if part.starts_with("max") => {
fctx.maximum = Some(parse_item!(max)?.parse()?);
}
min if part.starts_with("min") => {
fctx.minimum = parse_item!(min)?.parse()?;
}
arch if part.starts_with("arch") => {
fctx.architecture.push(parse_item!(arch)?.parse()?);
}
"api-permission" | "api-permissions" => {
fctx.api_permission = true;
}
perm if part.starts_with("api-permission") => {
let _ = parse_item!(perm, |perm: &str| {
fctx.api_permission = match perm {
t if t.eq_ignore_ascii_case("true") => true,
f if f.eq_ignore_ascii_case("false") => true,
_ => {
return Err(CliErrorKind::InlineFlightUnknownItem(
perm.to_string(),
)
.into_err());
}
};
Ok(())
});
}
_ => {
return Err(CliErrorKind::InlineFlightUnknownItem(part.to_string()).into_err());
}
}
}
if fctx.image.is_none() {
return Err(CliErrorKind::InlineFlightMissingImage.into_err());
}
Ok(fctx)
}
pub fn from_flight_common(
matches: &SeaplaneFlightCommonArgMatches,
ctx: &Ctx,
) -> Result<FlightCtx> {
let matches = matches.0;
let mut generated_name = false;
let name = matches
.get_one::<String>("name")
.map(ToOwned::to_owned)
.unwrap_or_else(|| {
generated_name = true;
generate_flight_name()
});
let image = if let Some(s) = matches.get_one::<String>("image") {
Some(str_to_image_ref(&ctx.registry, s)?)
} else {
None
};
Ok(FlightCtx {
image,
name_id: name,
minimum: *matches
.get_one::<u64>("minimum")
.unwrap_or(&FLIGHT_MINIMUM_DEFAULT),
maximum: matches.get_one::<u64>("maximum").copied(),
architecture: matches
.get_many::<Architecture>("architecture")
.unwrap_or_default()
.map(|a| a.into())
.collect(),
api_permission: matches.get_flag("api-permission"),
reset_maximum: matches.get_flag("no-maximum"),
generated_name,
})
}
pub fn model(&self) -> FlightModel {
let mut flight_model = FlightModel::builder()
.name(self.name_id.clone())
.minimum(self.minimum);
#[cfg(feature = "unstable")]
{
flight_model = flight_model.api_permission(self.api_permission);
}
if let Some(image) = self.image.clone() {
flight_model = flight_model.image_reference(image);
}
if let Some(n) = self.maximum {
flight_model = flight_model.maximum(n);
}
for arch in &self.architecture {
flight_model = flight_model.add_architecture(*arch);
}
flight_model
.build()
.expect("Failed to build Flight from inputs")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::DEFAULT_IMAGE_REGISTRY_URL as IR;
#[test]
fn from_inline_flight_valid() {
assert!(FlightCtx::from_inline_flight(
"image=demos/nginx:latest,name=foo,maximum=2,minimum=2,api-permission,architecture=amd64", IR
)
.is_ok());
assert!(FlightCtx::from_inline_flight(
"image=demos/nginx:latest,name=foo,maximum=2,minimum=2,api-permission",
IR
)
.is_ok());
assert!(FlightCtx::from_inline_flight(
"image=demos/nginx:latest,name=foo,maximum=2,minimum=2",
IR
)
.is_ok());
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,name=foo", IR).is_ok());
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest", IR).is_ok());
assert!(FlightCtx::from_inline_flight(
"image=demos/nginx:latest,name=foo,max=2,minimum=2,api-permission,architecture=amd64",
IR
)
.is_ok());
assert!(FlightCtx::from_inline_flight(
"image=demos/nginx:latest,name=foo,maximum=2,min=2,api-permission",
IR
)
.is_ok());
assert!(
FlightCtx::from_inline_flight("image=demos/nginx:latest,api-permissions", IR).is_ok()
);
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,arch=amd64", IR).is_ok());
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,arch=arm64", IR).is_ok());
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,api-permission=true", IR)
.is_ok(),);
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,api-permission=false", IR)
.is_ok());
}
#[test]
fn from_inline_flight_invalid() {
assert_eq!(FlightCtx::from_inline_flight(
"image= demos/nginx:latest,name=foo,maximum=2,minimum=2,api-permission,architecture=amd64", IR
)
.unwrap_err().kind(), &CliErrorKind::InlineFlightHasSpace);
assert_eq!(
FlightCtx::from_inline_flight(
"image=demos/nginx:latest, name=foo,maximum=2,minimum=2,api-permission",
IR
)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightHasSpace
);
assert_eq!(
FlightCtx::from_inline_flight("name=foo,maximum=2,minimum=2", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightMissingImage
);
assert_eq!(
FlightCtx::from_inline_flight(",image=demos/nginx:latest,name=foo", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightUnknownItem("".into())
);
assert_eq!(
FlightCtx::from_inline_flight("image=demos/nginx:latest,", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightUnknownItem("".into())
);
assert_eq!(
FlightCtx::from_inline_flight("image=demos/nginx:latest,foo", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightUnknownItem("foo".into())
);
assert_eq!(
FlightCtx::from_inline_flight("image=demos/nginx:latest,name=invalid_name", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightInvalidName("invalid_name".into())
);
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,max=2.3", IR)
.unwrap_err()
.kind()
.is_parse_int(),);
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,max=foo", IR)
.unwrap_err()
.kind()
.is_parse_int());
assert!(FlightCtx::from_inline_flight("image=demos/nginx:latest,arch=foo", IR)
.unwrap_err()
.kind()
.is_strum_parse(),);
assert_eq!(
FlightCtx::from_inline_flight("image=demos/nginx:latest,name", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightMissingValue("name".into())
);
assert_eq!(
FlightCtx::from_inline_flight("image=demos/nginx:latest,name=foo,arch", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightMissingValue("arch".into())
);
assert_eq!(
FlightCtx::from_inline_flight("image,name=foo", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightMissingValue("image".into())
);
assert_eq!(
FlightCtx::from_inline_flight("image=demos/nginx:latest,name=foo,min=", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightMissingValue("min".into())
);
assert_eq!(
FlightCtx::from_inline_flight("image=demos/nginx:latest,name=foo,max=", IR)
.unwrap_err()
.kind(),
&CliErrorKind::InlineFlightMissingValue("max".into())
);
}
}