Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
edd4218
chore: add Podman v5.3 to Podman versions
k9withabone Feb 3, 2026
896712a
feat(container): add `AddHost=` Quadlet option
k9withabone Feb 3, 2026
400aca6
feat(container): add `CgroupsMode=` Quadlet option
k9withabone Feb 3, 2026
71abf83
feat(container): add `HealthLogDestination=` Quadlet option
k9withabone Feb 4, 2026
6acc7bd
feat(container): add `HealthMaxLogCount=` Quadlet option
k9withabone Feb 4, 2026
3c38d5d
feat(container): add `HealthMaxLogSize=` Quadlet option
k9withabone Feb 4, 2026
f3d1ef9
feat(compose): support `network_mode: "service:service_name"`
k9withabone Feb 4, 2026
85a083e
feat(pod): add `AddHost=` Quadlet option
k9withabone Feb 4, 2026
ef9386d
feat(pod): add `DNS=` Quadlet option
k9withabone Feb 4, 2026
516bd92
feat(pod): add `DNSOption=` Quadlet option
k9withabone Feb 4, 2026
a5fe097
feat(pod): add `DNSSearch=` Quadlet option
k9withabone Feb 4, 2026
0aec632
feat(pod): add `GIDMap=` Quadlet option
k9withabone Feb 5, 2026
d723602
feat(pod): add `IP=` Quadlet option
k9withabone Feb 5, 2026
d7a9ba4
feat(pod): add `IP6=` Quadlet option
k9withabone Feb 5, 2026
681aa14
feat(pod): add `SubGIDMap=` Quadlet option
k9withabone Feb 5, 2026
4dea6c1
feat(pod): add `SubUIDMap=` Quadlet option
k9withabone Feb 5, 2026
c1c386a
feat(pod): add `UIDMap=` Quadlet option
k9withabone Feb 5, 2026
310ffb8
feat(pod): add `UserNS=` Quadlet option
k9withabone Feb 5, 2026
067664f
feat(pod): support setting `ImageTag=` Quadlet option multiple times
k9withabone Feb 5, 2026
5240b6c
feat: add `--service-name` option
k9withabone Feb 12, 2026
b7eaad7
refactor: make `podlet::quadlet::File` fields non-optional
k9withabone Feb 14, 2026
af66fa5
refactor: move `cli::{service, unit}` to `quadlet`
k9withabone Feb 15, 2026
30d6147
feat: add `--disable-default-quadlet-dependencies` flag
k9withabone Feb 15, 2026
7f94566
feat(container): add `--no-start-with-pod` flag
k9withabone Feb 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "podlet"
version = "0.3.1"
version = "0.3.2-alpha.1"
authors = ["Paul Nettleton <k9@k9withabone.dev>"]
edition = "2024"
rust-version = "1.85"
Expand Down
112 changes: 94 additions & 18 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ mod k8s;
mod kube;
mod network;
mod pod;
pub mod service;
pub mod unit;
pub mod volume;

#[cfg(unix)]
Expand All @@ -35,16 +33,21 @@ use compose_spec::service::blkio_config::Weight;
use path_clean::PathClean;

use crate::quadlet::{
self, Downgrade, DowngradeError, Globals, HostPaths, JoinOption, PodmanVersion,
self, Downgrade, DowngradeError, GenericSections, Globals, HostPaths, JoinOption,
PodmanVersion, Quadlet, Service, Unit,
};

use self::{
build::Build, compose::Compose, container::Container, generate::Generate,
global_args::GlobalArgs, image::Image, install::Install, kube::Kube, network::Network,
pod::Pod, service::Service, unit::Unit, volume::Volume,
pod::Pod, volume::Volume,
};

