mod render;
mod unit;
mod warnings;
use crate::compose::types::ComposeFile;
use unit::{build_unit, container_unit, network_unit, volume_unit};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct QuadletUnit {
pub filename: String,
pub contents: String,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct QuadletOutput {
pub units: Vec<QuadletUnit>,
pub warnings: Vec<String>,
}
impl QuadletOutput {
pub fn duplicate_filename(&self) -> Option<&str> {
let mut seen = std::collections::HashSet::new();
self.units
.iter()
.find(|u| !seen.insert(u.filename.as_str()))
.map(|u| u.filename.as_str())
}
}
pub fn write_units(
dir: &std::path::Path,
units: &[QuadletUnit],
) -> std::io::Result<Vec<std::path::PathBuf>> {
std::fs::create_dir_all(dir)?;
let mut written = Vec::with_capacity(units.len());
for unit in units {
if std::path::Path::new(&unit.filename).file_name()
!= Some(std::ffi::OsStr::new(&unit.filename))
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("refusing unsafe quadlet unit file name: {}", unit.filename),
));
}
let path = dir.join(&unit.filename);
std::fs::write(&path, &unit.contents)?;
written.push(path);
}
Ok(written)
}
pub fn generate(file: &ComposeFile, project: &str) -> QuadletOutput {
generate_at(file, project, &std::env::current_dir().unwrap_or_default())
}
pub fn generate_at(file: &ComposeFile, project: &str, base_dir: &std::path::Path) -> QuadletOutput {
let mut out = QuadletOutput::default();
for (name, cfg) in &file.networks {
if cfg.as_ref().is_some_and(|c| c.external == Some(true)) {
continue;
}
out.units.push(network_unit(name, project, cfg.as_ref()));
if let Some(c) = cfg {
if let Some(ipam) = &c.ipam {
if !ipam.options.is_empty() {
out.warnings.push(format!(
"network '{name}': ipam.options have no Quadlet key and are not emitted; \
the live engine forwards them but `generate` cannot"
));
}
}
}
}
for (name, cfg) in &file.volumes {
if cfg.as_ref().is_some_and(|c| c.external == Some(true)) {
continue;
}
out.units.push(volume_unit(name, project, cfg.as_ref()));
}
let declared_volumes: Vec<&str> = file
.volumes
.iter()
.filter(|(_, cfg)| cfg.as_ref().is_none_or(|c| c.external != Some(true)))
.map(|(name, _)| name.as_str())
.collect();
let declared_networks: Vec<&str> = file
.networks
.iter()
.filter(|(_, cfg)| cfg.as_ref().is_none_or(|c| c.external != Some(true)))
.map(|(name, _)| name.as_str())
.collect();
for (name, service) in &file.services {
if let Some(unit) = build_unit(name, project, service, base_dir, &mut out.warnings) {
out.units.push(unit);
}
out.units.push(container_unit(
name,
project,
service,
&declared_volumes,
&declared_networks,
&file.secrets,
&mut out.warnings,
));
}
out
}
#[cfg(test)]
mod tests;