use clap::Args; use rocket::{ response::{self, Responder}, serde::json::Json, FromForm, FromFormField, Request, }; use rocket_okapi::okapi::schemars::{self, JsonSchema}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use strum_macros::{Display, EnumString}; use tabled::Tabled; use zone_zfs::FileSystem; pub static DEFAULT_ENDPOINT: &str = "127.0.0.1:8000"; pub trait PartialEqOrDefault { fn eq_or_default(&self, other: &Self) -> bool; } #[derive( Debug, Serialize, Deserialize, JsonSchema, Tabled, FromFormField, PartialEq, Clone, EnumString, Display, )] #[serde(rename_all = "camelCase")] #[strum(ascii_case_insensitive)] pub enum ContainerStatus { Running, Stopped, Unknown, } impl Default for ContainerStatus { fn default() -> Self { ContainerStatus::Unknown } } #[derive(Debug, Default, Serialize, Deserialize, JsonSchema, Tabled, FromForm, Clone, Args)] #[serde(rename_all = "camelCase")] pub struct Container { #[header("ID")] pub id: u64, #[header("Template")] pub template: String, #[header("User")] pub user: String, #[header("Status")] pub status: ContainerStatus, } impl Container { pub fn filter_from(&self, items: Vec) -> Vec where T: TryInto, { items .into_iter() .map(|c| c.try_into().unwrap_or_default()) .filter(|c| self.eq_or_default(c)) .collect::>() } } impl PartialEqOrDefault for Container { fn eq_or_default(&self, other: &Self) -> bool { (self.id == other.id || self.id == Self::default().id) && (self.template == other.template || self.template == Self::default().template) && (self.user == other.user || self.user == Self::default().user) && (self.status == other.status || self.status == Self::default().status) } } #[rocket::async_trait] impl<'r> Responder<'r, 'static> for Container { fn respond_to(self, request: &'r Request<'_>) -> response::Result<'static> { Json(self).respond_to(request) } } impl TryFrom for Container { type Error = zone_zfs::Error; fn try_from(file_system: FileSystem) -> Result { let path_buf = PathBuf::from(file_system.dataset()) .file_name() .ok_or_else(|| { Self::Error::FileSystem(format!("Invalid FileSystem path: {:?}", file_system)) })? .to_string_lossy() .into_owned(); let (user, id) = path_buf.rsplit_once("-").ok_or_else(|| { Self::Error::FileSystem(format!("Invalid FileSystem name: {:?}", file_system)) })?; let id = id.parse::().map_err(|err| { Self::Error::FileSystem(format!("Failed to parse container ID: {:?}", err)) })?; let template = PathBuf::from(file_system.dataset()) .parent() .ok_or_else(|| { Self::Error::FileSystem(format!("Invalid path for filesystem: {:?}", &file_system)) })? .to_string_lossy() .into_owned(); Ok(Container { id, template, user: user.to_string(), status: ContainerStatus::default(), }) } } impl TryFrom for Container { type Error = zone_nspawn::Error; fn try_from(value: zone_nspawn::Container) -> Result { // id, template, user let machine = value.machine.to_string_lossy(); let v: Vec<&str> = machine.split('-').collect(); Ok(Container { id: v[0].parse().map_err(|err| { Self::Error::Parsing(format!("Failed to parse container id: {:?}", err)) })?, template: v[1].to_owned(), user: v[2].to_owned(), status: ContainerStatus::Running, }) } }