Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.2-alpha.5"
version = "0.3.2-alpha.6"
authors = ["Paul Nettleton <k9@k9withabone.dev>"]
edition = "2024"
rust-version = "1.85"
Expand Down
154 changes: 113 additions & 41 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ use self::{
#[derive(Parser, Debug, Clone, PartialEq)]
#[command(author, version, about, subcommand_precedence_over_arg = true)]
pub struct Cli {
/// Generate a file instead of printing to stdout
/// Generate file(s) instead of printing to stdout
///
/// Optionally provide a path for the file,
/// if no path is provided the file will be placed in the current working directory.
Expand All @@ -62,34 +62,49 @@ pub struct Cli {
/// the filename of the kube file,
/// the container name,
/// or the name of the container image.
#[arg(short, long, group = "file_out")]
#[arg(short, long, groups = ["file_out", "file_or_unit_dir"])]
file: Option<Option<PathBuf>>,

/// Generate a file in the Podman unit directory instead of printing to stdout
/// Generate file(s) in the Podman unit directory instead of printing to stdout
///
/// Conflicts with the --file option
///
/// Equivalent to `--file $XDG_CONFIG_HOME/containers/systemd/` for non-root users,
/// or `--file /etc/containers/systemd/` for root.
///
/// The name of the file can be specified with the --name option.
/// The name of the file can be specified with the `--name` option.
#[arg(
short,
long,
visible_alias = "unit-dir",
conflicts_with = "file",
group = "file_out"
groups = ["file_out", "file_or_unit_dir"]
)]
unit_directory: bool,

/// Generate a single `.quadlets` file instead of separate Quadlet files
///
/// Provide the name (without the extension) that the `.quadlets` file should use.
///
/// If `--file` or `--unit-directory` is not used, `--file` is implied with the current working
/// directory.
#[arg(long, value_name = "NAME", group = "file_out")]
quadlets_file: Option<String>,

/// Override the name of the generated file (without the extension)
///
/// This only applies if a file was not given to the --file option,
/// or the --unit-directory option was used.
/// This only applies if a directory was given to `--file`, or `--unit-directory` was used.
///
/// E.g. `podlet --file --name hello-world podman run quay.io/podman/hello`
/// will generate a file with the name "hello-world.container".
#[arg(short, long, requires = "file_out")]
///
/// Conflicts with the `--quadlets-file` option as the file name is set with it.
#[arg(
short,
long,
requires = "file_or_unit_dir",
conflicts_with = "quadlets_file"
)]
name: Option<String>,

