use clap::Args; use derive_builder::Builder; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use tabled::Tabled; use zone_nspawn::NSpawn; use zone_zfs::{FileSystem, ZFS}; use crate::{FilterContainer, Result}; pub use status::ContainerStatus; mod status; #[derive(Debug, Default, Serialize, Deserialize, Builder, Tabled, Clone, Args)] #[builder( name = "ContainerOptions", build_fn(private, name = "build_container"), derive(Debug, Serialize, Deserialize), field(public) )] #[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 builder() -> ContainerOptions { ContainerOptions::default() } } impl ContainerOptions { pub fn build(&self, zfs: &ZFS, nspawn: &NSpawn) -> Result { let container = self.build_container()?; let user = Self::default() .user(container.user.clone()) .template(container.template.clone()) .to_owned(); let mut user_containers = zfs.get_file_systems()?.into_iter().filter_container(user); user_containers.sort_by_key(|c| c.id); let current_last_id = user_containers.last().map_or(0, |c| c.id); let _ = zfs.clone_from_latest( PathBuf::from(format!("{}-{}", container.user, current_last_id)), PathBuf::from(&container.template), ); let _ = nspawn.create_container(); Ok(container) } } impl FilterContainer for T where T: Iterator, T::Item: TryInto, { fn filter_container(&mut self, pred: ContainerOptions) -> Vec { self.filter_map(|c| -> Option { c.try_into().ok() }) .filter(|c| { pred.id.map_or(false, |p| p == c.id) && pred.template.as_ref().map_or(false, |p| p == &c.template) && pred.user.as_ref().map_or(false, |p| p == &c.user) }) .collect() } } impl TryFrom for Container { type Error = zone_zfs::Error; fn try_from(file_system: FileSystem) -> zone_zfs::Result { let path_buf = PathBuf::from(file_system.dataset()) .file_name() .ok_or_else(|| { zone_zfs::Error::FileSystem(format!("Invalid FileSystem path: {:?}", file_system)) })? .to_string_lossy() .into_owned(); let (user, id) = path_buf.rsplit_once('-').ok_or_else(|| { zone_zfs::Error::FileSystem(format!("Invalid FileSystem name: {:?}", file_system)) })?; let id = id.parse::().map_err(|err| { zone_zfs::Error::FileSystem(format!("Failed to parse container ID: {:?}", err)) })?; let template = PathBuf::from(file_system.dataset()) .parent() .ok_or_else(|| { zone_zfs::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) -> zone_nspawn::Result { // user, template, id let machine = value.machine.to_string_lossy(); let v: Vec<&str> = machine.split('-').collect(); Ok(Container { id: v[2].parse().map_err(|err| { zone_nspawn::Error::Parsing(format!("Failed to parse container id: {:?}", err)) })?, template: v[1].to_owned(), user: v[0].to_owned(), status: ContainerStatus::Running, }) } }