From 326cb6aee605ea6de7c76e333dcc18af309ccdb7 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Sat, 7 Mar 2026 12:15:17 -0600 Subject: [PATCH 01/16] chore: add Podman v5.5 to Podman versions Added Podman version 5.5.0, 5.5.1, and 5.5.2 to `podlet::quadlet::PodmanVersion`. Signed-off-by: Paul Nettleton --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/quadlet.rs | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02d0b46..5592b67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "podlet" -version = "0.3.2-alpha.2" +version = "0.3.2-alpha.3" dependencies = [ "clap", "color-eyre", diff --git a/Cargo.toml b/Cargo.toml index 15b7bc5..b57b95f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "podlet" -version = "0.3.2-alpha.2" +version = "0.3.2-alpha.3" authors = ["Paul Nettleton "] edition = "2024" rust-version = "1.85" diff --git a/src/quadlet.rs b/src/quadlet.rs index 2a3b5c0..09b5132 100644 --- a/src/quadlet.rs +++ b/src/quadlet.rs @@ -597,13 +597,17 @@ pub enum PodmanVersion { V5_3, /// Podman v5.4 - #[value(name = "5.4", aliases = ["latest", "5.4.0", "5.4.1", "5.4.2"])] + #[value(name = "5.4", aliases = ["5.4.0", "5.4.1", "5.4.2"])] V5_4, + + /// Podman v5.5 + #[value(name = "5.5", aliases = ["latest", "5.5.0", "5.5.1", "5.5.2"])] + V5_5, } impl PodmanVersion { /// Latest supported version of Podman with regards to Quadlet. - pub const LATEST: Self = Self::V5_4; + pub const LATEST: Self = Self::V5_5; /// Podman version as a static string slice. pub const fn as_str(self) -> &'static str { @@ -618,6 +622,7 @@ impl PodmanVersion { Self::V5_2 => "5.2", Self::V5_3 => "5.3", Self::V5_4 => "5.4", + Self::V5_5 => "5.5", } } } From 5619e766411de225d1f1a15a32c50a5c878f85ab Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Sun, 8 Mar 2026 14:20:34 -0500 Subject: [PATCH 02/16] feat(container): add `artifact` mount type Added `podlet::quadlet::container::mount::Mount::Artifact` variant. Added `podlet::quadlet::container::mount::{Artifact, DigestOrTitle}` struct and enum respectively. Used a flattened version of `Artifact` (`podlet::quadlet::container::mount::ArtifactFlat`) for (de)serilization as `digest` and `title` mount options cannot both be used. Signed-off-by: Paul Nettleton --- src/quadlet/container.rs | 14 +++ src/quadlet/container/mount.rs | 156 ++++++++++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 2 deletions(-) diff --git a/src/quadlet/container.rs b/src/quadlet/container.rs index 858322b..8324042 100644 --- a/src/quadlet/container.rs +++ b/src/quadlet/container.rs @@ -306,6 +306,20 @@ pub struct Container { impl Downgrade for Container { fn downgrade(&mut self, version: PodmanVersion) -> Result<(), DowngradeError> { + if version < PodmanVersion::V5_5 { + self.mount.iter().try_for_each(|mount| { + if let Mount::Artifact(_) = mount { + Err(DowngradeError::Option { + quadlet_option: "Mount", + value: mount.to_string(), + supported_version: PodmanVersion::V5_5, + }) + } else { + Ok(()) + } + })?; + } + if version < PodmanVersion::V5_4 { self.mount.iter().try_for_each(|mount| { if let Mount::Volume(mount::Volume { diff --git a/src/quadlet/container/mount.rs b/src/quadlet/container/mount.rs index 82511e6..5d9e982 100644 --- a/src/quadlet/container/mount.rs +++ b/src/quadlet/container/mount.rs @@ -38,6 +38,7 @@ pub use self::{idmap::Idmap, tmpfs::Tmpfs}; #[derive(Serialize, Debug, Clone, PartialEq, Eq)] #[serde(tag = "type", rename_all = "lowercase")] pub enum Mount { + Artifact(Artifact), Bind(Bind), DevPts(DevPts), Glob(Bind), @@ -51,6 +52,7 @@ pub enum Mount { #[derive(Deserialize, Debug, Clone, Copy)] #[serde(rename_all = "lowercase")] enum MountType { + Artifact, Bind, DevPts, Glob, @@ -77,6 +79,7 @@ impl MountType { let map = MapAccessDeserializer::new(map); match_type_deserialize! { self, map, + Artifact => Artifact, Bind => Bind, DevPts => DevPts, Glob => Bind, @@ -120,7 +123,7 @@ impl<'de> Visitor<'de> for MountVisitor { impl Display for Mount { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let mount = mount_options::to_string(self).map_err(|_| fmt::Error)?; + let mount = mount_options::to_string(self).expect("mount serialization cannot fail"); f.write_str(&mount) } } @@ -155,7 +158,8 @@ impl HostPaths for Mount { fn host_paths(&mut self) -> impl Iterator { match self { Self::Bind(bind) | Self::Glob(bind) => Some(&mut bind.source), - Self::DevPts(_) + Self::Artifact(_) + | Self::DevPts(_) | Self::Image(_) | Self::Ramfs(_) | Self::Tmpfs(_) @@ -165,6 +169,102 @@ impl HostPaths for Mount { } } +/// Artifact type [`Mount`]. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(into = "ArtifactFlat", try_from = "ArtifactFlat")] +pub struct Artifact { + /// Mount source spec. + pub source: String, + + /// Mount destination spec. + pub destination: PathBuf, + + /// If the artifact contains multiple blobs, the digest or title of the blob to use. + pub digest_or_title: Option, +} + +/// A flattened version of [`Artifact`] for (de)serialization. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct ArtifactFlat { + /// Mount source spec. + #[serde(alias = "src")] + source: String, + + /// Mount destination spec. + #[serde(alias = "dst", alias = "target")] + destination: PathBuf, + + /// If the artifact contains multiple blobs, the digest to use. + #[serde(default, skip_serializing_if = "Option::is_none")] + digest: Option, + + /// If the artifact contains multiple blobs, the title to use. + #[serde(default, skip_serializing_if = "Option::is_none")] + title: Option, +} + +impl From for ArtifactFlat { + fn from( + Artifact { + source, + destination, + digest_or_title, + }: Artifact, + ) -> Self { + let (digest, title) = match digest_or_title { + Some(DigestOrTitle::Digest(digest)) => (Some(digest), None), + Some(DigestOrTitle::Title(title)) => (None, Some(title)), + None => (None, None), + }; + + Self { + source, + destination, + digest, + title, + } + } +} + +impl TryFrom for Artifact { + type Error = &'static str; + + fn try_from( + ArtifactFlat { + source, + destination, + digest, + title, + }: ArtifactFlat, + ) -> Result { + let digest_or_title = match (digest, title) { + (Some(digest), None) => Some(DigestOrTitle::Digest(digest)), + (None, Some(title)) => Some(DigestOrTitle::Title(title)), + (None, None) => None, + (Some(_), Some(_)) => return Err("cannot set both `digest` and `title`"), + }; + + Ok(Self { + source, + destination, + digest_or_title, + }) + } +} + +/// The `digest` or `title` field of the [`Artifact`] [`Mount`] type. +/// +/// Used if an artifact contains more than one blob to select the blob to mount. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DigestOrTitle { + /// The digest to select the artifact blob with. + Digest(String), + + /// The title to select the artifact blob with. + Title(String), +} + /// Bind or glob type [`Mount`]. #[allow(clippy::struct_field_names, clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] @@ -446,6 +546,58 @@ impl Volume { mod tests { use super::*; + #[test] + fn artifact() -> Result<(), ParseMountError> { + // No digest or title + let string = "type=artifact,source=artifact,destination=/dst"; + let mount: Mount = string.parse()?; + assert_eq!( + mount, + Mount::Artifact(Artifact { + source: "artifact".to_owned(), + destination: "/dst".into(), + digest_or_title: None + }) + ); + assert_eq!(mount.to_string(), string); + + // Digest + let string = "type=artifact,source=artifact,destination=/dst,digest=digest"; + let mount: Mount = string.parse()?; + assert_eq!( + mount, + Mount::Artifact(Artifact { + source: "artifact".to_owned(), + destination: "/dst".into(), + digest_or_title: Some(DigestOrTitle::Digest("digest".to_owned())), + }) + ); + assert_eq!(mount.to_string(), string); + + // Title + let string = "type=artifact,source=artifact,destination=/dst,title=title"; + let mount: Mount = string.parse()?; + assert_eq!( + mount, + Mount::Artifact(Artifact { + source: "artifact".to_owned(), + destination: "/dst".into(), + digest_or_title: Some(DigestOrTitle::Title("title".to_owned())), + }) + ); + assert_eq!(mount.to_string(), string); + + // Digest and title is error + assert!( + Mount::from_str( + "type=artifact,source=artifact,destination=/dst,digest=digest,title=title" + ) + .is_err() + ); + + Ok(()) + } + #[test] fn bind() -> Result<(), ParseMountError> { let string = "type=bind,source=/src,destination=/dst"; From b29bcaceb060ccd7906e8453585d9c3860814eed Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Sun, 8 Mar 2026 14:47:00 -0500 Subject: [PATCH 03/16] refactor: push downgrade errors into functions In `podlet::quadlet::container`, moved the creation of `DowngradeError`s from `impl Downgrade for Container` to the `Container::remove_v*_*_options()` functions. The `clippy::too_many_lines` lint was triggering for `::downgrade()`. Signed-off-by: Paul Nettleton --- src/quadlet/container.rs | 202 +++++++++++++++++++++------------------ 1 file changed, 109 insertions(+), 93 deletions(-) diff --git a/src/quadlet/container.rs b/src/quadlet/container.rs index 8324042..b322565 100644 --- a/src/quadlet/container.rs +++ b/src/quadlet/container.rs @@ -307,82 +307,15 @@ pub struct Container { impl Downgrade for Container { fn downgrade(&mut self, version: PodmanVersion) -> Result<(), DowngradeError> { if version < PodmanVersion::V5_5 { - self.mount.iter().try_for_each(|mount| { - if let Mount::Artifact(_) = mount { - Err(DowngradeError::Option { - quadlet_option: "Mount", - value: mount.to_string(), - supported_version: PodmanVersion::V5_5, - }) - } else { - Ok(()) - } - })?; + self.remove_v5_5_options()?; } if version < PodmanVersion::V5_4 { - self.mount.iter().try_for_each(|mount| { - if let Mount::Volume(mount::Volume { - subpath: Some(_), .. - }) = mount - { - Err(DowngradeError::Option { - quadlet_option: "Mount", - value: mount.to_string(), - supported_version: PodmanVersion::V5_4, - }) - } else { - Ok(()) - } - })?; + self.remove_v5_4_options()?; } if version < PodmanVersion::V5_3 { - if let Some(health_log_destination) = self.health_log_destination.take() { - return Err(DowngradeError::Option { - quadlet_option: "HealthLogDestination", - value: health_log_destination, - supported_version: PodmanVersion::V5_3, - }); - } - - if let Some(health_max_log_count) = self.health_max_log_count.take() { - return Err(DowngradeError::Option { - quadlet_option: "HealthMaxLogCount", - value: health_max_log_count.to_string(), - supported_version: PodmanVersion::V5_3, - }); - } - - if let Some(health_max_log_size) = self.health_max_log_size.take() { - return Err(DowngradeError::Option { - quadlet_option: "HealthMaxLogSize", - value: health_max_log_size.to_string(), - supported_version: PodmanVersion::V5_3, - }); - } - - if !self.start_with_pod { - return Err(DowngradeError::Option { - quadlet_option: "StartWithPod", - value: "false".to_owned(), - supported_version: PodmanVersion::V5_3, - }); - } - - self.network.iter().try_for_each(|network| { - if network.ends_with(".container") { - Err(DowngradeError::Option { - quadlet_option: "Network", - value: network.clone(), - supported_version: PodmanVersion::V5_3, - }) - } else { - Ok(()) - } - })?; - - self.remove_v5_3_options(); + self.remove_v5_3_options()?; } if version < PodmanVersion::V5_2 { @@ -394,27 +327,7 @@ impl Downgrade for Container { } if version < PodmanVersion::V5_0 { - if self.notify.is_healthy() { - if version < PodmanVersion::V4_7 { - return Err(DowngradeError::Option { - quadlet_option: "Notify", - value: "healthy".to_owned(), - supported_version: PodmanVersion::V4_7, - }); - } - self.notify = Notify::default(); - self.push_arg("sdnotify", "healthy"); - } - - if let Some(pod) = self.pod.take() { - return Err(DowngradeError::Option { - quadlet_option: "Pod", - value: pod, - supported_version: PodmanVersion::V5_0, - }); - } - - self.remove_v5_0_options(); + self.remove_v5_0_options(version)?; } if version < PodmanVersion::V4_8 { @@ -447,8 +360,87 @@ macro_rules! extract { } impl Container { + /// Remove Quadlet options added in Podman v5.5.0 + fn remove_v5_5_options(&mut self) -> Result<(), DowngradeError> { + self.mount.iter().try_for_each(|mount| { + if let Mount::Artifact(_) = mount { + Err(DowngradeError::Option { + quadlet_option: "Mount", + value: mount.to_string(), + supported_version: PodmanVersion::V5_5, + }) + } else { + Ok(()) + } + })?; + + Ok(()) + } + + /// Remove Quadlet options added in Podman v5.4.0 + fn remove_v5_4_options(&mut self) -> Result<(), DowngradeError> { + self.mount.iter().try_for_each(|mount| { + if let Mount::Volume(mount::Volume { + subpath: Some(_), .. + }) = mount + { + Err(DowngradeError::Option { + quadlet_option: "Mount", + value: mount.to_string(), + supported_version: PodmanVersion::V5_4, + }) + } else { + Ok(()) + } + }) + } + /// Remove Quadlet options added in Podman v5.3.0 - fn remove_v5_3_options(&mut self) { + fn remove_v5_3_options(&mut self) -> Result<(), DowngradeError> { + if let Some(health_log_destination) = self.health_log_destination.take() { + return Err(DowngradeError::Option { + quadlet_option: "HealthLogDestination", + value: health_log_destination, + supported_version: PodmanVersion::V5_3, + }); + } + + if let Some(health_max_log_count) = self.health_max_log_count.take() { + return Err(DowngradeError::Option { + quadlet_option: "HealthMaxLogCount", + value: health_max_log_count.to_string(), + supported_version: PodmanVersion::V5_3, + }); + } + + if let Some(health_max_log_size) = self.health_max_log_size.take() { + return Err(DowngradeError::Option { + quadlet_option: "HealthMaxLogSize", + value: health_max_log_size.to_string(), + supported_version: PodmanVersion::V5_3, + }); + } + + if !self.start_with_pod { + return Err(DowngradeError::Option { + quadlet_option: "StartWithPod", + value: "false".to_owned(), + supported_version: PodmanVersion::V5_3, + }); + } + + self.network.iter().try_for_each(|network| { + if network.ends_with(".container") { + Err(DowngradeError::Option { + quadlet_option: "Network", + value: network.clone(), + supported_version: PodmanVersion::V5_3, + }) + } else { + Ok(()) + } + })?; + let options = extract!( self, OptionsV5_3 { @@ -459,6 +451,8 @@ impl Container { self.push_args(options) .expect("OptionsV5_3 serializable as args"); + + Ok(()) } /// Remove Quadlet options added in Podman v5.2.0 @@ -485,7 +479,27 @@ impl Container { } /// Remove Quadlet options added in Podman v5.0.0 - fn remove_v5_0_options(&mut self) { + fn remove_v5_0_options(&mut self, version: PodmanVersion) -> Result<(), DowngradeError> { + if self.notify.is_healthy() { + if version < PodmanVersion::V4_7 { + return Err(DowngradeError::Option { + quadlet_option: "Notify", + value: "healthy".to_owned(), + supported_version: PodmanVersion::V4_7, + }); + } + self.notify = Notify::default(); + self.push_arg("sdnotify", "healthy"); + } + + if let Some(pod) = self.pod.take() { + return Err(DowngradeError::Option { + quadlet_option: "Pod", + value: pod, + supported_version: PodmanVersion::V5_0, + }); + } + let options = extract!( self, OptionsV5_0 { @@ -496,6 +510,8 @@ impl Container { self.push_args(options) .expect("OptionsV5_0 serializable as args"); + + Ok(()) } /// Remove Quadlet options added in Podman v4.8.0 From 5722d59491807c2714d256964aac6dfec0807d29 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Sun, 8 Mar 2026 15:04:20 -0500 Subject: [PATCH 04/16] feat(container): add `Memory=` Quadlet option Signed-off-by: Paul Nettleton --- src/cli/container/compose.rs | 4 ++-- src/cli/container/podman.rs | 6 ------ src/cli/container/quadlet.rs | 10 ++++++++++ src/quadlet/container.rs | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/cli/container/compose.rs b/src/cli/container/compose.rs index 60e3740..fe04834 100644 --- a/src/cli/container/compose.rs +++ b/src/cli/container/compose.rs @@ -166,6 +166,7 @@ impl From for Service { labels, log_driver, log_options, + mem_limit, network_config, pids_limit, ports, @@ -198,7 +199,6 @@ impl From for Service { ipc, uts, mac_address, - mem_limit, mem_reservation, mem_swappiness, memswap_limit, @@ -324,6 +324,7 @@ pub struct Quadlet { pub labels: ListOrMap, pub log_driver: Option, pub log_options: IndexMap>, + pub mem_limit: Option, pub network_config: Option, pub pids_limit: Option>, pub ports: Ports, @@ -359,7 +360,6 @@ pub struct PodmanArgs { pub ipc: Option, pub uts: Option, pub mac_address: Option, - pub mem_limit: Option, pub mem_reservation: Option, pub mem_swappiness: Option, pub memswap_limit: Option>, diff --git a/src/cli/container/podman.rs b/src/cli/container/podman.rs index c78a082..e1e8fae 100644 --- a/src/cli/container/podman.rs +++ b/src/cli/container/podman.rs @@ -219,10 +219,6 @@ pub struct PodmanArgs { #[arg(long, value_name = "ADDRESS")] mac_address: Option, - /// Memory limit - #[arg(short, long, value_name = "NUMBER[UNIT]")] - memory: Option, - /// Memory soft limit #[arg(long, value_name = "NUMBER[UNIT]")] memory_reservation: Option, @@ -439,7 +435,6 @@ impl TryFrom for PodmanArgs { ipc, uts, mac_address, - mem_limit, mem_reservation, mem_swappiness, memswap_limit, @@ -504,7 +499,6 @@ impl TryFrom for PodmanArgs { .wrap_err("`ipc` invalid")?, uts: uts.as_ref().map(ToString::to_string), mac_address: mac_address.as_ref().map(ToString::to_string), - memory: mem_limit.as_ref().map(ToString::to_string), memory_reservation: mem_reservation.as_ref().map(ToString::to_string), memory_swap: memswap_limit, memory_swappiness: mem_swappiness.map(Into::into), diff --git a/src/cli/container/quadlet.rs b/src/cli/container/quadlet.rs index b0ebaed..ee0a271 100644 --- a/src/cli/container/quadlet.rs +++ b/src/cli/container/quadlet.rs @@ -296,6 +296,12 @@ pub struct QuadletOptions { #[arg(long, value_name = "NAME=VALUE")] log_opt: Vec, + /// Memory limit + /// + /// Converts to "Memory=NUMBER[UNIT]" + #[arg(short, long, value_name = "NUMBER[UNIT]")] + memory: Option, + /// Attach a filesystem mount to the container /// /// Converts to "Mount=MOUNT" @@ -548,6 +554,7 @@ impl From for crate::quadlet::Container { log_driver, log_opt, mount, + memory, network, network_alias, sdnotify: notify, @@ -626,6 +633,7 @@ impl From for crate::quadlet::Container { label, log_driver, log_opt, + memory, mount, network, network_alias, @@ -683,6 +691,7 @@ impl TryFrom for QuadletOptions { labels, log_driver, log_options, + mem_limit, network_config, pids_limit, ports, @@ -788,6 +797,7 @@ impl TryFrom for QuadletOptions { option }) .collect(), + memory: mem_limit.as_ref().map(ToString::to_string), network: network_config .map(network_config_try_into_network_options) .transpose() diff --git a/src/quadlet/container.rs b/src/quadlet/container.rs index b322565..6ae24bd 100644 --- a/src/quadlet/container.rs +++ b/src/quadlet/container.rs @@ -169,6 +169,9 @@ pub struct Container { #[serde(serialize_with = "seq_quote_whitespace")] pub mask: Vec, + /// Specify the amount of memory for the container. + pub memory: Option, + /// Attach a filesystem mount to the container. #[serde(serialize_with = "serialize_display_seq")] pub mount: Vec, @@ -374,6 +377,11 @@ impl Container { } })?; + let options = extract!(self, OptionsV5_5 { memory }); + + self.push_args(options) + .expect("OptionsV5_5 serializable as args"); + Ok(()) } @@ -643,6 +651,13 @@ impl Container { } } +/// Container Quadlet options added in Podman v5.5.0 +#[derive(Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +struct OptionsV5_5 { + memory: Option, +} + /// Container Quadlet options added in Podman v5.3.0 #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] From e1f4f2ba887c1f5a76589de1cd3738311329b8f1 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Sun, 8 Mar 2026 15:17:48 -0500 Subject: [PATCH 05/16] feat(container): add `Retry=` Quadlet option Signed-off-by: Paul Nettleton --- src/cli/container/podman.rs | 6 ------ src/cli/container/quadlet.rs | 10 ++++++++++ src/quadlet/container.rs | 6 +++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/cli/container/podman.rs b/src/cli/container/podman.rs index e1e8fae..3b86374 100644 --- a/src/cli/container/podman.rs +++ b/src/cli/container/podman.rs @@ -326,12 +326,6 @@ pub struct PodmanArgs { #[arg(long, value_name = "CONTAINER[,...]")] requires: Option, - /// Number of times to retry pulling or pushing images between the registry and local storage - /// - /// Default is 3 - #[arg(long, value_name = "ATTEMPTS")] - retry: Option, - /// Duration of delay between retry attempts when pulling or pushing images /// /// Default is to start at two seconds and then exponentially back off diff --git a/src/cli/container/quadlet.rs b/src/cli/container/quadlet.rs index ee0a271..524e54d 100644 --- a/src/cli/container/quadlet.rs +++ b/src/cli/container/quadlet.rs @@ -375,6 +375,14 @@ pub struct QuadletOptions { #[default = true] read_only_tmpfs: bool, + /// Number of times to retry pulling or pushing images between the registry and local storage + /// + /// Converts to "Retry=ATTEMPTS" + /// + /// Default is 3 + #[arg(long, value_name = "ATTEMPTS")] + retry: Option, + /// The rootfs to use for the container /// /// Converts to "Rootfs=PATH" @@ -563,6 +571,7 @@ impl From for crate::quadlet::Container { pull, read_only, read_only_tmpfs, + retry, rootfs, init: run_init, secret, @@ -643,6 +652,7 @@ impl From for crate::quadlet::Container { pull, read_only, read_only_tmpfs, + retry, rootfs, run_init, secret, diff --git a/src/quadlet/container.rs b/src/quadlet/container.rs index 6ae24bd..5ffa3c6 100644 --- a/src/quadlet/container.rs +++ b/src/quadlet/container.rs @@ -217,6 +217,9 @@ pub struct Container { #[default = true] pub read_only_tmpfs: bool, + /// Number of times to retry the image pull when a HTTP error occurs. + pub retry: Option, + /// The rootfs to use for the container. pub rootfs: Option, @@ -377,7 +380,7 @@ impl Container { } })?; - let options = extract!(self, OptionsV5_5 { memory }); + let options = extract!(self, OptionsV5_5 { memory, retry }); self.push_args(options) .expect("OptionsV5_5 serializable as args"); @@ -656,6 +659,7 @@ impl Container { #[serde(rename_all = "kebab-case")] struct OptionsV5_5 { memory: Option, + retry: Option, } /// Container Quadlet options added in Podman v5.3.0 From f1f7ae773ee7d439a2b3dc063a1d34b7104637d3 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Sun, 8 Mar 2026 15:23:12 -0500 Subject: [PATCH 06/16] feat(container): add `RetryDelay=` Quadlet option Signed-off-by: Paul Nettleton --- src/cli/container/podman.rs | 6 ------ src/cli/container/quadlet.rs | 10 ++++++++++ src/quadlet/container.rs | 13 ++++++++++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/cli/container/podman.rs b/src/cli/container/podman.rs index 3b86374..78d3a44 100644 --- a/src/cli/container/podman.rs +++ b/src/cli/container/podman.rs @@ -326,12 +326,6 @@ pub struct PodmanArgs { #[arg(long, value_name = "CONTAINER[,...]")] requires: Option, - /// Duration of delay between retry attempts when pulling or pushing images - /// - /// Default is to start at two seconds and then exponentially back off - #[arg(long, value_name = "DURATION")] - retry_delay: Option, - /// Remove container (and pod if created) after exit /// /// Automatically set by Quadlet diff --git a/src/cli/container/quadlet.rs b/src/cli/container/quadlet.rs index 524e54d..bdfcd5c 100644 --- a/src/cli/container/quadlet.rs +++ b/src/cli/container/quadlet.rs @@ -383,6 +383,14 @@ pub struct QuadletOptions { #[arg(long, value_name = "ATTEMPTS")] retry: Option, + /// Duration of delay between retry attempts when pulling or pushing images + /// + /// Converts to "RetryDelay=DURATION" + /// + /// Default is to start at two seconds and then exponentially back off + #[arg(long, value_name = "DURATION")] + retry_delay: Option, + /// The rootfs to use for the container /// /// Converts to "Rootfs=PATH" @@ -572,6 +580,7 @@ impl From for crate::quadlet::Container { read_only, read_only_tmpfs, retry, + retry_delay, rootfs, init: run_init, secret, @@ -653,6 +662,7 @@ impl From for crate::quadlet::Container { read_only, read_only_tmpfs, retry, + retry_delay, rootfs, run_init, secret, diff --git a/src/quadlet/container.rs b/src/quadlet/container.rs index 5ffa3c6..ba14ec0 100644 --- a/src/quadlet/container.rs +++ b/src/quadlet/container.rs @@ -220,6 +220,9 @@ pub struct Container { /// Number of times to retry the image pull when a HTTP error occurs. pub retry: Option, + /// Delay between retries. + pub retry_delay: Option, + /// The rootfs to use for the container. pub rootfs: Option, @@ -380,7 +383,14 @@ impl Container { } })?; - let options = extract!(self, OptionsV5_5 { memory, retry }); + let options = extract!( + self, + OptionsV5_5 { + memory, + retry, + retry_delay, + } + ); self.push_args(options) .expect("OptionsV5_5 serializable as args"); @@ -660,6 +670,7 @@ impl Container { struct OptionsV5_5 { memory: Option, retry: Option, + retry_delay: Option, } /// Container Quadlet options added in Podman v5.3.0 From fc0b6e7e03071ec8b078ad8409d9fa0d978e05fe Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Sun, 8 Mar 2026 19:22:17 -0500 Subject: [PATCH 07/16] feat(pod): add `HostName=` Quadlet option Signed-off-by: Paul Nettleton --- src/cli/pod.rs | 12 ++++++++---- src/quadlet/pod.rs | 9 +++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/cli/pod.rs b/src/cli/pod.rs index f010580..bd4feb0 100644 --- a/src/cli/pod.rs +++ b/src/cli/pod.rs @@ -117,6 +117,12 @@ pub struct Create { )] gidmap: Vec, + /// Set the hostname of the pod. + /// + /// Converts to "HostName=NAME". + #[arg(long, value_name = "NAME")] + hostname: Option, + /// Specify a static IPv4 address for the pod. /// /// Converts to "IP=IPV4". @@ -248,6 +254,7 @@ impl From for quadlet::Pod { dns_option, dns_search, gidmap, + hostname, ip, ip6, network, @@ -273,6 +280,7 @@ impl From for quadlet::Pod { dns_option, dns_search, gid_map: gidmap, + host_name: hostname, ip, ip6, network, @@ -356,10 +364,6 @@ struct PodmanArgs { #[arg(long, value_name = "ENTRY")] gpus: Vec, - /// Set the hostname of the pod. - #[arg(long, value_name = "NAME")] - hostname: Option, - /// Base file to create the `/etc/hosts` file inside the pod's containers. #[arg(long, value_name = "PATH | none | image")] hosts_file: Option, diff --git a/src/quadlet/pod.rs b/src/quadlet/pod.rs index a6bb0b7..d89a9f7 100644 --- a/src/quadlet/pod.rs +++ b/src/quadlet/pod.rs @@ -33,6 +33,9 @@ pub struct Pod { #[serde(rename = "GIDMap")] pub gid_map: Vec, + /// Set the pod’s hostname inside all containers. + pub host_name: Option, + /// Specify a static IPv4 address for the pod. #[serde(rename = "IP")] pub ip: Option, @@ -91,6 +94,12 @@ impl HostPaths for Pod { impl Downgrade for Pod { fn downgrade(&mut self, version: PodmanVersion) -> Result<(), DowngradeError> { + if version < PodmanVersion::V5_5 { + if let Some(host_name) = self.host_name.take() { + self.push_arg("hostname", &host_name); + } + } + if version < PodmanVersion::V5_4 { if let Some(shm_size) = self.shm_size.take() { self.push_arg("shm-size", &shm_size); From b1137ad1ecf26dcd74295d56ee42ade34287e080 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Sun, 8 Mar 2026 23:08:38 -0500 Subject: [PATCH 08/16] refactor: unify `push_arg()` impls Added `podlet::quadlet::{push_arg(), push_arg_display()}` based on `podlet::quadlet::Pod::push_arg()`. Changed `podlet::quadlet::{Container, Kube, Network, Pod, Volume}::push_arg()` to use the new functions. Signed-off-by: Paul Nettleton --- src/quadlet.rs | 32 +++++++++++++++++++++++++++++++- src/quadlet/container.rs | 9 ++++++--- src/quadlet/kube.rs | 12 +++--------- src/quadlet/network.rs | 12 +++--------- src/quadlet/pod.rs | 17 +++-------------- src/quadlet/volume.rs | 12 +++--------- 6 files changed, 49 insertions(+), 45 deletions(-) diff --git a/src/quadlet.rs b/src/quadlet.rs index 09b5132..a2b6bd3 100644 --- a/src/quadlet.rs +++ b/src/quadlet.rs @@ -12,7 +12,7 @@ mod volume; use std::{ collections::HashSet, - fmt::{self, Display, Formatter}, + fmt::{self, Display, Formatter, Write}, iter, path::PathBuf, str::FromStr, @@ -780,3 +780,33 @@ impl HostPaths for Context { .into_iter() } } + +/// Add "--{flag} {arg}" to `podman_args`, quoting whitespace in `arg`. +fn push_arg(podman_args: &mut String, flag: &str, arg: &str) { + if !podman_args.is_empty() { + podman_args.push(' '); + } + + podman_args.push_str("--"); + podman_args.push_str(flag); + podman_args.push(' '); + + if arg.contains(char::is_whitespace) { + podman_args.push('"'); + podman_args.push_str(arg); + podman_args.push('"'); + } else { + podman_args.push_str(arg); + } +} + +/// Add "--{flag} {arg}" to `podman_args` using `arg`'s [`Display`] implementation. +/// +/// Prefer using [`push_arg()`] as it quotes whtiespace. +fn push_arg_display(podman_args: &mut String, flag: &str, arg: impl Display) { + if !podman_args.is_empty() { + podman_args.push(' '); + } + + write!(podman_args, "--{flag} {arg}").expect("write to String cannot fail"); +} diff --git a/src/quadlet/container.rs b/src/quadlet/container.rs index ba14ec0..c34afb9 100644 --- a/src/quadlet/container.rs +++ b/src/quadlet/container.rs @@ -22,7 +22,7 @@ use crate::serde::{quadlet::seq_quote_whitespace, serialize_display_seq, skip_tr pub use self::{device::Device, mount::Mount, rootfs::Rootfs, volume::Volume}; -use super::{AutoUpdate, Downgrade, DowngradeError, HostPaths, PodmanVersion}; +use super::{AutoUpdate, Downgrade, DowngradeError, HostPaths, PodmanVersion, push_arg_display}; #[allow(clippy::struct_excessive_bools)] #[derive(Serialize, SmartDefault, Debug, Clone, PartialEq)] @@ -650,13 +650,16 @@ impl Container { } /// Add `--{flag} {arg}` to `PodmanArgs=`. + /// + /// Ensure `arg` does not contain whitespace. fn push_arg(&mut self, flag: &str, arg: impl Display) { - self.podman_args_push_str(&format!("--{flag} {arg}")); + let podman_args = self.podman_args.get_or_insert_default(); + push_arg_display(podman_args, flag, arg); } /// Push `string` to `podman_args`, adding a space if needed. fn podman_args_push_str(&mut self, string: &str) { - let podman_args = self.podman_args.get_or_insert_with(String::new); + let podman_args = self.podman_args.get_or_insert_default(); if !podman_args.is_empty() { podman_args.push(' '); } diff --git a/src/quadlet/kube.rs b/src/quadlet/kube.rs index a194727..ae54702 100644 --- a/src/quadlet/kube.rs +++ b/src/quadlet/kube.rs @@ -9,7 +9,7 @@ use std::{ use serde::{Serialize, Serializer}; use url::Url; -use super::{Downgrade, DowngradeError, HostPaths, PodmanVersion}; +use super::{Downgrade, DowngradeError, HostPaths, PodmanVersion, push_arg}; #[derive(Serialize, Debug, Clone, PartialEq)] #[serde(rename_all = "PascalCase")] @@ -59,14 +59,8 @@ impl Kube { /// Add `--{flag} {arg}` to `PodmanArgs=`. fn push_arg(&mut self, flag: &str, arg: &str) { - let podman_args = self.podman_args.get_or_insert_with(String::new); - if !podman_args.is_empty() { - podman_args.push(' '); - } - podman_args.push_str("--"); - podman_args.push_str(flag); - podman_args.push(' '); - podman_args.push_str(arg); + let podman_args = self.podman_args.get_or_insert_default(); + push_arg(podman_args, flag, arg); } } diff --git a/src/quadlet/network.rs b/src/quadlet/network.rs index efd99d4..0cfe1c7 100644 --- a/src/quadlet/network.rs +++ b/src/quadlet/network.rs @@ -12,7 +12,7 @@ use thiserror::Error; use crate::serde::quadlet::seq_quote_whitespace; -use super::{Downgrade, DowngradeError, PodmanVersion}; +use super::{Downgrade, DowngradeError, PodmanVersion, push_arg}; #[derive(Serialize, Debug, Default, Clone, PartialEq)] #[serde(rename_all = "PascalCase")] @@ -65,14 +65,8 @@ pub struct Network { impl Network { /// Add `--{flag} {arg}` to `PodmanArgs=`. fn push_arg(&mut self, flag: &str, arg: &str) { - let podman_args = self.podman_args.get_or_insert_with(String::new); - if !podman_args.is_empty() { - podman_args.push(' '); - } - podman_args.push_str("--"); - podman_args.push_str(flag); - podman_args.push(' '); - podman_args.push_str(arg); + let podman_args = self.podman_args.get_or_insert_default(); + push_arg(podman_args, flag, arg); } } diff --git a/src/quadlet/pod.rs b/src/quadlet/pod.rs index d89a9f7..7359db5 100644 --- a/src/quadlet/pod.rs +++ b/src/quadlet/pod.rs @@ -8,6 +8,7 @@ use serde::Serialize; use super::{ Downgrade, DowngradeError, HostPaths, PodmanVersion, ResourceKind, container::{Dns, Volume}, + push_arg, }; /// Options for the \[Pod\] section of a `.pod` Quadlet file. @@ -182,19 +183,7 @@ impl Pod { /// Add `--{flag} {arg}` to `PodmanArgs=`. fn push_arg(&mut self, flag: &str, arg: &str) { - let podman_args = self.podman_args.get_or_insert_with(String::new); - if !podman_args.is_empty() { - podman_args.push(' '); - } - podman_args.push_str("--"); - podman_args.push_str(flag); - podman_args.push(' '); - if arg.contains(char::is_whitespace) { - podman_args.push('"'); - podman_args.push_str(arg); - podman_args.push('"'); - } else { - podman_args.push_str(arg); - } + let podman_args = self.podman_args.get_or_insert_default(); + push_arg(podman_args, flag, arg); } } diff --git a/src/quadlet/volume.rs b/src/quadlet/volume.rs index 418b908..f09b298 100644 --- a/src/quadlet/volume.rs +++ b/src/quadlet/volume.rs @@ -5,7 +5,7 @@ use serde::Serialize; use crate::{cli::volume::Opt, serde::quadlet::seq_quote_whitespace}; -use super::{Downgrade, DowngradeError, HostPaths, PodmanVersion}; +use super::{Downgrade, DowngradeError, HostPaths, PodmanVersion, push_arg}; #[derive(Serialize, Debug, Default, Clone, PartialEq)] #[serde(rename_all = "PascalCase")] @@ -55,14 +55,8 @@ impl HostPaths for Volume { impl Volume { /// Add `--{flag} {arg}` to `PodmanArgs=`. fn push_arg(&mut self, flag: &str, arg: &str) { - let podman_args = self.podman_args.get_or_insert_with(String::new); - if !podman_args.is_empty() { - podman_args.push(' '); - } - podman_args.push_str("--"); - podman_args.push_str(flag); - podman_args.push(' '); - podman_args.push_str(arg); + let podman_args = self.podman_args.get_or_insert_default(); + push_arg(podman_args, flag, arg); } } From 68a20eb3f528c549be46fc5152574cf7aa610df9 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Mon, 9 Mar 2026 14:10:56 -0500 Subject: [PATCH 09/16] feat(image): add `Retry=` Quadlet option Signed-off-by: Paul Nettleton --- src/cli/image.rs | 11 +++++++++++ src/quadlet/image.rs | 29 ++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/cli/image.rs b/src/cli/image.rs index b409f80..7cfc4ca 100644 --- a/src/cli/image.rs +++ b/src/cli/image.rs @@ -107,6 +107,15 @@ pub struct Pull { #[arg(long, conflicts_with_all = ["os", "arch"], value_name = "OS/ARCH")] pub platform: Option, + /// Number of times to retry pulling or pushing images between the registry and local storage. + /// + /// Converts to "Retry=ATTEMPTS". + /// + /// Default is 3. + #[arg(long, value_name = "ATTEMPTS")] + #[arg(long)] + pub retry: Option, + /// Require HTTPS and verify certificates when contacting registries /// /// Converts to "TLSVerify=TLS_VERIFY" @@ -137,6 +146,7 @@ impl From for quadlet::Image { disable_content_trust: _, os, platform, + retry, tls_verify, variant, source: image, @@ -157,6 +167,7 @@ impl From for quadlet::Image { image_tag: None, os, podman_args: None, + retry, tls_verify, variant, } diff --git a/src/quadlet/image.rs b/src/quadlet/image.rs index fa9c1c3..b81b899 100644 --- a/src/quadlet/image.rs +++ b/src/quadlet/image.rs @@ -8,7 +8,7 @@ use std::{ use serde::{Serialize, Serializer}; -use super::{Downgrade, DowngradeError, HostPaths, PodmanVersion, ResourceKind}; +use super::{Downgrade, DowngradeError, HostPaths, PodmanVersion, ResourceKind, push_arg_display}; #[derive(Serialize, Debug, Clone, PartialEq)] #[serde(rename_all = "PascalCase")] @@ -49,6 +49,9 @@ pub struct Image { /// generated file. pub podman_args: Option, + /// Number of times to retry the image pull when a HTTP error occurs. + pub retry: Option, + /// Require HTTPS and verification of certificates when contacting registries. #[serde(rename = "TLSVerify")] pub tls_verify: Option, @@ -74,6 +77,20 @@ impl HostPaths for Image { impl Downgrade for Image { #[allow(clippy::unused_self)] fn downgrade(&mut self, version: PodmanVersion) -> Result<(), DowngradeError> { + if version < PodmanVersion::V5_5 { + if let Some(retry) = self.retry.take() { + // `podman image pull --retry` was added in Podman v5.0.0 + if version < PodmanVersion::V5_0 { + return Err(DowngradeError::Option { + quadlet_option: "Retry", + value: retry.to_string(), + supported_version: PodmanVersion::V5_0, + }); + } + self.push_arg_display("retry", retry); + } + } + if version < PodmanVersion::V4_8 { return Err(DowngradeError::Kind { kind: ResourceKind::Image, @@ -85,6 +102,16 @@ impl Downgrade for Image { } } +impl Image { + /// Add `--{flag} {arg}` to `PodmanArgs=`. + /// + /// Ensure `arg` does not contain whitespace. + fn push_arg_display(&mut self, flag: &str, arg: impl Display) { + let podman_args = self.podman_args.get_or_insert_default(); + push_arg_display(podman_args, flag, arg); + } +} + /// The key and optional passphrase for decryption of images. /// /// See the `--decryption-key` section of From 92b4cd21d66ec8025ae9b4d955b5f8aec630f020 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Mon, 9 Mar 2026 14:31:14 -0500 Subject: [PATCH 10/16] feat(image): add `RetryDelay=` Quadlet option Signed-off-by: Paul Nettleton --- src/cli/image.rs | 10 ++++++++++ src/quadlet/image.rs | 25 ++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/cli/image.rs b/src/cli/image.rs index 7cfc4ca..bcf45f4 100644 --- a/src/cli/image.rs +++ b/src/cli/image.rs @@ -116,6 +116,14 @@ pub struct Pull { #[arg(long)] pub retry: Option, + /// Duration of delay between retry attempts when pulling or pushing images. + /// + /// Converts to "RetryDelay=DURATION". + /// + /// Default is to start at two seconds and then exponentially back off. + #[arg(long, value_name = "DURATION")] + pub retry_delay: Option, + /// Require HTTPS and verify certificates when contacting registries /// /// Converts to "TLSVerify=TLS_VERIFY" @@ -147,6 +155,7 @@ impl From for quadlet::Image { os, platform, retry, + retry_delay, tls_verify, variant, source: image, @@ -168,6 +177,7 @@ impl From for quadlet::Image { os, podman_args: None, retry, + retry_delay, tls_verify, variant, } diff --git a/src/quadlet/image.rs b/src/quadlet/image.rs index b81b899..5cad27c 100644 --- a/src/quadlet/image.rs +++ b/src/quadlet/image.rs @@ -8,7 +8,9 @@ use std::{ use serde::{Serialize, Serializer}; -use super::{Downgrade, DowngradeError, HostPaths, PodmanVersion, ResourceKind, push_arg_display}; +use super::{ + Downgrade, DowngradeError, HostPaths, PodmanVersion, ResourceKind, push_arg, push_arg_display, +}; #[derive(Serialize, Debug, Clone, PartialEq)] #[serde(rename_all = "PascalCase")] @@ -52,6 +54,9 @@ pub struct Image { /// Number of times to retry the image pull when a HTTP error occurs. pub retry: Option, + /// Delay between retries. + pub retry_delay: Option, + /// Require HTTPS and verification of certificates when contacting registries. #[serde(rename = "TLSVerify")] pub tls_verify: Option, @@ -89,6 +94,18 @@ impl Downgrade for Image { } self.push_arg_display("retry", retry); } + + if let Some(retry_delay) = self.retry_delay.take() { + // `podman image pull --retry-delay` was added in Podman v5.0.0 + if version < PodmanVersion::V5_0 { + return Err(DowngradeError::Option { + quadlet_option: "RetryDelay", + value: retry_delay, + supported_version: PodmanVersion::V5_0, + }); + } + self.push_arg("retry-delay", &retry_delay); + } } if version < PodmanVersion::V4_8 { @@ -103,6 +120,12 @@ impl Downgrade for Image { } impl Image { + /// Add `--{flag} {arg}` to `PodmanArgs=`. + fn push_arg(&mut self, flag: &str, arg: &str) { + let podman_args = self.podman_args.get_or_insert_default(); + push_arg(podman_args, flag, arg); + } + /// Add `--{flag} {arg}` to `PodmanArgs=`. /// /// Ensure `arg` does not contain whitespace. From ed48de0f93fd0386a257ca8a9a33024151fd8e97 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Mon, 9 Mar 2026 23:49:29 -0500 Subject: [PATCH 11/16] feat(build): add `Retry=` Quadlet option Signed-off-by: Paul Nettleton --- src/cli/build.rs | 14 ++++++++++---- src/quadlet/build.rs | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/cli/build.rs b/src/cli/build.rs index f29cc94..429d8fe 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -148,6 +148,14 @@ pub struct Build { #[arg(long, value_name = "POLICY")] pull: Option, + /// Number of times to retry pulling images from the registry in case of failure. + /// + /// Converts to "Retry=ATTEMPTS". + /// + /// Default is 3. + #[arg(long, value_name = "ATTEMPTS")] + retry: Option, + /// Pass secret information in a safe way to the build container. /// /// Converts to "Secret=id=ID,src=PATH". @@ -221,6 +229,7 @@ impl From for quadlet::Build { label, network, pull, + retry, secret, target, tls_verify, @@ -248,6 +257,7 @@ impl From for quadlet::Build { network, podman_args: (!podman_args.is_empty()).then_some(podman_args), pull, + retry, secret, set_working_directory: context, target, @@ -673,10 +683,6 @@ struct PodmanArgs { #[serde(skip_serializing_if = "Not::not")] quiet: bool, - /// Number of times to retry pulling images from the registry in case of failure. - #[arg(long, value_name = "ATTEMPTS")] - retry: Option, - /// Duration of delay between retry attempts. #[arg(long, value_name = "DURATION")] retry_delay: Option, diff --git a/src/quadlet/build.rs b/src/quadlet/build.rs index 3e9096e..5aeaa00 100644 --- a/src/quadlet/build.rs +++ b/src/quadlet/build.rs @@ -14,6 +14,7 @@ use crate::serde::{quadlet::seq_quote_whitespace, skip_true}; use super::{ Downgrade, DowngradeError, HostPaths, PodmanVersion, ResourceKind, container::{Dns, PullPolicy}, + push_arg_display, }; /// Options for the \[Build\] section of a `.build` Quadlet file. @@ -73,6 +74,9 @@ pub struct Build { /// Set the image pull policy. pub pull: Option, + /// Number of times to retry the image pull when a HTTP error occurs. + pub retry: Option, + /// Pass secret information used in Containerfile build stages in a safe way. pub secret: Vec, @@ -105,6 +109,12 @@ impl HostPaths for Build { impl Downgrade for Build { fn downgrade(&mut self, version: PodmanVersion) -> Result<(), DowngradeError> { + if version < PodmanVersion::V5_5 { + if let Some(retry) = self.retry.take() { + self.push_arg_display("retry", retry); + } + } + if version < PodmanVersion::V5_3 && self.image_tag.len() > 1 { return Err(DowngradeError::Multiple { quadlet_option: "ImageTag", @@ -123,6 +133,16 @@ impl Downgrade for Build { } } +impl Build { + /// Add `--{flag} {arg}` to `PodmanArgs=`. + /// + /// Ensure `arg` does not contain whitespace. + fn push_arg_display(&mut self, flag: &str, arg: impl Display) { + let podman_args = self.podman_args.get_or_insert_default(); + push_arg_display(podman_args, flag, arg); + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Secret { id: String, From cdc709b5dd16ba54ea00227e5a4d47e5ac9ff141 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Mon, 9 Mar 2026 23:54:38 -0500 Subject: [PATCH 12/16] feat(build): add `RetryDelay=` Quadlet option Signed-off-by: Paul Nettleton --- src/cli/build.rs | 14 ++++++++++---- src/quadlet/build.rs | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cli/build.rs b/src/cli/build.rs index 429d8fe..32e5572 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -156,6 +156,14 @@ pub struct Build { #[arg(long, value_name = "ATTEMPTS")] retry: Option, + /// Duration of delay between retry attempts. + /// + /// Converts to "RetryDelay=DURATION". + /// + /// Default is to start at two seconds and then exponentially back off. + #[arg(long, value_name = "DURATION")] + retry_delay: Option, + /// Pass secret information in a safe way to the build container. /// /// Converts to "Secret=id=ID,src=PATH". @@ -230,6 +238,7 @@ impl From for quadlet::Build { network, pull, retry, + retry_delay, secret, target, tls_verify, @@ -258,6 +267,7 @@ impl From for quadlet::Build { podman_args: (!podman_args.is_empty()).then_some(podman_args), pull, retry, + retry_delay, secret, set_working_directory: context, target, @@ -683,10 +693,6 @@ struct PodmanArgs { #[serde(skip_serializing_if = "Not::not")] quiet: bool, - /// Duration of delay between retry attempts. - #[arg(long, value_name = "DURATION")] - retry_delay: Option, - /// Remove intermediate containers after a successful build. #[arg(long, action = ArgAction::Set, default_value_t = true)] #[serde(skip_serializing_if = "skip_true")] diff --git a/src/quadlet/build.rs b/src/quadlet/build.rs index 5aeaa00..6d84772 100644 --- a/src/quadlet/build.rs +++ b/src/quadlet/build.rs @@ -14,7 +14,7 @@ use crate::serde::{quadlet::seq_quote_whitespace, skip_true}; use super::{ Downgrade, DowngradeError, HostPaths, PodmanVersion, ResourceKind, container::{Dns, PullPolicy}, - push_arg_display, + push_arg, push_arg_display, }; /// Options for the \[Build\] section of a `.build` Quadlet file. @@ -77,6 +77,9 @@ pub struct Build { /// Number of times to retry the image pull when a HTTP error occurs. pub retry: Option, + /// Delay between retries. + pub retry_delay: Option, + /// Pass secret information used in Containerfile build stages in a safe way. pub secret: Vec, @@ -113,6 +116,10 @@ impl Downgrade for Build { if let Some(retry) = self.retry.take() { self.push_arg_display("retry", retry); } + + if let Some(retry_delay) = self.retry_delay.take() { + self.push_arg("retry-delay", &retry_delay); + } } if version < PodmanVersion::V5_3 && self.image_tag.len() > 1 { @@ -134,6 +141,12 @@ impl Downgrade for Build { } impl Build { + /// Add `--{flag} {arg}` to `PodmanArgs=`. + fn push_arg(&mut self, flag: &str, arg: &str) { + let podman_args = self.podman_args.get_or_insert_default(); + push_arg(podman_args, flag, arg); + } + /// Add `--{flag} {arg}` to `PodmanArgs=`. /// /// Ensure `arg` does not contain whitespace. From cab986d73c12d886077ecf25e7966b68052236ae Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Tue, 10 Mar 2026 00:08:35 -0500 Subject: [PATCH 13/16] feat(build): add `podman build --inherit-labels` option Signed-off-by: Paul Nettleton --- src/cli/build.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cli/build.rs b/src/cli/build.rs index 32e5572..f2f94f9 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -595,6 +595,12 @@ struct PodmanArgs { #[arg(long, value_name = "IMAGE_ID_FILE")] iidfile: Option, + /// Inherit the labels from the base image or base stages. + #[arg(long, action = ArgAction::Set, default_value_t = true)] + #[serde(skip_serializing_if = "skip_true")] + #[default = true] + inherit_labels: bool, + /// Sets the configuration for IPC namespaces when handling `RUN` instructions. #[arg(long, value_name = "HOW")] ipc: Option, From ed7427517a59505541c13be50dc319b1af8510b9 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Tue, 10 Mar 2026 00:33:44 -0500 Subject: [PATCH 14/16] feat(install): add `podlet --upheld-by` option Signed-off-by: Paul Nettleton --- src/cli/install.rs | 36 ++++++++++++++++++++++++++---------- src/quadlet.rs | 6 ++++++ src/quadlet/install.rs | 8 +++++++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/cli/install.rs b/src/cli/install.rs index 29c0f7e..11e121a 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -3,7 +3,7 @@ use clap::Args; #[allow(clippy::doc_markdown)] #[derive(Args, Debug, Clone, PartialEq)] pub struct Install { - /// Add an [Install] section to the unit + /// Add an [Install] section to the unit. /// /// By default, if the --wanted-by and --required-by options are not used, /// the section will have "WantedBy=default.target". @@ -11,25 +11,35 @@ pub struct Install { #[arg(short, long)] pub install: bool, - /// Add (weak) parent dependencies to the unit + /// Add (weak) parent dependencies to the unit. /// - /// Requires the --install option + /// Requires the --install option. /// - /// Converts to "WantedBy=WANTED_BY" + /// Converts to "WantedBy=WANTED_BY". /// - /// Can be specified multiple times + /// Can be specified multiple times. #[arg(long, requires = "install")] wanted_by: Vec, - /// Similar to --wanted-by, but adds stronger parent dependencies + /// Similar to --wanted-by, but adds stronger parent dependencies. /// - /// Requires the --install option + /// Requires the --install option. /// - /// Converts to "RequiredBy=REQUIRED_BY" + /// Converts to "RequiredBy=REQUIRED_BY". /// - /// Can be specified multiple times + /// Can be specified multiple times. #[arg(long, requires = "install")] required_by: Vec, + + /// Similar to --wanted-by, but ensures this unit is up if the parent dependency is. + /// + /// Requires the --install option. + /// + /// Converts to "UpheldBy=UPHELD_BY". + /// + /// Can be specified multiple times. + #[arg(long, requires = "install")] + upheld_by: Vec, } impl From for crate::quadlet::Install { @@ -38,15 +48,21 @@ impl From for crate::quadlet::Install { install, wanted_by, required_by, + upheld_by, }: Install, ) -> Self { Self { - wanted_by: if install && wanted_by.is_empty() && required_by.is_empty() { + wanted_by: if install + && wanted_by.is_empty() + && required_by.is_empty() + && upheld_by.is_empty() + { vec![String::from("default.target")] } else { wanted_by }, required_by, + upheld_by, } } } diff --git a/src/quadlet.rs b/src/quadlet.rs index a2b6bd3..6ed04de 100644 --- a/src/quadlet.rs +++ b/src/quadlet.rs @@ -205,6 +205,9 @@ pub enum JoinOption { /// `Unmask=`, used in [Container] sections. Unmask, + /// `UpheldBy=`, used in [Install] sections. + UpheldBy, + /// `WantedBy=`, used in [Install] sections. WantedBy, @@ -229,6 +232,7 @@ impl JoinOption { Self::Requires, Self::Sysctl, Self::Unmask, + Self::UpheldBy, Self::WantedBy, Self::Wants, ]; @@ -255,6 +259,7 @@ impl JoinOption { Self::Requires => "Requires", Self::Sysctl => "Sysctl", Self::Unmask => "Unmask", + Self::UpheldBy => "UpheldBy", Self::WantedBy => "WantedBy", Self::Wants => "Wants", } @@ -286,6 +291,7 @@ impl FromStr for JoinOption { "Requires" => Ok(Self::Requires), "Sysctl" => Ok(Self::Sysctl), "Unmask" => Ok(Self::Unmask), + "UpheldBy" => Ok(Self::UpheldBy), "WantedBy" => Ok(Self::WantedBy), "Wants" => Ok(Self::Wants), s => Err(ParseJoinOptionError(s.to_owned())), diff --git a/src/quadlet/install.rs b/src/quadlet/install.rs index c043090..23d969f 100644 --- a/src/quadlet/install.rs +++ b/src/quadlet/install.rs @@ -3,6 +3,7 @@ use serde::Serialize; use crate::serde::quadlet::seq_quote_whitespace; /// The `[Install]` section of a systemd unit / Quadlet file. +#[expect(clippy::struct_field_names, reason = "systemd directives")] #[derive(Serialize, Default, Debug, Clone, PartialEq)] #[serde(rename_all = "PascalCase")] pub struct Install { @@ -13,6 +14,10 @@ pub struct Install { /// Add stronger parent dependencies to the unit. #[serde(serialize_with = "seq_quote_whitespace")] pub required_by: Vec, + + /// Add stronger parent dependencies to the unit. + #[serde(serialize_with = "seq_quote_whitespace")] + pub upheld_by: Vec, } impl Install { @@ -21,8 +26,9 @@ impl Install { let Self { wanted_by, required_by, + upheld_by, } = self; - wanted_by.is_empty() && required_by.is_empty() + wanted_by.is_empty() && required_by.is_empty() && upheld_by.is_empty() } } From dd8fec960c3d10453d368b40292082bffdd578ea Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Tue, 10 Mar 2026 00:44:49 -0500 Subject: [PATCH 15/16] feat(unit): add `podlet --upholds` option Signed-off-by: Paul Nettleton --- src/quadlet.rs | 6 ++++++ src/quadlet/unit.rs | 33 ++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/quadlet.rs b/src/quadlet.rs index 6ed04de..becb926 100644 --- a/src/quadlet.rs +++ b/src/quadlet.rs @@ -208,6 +208,9 @@ pub enum JoinOption { /// `UpheldBy=`, used in [Install] sections. UpheldBy, + /// `Upholds=`, used in [Unit] sections. + Upholds, + /// `WantedBy=`, used in [Install] sections. WantedBy, @@ -233,6 +236,7 @@ impl JoinOption { Self::Sysctl, Self::Unmask, Self::UpheldBy, + Self::Upholds, Self::WantedBy, Self::Wants, ]; @@ -260,6 +264,7 @@ impl JoinOption { Self::Sysctl => "Sysctl", Self::Unmask => "Unmask", Self::UpheldBy => "UpheldBy", + Self::Upholds => "Upholds", Self::WantedBy => "WantedBy", Self::Wants => "Wants", } @@ -292,6 +297,7 @@ impl FromStr for JoinOption { "Sysctl" => Ok(Self::Sysctl), "Unmask" => Ok(Self::Unmask), "UpheldBy" => Ok(Self::UpheldBy), + "Upholds" => Ok(Self::Upholds), "WantedBy" => Ok(Self::WantedBy), "Wants" => Ok(Self::Wants), s => Err(ParseJoinOptionError(s.to_owned())), diff --git a/src/quadlet/unit.rs b/src/quadlet/unit.rs index 9150804..7b64e18 100644 --- a/src/quadlet/unit.rs +++ b/src/quadlet/unit.rs @@ -52,32 +52,41 @@ pub struct Unit { #[serde(serialize_with = "seq_quote_whitespace")] pub binds_to: Vec, - /// Configure ordering dependency between units. + /// Similar to --binds-to, but this unit only stops when the dependency is explicitly stopped. /// - /// Converts to "Before=BEFORE[ ...]". + /// Converts to "PartOf=PART_OF[ ...]". /// /// Can be specified multiple times. #[arg(long)] #[serde(serialize_with = "seq_quote_whitespace")] - pub before: Vec, + pub part_of: Vec, + + /// Similar to --wants, but dependencies are continuously started when inactive or failed. + /// + /// Converts to "Upholds=UPHOLDS[ ...]". + /// + /// Can be specified multiple times. + #[arg(long)] + #[serde(serialize_with = "seq_quote_whitespace")] + pub upholds: Vec, /// Configure ordering dependency between units. /// - /// Converts to "After=AFTER[ ...]". + /// Converts to "Before=BEFORE[ ...]". /// /// Can be specified multiple times. #[arg(long)] #[serde(serialize_with = "seq_quote_whitespace")] - pub after: Vec, + pub before: Vec, - /// Similar to --binds-to, but this unit only stops when the dependency is explicitly stopped + /// Configure ordering dependency between units. /// - /// Converts to "PartOf=PART_OF[ ...]" + /// Converts to "After=AFTER[ ...]". /// - /// Can be specified multiple times + /// Can be specified multiple times. #[arg(long)] #[serde(serialize_with = "seq_quote_whitespace")] - pub part_of: Vec, + pub after: Vec, } impl Unit { @@ -88,18 +97,20 @@ impl Unit { wants, requires, binds_to, + part_of, + upholds, before, after, - part_of, } = self; description.is_none() && wants.is_empty() && requires.is_empty() && binds_to.is_empty() + && part_of.is_empty() + && upholds.is_empty() && before.is_empty() && after.is_empty() - && part_of.is_empty() } /// Add a compose [`Service`](compose_spec::Service) [`Dependency`] to the unit. From 3f24112e736f77f1be2c3bfb8e8954aa7148cc73 Mon Sep 17 00:00:00 2001 From: Paul Nettleton Date: Tue, 10 Mar 2026 00:52:05 -0500 Subject: [PATCH 16/16] feat: add `podman --cdi-spec-dir` global option Signed-off-by: Paul Nettleton --- src/cli/global_args.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cli/global_args.rs b/src/cli/global_args.rs index 806e2f2..d56139a 100644 --- a/src/cli/global_args.rs +++ b/src/cli/global_args.rs @@ -13,6 +13,14 @@ use crate::quadlet::Globals; #[command(next_help_heading = "Podman Global Options")] #[serde(rename_all = "kebab-case")] pub struct GlobalArgs { + /// CDI spec directory path + /// + /// Default is `/etc/cdi` + /// + /// Can be specified multiple times + #[arg(long, global = true, value_name = "PATH")] + cdi_spec_dir: Vec, + /// Cgroup manager to use #[arg(long, global = true, value_name = "MANAGER")] cgroup_manager: Option,