#[allow(clippy::option_option)]
#[expect(
clippy::option_option,
clippy::struct_excessive_bools,
reason = "CLI args"
)]
#[derive(Parser, Debug, Clone, PartialEq)]
#[command(author, version, about, subcommand_precedence_over_arg = true)]
pub struct Cli {
Expand Down Expand Up @@ -170,6 +173,18 @@ pub struct Cli {
#[arg(short, long, value_name = "RESOLVE_DIR")]
absolute_host_paths: Option<Option<PathBuf>>,

/// Change the name of the systemd service Quadlet generates.
///
/// Converts to "ServiceName=SERVICE_NAME".
///
/// The name should **not** include the `.service` extension.
///
/// Podlet will error if this option is used while creating more than one Quadlet file as
/// setting the same service name for multiple files will conflict with each other.
#[expect(clippy::doc_markdown, reason = "Quadlet option")]
#[arg(long)]
service_name: Option<String>,

/// The \[Unit\] section
#[command(flatten)]
unit: Unit,
Expand All @@ -178,6 +193,24 @@ pub struct Cli {
#[command(flatten)]
install: Install,

/// Disable Quadlet's default network dependencies.
///
/// By default, Quadlet adds a dependency on `network-online.target` (for system units) or
/// `podman-user-wait-network-online.service` (for user units) to the generated unit. Using this
/// flag will disable the dependencies.
///
/// Converts to "DefaultDependencies=false" in the `[Quadlet]` section.
#[arg(long)]
disable_default_quadlet_dependencies: bool,

/// Do not start container units with their associated pod.
///
/// By default, container units are started alongside the pod.
///
/// Converts to "StartWithPod=false" in the `[Container]` section.
#[arg(long)]
no_start_with_pod: bool,

#[command(subcommand)]
command: Commands,
}
Expand Down Expand Up @@ -351,18 +384,51 @@ multiple times.";
.resolve_dir()
.wrap_err("error with `--absolute-host-paths` resolve directory")?;

let unit = (!self.unit.is_empty()).then_some(self.unit);
let install = self.install.install.then(|| self.install.into());
let sections = GenericSections {
unit: self.unit,
quadlet: Quadlet {
default_dependencies: !self.disable_default_quadlet_dependencies,
},
install: self.install.into(),
};

let mut files = self.command.try_into_files(self.name, unit, install)?;
let mut files = self.command.try_into_files(self.name, sections)?;

if let Some(service_name) = self.service_name {
let mut found_quadlet_file = false;
for _ in files
.iter()
.filter(|file| matches!(file, File::Quadlet(..)))
{
if found_quadlet_file {
return Err(eyre!(
"cannot set `--service-name` when creating more than one Quadlet file"
))
.note(
"setting the same service name for multiple Quadlet files will conflict \
with each other",
)
.suggestion("manually set `ServiceName=` Quadlet option in each Quadlet file");
}
found_quadlet_file = true;
}

if let Some(File::Quadlet(file)) = files.first_mut() {
file.globals.service_name = Some(service_name);
}
}

let downgrade = self.podman_version < PodmanVersion::LATEST;
if downgrade || resolve_dir.is_some() {
if resolve_dir.is_some() || self.no_start_with_pod || downgrade {
for file in &mut files {
if let Some(resolve_dir) = &resolve_dir {
file.absolutize_host_paths(resolve_dir);
}

if self.no_start_with_pod {
file.set_start_with_pod(false);
}

if downgrade {
file.downgrade(self.podman_version).wrap_err_with(|| {
format!(
Expand Down Expand Up @@ -445,23 +511,22 @@ impl Commands {
fn try_into_files(
self,
name: Option<String>,
unit: Option<Unit>,
install: Option<quadlet::Install>,
sections: GenericSections,
) -> color_eyre::Result<Vec<File>> {
match self {
Self::Podman {
global_args,
command,
} => Ok(vec![
command
.into_quadlet(name, unit, (*global_args).into(), install)
.into_quadlet(name, sections, (*global_args).into())
.into(),
]),
Self::Compose(compose) => compose
.try_into_files(unit, install)
.try_into_files(sections)
.wrap_err("error converting compose file"),
Self::Generate(command) => Ok(command
.try_into_quadlet_files(name, unit, install)
.try_into_quadlet_files(name, sections)
.wrap_err("error creating Quadlet file(s) from an existing object")?
.into_iter()
.map(Into::into)
Expand Down Expand Up @@ -579,24 +644,28 @@ impl PodmanCommands {
fn into_quadlet(
self,
name: Option<String>,
unit: Option<Unit>,
GenericSections {
unit,
quadlet,
install,
}: GenericSections,
globals: Globals,
install: Option<quadlet::Install>,
) -> quadlet::File {
let service = self.service().cloned();
let service = self.service().cloned().unwrap_or_default();
quadlet::File {
name: name.unwrap_or_else(|| self.name().into()),
unit,
resource: self.into(),
globals,
quadlet,
service,
install,
}
}

fn service(&self) -> Option<&Service> {
match self {
Self::Run { service, .. } => (!service.is_empty()).then_some(service),
Self::Run { service, .. } => Some(service),
_ => None,
}
}
Expand Down Expand Up @@ -665,6 +734,13 @@ impl File {
}
}

/// If this [`File`] is a Quadlet container unit, set the `StartWithPod=` Quadlet option.
fn set_start_with_pod(&mut self, start_with_pod: bool) {
if let Self::Quadlet(file) = self {
file.set_start_with_pod(start_with_pod);
}
}

/// If a Quadlet file, make all host paths absolute and clean.
///
/// Relative paths are resolved using `resolve_dir` as the base.
Expand Down
30 changes: 15 additions & 15 deletions src/cli/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use std::{
};

use clap::{ArgAction, Args};
use color_eyre::eyre::{OptionExt, WrapErr, bail, ensure, eyre};
use color_eyre::{
Section,
eyre::{WrapErr, bail, ensure, eyre},
};
use compose_spec::{
ShortOrLong,
service::{
Expand Down Expand Up @@ -117,9 +120,11 @@ pub struct Build {
///
/// Converts to "ImageTag=IMAGE_NAME".
///
/// This option is required by Quadlet.
#[arg(short, long, value_name = "IMAGE_NAME")]
tag: String,
/// At least one tag is required by Quadlet.
///
/// Can be specified multiple times.
#[arg(short, long, value_name = "IMAGE_NAME", required = true)]
tag: Vec<String>,

/// Add an image label (e.g. label=value) to the image metadata.
///
Expand Down Expand Up @@ -195,7 +200,7 @@ pub struct Build {
impl Build {
/// The name (without extension) of the generated Quadlet file.
pub fn name(&self) -> &str {
image_to_name(&self.tag)
image_to_name(self.tag.first().expect("at least one tag"))
}
}

Expand Down Expand Up @@ -354,19 +359,14 @@ impl TryFrom<service::Build> for Build {
..PodmanArgs::default()
};

let mut tags = tags.into_iter();
let tag = tags
.next()
.ok_or_eyre("an image tag is required")?
.into_inner();
ensure!(
tags.next().is_none(),
"Quadlet only supports setting a single tag"
);
if tags.is_empty() {
return Err(eyre!("at least one image tag is required")
.suggestion("add a `tags` list to the `build` section"));
}

Ok(Self {
file,
tag,
tag: tags.into_iter().map(Into::into).collect(),
label: labels.into_list().into_iter().collect(),
network: network.map(Into::into).into_iter().collect(),
pull: pull.then_some(PullPolicy::Always),
Expand Down
Loading