Skip to content
Draft
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
6 changes: 2 additions & 4 deletions Cargo.lock

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

17 changes: 17 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ license = "MPL-2.0"
keywords = ["podman", "quadlet", "containers"]
categories = ["command-line-utilities"]

[lib]
name = "podlet"
path = "src/lib.rs"

[[bin]]
name = "podlet"
path = "src/main.rs"

[lints.rust]
unused_crate_dependencies = "warn"
unused_import_braces = "warn"
Expand Down Expand Up @@ -78,6 +86,15 @@ url = "2.3"
rustix = { version = "1.0.0", features = ["process"] }
zbus = "5.0.0"

# `compose_spec` uses `Path::is_absolute()` to validate volume paths, which returns `false` for
# Podman-style absolute paths (starting with `/`) on non-Unix targets such as `wasm32-unknown-unknown`.
# Until the fix is released, patch to the branch from
# https://github.com/k9withabone/compose_spec_rs/pull/42 so compose conversion is correct on WASM.
# Note: `[patch]` only applies when building Podlet itself; downstream library consumers targeting
# WASM should add the same patch to their own workspace `Cargo.toml`.
[patch.crates-io]
compose_spec = { git = "https://github.com/maximeborges/compose_spec_rs.git", branch = "bugfix/volume_non-unix" }