/// Overwrite existing files when generating a file
Expand All @@ -105,7 +120,7 @@ pub struct Cli {
/// For example, `podlet podman run -e ONE=one -e TWO=two image` results in:
///
/// ```ini
/// # image.container
/// # FileName=image
/// [Container]
/// Environment=ONE=one TWO=two
/// Image=image
Expand All @@ -114,7 +129,7 @@ pub struct Cli {
/// While `podlet -s Environment podman run -e ONE=one -e TWO=two image` results in:
///
/// ```ini
/// # image.container
/// # FileName=image
/// [Container]
/// Environment=ONE=one
/// Environment=TWO=two
Expand Down Expand Up @@ -227,14 +242,14 @@ By default, Podlet will combine all Quadlet options that can be joined into a si

For example, `podlet podman run -e ONE=one -e TWO=two image` results in:

# image.container
# FileName=image
[Container]
Environment=ONE=one TWO=two
Image=image

While `podlet -s Environment podman run -e ONE=one -e TWO=two image` results in:

# image.container
# FileName=image
[Container]
Environment=ONE=one
Environment=TWO=two
Expand All @@ -249,18 +264,33 @@ multiple times.";
let split_options = self.split_options.iter().copied().collect();
let join_options = &JoinOption::all_set() - &split_options;

if self.unit_directory || self.file.is_some() {
if self.unit_directory || self.file.is_some() || self.quadlets_file.is_some() {
// file out
let path = self.file_path()?;
if matches!(path, FilePath::Full(..))
&& matches!(self.command, Commands::Compose { .. })
let quadlets_file = self.quadlets_file.clone();
if matches!(path, FilePath::Full(_)) {
if quadlets_file.is_some() {
return Err(eyre!(
"A file path was provided to `--file` and `--quadlets-file` was used`"
)
.note("The file name is set by `--quadlets-file`.")
.suggestion("Provide a directory to `--file`."));
}
if matches!(self.command, Commands::Compose(_)) {
return Err(eyre!(
"A file path was provided to `--file` and the `compose` command was used"
)
.warning("`compose` can generate multiple files so a directory is needed.")
.suggestion("Provide a directory to `--file`."));
}
}
if quadlets_file.is_some()
&& matches!(self.command, Commands::Compose(Compose { kube: true, .. }))
{
return Err(eyre!(
"A file path was provided to `--file` and the `compose` command was used"
)
.suggestion(
"Provide a directory to `--file`. \
`compose` can generate multiple files so a directory is needed.",
));
return Err(
eyre!("cannot set both `--quadlets-file` and `compose --kube`")
.warning("`.quadlets` files cannot include Kubernetes YAML in them."),
);
}

let overwrite = self.overwrite;
Expand All @@ -278,24 +308,25 @@ multiple times.";
)?;
}

for file in files {
file.write(&path, overwrite, &join_options)?;
if let Some(quadlets_file) = quadlets_file {
let contents = files_to_quadlets_file(&files, &join_options)?;
let path = path.to_full(&quadlets_file, "quadlets");

open_file(&path, overwrite)?
.write_all(contents.as_bytes())
.wrap_err_with(|| format!("error writing to file `{}`", path.display()))?;

println!("Wrote to file: {}", path.display());
} else {
for file in files {
file.write(&path, overwrite, &join_options)?;
}
}

Ok(())
} else {
let files = self
.try_into_files()?
.into_iter()
.map(|file| {
let file_name = format!("{}.{}", file.name(), file.extension());
let file = file
.serialize(&join_options)
.wrap_err_with(|| format! {"error serializing file `{file_name}`"})?;
Ok(format!("# {file_name}\n{file}"))
})
.collect::<color_eyre::Result<Vec<_>>>()?
.join("\n---\n\n");
let files = self.try_into_files()?;
let files = files_to_quadlets_file(&files, &join_options)?;
print!("{files}");
Ok(())
}
Expand Down Expand Up @@ -457,19 +488,60 @@ enum FilePath {
impl FilePath {
/// Convert to full file path
///
/// If `self` is a directory, the [`File`] is used to set the filename.
fn to_full(&self, file: &File) -> Cow<'_, Path> {
/// If `self` is a directory, `filename` and `extension` are used to set the filename.
fn to_full(&self, filename: &str, extension: &str) -> Cow<'_, Path> {
match self {
Self::Full(path) => path.into(),
Self::Dir(path) => {
let mut path = path.join(file.name());
path.set_extension(file.extension());
let mut path = path.join(filename);
path.set_extension(extension);
path.into()
}
}
}
}

/// Serialize each [`File`] and join them together in the `.quadlets` file format.
///
/// # Errors
///
/// Returns an error if a file fails to serialize.
fn files_to_quadlets_file<'a>(
files: impl IntoIterator<Item = &'a File>,
join_options: &HashSet<JoinOption>,
) -> color_eyre::Result<String> {
files
.into_iter()
.map(|file| {
let file_name = file.name();

let serialized_file = file.serialize(join_options).wrap_err_with(|| {
format!("error serializing {} file `{file_name}`", file.extension())
})?;

let mut content = String::with_capacity(serialized_file.capacity());
content.push_str("# ");

match file {
File::Quadlet(_) => {
content.push_str("FileName=");
content.push_str(file_name);
}
File::Kubernetes(_) => {
content.push_str(file_name);
content.push_str(".yaml");
}
}

content.push('\n');
content.push_str(&serialized_file);

Ok(content)
})
.collect::<color_eyre::Result<Vec<_>>>()
.map(|files| files.join("\n---\n\n"))
}

#[derive(Subcommand, Debug, Clone, PartialEq)]
enum Commands {
/// Generate a Podman Quadlet file from a Podman command
Expand Down Expand Up @@ -795,7 +867,7 @@ impl File {
overwrite: bool,
join_options: &HashSet<JoinOption>,
) -> color_eyre::Result<()> {
let path = path.to_full(self);
let path = path.to_full(self.name(), self.extension());
let mut file = open_file(&path, overwrite)?;
let path = path.display();

Expand Down
2 changes: 2 additions & 0 deletions src/cli/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub struct Compose {
///
/// The top-level `name` field in the compose file is required when using this option.
/// It is used for the name of the pod and in the filenames of the created files.
///
/// Conflicts with `--quadlets-file` as a YAML file cannot be included in `.quadlets` file.
#[arg(long, conflicts_with = "pod")]
pub kube: bool,

Expand Down
10 changes: 3 additions & 7 deletions src/cli/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ impl From<Container> for crate::quadlet::Container {
let mut podman_args = podman_args.to_string();

let security_opt::QuadletOptions {
app_armor,
mask,
no_new_privileges,
seccomp_profile,
Expand All @@ -129,20 +130,15 @@ impl From<Container> for crate::quadlet::Container {
security_label_type,
unmask,
podman_args: security_podman_args,
} = security_opt.into_iter().fold(
security_opt::QuadletOptions::default(),
|mut security_options, security_opt| {
security_options.add_security_opt(security_opt);
security_options
},
);
} = security_opt.into_iter().collect();

for arg in security_podman_args {
podman_args.push_str(" --security-opt ");
podman_args.push_str(&arg);
}

Self {
app_armor,
image,
mask,
no_new_privileges,
Expand Down
17 changes: 14 additions & 3 deletions src/cli/container/security_opt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub struct InvalidLabelOpt(pub String);

#[derive(Debug, Default, Clone, PartialEq)]
pub struct QuadletOptions {
pub app_armor: Option<String>,
pub mask: Vec<String>,
pub no_new_privileges: bool,
pub seccomp_profile: Option<PathBuf>,
Expand All @@ -100,10 +101,20 @@ pub struct QuadletOptions {
pub podman_args: Vec<String>,
}

impl FromIterator<SecurityOpt> for QuadletOptions {
fn from_iter<T: IntoIterator<Item = SecurityOpt>>(iter: T) -> Self {
iter.into_iter()
.fold(Self::default(), |mut quadlet_options, security_opt| {
quadlet_options.add_security_opt(security_opt);
quadlet_options
})
}
}

impl QuadletOptions {
pub fn add_security_opt(&mut self, security_opt: SecurityOpt) {
fn add_security_opt(&mut self, security_opt: SecurityOpt) {
match security_opt {
SecurityOpt::Apparmor(policy) => self.podman_args.push(format!("apparmor={policy}")),
SecurityOpt::Apparmor(policy) => self.app_armor = Some(policy),
SecurityOpt::Label(label_opt) => self.add_label_opt(label_opt),
SecurityOpt::Mask(mask) => self.mask.extend(mask.split(':').map(Into::into)),
SecurityOpt::NoNewPrivileges => self.no_new_privileges = true,
Expand All @@ -118,7 +129,7 @@ impl QuadletOptions {
}
}

pub fn add_label_opt(&mut self, label_opt: LabelOpt) {
fn add_label_opt(&mut self, label_opt: LabelOpt) {
match label_opt {
LabelOpt::User(user) => self.podman_args.push(format!("label=user:{user}")),
LabelOpt::Role(role) => self.podman_args.push(format!("label=role:{role}")),
Expand Down
9 changes: 7 additions & 2 deletions src/quadlet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,13 +645,17 @@ pub enum PodmanVersion {
V5_6,

/// Podman v5.7
#[value(name = "5.7", aliases = ["latest", "5.7.0", "5.7.1"])]
#[value(name = "5.7", aliases = ["5.7.0", "5.7.1"])]
V5_7,

/// Podman v5.8
#[value(name = "5.8", aliases = ["latest", "5.8.0", "5.8.1"])]
V5_8,
}

impl PodmanVersion {
/// Latest supported version of Podman with regards to Quadlet.
pub const LATEST: Self = Self::V5_7;
pub const LATEST: Self = Self::V5_8;

/// Podman version as a static string slice.
pub const fn as_str(self) -> &'static str {
Expand All @@ -669,6 +673,7 @@ impl PodmanVersion {
Self::V5_5 => "5.5",
Self::V5_6 => "5.6",
Self::V5_7 => "5.7",
Self::V5_8 => "5.8",
}
}
}
Expand Down
Loading
Loading