# The profile that 'cargo dist' will build with
[profile.dist]
inherits = "release"
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Demo created with [Autocast](https://github.com/k9withabone/autocast). You can a
- Opt-out with `--skip-services-check`.
- Set Podman version compatibility with `--podman-version`.
- Resolve relative host paths with `--absolute-host-paths`.
- Usable as a library crate, in addition to the CLI.

## Communication

Expand Down Expand Up @@ -396,6 +397,36 @@ Alternatively, if you just want Podlet to read a specific compose file you can u

`podman run --rm -v ./compose.yaml:/compose.yaml:Z ghcr.io/containers/podlet compose /compose.yaml`

## Use as a Library

In addition to the CLI, Podlet can be used as a library. The most common use case is converting a compose file into Quadlet files entirely in memory, without touching the filesystem:

```rust
use podlet::{compose_to_files, ComposeOptions};

let compose = "\
services:
caddy:
image: docker.io/library/caddy:latest
ports:
- 8000:80
";

let files = compose_to_files(compose, ComposeOptions::default()).unwrap();
assert_eq!(files[0].name, "caddy.container");
assert!(files[0].content.contains("Image=docker.io/library/caddy:latest"));
```

For full control (equivalent to the CLI, including the `podman ...` and `generate` subcommands), build a `Cli` and call `Cli::try_into_generated_files()` to obtain the generated files in memory instead of printing or writing them.

See the [API documentation](https://docs.rs/podlet) for details.

### WebAssembly

Because the conversion is pure and entirely in memory, the library can be compiled to `wasm32-unknown-unknown` and run in the browser with no server component. The WASM/JavaScript bindings are left to the consuming project so that the library stays platform-independent.

Until the [`compose_spec` fix](https://github.com/k9withabone/compose_spec_rs/pull/42) for absolute volume paths on non-Unix targets is released, projects targeting WASM must replicate the `[patch.crates-io]` from this repository's `Cargo.toml` in their own workspace.

## Cautions

Podlet is primarily a tool for helping to get started with Podman systemd units, aka Quadlet files. It is not meant to be an end-all solution for creating and maintaining Quadlet files. Files created with Podlet should always be reviewed before starting the unit.
Expand Down
64 changes: 55 additions & 9 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mod artifact;
mod build;
mod compose;
pub(crate) mod compose;
mod container;
mod generate;
mod global_args;
Expand All @@ -18,13 +18,15 @@ mod systemd_dbus;
use std::{
borrow::Cow,
collections::HashSet,
env,
ffi::OsStr,
fs,
env, fs,
io::{self, Write},
path::{Path, PathBuf},
};

// Only used by the `#[cfg(unix)]` existing-services check.
#[cfg(unix)]
use std::ffi::OsStr;

use clap::{ArgAction, Parser, Subcommand, builder::TypedValueParser};
use color_eyre::{
Help,
Expand Down Expand Up @@ -258,6 +260,16 @@ Image=image
Quadlet options can be specified in a comma (,) separated list and/or this option can be specified \
multiple times.";

/// Print the generated file(s) to stdout, or write them to disk, based on the CLI options.
///
/// This is the entry point used by the `podlet` binary. Library consumers that want the
/// generated files in memory instead should use
/// [`try_into_generated_files`](Self::try_into_generated_files).
///
/// # Errors
///
/// Returns an error if the input cannot be read or converted into files, or if writing the
/// generated file(s) to disk fails.
pub fn print_or_write_files(self) -> color_eyre::Result<()> {
// Determine which Quadlet options to join together into a single line by subtracting the
// selected options from the set of all possible options.
Expand Down Expand Up @@ -410,7 +422,33 @@ multiple times.";
.transpose()
}

/// Convert into [`File`]s
/// Convert into generated files, held in memory.
///
/// This applies all of the transformations requested by the CLI options (resolving host
/// paths, downgrading to an older Podman version, etc.) and returns the generated files as
/// [`GeneratedFile`](crate::GeneratedFile)s (file name and serialized contents) without
/// printing or writing them anywhere. It is the in-memory equivalent of
/// [`print_or_write_files`](Self::print_or_write_files).
///
/// Note that, depending on the command, this may read from the filesystem or standard input
/// (e.g. `podlet compose` reading a compose file) or spawn `podman` (`podlet generate`), which
/// is not available on all targets. For a fully in-memory compose conversion, use
/// [`compose_to_files`](crate::compose_to_files) instead.
///
/// # Errors
///
/// Returns an error if the input cannot be read or converted, or if a file fails to serialize.
pub fn try_into_generated_files(self) -> color_eyre::Result<Vec<crate::GeneratedFile>> {
// Determine which Quadlet options to join together into a single line by subtracting the
// selected options from the set of all possible options.
let split_options = self.split_options.iter().copied().collect();
let join_options = &JoinOption::all_set() - &split_options;

let files = self.try_into_files()?;
crate::serialize_files(&files, &join_options)
}

/// Convert into [`File`]s.
fn try_into_files(mut self) -> color_eyre::Result<Vec<File>> {
let resolve_dir = self
.resolve_dir()
Expand Down Expand Up @@ -769,9 +807,12 @@ impl PodmanCommands {
}
}

/// A single file generated by Podlet, held in memory.
///
/// Either a Quadlet file (`.container`, `.pod`, `.network`, ...) or a Kubernetes YAML file.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)] // false positive, [Pod] is not zero-sized
enum File {
pub(crate) enum File {
Quadlet(quadlet::File),
Kubernetes(k8s::File),
}
Expand All @@ -789,21 +830,23 @@ impl From<k8s::File> for File {
}

impl File {
fn name(&self) -> &str {
pub(crate) fn name(&self) -> &str {
match self {
Self::Quadlet(file) => &file.name,
Self::Kubernetes(file) => &file.name,
}
}

fn extension(&self) -> &str {
pub(crate) fn extension(&self) -> &str {
match self {
Self::Quadlet(file) => file.resource.extension(),
Self::Kubernetes(_) => "yaml",
}
}

/// Returns [`Some`] if a [`File::Quadlet`].
// Only used by the `#[cfg(unix)]` existing-services check.
#[cfg(unix)]
fn as_quadlet_file(&self) -> Option<&quadlet::File> {
match self {
Self::Quadlet(file) => Some(file),
Expand Down Expand Up @@ -843,7 +886,10 @@ impl File {
///
/// Returns an error if the contained [`quadlet::File`] or [`k8s::File`] returns an error while
/// serializing.
fn serialize(&self, join_options: &HashSet<JoinOption>) -> color_eyre::Result<String> {
pub(crate) fn serialize(
&self,
join_options: &HashSet<JoinOption>,
) -> color_eyre::Result<String> {
match self {
File::Quadlet(file) => file
.serialize_to_quadlet(join_options)
Expand Down
29 changes: 24 additions & 5 deletions src/cli/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,36 @@ impl Compose {
/// - Converting the compose file to Kubernetes YAML.
/// - Converting the compose file to Quadlet files.
pub fn try_into_files(self, sections: GenericSections) -> color_eyre::Result<Vec<File>> {
let mut options = compose_spec::Compose::options();
options.apply_merge(true);
let compose = read_from_file_or_stdin(self.compose_file.as_deref(), &options)
.wrap_err("error reading compose file")?;

self.into_files(compose, sections)
}

/// Convert an already parsed [`compose_spec::Compose`] into [`File`]s.
///
/// Unlike [`try_into_files`](Self::try_into_files), this performs no filesystem or standard
/// input access, making it suitable for restricted targets such as WASM. The `compose_file`
/// field is ignored.
///
/// # Errors
///
/// Returns an error if the compose file fails validation, uses an unsupported option, or
/// cannot be converted into Quadlet or Kubernetes YAML files.
pub fn into_files(
self,
compose: compose_spec::Compose,
sections: GenericSections,
) -> color_eyre::Result<Vec<File>> {
let Self {
pod,
kube,
add_container_name,
compose_file,
compose_file: _,
} = self;

let mut options = compose_spec::Compose::options();
options.apply_merge(true);
let compose = read_from_file_or_stdin(compose_file.as_deref(), &options)
.wrap_err("error reading compose file")?;
compose
.validate_all()
.wrap_err("error validating compose file")?;
Expand Down
Loading